diff --git a/docs/cs/AspNetCore/Widgets.md b/docs/cs/AspNetCore/Widgets.md deleted file mode 100644 index f0ee218804..0000000000 --- a/docs/cs/AspNetCore/Widgets.md +++ /dev/null @@ -1,506 +0,0 @@ -# Widgety - -ABP poskytuje model a infastrukturu k vytváření **znovu použitelných widgetů**. Systém widgetů je rozšíření pro [ASP.NET Core pohledové komponenty](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgety jsou zvláště užitečné, když chcete; - -* Mít závislosti na **skriptech & stylech** ve vašem widgetu. -* Vytvářet **řídící panely** za použítí widgetů. -* Definovat widgety v znovu použitelných **[modulech](../Module-Development-Basics.md)**. -* Spolupráci widgetů s **[authorizačními](../Authorization.md)** a **[svazovacími](Bundling-Minification.md)** systémy. - -## Základní definice widgetu - -### Tvorba pohledové komponenty - -Jako první krok, vytvořte běžnou ASP.NET Core pohledovou komponentu: - -![widget-basic-files](../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -Dědění z `AbpViewComponent` není vyžadováno. Můžete dědit ze standardního ASP.NET Core `ViewComponent`. `AbpViewComponent` pouze definuje pár základních a užitečných vlastnosti. - -Můžete vložit službu a pomocí metody `Invoke` z ní získat některá data. Možná budete muset provést metodu Invoke jako asynchronní `public async Task InvokeAsync()`. Podívejte se na dokument [ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) pro všechna další použítí. - -**Default.cshtml**: - -```xml -
-

My Simple Widget

-

This is a simple widget!

-
-``` - -### Definice widgetu - -Přidejte atribut `Widget` k třídě `MySimpleWidgetViewComponent` pro označení této pohledové komponenty jako widgetu: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## Vykreslení widgetu - -Vykreslení widgetu je vcelku standardní. Použijte metodu `Component.InvokeAsync` v razor pohledu/stránce jako s kteroukoliv jinou pohledovou komponentou. Příklady: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -První přístup používá název widgetu, zatímco druhý používá typ pohledové komponenty. - -### Widgety s argumenty - -Systém ASP.NET Core pohledových komponent umožňuje přijímat argumenty pro pohledové komponenty. Níže uvedená pohledová komponenta přijímá `startDate` a `endDate` a používá tyto argumenty k získání dat ze služby. - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget -{ - [Widget] - public class CountersWidgetViewComponent : AbpViewComponent - { - private readonly IDashboardAppService _dashboardAppService; - - public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) - { - _dashboardAppService = dashboardAppService; - } - - public async Task InvokeAsync( - DateTime startDate, DateTime endDate) - { - var result = await _dashboardAppService.GetCountersWidgetAsync( - new CountersWidgetInputDto - { - StartDate = startDate, - EndDate = endDate - } - ); - - return View(result); - } - } -} -```` - -Nyní musíte předat anonymní objekt k předání argumentů tak jak je ukázáno níže: - -````xml -@await Component.InvokeAsync("CountersWidget", new -{ - startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), - endDate = DateTime.Now -}) -```` - -## Název widgetu - -Výchozí název pohledových komponent je vypočítán na základě názvu typu pohledové komponenty. Pokud je typ pohledové komponenty `MySimpleWidgetViewComponent` potom název widgetu bude `MySimpleWidget` (odstraní se `ViewComponent` postfix). Takto ASP.NET Core vypočítává název pohledové komponenty. - -Chcete-li přizpůsobit název widgetu, stačí použít standardní atribut `ViewComponent` z ASP.NET Core: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP bude respektovat přizpůsobený název při zpracování widgetu. - -> Pokud jsou názvy pohledové komponenty a složky, která pohledovou komponentu obsahuje rozdílné, pravděpodobně budete muset ručně uvést cestu pohledu tak jako je to provedeno v tomto příkladu. - -### Zobrazovaný název - -Můžete také definovat čitelný & lokalizovatelný zobrazovaný název pro widget. Tento zobrazovaný název může být využít na uživatelském rozhraní kdykoliv je to potřeba. Zobrazovaný název je nepovinný a lze ho definovat pomocí vlastností atributu `Widget`: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", // Lokalizační klíč - DisplayNameResource = typeof(DashboardDemoResource) // Lokalizační zdroj - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -Podívejte se na [dokument lokalizace](../Localization.md) pro více informací o lokalizačních zdrojích a klíčích. - -## Závislosti na stylech & skriptech - -Problémy když má widget soubory skriptů a stylů; - -* Každý stránka, která používá widget musí také přidat soubory **skriptů & stylů** tohoto widgetu. -* Stránka se také musí postarat o **závislé knihovny/soubory** widgetu. - -ABP tyto problémy řeší, když správně propojíme zdroje s widgetem. O závislosti widgetu se při jeho používání nestaráme. - -### Definování jednoduchých cest souborů - -Níže uvedený příklad widgetu přidá stylové a skriptové soubory: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP bere v úvahu tyto závislosti a správně je přidává do pohledu/stránky při použití widgetu. Stylové/skriptové soubory mohou být **fyzické nebo virtuální**. Plně integrováno do [virtuálního systému souborů](../Virtual-File-System.md). - -### Definování přispěvatelů balíku - -Všechny zdroje použité ve widgetech na stránce jsou přidány jako **svazek** (svázány & minifikovány v produkci pokud nenastavíte jinak). Kromě přidání jednoduchého souboru můžete využít plnou funkčnost přispěvatelů balíčků. - -Níže uvedený ukázkový kód provádí totéž co výše uvedený kód, ale definuje a používá přispěvatele balíků: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -Systém přispěvatelů balíků je velmi schopný. Pokud váš widget používá k vykreslení grafu JavaScript knihovnu, můžete ji deklarovat jako závislost, díky tomu se knihovna pokud nebyla dříve přidána automaticky přidá na stránku Tímto způsobem se stránka využívající váš widget nestará o závislosti. - -Podívejte se na dokumentaci [svazování & minifikace](Bundling-Minification.md) pro více informací o tomto systému. - -## RefreshUrl - -Widget může navrhnout `RefreshUrl`, který se používá vždy, když je potřeba widget aktualizovat. Je-li definován, widget se při každé aktualizaci znovu vykreslí na straně serveru (viz refresh `methoda` u `WidgetManager` níže). - -````csharp -[Widget(RefreshUrl = "Widgets/Counters")] -public class CountersWidgetViewComponent : AbpViewComponent -{ - -} -```` - -Jakmile pro svůj widget definujete `RefreshUrl`, musíte poskytnout koncový bod pro jeho vykreslení a vrátit ho: - -````csharp -[Route("Widgets")] -public class CountersWidgetController : AbpController -{ - [HttpGet] - [Route("Counters")] - public IActionResult Counters(DateTime startDate, DateTime endDate) - { - return ViewComponent("CountersWidget", new {startDate, endDate}); - } -} -```` - -Trasa `Widgets/Counters` předchozímu `RefreshUrl`. - -> Widget lze obnovit dvěma způsoby: Prvním způsobem je použití `RefreshUrl`, kdy se znovu vykreslí na serveru a nahradí HTML vrácené tím ze serveru. Druhým způsobem widget získá data (obvykle JSON objekt) ze serveru a obnoví se sám u klienta (viz refresh metoda v sekci Widget JavaScript API). - -## JavaScript API - -Možná bude potřeba vykreslit a obnovit widget na straně klienta. V takových případech můžete použít ABP `WidgetManager` a definovat API pro vaše widgety. - -### WidgetManager - -`WidgetManager` se používá k inicializaci a aktualizaci jednoho nebo více widgetů. Vytvořte nový `WidgetManager` jako je ukázáno níže: - -````js -$(function() { - var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); -}) -```` - -`MyDashboardWidgetsArea` může obsahovat jeden nebo více widgetů. - -> Použíti `WidgetManager` uvnitř document.ready (jako nahoře) je dobrá praktika jelikož jeho funkce používají DOM a potřebují, aby byl DOM připraven. - -#### WidgetManager.init() - -`init` jednoduše inicializuje `WidgetManager` a volá metody `init` v souvisejících widgetech pokud je obsahují (podívejte se na sekci Widget JavaScript API section níže) - -```js -myWidgetManager.init(); -``` - -#### WidgetManager.refresh() - -`refresh` metoda obnoví všechny widgety související s tímto `WidgetManager`: - -```` -myWidgetManager.refresh(); -```` - -#### WidgetManager možnosti - -WidgetManager má několik dalších možností. - -##### Filtrační formulář - -Pokud vaše widgety vyžadují parametry/filtry pak budete obvykle mít formulář pro filtrování widgetů. V takových případech můžete vytvořit formulář, který obsahuje prvky formuláře a oblast řídicího panelu s nějakými widgety uvnitř. Příklad: - -````xml -
- ...prvky formuláře -
- -
- ...widgety -
-```` - -`data-widget-filter` atribut propojuje formulář s widgety. Kdykoli je formulář odeslán, všechny widgety jsou automaticky aktualizovány pomocí polí formuláře jako filtru. - -Místo atributu `data-widget-filter`, můžete použít parametr `filterForm` v konstruktoru `WidgetManager`. Příklad: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterForm: '#MyDashboardFilterForm' -}); -```` - -##### Zpětné volání filtru - -Možná budete chtít mít lepší kontrolu nad poskytováním filtrů při inicializaci a aktualizaci widgetů. V tomto případě můžete použít volbu `filterCallback`: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterCallback: function() { - return $('#MyDashboardFilterForm').serializeFormToObject(); - } -}); -```` - -Tento příklad ukazuje výchozí implementaci `filterCallback`. Pomocí polí můžete vrátit jakýkoli JavaScript objekt. Příklad: - -````js -filterCallback: function() { - return { - 'startDate': $('#StartDateInput').val(), - 'endDate': $('#EndDateInput').val() - }; -} -```` - -Vrácené filtry jsou předávány všem widgetům na `init` a` refresh`. - -### Widget JavaScript API - -Widget může definovat rozhraní API jazyka JavaScript, které je v případě potřeby vyvoláno přes `WidgetManager`. Ukázku kódu níže lze použít k definování API pro widget. - -````js -(function () { - abp.widgets.NewUserStatisticWidget = function ($wrapper) { - - var getFilters = function () { - return { - ... - }; - } - - var refresh = function (filters) { - ... - }; - - var init = function (filters) { - ... - }; - - return { - getFilters: getFilters, - init: init, - refresh: refresh - }; - }; -})(); -```` - -`NewUserStatisticWidget` je tady název widgetu. Měl by odpovídat názvu widgetu definovanému na straně serveru. Všechny funkce jsou volitelné. - -#### getFilters - -Pokud má widget vlastní interní filtry, měla by tato funkce vrátit objekt filtru. Příklad: - -````js -var getFilters = function() { - return { - frequency: $wrapper.find('.frequency-filter option:selected').val() - }; -} -```` - -Tuto metodu používá `WidgetManager` při vytváření filtrů. - -#### init - -Slouží k inicializaci widgetu kdykoli je potřeba. Má argument filtru, který lze použít při získávání dat ze serveru. Metoda `init` je použita když je volána funkce `WidgetManager.init()`. Použita je i v případě že váš widget vyžaduje úplné obnovení při aktualizaci. Viz `RefreshUrl` v možnostech widgetu. - -#### refresh - -Slouží k aktualizaci widgetu kdykoli je potřeba. Má argument filtru, který lze použít při získávání dat ze serveru. Metoda `refresh` se používá kdykoliv je volána funkce `WidgetManager.refresh()`. - -## Autorizace - -Některé widgety budou pravděpodobně muset být dostupné pouze pro ověřené nebo autorizované uživatele. V tomto případě použijte následující vlastnosti atributu `Widget`: - -* `RequiresAuthentication` (`bool`): Nastavte na true, aby byl tento widget použitelný pouze pro ověřené uživatele (uživatel je přihlášen do aplikace). -* `RequiredPolicies` (`List`): Seznam názvů zásad k autorizaci uživatele. Další informace o zásadách naleznete v [dokumentu autorizace](../Authorization.md). - -Příklad: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## WidgetOptions - -Jako alternativu k atributu `Widget` můžete ke konfiguraci widgetů použít `AbpWidgetOptions`: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -Toto vepište do metody `ConfigureServices` vašeho [modulu](../Module-Development-Basics.md). Veškerá konfigurace udělaná přes atribut `Widget` je dostupná i za pomoci `AbpWidgetOptions`. Příklad konfigurace, která přidává styl pro widget: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> Tip: `AbpWidgetOptions` lze také použít k získání existujícího widgetu a ke změně jeho konfigurace. To je obzvláště užitečné, pokud chcete změnit konfiguraci widgetu uvnitř modulu používaného vaší aplikací. Použíjte `options.Widgets.Find` k získání existujícího `WidgetDefinition`. - -## Podívejte se také na - -* [Příklad projektu (zdrojový kód)](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo). - diff --git a/docs/cs/Autofac-Integration.md b/docs/cs/Autofac-Integration.md deleted file mode 100644 index 109dca6286..0000000000 --- a/docs/cs/Autofac-Integration.md +++ /dev/null @@ -1,84 +0,0 @@ -# Autofac integrace - -Autofac je jedním z nejpoužívanějších frameworků pro .Net pro vkládání závislostí (DI). Poskytuje pokročilejší funkce v porovnáním se standardní .Net Core DI knihovnou, jako dynamickou proxy a injekci vlastností. - -## Instalace Autofac integrace - -> Všechny startovací šablony a vzorky jsou s Autofac již integrovány. Takže většinou nemusíte tento balíček instalovat ručně. - -Nainstalujte do vašeho projektu balíček [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) (pro víceprojektovou aplikaci se doporučuje přidat do spustitelného/webového projektu.) - -```` -Install-Package Volo.Abp.Autofac -```` - -Poté přídejte k vašemu modulu závislost na `AbpAutofacModule`: - -```csharp -using Volo.Abp.Modularity; -using Volo.Abp.Autofac; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAutofacModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -Nakonec nastavte `AbpApplicationCreationOptions` aby nahradil výchozí služby pro vkládání závislostí na Autofac. Záleží na typu aplikace. - -### ASP.NET Core aplikace - -Volejte `UseAutofac()` v souboru **Startup.cs** jako je ukázáno níže: - -````csharp -public class Startup -{ - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddApplication(options => - { - //Integrace Autofac! - options.UseAutofac(); - }); - - return services.BuildServiceProviderFromFactory(); - } - - public void Configure(IApplicationBuilder app) - { - app.InitializeApplication(); - } -} -```` - -### Konzolová aplikace - -Volejte metodu `UseAutofac()` v možnostech `AbpApplicationFactory.Create` jako je ukázáno níže: - -````csharp -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); //Autofac integrace - })) - { - //... - } - } - } -} -```` - diff --git a/docs/cs/CLI.md b/docs/cs/CLI.md deleted file mode 100644 index f77c3ad969..0000000000 --- a/docs/cs/CLI.md +++ /dev/null @@ -1,167 +0,0 @@ -# ABP CLI - -ABP CLI (Command Line Interface) je nástroj v příkazovém řádku k provádění některých běžných úkonů v řešeních založených na ABP. - -## Instalace - -ABP CLI je [dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). Nainstalujete jej pomocí okna příkazového řádku: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Aktualizace stávající instalace: - -````bash -dotnet tool update -g Volo.Abp.Cli -```` - -## Příkazy - -### new - -Vygeneruje nové řešení založené na ABP [startovací šabloně](Startup-Templates/Index.md). - -Základní použití: - -````bash -abp new [možnosti] -```` - -Příklad: - -````bash -abp new Acme.BookStore -```` - -* `Acme.BookStore` je tady název řešení. -* Běžná konvence je nazvat řešení stylem *VaseSpolecnost.VasProjekt*. Nicméně můžete použít i jiné pojmenování jako *VasProjekt* (jednostupňový jmenný prostor) nebo *VaseSpolecnost.VasProjekt.VasModul* (třístupňový jmenný prostor). - -#### Možnosti - -* `--template` nebo `-t`: Určuje název šablony. Výchozí šablona je `app`, která generuje webovou aplikaci. Dostupné šablony: - * `app` (výchozí): [Aplikační šablona](Startup-Templates/Application.md). Dodatečné možnosti: - * `--ui` nebo `-u`: Určuje UI framework. Výchozí framework je `mvc`. Dostupné frameworky: - * `mvc`: ASP.NET Core MVC. Pro tuto šablonu jsou dostupné dodatečné možnosti: - * `--tiered`: Vytvoří stupňovité řešení, kde jsou vrstvy Web a Http API fyzicky odděleny. Pokud není uvedeno, tak vytvoří vrstvené řešení, které je méně složité a vhodné pro většinu scénářů. - * `angular`: Angular. Pro tuto šablonu jsou dostupné dodatečné možnosti: - * `--separate-auth-server`: Oddělí Auth Server aplikaci od API host aplikace. Pokud není uvedeno, bude na straně serveru jediný koncový bod. - * `none`: Bez UI. Pro tuto šablonu jsou dostupné dodatečné možnosti: - * `--separate-auth-server`: Oddělí Auth Server aplikaci od API host aplikace. Pokud není uvedeno, bude na straně serveru jediný koncový bod. - * `--database-provider` nebo `-d`: Určuje poskytovatele databáze. Výchozí poskytovatel je `ef`. Dostupní poskytovatelé: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `module`: [Šablona modulu](Startup-Templates/Module.md). Dodatečné možnosti: - * `--no-ui`: Určuje nezahrnutí uživatelského rozhraní. Umožňuje vytvořit moduly pouze pro služby (a.k.a. mikroslužby - bez uživatelského rozhraní). -* `--output-folder` nebo `-o`: Určuje výstupní složku. Výchozí hodnota je aktuální adresář. -* `--version` nebo `-v`: Určuje verzi ABP & šablony. Může to být [štítek vydání](https://github.com/abpframework/abp/releases) nebo [název větve](https://github.com/abpframework/abp/branches). Pokud není uvedeno, používá nejnovější vydání. Většinou budete chtít použít nejnovější verzi. - - -### add-package - -Přidá ABP balíček do projektu, - -* Přidáním souvisejícícho nuget balíčku jako závislost do projektu. -* Přidáním `[DependsOn(...)]` atributu k modulové tříde v projektu (podívejte se na [dokument vývoje modulu](Module-Development-Basics.md)). - -> Všimněte si, že přidaný modul může vyžadovat další konfiguraci, která je obecně uvedena v dokumentaci příslušného balíčku. - -Základní použití: - -````bash -abp add-package [možnosti] -```` - -Příklad: - -```` -abp add-package Volo.Abp.MongoDB -```` - -* Tento příklad přidá do projektu balíček Volo.Abp.MongoDB. - -#### Možnosti - -* `--project` nebo `-p`: Určuje cestu k projektu (.csproj). Pokud není zadáno, CLI se pokusí najít soubor .csproj v aktuálním adresáři. - -### add-module - -Přidá [více-balíčkový aplikační modul](Modules/Index) k řešení tím, že najde všechny balíčky modulu, vyhledá související projekty v řešení a přidá každý balíček do odpovídajícího projektu v řešení. - -> Modul se obecně skládá z několika balíčků (z důvodu vrstvení, různých možností poskytovatele databáze nebo jiných důvodů). Použití příkazu `add-module` dramaticky zjednodušuje přidání modulu do řešení. Každý modul však může vyžadovat další konfiguraci, která je obecně uvedena v dokumentaci příslušného modulu. - -Základní použití: - -````bash -abp add-module [možnosti] -```` - -Příklad: - -```bash -abp add-module Volo.Blogging -``` - -* Tento příklad přidá do projektu modul Volo.Blogging. - -#### Možnosti - -* `--solution` nebo `-s`: Určuje cestu k řešení (.sln). Pokud není zadáno, CLI se pokusí najít soubor .sln v aktuálním adresáři. -* `--skip-db-migrations`: Pro poskytovatele databáze EF Core automaticky přidá nový kód první migrace (`Add-Migration`) a v případě potřeby aktualizuje databázi (`Update-Database`). Tuto možnost určete k vynechání této operace. -* `-sp` nebo `--startup-project`: Relativní cesta ke složce spouštěcího projektu. Výchozí hodnota je aktuální adresář. -* `--with-source-code`: Místo balíčků NuGet/NPM přidejte zdrojový kód modulu. - -### update - -Aktualizace všech balíčků souvisejících s ABP může být únavná, protože existuje mnoho balíčků frameworku a modulů. Tento příkaz automaticky aktualizuje na poslední verze všechny související ABP NuGet a NPM balíčky v řešení nebo projektu. - -Použití: - -````bash -abp update [možnosti] -```` - -* Pokud spouštíte v adresáři se souborem .sln, aktualizuje všechny balíčky všech projektů v řešení souvisejících s ABP na nejnovější verze. -* Pokud spouštíte v adresáři se souborem .csproj, aktualizuje všechny balíčky v projektu na nejnovější verze. - -#### Možnosti - -* `--include-previews` nebo `-p`: Zahrne náhledové, beta a rc balíčky při kontrole nových verzí. -* `--npm`: Aktualizuje pouze balíčky NPM. -* `--nuget`: Aktualizuje pouze balíčky NuGet. - -### login - -Některé funkce CLI vyžadují přihlášení k platformě abp.io. Chcete-li se přihlásit pomocí svého uživatelského jména, napište - -```bash -abp login -``` - -Všimněte si, že nové přihlášení s již aktivní relací ukončí předchozí relaci a vytvoří novou. - -### logout - -Odhlásí vás odebráním tokenu relace z počítače. - -``` -abp logout -``` - -### help - -Vypíše základní informace k používání CLI. - -Použítí: - -````bash -abp help [název-příkazu] -```` - -Příklady: - -````bash -abp help # Zobrazí obecnou nápovědu. -abp help new # Zobrazí nápovědu k příkazu "new". -```` - diff --git a/docs/cs/Contribution/Index.md b/docs/cs/Contribution/Index.md deleted file mode 100644 index ac1aaf00c1..0000000000 --- a/docs/cs/Contribution/Index.md +++ /dev/null @@ -1,64 +0,0 @@ -## Průvodce pro přispěvatele - -ABP je [open source](https://github.com/abpframework) a komunitně řízený projekt. Tento průvodce má za cíl pomoci každému kdo chce do projektu nějak přispět. - -### Příspěvek kódu - -Vždy můžete zaslat pull request do Github repositáře. - -- Naklonujte [ABP repozitář](https://github.com/abpframework/abp/) z Githubu. -- Učiňte potřebné změny. -- Zašlete pull request. - -Než budete dělat nějaké změny, diskutujte o nich prosím na [Github problémy](https://github.com/abpframework/abp/issues). Díky tomu nebude žádný jiný vývojář pracovat na stejném problému a Váš PR má lepší šanci na to být přijat. - -#### Opravy chyb a vylepšení - -Pokud chcete opravit známou chybu nebo pracovat na plánovaném vylepšení podívejte se na [seznam problémů](https://github.com/abpframework/abp/issues) na Githubu. - -#### Požadavky na funkce - -Pokud máte nápad na funkci pro framework nebo modul [vytvořte problém](https://github.com/abpframework/abp/issues/new) na Githubu nebo se připojte ke stávající diskuzi. V případě přijetí komunitou ho pak můžete implementovat. - -### Překlad dokumentů - -Pokud chcete přeložit celou [dokumentaci](https://abp.io/documents/) (včetně této stránky) do Vašeho rodného jazyka, následujte tyto kroky: - -* Naklonujte [ABP repozitář](https://github.com/abpframework/abp/) z Githubu. -* K přidání nového jazyka vytvořte novou složku v [docs](https://github.com/abpframework/abp/tree/master/docs). Název složky musí být "en", "es", "fr", "tr" atd. v závislosti na jazyku (navštivte [všechny jazykové kódy](https://msdn.microsoft.com/en-us/library/hh441729.aspx)). -* Pro referenci použijte ["en" složku](https://github.com/abpframework/abp/tree/master/docs/en) a její názvy souborů a strom složek. Při překladu této dokumentace zachovejte prosím tyto názvy stejné. -* Zašlete pull request (PR) po překladu jakéhokoliv dokumentu klidně i po jednom. Nečekejte až budete mít překlad všech dokumentů. - -Existuje několik základních dokumentů, které je třeba přeložit než bude jazyk uveřejněn na [stránkách ABP dokumentace](https://docs.abp.io) - -* Začínáme dokumenty -* Tutoriály -* CLI - -Nový jazyk je publikován jakmile jsou minimálně tyto překlady dokončeny. - -### Lokalizace zdrojů - -ABP framework má flexibilní [lokalizační systém](../Localization.md). Můžete tak vytvořit lokalizované uživatelské prostředí pro svou vlastní aplikaci. - -K tomu mají framework a vestavěné moduly již lokalizované texty. Například [lokalizační texty pro Volo.Abp.UI balík](https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json). - -Můžete vytvořit nový soubor ve [stejné složce](https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi) k přidání překladu. - -* Naklonujte [ABP repozitář](https://github.com/abpframework/abp/) z Githubu. -* Vytvořte nový soubor pro cílový jazyk pro lokalizační text v (json) souboru (u souboru en.json). -* Zkopírujte veškerý text ze souboru en.json. -* Přeložte texty. -* Zašlete pull request na Githubu. - -K překladu lokalizovaných textů můžete také použít příkaz `abp translate` of [ABP CLI](CLI.md). - -ABP je modulářní framework, proto je zde mnoho zdrojů lokalizačních textů, jeden pro každý modul. K najití všech .json souborů, vyhledejte po naklonování repozitáře soubory "en.json". Můžete se taky podívat na [tento seznam](Localization-Text-Files.md) souborů lokalizačních textů. - -### Příspevky do blogu a návody - -Pokud se rozhodnete pro ABP vytvořit nějaké návody nebo příspěvky do blogu, dejte nám vědět (prostřednictvím [Github problémy](https://github.com/abpframework/abp/issues)), ať můžeme přidat odkaz na Váš návod/příspěvek v oficiální dokumentaci a oznámit na našem [Twitter účtu](https://twitter.com/abpframework). - -### Zpráva o chybě - -Pokud najdete chybu, [vytvořte prosím problém v Github repozitáři](https://github.com/abpframework/abp/issues/new). diff --git a/docs/cs/Dapper.md b/docs/cs/Dapper.md deleted file mode 100644 index 94e40347f0..0000000000 --- a/docs/cs/Dapper.md +++ /dev/null @@ -1,61 +0,0 @@ -# Dapper integrace - -Jelikož myšlenka Dapper je taková, že sql příkaz má přednost, tak hlavně poskytuje metody rozšíření pro `IDbConnection` rozhraní. - -Abp nezapouzdřuje přílíš mnoho funkcí pro Dapper. Abp Dapper poskytuje základní třídu `DapperRepository` založenou na Abp EntityFrameworkCore, který poskytuje vlastnosti `IDbConnection` a `IDbTransaction` vyžadované v Dapper. - -Tyto dvě vlastnosti fungují dobře s [jednotkou práce](Unit-Of-Work.md). - -## Instalace - -Nainstalujte a nakonfigurujte EF Core podle [EF Core integrační dokumentace](Entity-Framework-Core.md). - -`Volo.Abp.Dapper` je hlavní NuGet balík pro Dapper integraci. Nainstalujte jej proto do vašeho projektu (pro strukturovanou aplikaci do datové/infrastrukturní vrstvy): - -```shell -Install-Package Volo.Abp.Dapper -``` - -Poté přidejte závislost na `AbpDapperModule` modulu (atribut `DependsOn`) do Vašeho [modulu](Module-Development-Basics.md): - -````C# -using Volo.Abp.Dapper; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpDapperModule))] - public class MyModule : AbpModule - { - //... - } -} -```` - -## Implementace Dapper repozitáře - -Následující kód implementuje repozitář `Person`, který vyžaduje `DbContext` z EF Core (MyAppDbContext). Můžete vložit `PersonDapperRepository` k volání jeho metod. - -`DbConnection` a `DbTransaction` jsou ze základní třídy `DapperRepository`. - -```C# -public class PersonDapperRepository : DapperRepository, ITransientDependency -{ - public PersonDapperRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public virtual async Task> GetAllPersonNames() - { - return (await DbConnection.QueryAsync("select Name from People", transaction: DbTransaction)) - .ToList(); - } - - public virtual async Task UpdatePersonNames(string name) - { - return await DbConnection.ExecuteAsync("update People set Name = @NewName", new { NewName = name }, - DbTransaction); - } -} -``` diff --git a/docs/cs/Domain-Driven-Design.md b/docs/cs/Domain-Driven-Design.md deleted file mode 100644 index 161e92c8a2..0000000000 --- a/docs/cs/Domain-Driven-Design.md +++ /dev/null @@ -1,33 +0,0 @@ -# Domain Driven Design - -## Co je DDD? - -ABP framework poskytuje **infrastrukturu**, která zjednodušuje implementaci vývoje založeného na **DDD**. DDD je [definován ve Wikipedii](https://en.wikipedia.org/wiki/Domain-driven_design) takto: - -> **Domain-driven design** (**DDD**) je přístup k vývoji softwaru pro komplexní potřeby propojením implementace s vyvíjejícím se modelem. Předpoklad DDD je následující: -> -> - Primární zaměření projektu je na jádře domény a doménové logice; -> - Zakládání komplexních návrhů na modelu domény; -> - Iniciování tvůrčí spolupráce mezi technickými a doménovými odborníky s cílem iterativně zdokonalit koncepční model, který řeší konkrétní problémy v doméně. - -### Vrstvy - -ABP dodržuje principy a vzorce DDD pro dosažení vrstveného aplikačního modelu, který se skládá ze čtyř základních vrstev: - -- **Prezentační vrstva**: Poskytuje uživateli rozhraní. Používá *Aplikační vrstvu* k dosažení uživatelských interakcí. -- **Aplikační vrstva**: Prostředník mezi prezentační a doménovou vrstvou. Instrumentuje business objekty k provádění specifických úloh aplikace. Implementuje případy použití jako logiku aplikace. -- **Doménová vrstva**: Zahrnuje business objekty a jejich business pravidla. Je jádrem aplikace. -- **Vrstva infrastruktury**: Poskytuje obecné technické možnosti, které podporují vyšší vrstvy většinou pomocí knihoven třetích stran. - -## Obsah - -* **Doménová vrstva** - * [Entity & agregované kořeny](Entities.md) - * Hodnotové objekty - * [Repozitáře](Repositories.md) - * Doménové služby - * Specifikace -* **Aplikační vrstva** - * [Aplikační služby](Application-Services.md) - * [Objekty přenosu dat (DTOs)](Data-Transfer-Objects.md) - * Jednotka práce \ No newline at end of file diff --git a/docs/cs/Entity-Framework-Core-PostgreSQL.md b/docs/cs/Entity-Framework-Core-PostgreSQL.md deleted file mode 100644 index dc50c8a742..0000000000 --- a/docs/cs/Entity-Framework-Core-PostgreSQL.md +++ /dev/null @@ -1,39 +0,0 @@ -# Přepnutí na EF Core PostgreSQL providera - -Tento dokument vysvětluje, jak přepnout na poskytovatele databáze **PostgreSQL** pro **[spouštěcí šablonu aplikace](Startup-Templates/Application.md)**, která je dodávána s předem nakonfigurovaným SQL poskytovatelem. - -## Výměna balíku Volo.Abp.EntityFrameworkCore.SqlServer - -Projekt `.EntityFrameworkCore` v řešení závisí na NuGet balíku [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer). Odstraňte tento balík a přidejte stejnou verzi balíku [Volo.Abp.EntityFrameworkCore.PostgreSql](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.PostgreSql). - -## Nahrazení závislosti modulu - -Najděte třídu ***YourProjectName*EntityFrameworkCoreModule** v projektu `.EntityFrameworkCore`, odstraňte `typeof(AbpEntityFrameworkCoreSqlServerModule)` z atributu `DependsOn`, přidejte `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (také nahraďte `using Volo.Abp.EntityFrameworkCore.SqlServer;` za `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). - -## UseNpgsql() - -Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UseNpgsql()`. - -Najděte volání `UseSqlServer()` v *YourProjectName*MigrationsDbContextFactory.cs uvnitř projektu `.EntityFrameworkCore.DbMigrations` a nahraďte za `UseNpgsql()`. - -> V závislosti na struktuře řešení můžete najít více volání `UseSqlServer()`, které je třeba změnit. - -## Změna connection stringů - -PostgreSql connection stringy se od těch pro SQL Server liší. Je proto potřeba zkontrolovat všechny soubory `appsettings.json` v řešení a connection stringy v nich nahradit. Podívejte se na [connectionstrings.com](https://www.connectionstrings.com/postgresql/) pro více detailů o možnostech PostgreSql connection stringů. - -Typicky je potřeba změnit `appsettings.json` v projektech `.DbMigrator` a `.Web` projects, ale to záleží na vaší struktuře řešení. - -## Regenerace migrací - -Startovací šablona používá [Entity Framework Core Code First migrace](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core migrace závisí na zvoleném DBMS poskytovateli. Tudíž změna DBMS poskytovatele způsobí selhání migrace. -* Smažte složku Migrations v projektu `.EntityFrameworkCore.DbMigrations` and znovu sestavte řešení. -* Spusťte `Add-Migration "Initial"` v Package Manager Console (je nutné zvolit `.DbMigrator` (nebo `.Web`) projekt jako startovací projekt v Solution Explorer a zvolit projekt `.EntityFrameworkCore.DbMigrations` jako výchozí v Package Manager Console). - -Tímto vytvoříte migraci databáze se všemi nakonfigurovanými databázovými objekty (tabulkami). - -Spusťte projekt `.DbMigrator` k vytvoření databáze a vložení počátečních dat. - -## Spuštění aplikace - -Vše je připraveno. Stačí už jen spustit aplikaci a užívat si kódování. diff --git a/docs/cs/Getting-Started-Angular-Template.md b/docs/cs/Getting-Started-Angular-Template.md deleted file mode 100644 index 076fb45671..0000000000 --- a/docs/cs/Getting-Started-Angular-Template.md +++ /dev/null @@ -1,126 +0,0 @@ -## Začínáme s Angular aplikační šablonou - -Tento tutoriál vysvětluje, jak vytvořit novou Angular aplikaci pomocí spouštěcí šablony, jak ji nakonfigurovat a spustit. - -### Tvorba nového projektu - -Tento tutorial používá k vytvoření nového projektu **ABP CLI**. Podívejte se na stránku [začínáme](https://abp.io/get-started) pro více možností. - -Pokud jste tak dosud neučinili, nainstalujte ABP CLI pomocí okna příkazového řádku: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Použíjte příkaz `abp new` v prázdné složce k vytvoření Vašeho projektu: - -````bash -abp new Acme.BookStore -u angular -```` - -> Můžete použít různé úrovně jmenných prostorů; např. BookStore, Acme.BookStore nebo Acme.Retail.BookStore. - -`-u angular` volba specifikuje Angular jako UI framework. Výchozí poskytovatel databáze je EF Core. Podívejte se na [CLI dokumentaci](CLI.md) pro všechny dostupné možnosti. - -#### Předběžné požadavky - -Vytvořené řešení vyžaduje; - -* [Visual Studio 2019 (v16.4.0+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### Struktura řešení - -Otevřete řešení ve **Visual Studio**: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-for-spa.png) - -Řešení má vrstvenou strukturu (založenou na [domain driven designu](Domain-Driven-Design.md)) a obsahuje projekty testů jednotek a integrace správně nakonfigurované pro práci s **EF Core** & **SQLite in-memory** databází. - -> Podívejte se na [dokument šablony aplikace](Startup-Templates/Application.md) k detailnímu pochopení struktury řešení. - -### Databázový connection string - -Zkontrolujte **connection string** v souboru `appsettings.json` u projektu `.HttpApi.Host`: - -````json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -```` - -Řešení je nakonfigurováno pro použití **Entity Framework Core** s **MS SQL Server**. EF Core podporuje [různé](https://docs.microsoft.com/en-us/ef/core/providers/) poskytovatele databáze, takže pokud chcete můžete použít jiný DBMS. V případě potřeby změňte connection string. - -### Tvorba databáze & aplikace migrací databáze - -K vytvoření databáze máte dvě možnosti. - -#### Použití aplikace DbMigrator - -Řešení obsahuje konzolovou aplikaci (v tomto příkladu nazvanou `Acme.BookStore.DbMigrator`), která dokáže vytvořit databázi, aplikovat migrace a vložit počáteční data. Ta je užitečná jak pro vývojové tak pro produkční prostředí. - -> `.DbMigrator` má vlastní `appsettings.json`. Pokud jste změnili connection string výše, měli byste změnit i tento. - -Klikněte pravým na projekt `.DbMigrator` zvolte **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Zmáčkněte F5 (nebo Ctrl+F5) ke spuštění aplikace. Výstup by měl být podobný vyobrazení níže: - -![set-as-startup-project](images/db-migrator-app.png) - -#### Použití příkazu EF Core Update-Database - -Ef Core máš příkaz `Update-Database`, který v případě potřeby vytvoří databázi a aplikuje čekající migrace. Klikněte pravým na projekt `.HttpApi.Host` a zvolte **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Otevřete **Package Manager Console**, zvolte `.EntityFrameworkCore.DbMigrations` jako **Default Project** a proveďte příkaz `Update-Database`: - -![pcm-update-database](images/pcm-update-database-v2.png) - -Tímto vytvoříte novou databáze podle nakonfigurovaného connection string. - -> Je doporučeno užití nástroje `.DbMigrator`, protože zároveň vloží i počáteční data ke správnému běhu webové aplikace. - -### Spuštění aplikace - -#### Spuštění API Host (na straně serveru) - -Ujistěte se že je projekt `.HttpApi.Host` nastaven jako startovací a spusťte aplikaci což otevře Swagger UI: - -![bookstore-homepage](images/bookstore-swagger-ui-host.png) - -Tady můžete vidět API aplikace a zároveň je i otestovat. Získejte [více informací](https://swagger.io/tools/swagger-ui/) o Swagger UI. - -##### Autorizace pro Swagger UI - -Vetšina API aplikace vyžaduje autentizaci & autorizaci. Pokud chcete otestovat autorizované API, manuálně přejděte na stránku `/Account/Login`, vložte `admin` jako uživatelské jméno a `1q2w3E*` jako heslo k příhlášení do aplikace. Poté budete moci provádět autorizované požadavky API. - -#### Spuštění Angular aplikace (na straně klienta) - -Přejděte do složky `angular`, otevřete terminál příkazového řádku, proveďte příkaz `yarn` (doporučujeme používat správce balíků [yarn](https://yarnpkg.com), npm install bude v mnoha případech také fungovat): - -````bash -yarn -```` - -Jakmile jsou načteny všechny node moduly, proveďte příkaz `yarn start` nebo `npm start`: - -````bash -yarn start -```` - -Otevřete Váš oblíbený prohlížeč a přejděte na adresu `localhost:4200`. Počáteční uživatelské jméno je `admin` a heslo `1q2w3E*`. - -Startovací šablona obsahuje moduly **správa identit** a **správa tenantů**. Jakmile se přihlásíte, zprístupní se administrační menu kde můžete spravovat **tenanty**, **role**, **uživatele** a jejich **oprávnění**. - -> Doporučujeme [Visual Studio Code](https://code.visualstudio.com/) jako editor pro Angular projekt, ale klidně použijte Váš oblíbený editor. - -### Co dále? - -* [Tutoriál vývoje aplikace](Tutorials/Angular/Part-I.md) diff --git a/docs/cs/Getting-Started-AspNetCore-Application.md b/docs/cs/Getting-Started-AspNetCore-Application.md deleted file mode 100644 index e269a08ba2..0000000000 --- a/docs/cs/Getting-Started-AspNetCore-Application.md +++ /dev/null @@ -1,157 +0,0 @@ -# Začínáme s ASP.NET Core MVC aplikací - -Tento tutoriál vysvětluje jak začít s ABP z ničeho s minimem závislostí. Obvykle chcete začít se **[startovací šablonou](https://abp.io/Templates)**. - -## Tvorba nového projektu - -1. Vytvořte novou AspNet Core Web aplikaci ve Visual Studio 2019 (16.4.0+): - -![](images/create-new-aspnet-core-application-v2.png) - -2. Nakonfigurujte váš nový projekt: - -![](images/select-empty-web-application-v2.png) - -3. Potvrďte kliknutím na tlačítko vytvořit - -![create-aspnet-core-application](images/create-aspnet-core-application.png) - -## Instalace Volo.Abp.AspNetCore.Mvc balíku - -Volo.Abp.AspNetCore.Mvc je AspNet Core MVC integrační balík pro ABP. Takže ho nainstalujeme do projektu: - -```` -Install-Package Volo.Abp.AspNetCore.Mvc -```` - -## Tvorba prvního ABP modulu - -ABP je modulární framework a proto vyžaduje **spouštěcí (kořenový) modul** což je třída dědící z ``AbpModule``: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization( - ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - } - - app.UseStaticFiles(); - app.UseRouting(); - app.UseConfiguredEndpoints(); - } - } -} -```` - -``AppModule`` je dobrý název pro spouštěcí modul aplikace. - -ABP balíky definují modulové třídy a modul může mít závislost na jiném. V kódu výše, ``AppModule`` má závislost na ``AbpAspNetCoreMvcModule`` (definován v balíku [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)). Je běžné přidat ``DependsOn`` atribute po instalaci nového ABP NuGet balíku. - -Místo třídy Startup, konfigurujeme ASP.NET Core pipeline v této modulové třídě. - -## Třída Startup - -V dalším kroku upravíme Startup třídu k integraci ABP modulového systému: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicAspNetCoreApplication -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddApplication(); - } - - public void Configure(IApplicationBuilder app) - { - app.InitializeApplication(); - } - } -} -```` - -``services.AddApplication()`` přidává všechny služby definované ve všech modulech počínaje od ``AppModule``. - -``app.InitializeApplication()`` v metodě ``Configure`` inicializuje a spustí aplikaci. - -## Spusťte aplikaci! - -To je vše! Spusťte aplikaci, bude fungovat podle očekávání. - -## Použití Autofac jako frameworku pro vkládání závislostí - -Ačkoliv je AspNet Core systém pro vkládání závíslostí (DI) dostatečný pro základní požadavky, [Autofac](https://autofac.org/) poskytuje pokročilé funkce jako injekce vlastností nebo záchyt metod, které jsou v ABP užity k provádění pokročilých funkcí frameworku. - -Nahrazení AspNet Core DI systému za Autofac a integrace s ABP je snadná. - -1. Nainstalujeme [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) balík - -```` -Install-Package Volo.Abp.Autofac -```` - -2. Přidáme ``AbpAutofacModule`` závislost - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] // Přidá závislost na AbpAutofacModule -public class AppModule : AbpModule -{ - ... -} -```` - -3. Upravíme `Program.cs` aby používal Autofac: - -````csharp -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace BasicAspNetCoreApplication -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .UseAutofac(); // Přidejte tento řádek - } -} -```` - -## Zdrojový kód - -Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). - diff --git a/docs/cs/Getting-Started-AspNetCore-MVC-Template.md b/docs/cs/Getting-Started-AspNetCore-MVC-Template.md deleted file mode 100644 index c053e1d76a..0000000000 --- a/docs/cs/Getting-Started-AspNetCore-MVC-Template.md +++ /dev/null @@ -1,104 +0,0 @@ -## Začínáme s ASP.NET Core MVC šablonou - -Tento tutoriál vysvětluje, jak vytvořit novou ASP.NET Core MVC webovou aplikaci pomocí úvodní šablony, jak ji nakonfigurovat a spustit. - -### Tvorba nového projektu - -Tento tutoriál používá k tvorbě nového projektu **ABP CLI**. Podívejte se na stránku [Začínáme](https://abp.io/get-started) pro více možností. - -Pokud ještě nemáte ABP CLI nainstalováno, učiňte tak pomocí okna příkazového řádku: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -K tvorbě vašeho projektu použijte příkaz `abp new` v prázdné složce: - -````bash -abp new Acme.BookStore -```` - -> Můžete použít různé úrovně jmenných prostorů; např. BookStore, Acme.BookStore nebo Acme.Retail.BookStore. - -Příkaz `new` vytvoří **vrstvenou MVC aplikaci** s **Entity Framework Core** jako databázovým poskytovatelem. Jsou zde však i jiné možnosti. Podívejte se na [CLI dokumnentaci](CLI.md) pro všechny další možností. - -#### Požadavky - -Vytvořené řešení vyžaduje; - -* [Visual Studio 2019 (v16.4.0+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### Struktura řešení - -Otevřete řešení ve **Visual Studio**: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) - -Řešení má vrstvenou strukturu (založenou na [Domain Driven Design](Domain-Driven-Design.md)) a obsahuje projekty jednotkovových a integračních testů předkonfigurované pro práci s **EF Core** & **SQLite in-memory** databází. - -> Podívejte se na [dokument šablony aplikace](Startup-Templates/Application.md) k detailnímu pochopení struktury řešení. - -### Connection string databáze - -Zkontrolujte **connection string** v souboru `appsettings.json` v projektu `.Web`: - -````json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -```` - -Řešení je nakonfigurováno k používání **Entity Framework Core** s **MS SQL Server**. EF Core podporuje [různé](https://docs.microsoft.com/en-us/ef/core/providers/) databázové poskytovatele, takže můžete použít i jiné DBMS. V případě potřeby změňte connection string. - -### Tvorba databáze & aplikace databázových migrací - -K vytvoření databáze máte dvě možnosti. - -#### Použití DbMigrator aplikace - -Řešení obsahuje konzolovou aplikaci (v tomto příkladu nazvanou `Acme.BookStore.DbMigrator`), která může vytvářet databáze, aplikovat migrace a vkládat seed data. Je užitečná jak pro vývojové, tak pro produkční prostředí. - -> Projekt `.DbMigrator` má vlastní `appsettings.json`. Takže pokud jste změnili connection string uvedený výše, musíte změnit také tento. - -Klikněte pravým na projekt `.DbMigrator` a vyberte **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Zmáčkněte F5 (nebo Ctrl+F5) ke spuštění aplikace. Výstup bude vypadat následovně: - -![set-as-startup-project](images/db-migrator-app.png) - -#### Použití EF Core Update-Database příkazu - -Ef Core má `Update-Database` příkaz, který v případě potřeby vytvoří databázi a aplikuje čekající migrace. Klikněte pravým na projekt `.Web` a vyberte **Set as StartUp Project**: - -![set-as-startup-project](images/set-as-startup-project.png) - -Otevřete **Package Manager Console**, vyberte projekt `.EntityFrameworkCore.DbMigrations` jako **Default Project** and spusťte příkaz `Update-Database`: - -![pcm-update-database](images/pcm-update-database-v2.png) - -Dojde k vytvoření nové databáze na základě nakonfigurovaného connection stringu. - -> Použití nástroje `.Migrator` je doporučený způsob, jelikož zároveň vloží seed data nutné k správnému běhu webové aplikace. - -### Spuštění aplikace - -Ujistěte se že je projekt `.Web` nastaven jako startovací projekt. Spusťte aplikaci což následně otevře **úvodní** stránku ve vašem prohlížeči: - -![bookstore-homepage](images/bookstore-homepage.png) - -Klikněte na tlačítko **Přihlásit**, vložte `admin` jako uživatelské jméno a `1q2w3E*` jako heslo k přihlášení do aplikace. - -Startovací šabloná obsahuje **identity management** a **tenant management** moduly. Jakmile se přihlásite, budete mít přístup do nabídky Administrace, kde můžete spravovat **tenanty**, **role**, **uživatele** a jejich **oprávnění**. Správa uživatelů vypadá takto: - -![bookstore-user-management](images/bookstore-user-management-v2.png) - -### Co dále? - -* [Tutoriál vývoje aplikace](Tutorials/AspNetCore-Mvc/Part-I.md) diff --git a/docs/cs/Getting-Started-Console-Application.md b/docs/cs/Getting-Started-Console-Application.md deleted file mode 100644 index a6ee50dbe6..0000000000 --- a/docs/cs/Getting-Started-Console-Application.md +++ /dev/null @@ -1,181 +0,0 @@ -# Začínáme s konzolovou aplikací - -Tento tutoriál vysvětluje jak začít s ABP z ničeho s minimem závislostí. Obvykle chcete začít se **[startovací šablonou](https://abp.io/Templates)**. - -## Tvorba nového projektu - -Vytvořte regulérní .NET Core konzolovou aplikaci z Visual Studio: - -![](images/create-new-net-core-console-application.png) - -## Instalace Volo.Abp balíku - -Volo.Abp.Core je základní NuGet balík k tvorbě aplikací založených na ABP. Takže ho nainstalujeme do projektu: - -```` -Install-Package Volo.Abp.Core -```` - -## Tvorba prvního ABP modulu - -ABP je modulární framework a proto vyžaduje **spouštěcí (kořenový) modul** což je třída dědící z ``AbpModule``: - -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; - -namespace AbpConsoleDemo -{ - public class AppModule : AbpModule - { - - } -} -```` - -``AppModule`` je dobrý název pro spouštěcí modul aplikace. - -## Inicializace aplikace - -Dalším krokem je bootstrap aplikace pomocí spouštěcího modulu vytvořeného výše: - -````C# -using System; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create()) - { - application.Initialize(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} - -```` - -``AbpApplicationFactory`` se používá k vytvoření aplikace a načtení všech modulů, s využitím ``AppModule`` jako spouštěcím modulem. ``Initialize()`` metoda spouští aplikaci. - -## Ahoj světe! - -Aplikace výše zatím nic nedělá. Pojďme proto vytvořit službu která už něco dělá: - -````C# -using System; -using Volo.Abp.DependencyInjection; - -namespace AbpConsoleDemo -{ - public class HelloWorldService : ITransientDependency - { - public void SayHello() - { - Console.WriteLine("Hello World!"); - } - } -} - -```` - -``ITransientDependency`` je speciální rozhraní ABP, které automaticky registruje službu jako přechodnou (více v [dokumentu vkládání závislostí](Dependency-Injection.md)). - -Nyní můžeme vyřešit ``HelloWorldService`` a vypsat naše ahoj. Změníme Program.cs podle vyobrazení níže: - -````C# -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create()) - { - application.Initialize(); - - // Vyřeší službu a použije ji - var helloWorldService = - application.ServiceProvider.GetService(); - helloWorldService.SayHello(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} -```` - -I když je to dostačující pro tento jednoduchý príklad kódu, je vždy lepší v případě přímého řešení závislostí z ``IServiceProvider`` vytvořit rámce (více v [dokumentu vkládání závislostí](Dependency-Injection.md)). - -## Využití Autofac jako frameworku pro vkládání závislostí - -Ačkoliv je AspNet Core systém pro vkládání závíslostí (DI) skvělý pro základní požadavky, Autofac poskytuje pokročilé funkce jako injekce vlastností nebo záchyt metod, které jsou v ABP užity k provádění pokročilých funkcí frameworku. - -Nahrazení AspNet Core DI systému za Autofac a integrace s ABP je snadná. - -1. Nainstalujeme [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) balík - -``` -Install-Package Volo.Abp.Autofac -``` - -1. Přidáme ``AbpAutofacModule`` závislost - -```c# -[DependsOn(typeof(AbpAutofacModule))] // Přidá závislost na AbpAutofacModule -public class AppModule : AbpModule -{ - -} -``` - -1. Změníme soubor ``Program.cs`` podle vyobrazení níže: - -```c# -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); // Autofac integrace - })) - { - application.Initialize(); - - // Vyřeší službu a použije ji - var helloWorldService = - application.ServiceProvider.GetService(); - helloWorldService.SayHello(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} -``` - -Stačí volat metodu `options.UseAutofac()` v možnostech `AbpApplicationFactory.Create`. - -## Zdrojový kód - -Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication). diff --git a/docs/cs/Index.md b/docs/cs/Index.md deleted file mode 100644 index 875f4e066e..0000000000 --- a/docs/cs/Index.md +++ /dev/null @@ -1,25 +0,0 @@ -# ABP dokumentace - -ABP je **open source aplikační framework** se zaměřením na vývoj webových aplikací založených na ASP.NET Core, zároveň ho však lze využít i k vývoji jiných typů aplikací. - -K procházení dokumentace využijte navigační nabídky vlevo. - -## Začínáme - -Nejsnazší cestou jak začít nový projekt s ABP je užití startovací šablony: - -* [ASP.NET Core MVC (Razor Pages) UI Počáteční Šablona](Getting-Started-AspNetCore-MVC-Template.md) -* [Angular UI Počáteční Šablona](Getting-Started-Angular-Template.md) - -Pokud chcete začít od nuly (s prázdným projektem) tak manuálně nainstalujte ABP Framework s pomocí následujících tutoriálů: - -* [Konzolová Aplikace](Getting-Started-Console-Application.md) -* [ASP.NET Core Web Aplikace](Getting-Started-AspNetCore-Application.md) - -## Zdrojový kód - -ABP je hostovaný na GitHub. Zobrazit [zdrojový kód](https://github.com/abpframework/abp). - -## Chcete přispět? - -ABP je komunitně řízený open source projekt. Podívejte se na [průvodce pro přispěvatele](Contribution/Index.md) pokud chcete být součástí tohoto projektu. diff --git a/docs/cs/Nightly-Builds.md b/docs/cs/Nightly-Builds.md deleted file mode 100644 index e69588f5ff..0000000000 --- a/docs/cs/Nightly-Builds.md +++ /dev/null @@ -1,26 +0,0 @@ -# Noční sestavení - -Všechny balíky frameworku a modulů jsou každý večer nasazeny na MyGet. Takže můžete používat nebo testovat nejnovější kód bez čekání na další vydání. - -## Konfigurace Visual Studia - -> Vyžaduje Visual Studio 2017+ - -1. Přejděte do `Tools > Options > NuGet Package Manager > Package Source`. -2. Klikněte na zelenou ikonku `+`. -3. Nastavte `ABP Nightly` jako *Name* a `https://www.myget.org/F/abp-nightly/api/v3/index.json` jako *Source* podle vyobrazení níže: - ![night-build-add-nuget-source](images/night-build-add-nuget-source.png) -4. Klikněte na `Update`. -5. Klikněte na `OK` k uložení změn. - -## Instalace balíku - -Nyní můžete instalovat náhledové / noční balíky do Vašeho projektu z NuGet prohlížeče nebo Package Manager Console. - -![night-build-add-nuget-package](images/night-build-add-nuget-package.png) - -1. V nuget prohlížeči, vyberte "Include prereleases". -2. Změňte zdroj balíků na "All". -3. Vyhledejte balík. Uvidíte prerelease balík formátovaný jako `(VERZE)-preview(DATUM)` (např *v0.16.0-preview20190401* jako v tomto vzorku). -4. Můžete kliknout na `Install` k přídání balíku do projektu. - diff --git a/docs/cs/docs-nav.json b/docs/cs/docs-nav.json deleted file mode 100644 index 6c05d77dcf..0000000000 --- a/docs/cs/docs-nav.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "items": [ - { - "text": "Začínáme", - "items": [ - { - "text": "Ze startovacích šablon", - "items": [ - { - "text": "Aplikace s MVC (Razor Pages) UI", - "path": "Getting-Started-AspNetCore-MVC-Template.md" - }, - { - "text": "Aplikace s Angular UI", - "path": "Getting-Started-Angular-Template.md" - } - ] - }, - { - "text": "Z prázdných projektů", - "items": [ - { - "text": "S ASP.NET Core Web aplikací", - "path": "Getting-Started-AspNetCore-Application.md" - }, - { - "text": "S konzolovou aplikací", - "path": "Getting-Started-Console-Application.md" - } - ] - } - ] - }, - { - "text": "Tutoriály", - "items": [ - { - "text": "Vývoj aplikace", - "items": [ - { - "text": "S ASP.NET Core MVC UI", - "path": "Tutorials/AspNetCore-Mvc/Part-I.md" - }, - { - "text": "S Angular UI", - "path": "Tutorials/Angular/Part-I.md" - } - ] - } - ] - }, - { - "text": "CLI", - "path": "CLI.md" - }, - { - "text": "Základy", - "items": [ - { - "text": "Konfigurace", - "path": "Configuration.md" - }, - { - "text": "Možnosti", - "path": "Options.md" - }, - { - "text": "Vkládání závislostí", - "path": "Dependency-Injection.md", - "items": [ - { - "text": "AutoFac integrace", - "path": "Autofac-Integration.md" - } - ] - }, - { - "text": "Virtuální systém souborů", - "path": "Virtual-File-System.md" - }, - { - "text": "Lokalizace", - "path": "Localization.md" - }, - { - "text": "Zpracování výjimek", - "path": "Exception-Handling.md" - }, - { - "text": "Validace", - "path": "Validation.md", - "items": [ - { - "text": "FluentValidation integrace", - "path": "FluentValidation.md" - } - ] - }, - { - "text": "Autorizace", - "path": "Authorization.md" - }, - { - "text": "Ukládání do mezipaměti", - "path": "Caching.md" - }, - { - "text": "Audit" - }, - { - "text": "Nastavení", - "path": "Settings.md" - } - ] - }, - { - "text": "Události", - "items": [ - { - "text": "Event bus (místní)" - }, - { - "text": "Distribuovaný event bus", - "items": [ - { - "text": "RabbitMQ integrace" - } - ] - } - ] - }, - { - "text": "Služby", - "items": [ - { - "text": "Současný uživatel", - "path": "CurrentUser.md" - }, - { - "text": "Mapování objekt na objekt", - "path": "Object-To-Object-Mapping.md" - }, - { - "text": "Serializace objektu" - }, - { - "text": "Serializace JSON" - }, - { - "text": "Emailování" - }, - { - "text": "GUIDy" - }, - { - "text": "Vláknování" - }, - { - "text": "Časování" - } - ] - }, - { - "text": "Multitenance", - "path": "Multi-Tenancy.md" - }, - { - "text": "Vývoj modulů", - "items": [ - { - "text": "Základy", - "path": "Module-Development-Basics.md" - }, - { - "text": "Zásuvné moduly" - }, - { - "text": "Nejlepší praktiky", - "path": "Best-Practices/Index.md" - } - ] - }, - { - "text": "Domain driven design", - "path": "Domain-Driven-Design.md", - "items": [ - { - "text": "Doménová vrstva", - "items": [ - { - "text": "Entity & agregované kořeny", - "path": "Entities.md" - }, - { - "text": "Hodnotové objekty" - }, - { - "text": "Repozitáře", - "path": "Repositories.md" - }, - { - "text": "Doménové služby" - }, - { - "text": "Specifikace" - } - ] - }, - { - "text": "Aplikační vrstva", - "items": [ - { - "text": "Aplikační služby", - "path": "Application-Services.md" - }, - { - "text": "Objekty přenosu dat" - }, - { - "text": "Jednotka práce" - } - ] - } - ] - }, - { - "text": "ASP.NET Core", - "items": [ - { - "text": "API", - "items": [ - { - "text": "Automatické API řadiče", - "path": "AspNetCore/Auto-API-Controllers.md" - }, - { - "text": "Dynamičtí C# API klienti", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" - } - ] - }, - { - "text": "Uživatelské rozhraní", - "items": [ - { - "text": "Správa klientských balíčků", - "path": "AspNetCore/Client-Side-Package-Management.md" - }, - { - "text": "Svazování & minifikace", - "path": "AspNetCore/Bundling-Minification.md" - }, - { - "text": "Tag pomocníci", - "path": "AspNetCore/Tag-Helpers/Index.md" - }, - { - "text": "Widgety", - "path": "AspNetCore/Widgets.md" - }, - { - "text": "Motivy", - "path": "AspNetCore/Theming.md" - } - ] - } - ] - }, - { - "text": "Přístup k datům", - "path": "Data-Access.md", - "items": [ - { - "text": "Connection stringy", - "path": "Connection-Strings.md" - }, - { - "text": "Poskytovatelé databází", - "items": [ - { - "text": "Entity Framework Core", - "path": "Entity-Framework-Core.md", - "items": [ - { - "text": "Přepnutí na MySQL", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "Přepnutí na PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": "Přepnutí na SQLite", - "path": "Entity-Framework-Core-SQLite.md" - }, - { - "text": "Přepnutí na jiný DBMS", - "path": "Entity-Framework-Core-Other-DBMS.md" - } - ] - }, - { - "text": "MongoDB", - "path": "MongoDB.md" - }, - { - "text": "Dapper", - "path": "Dapper.md" - } - ] - } - ] - }, - { - "text": "Pozadí", - "items": [ - { - "text": "Úkony na pozadí", - "path": "Background-Jobs.md", - "items": [ - { - "text": "Hangfire integrace", - "path": "Background-Jobs-Hangfire.md" - }, - { - "text": "RabbitMQ integrace", - "path": "Background-Jobs-RabbitMq.md" - } - ] - } - ] - }, - { - "text": "Startovací šablony", - "path": "Startup-Templates/Index.md", - "items": [ - { - "text": "Aplikace", - "path": "Startup-Templates/Application.md" - }, - { - "text": "Modul", - "path": "Startup-Templates/Module.md" - } - ] - }, - { - "text": "Vzorky", - "items": [ - { - "text": "Mikroslužby demo", - "path": "Samples/Microservice-Demo.md" - } - ] - }, - { - "text": "Moduly aplikace", - "path": "Modules/Index.md" - }, - { - "text": "Architektura mikroslužby", - "path": "Microservice-Architecture.md" - }, - { - "text": "Testování" - }, - { - "text": "Noční sestavení", - "path": "Nightly-Builds.md" - }, - { - "text": "Průvodce pro přispěvatele", - "path": "Contribution/Index.md" - } - ] -} \ No newline at end of file diff --git a/docs/cs/images/MonthlyProfitWidgetFiles.png b/docs/cs/images/MonthlyProfitWidgetFiles.png deleted file mode 100644 index c3e4d6f1ab..0000000000 Binary files a/docs/cs/images/MonthlyProfitWidgetFiles.png and /dev/null differ diff --git a/docs/cs/images/authorization-new-permission-ui-hierarcy.png b/docs/cs/images/authorization-new-permission-ui-hierarcy.png deleted file mode 100644 index 07abfc7132..0000000000 Binary files a/docs/cs/images/authorization-new-permission-ui-hierarcy.png and /dev/null differ diff --git a/docs/cs/images/authorization-new-permission-ui-localized.png b/docs/cs/images/authorization-new-permission-ui-localized.png deleted file mode 100644 index 948fd618d9..0000000000 Binary files a/docs/cs/images/authorization-new-permission-ui-localized.png and /dev/null differ diff --git a/docs/cs/images/authorization-new-permission-ui.png b/docs/cs/images/authorization-new-permission-ui.png deleted file mode 100644 index 1190f04b70..0000000000 Binary files a/docs/cs/images/authorization-new-permission-ui.png and /dev/null differ diff --git a/docs/cs/images/bookstore-apis.png b/docs/cs/images/bookstore-apis.png deleted file mode 100644 index b7928c9637..0000000000 Binary files a/docs/cs/images/bookstore-apis.png and /dev/null differ diff --git a/docs/cs/images/bookstore-create-template.png b/docs/cs/images/bookstore-create-template.png deleted file mode 100644 index bae34a3b64..0000000000 Binary files a/docs/cs/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/cs/images/bookstore-swagger-ui-host.png b/docs/cs/images/bookstore-swagger-ui-host.png deleted file mode 100644 index 7ebd8d8e37..0000000000 Binary files a/docs/cs/images/bookstore-swagger-ui-host.png and /dev/null differ diff --git a/docs/cs/images/bookstore-user-management-v2.png b/docs/cs/images/bookstore-user-management-v2.png deleted file mode 100644 index cd66010e05..0000000000 Binary files a/docs/cs/images/bookstore-user-management-v2.png and /dev/null differ diff --git a/docs/cs/images/bookstore-visual-studio-solution-for-spa.png b/docs/cs/images/bookstore-visual-studio-solution-for-spa.png deleted file mode 100644 index d114ed188c..0000000000 Binary files a/docs/cs/images/bookstore-visual-studio-solution-for-spa.png and /dev/null differ diff --git a/docs/cs/images/bookstore-visual-studio-solution-tiered.png b/docs/cs/images/bookstore-visual-studio-solution-tiered.png deleted file mode 100644 index 9affe841aa..0000000000 Binary files a/docs/cs/images/bookstore-visual-studio-solution-tiered.png and /dev/null differ diff --git a/docs/cs/images/build-action-embedded-resource-sample.png b/docs/cs/images/build-action-embedded-resource-sample.png deleted file mode 100644 index 700e9921f4..0000000000 Binary files a/docs/cs/images/build-action-embedded-resource-sample.png and /dev/null differ diff --git a/docs/cs/images/create-aspnet-core-application.png b/docs/cs/images/create-aspnet-core-application.png deleted file mode 100644 index 03fde7e38a..0000000000 Binary files a/docs/cs/images/create-aspnet-core-application.png and /dev/null differ diff --git a/docs/cs/images/create-new-aspnet-core-application-v2.png b/docs/cs/images/create-new-aspnet-core-application-v2.png deleted file mode 100644 index d2bce84775..0000000000 Binary files a/docs/cs/images/create-new-aspnet-core-application-v2.png and /dev/null differ diff --git a/docs/cs/images/create-new-aspnet-core-application.png b/docs/cs/images/create-new-aspnet-core-application.png deleted file mode 100644 index 2c38289810..0000000000 Binary files a/docs/cs/images/create-new-aspnet-core-application.png and /dev/null differ diff --git a/docs/cs/images/create-new-net-core-console-application.png b/docs/cs/images/create-new-net-core-console-application.png deleted file mode 100644 index 0c2b3dbcb8..0000000000 Binary files a/docs/cs/images/create-new-net-core-console-application.png and /dev/null differ diff --git a/docs/cs/images/dashboard1.png b/docs/cs/images/dashboard1.png deleted file mode 100644 index 8c542b8786..0000000000 Binary files a/docs/cs/images/dashboard1.png and /dev/null differ diff --git a/docs/cs/images/db-migrator-app.png b/docs/cs/images/db-migrator-app.png deleted file mode 100644 index d2248d4588..0000000000 Binary files a/docs/cs/images/db-migrator-app.png and /dev/null differ diff --git a/docs/cs/images/docs-create-project.jpg b/docs/cs/images/docs-create-project.jpg deleted file mode 100644 index d2baa3242a..0000000000 Binary files a/docs/cs/images/docs-create-project.jpg and /dev/null differ diff --git a/docs/cs/images/docs-module_download-new-abp-project.png b/docs/cs/images/docs-module_download-new-abp-project.png deleted file mode 100644 index bc7aaacbd6..0000000000 Binary files a/docs/cs/images/docs-module_download-new-abp-project.png and /dev/null differ diff --git a/docs/cs/images/docs-module_download-sample-navigation-menu.png b/docs/cs/images/docs-module_download-sample-navigation-menu.png deleted file mode 100644 index 8d8eb42d52..0000000000 Binary files a/docs/cs/images/docs-module_download-sample-navigation-menu.png and /dev/null differ diff --git a/docs/cs/images/docs-module_solution-explorer.png b/docs/cs/images/docs-module_solution-explorer.png deleted file mode 100644 index 97bd3fc18e..0000000000 Binary files a/docs/cs/images/docs-module_solution-explorer.png and /dev/null differ diff --git a/docs/cs/images/docs-section-ui.png b/docs/cs/images/docs-section-ui.png deleted file mode 100644 index 1c63d1ad3a..0000000000 Binary files a/docs/cs/images/docs-section-ui.png and /dev/null differ diff --git a/docs/cs/images/github-access-token-private-repo.jpg b/docs/cs/images/github-access-token-private-repo.jpg deleted file mode 100644 index cb74f1eea3..0000000000 Binary files a/docs/cs/images/github-access-token-private-repo.jpg and /dev/null differ diff --git a/docs/cs/images/github-access-token-public-repo.jpg b/docs/cs/images/github-access-token-public-repo.jpg deleted file mode 100644 index d091a6d511..0000000000 Binary files a/docs/cs/images/github-access-token-public-repo.jpg and /dev/null differ diff --git a/docs/cs/images/github-myusername.jpg b/docs/cs/images/github-myusername.jpg deleted file mode 100644 index a723c17713..0000000000 Binary files a/docs/cs/images/github-myusername.jpg and /dev/null differ diff --git a/docs/cs/images/issuemanagement-module-solution.png b/docs/cs/images/issuemanagement-module-solution.png deleted file mode 100644 index d5f64b01d2..0000000000 Binary files a/docs/cs/images/issuemanagement-module-solution.png and /dev/null differ diff --git a/docs/cs/images/layered-project-dependencies-module.png b/docs/cs/images/layered-project-dependencies-module.png deleted file mode 100644 index de3b7a412f..0000000000 Binary files a/docs/cs/images/layered-project-dependencies-module.png and /dev/null differ diff --git a/docs/cs/images/layered-project-dependencies.png b/docs/cs/images/layered-project-dependencies.png deleted file mode 100644 index ed3e03fe4d..0000000000 Binary files a/docs/cs/images/layered-project-dependencies.png and /dev/null differ diff --git a/docs/cs/images/localization-resource-json-files.png b/docs/cs/images/localization-resource-json-files.png deleted file mode 100644 index 1a1d43403c..0000000000 Binary files a/docs/cs/images/localization-resource-json-files.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-authserver-home.png b/docs/cs/images/microservice-sample-authserver-home.png deleted file mode 100644 index 684fdb1a5a..0000000000 Binary files a/docs/cs/images/microservice-sample-authserver-home.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-authserver-login.png b/docs/cs/images/microservice-sample-authserver-login.png deleted file mode 100644 index 99d898ccd2..0000000000 Binary files a/docs/cs/images/microservice-sample-authserver-login.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-backend-ui-permissions.png b/docs/cs/images/microservice-sample-backend-ui-permissions.png deleted file mode 100644 index 3e1610294f..0000000000 Binary files a/docs/cs/images/microservice-sample-backend-ui-permissions.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-backend-ui.png b/docs/cs/images/microservice-sample-backend-ui.png deleted file mode 100644 index e649c8da59..0000000000 Binary files a/docs/cs/images/microservice-sample-backend-ui.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-blogservice-permission-in-database.png b/docs/cs/images/microservice-sample-blogservice-permission-in-database.png deleted file mode 100644 index 45d7f2b115..0000000000 Binary files a/docs/cs/images/microservice-sample-blogservice-permission-in-database.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-diagram-2.png b/docs/cs/images/microservice-sample-diagram-2.png deleted file mode 100644 index 414a942aca..0000000000 Binary files a/docs/cs/images/microservice-sample-diagram-2.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-diagram.png b/docs/cs/images/microservice-sample-diagram.png deleted file mode 100644 index 47e6443852..0000000000 Binary files a/docs/cs/images/microservice-sample-diagram.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-kibana-1.png b/docs/cs/images/microservice-sample-kibana-1.png deleted file mode 100644 index 51777f6bd7..0000000000 Binary files a/docs/cs/images/microservice-sample-kibana-1.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-kibana-2.png b/docs/cs/images/microservice-sample-kibana-2.png deleted file mode 100644 index cb1d0b748c..0000000000 Binary files a/docs/cs/images/microservice-sample-kibana-2.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-product-module-in-solution.png b/docs/cs/images/microservice-sample-product-module-in-solution.png deleted file mode 100644 index 2c07ad4b99..0000000000 Binary files a/docs/cs/images/microservice-sample-product-module-in-solution.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-public-product-list.png b/docs/cs/images/microservice-sample-public-product-list.png deleted file mode 100644 index 932b0b531c..0000000000 Binary files a/docs/cs/images/microservice-sample-public-product-list.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-solution.png b/docs/cs/images/microservice-sample-solution.png deleted file mode 100644 index d1497d9be2..0000000000 Binary files a/docs/cs/images/microservice-sample-solution.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-update-database-authserver.png b/docs/cs/images/microservice-sample-update-database-authserver.png deleted file mode 100644 index 094fd20fa6..0000000000 Binary files a/docs/cs/images/microservice-sample-update-database-authserver.png and /dev/null differ diff --git a/docs/cs/images/microservice-sample-update-database-products.png b/docs/cs/images/microservice-sample-update-database-products.png deleted file mode 100644 index 32a0927c1f..0000000000 Binary files a/docs/cs/images/microservice-sample-update-database-products.png and /dev/null differ diff --git a/docs/cs/images/module-layers-and-packages.jpg b/docs/cs/images/module-layers-and-packages.jpg deleted file mode 100644 index f71a91eb8d..0000000000 Binary files a/docs/cs/images/module-layers-and-packages.jpg and /dev/null differ diff --git a/docs/cs/images/night-build-add-nuget-package.png b/docs/cs/images/night-build-add-nuget-package.png deleted file mode 100644 index f475d3aaa0..0000000000 Binary files a/docs/cs/images/night-build-add-nuget-package.png and /dev/null differ diff --git a/docs/cs/images/night-build-add-nuget-source.png b/docs/cs/images/night-build-add-nuget-source.png deleted file mode 100644 index df3176aa12..0000000000 Binary files a/docs/cs/images/night-build-add-nuget-source.png and /dev/null differ diff --git a/docs/cs/images/pcm-update-database-v2.png b/docs/cs/images/pcm-update-database-v2.png deleted file mode 100644 index 72d02e9186..0000000000 Binary files a/docs/cs/images/pcm-update-database-v2.png and /dev/null differ diff --git a/docs/cs/images/pcm-update-database.png b/docs/cs/images/pcm-update-database.png deleted file mode 100644 index a9379d2571..0000000000 Binary files a/docs/cs/images/pcm-update-database.png and /dev/null differ diff --git a/docs/cs/images/select-empty-web-application-v2.png b/docs/cs/images/select-empty-web-application-v2.png deleted file mode 100644 index 9bfd2ec6a8..0000000000 Binary files a/docs/cs/images/select-empty-web-application-v2.png and /dev/null differ diff --git a/docs/cs/images/select-empty-web-application.png b/docs/cs/images/select-empty-web-application.png deleted file mode 100644 index f4b884140d..0000000000 Binary files a/docs/cs/images/select-empty-web-application.png and /dev/null differ diff --git a/docs/cs/images/set-as-startup-project.png b/docs/cs/images/set-as-startup-project.png deleted file mode 100644 index 8a5445bf38..0000000000 Binary files a/docs/cs/images/set-as-startup-project.png and /dev/null differ diff --git a/docs/cs/images/tiered-solution-applications.png b/docs/cs/images/tiered-solution-applications.png deleted file mode 100644 index df8d2b5f4a..0000000000 Binary files a/docs/cs/images/tiered-solution-applications.png and /dev/null differ diff --git a/docs/cs/images/tiered-solution-servers.png b/docs/cs/images/tiered-solution-servers.png deleted file mode 100644 index 68e72990d7..0000000000 Binary files a/docs/cs/images/tiered-solution-servers.png and /dev/null differ diff --git a/docs/cs/images/volodocs-iis-add-website.png b/docs/cs/images/volodocs-iis-add-website.png deleted file mode 100644 index aa7da8095b..0000000000 Binary files a/docs/cs/images/volodocs-iis-add-website.png and /dev/null differ diff --git a/docs/cs/images/volodocs-iis-application-pool.png b/docs/cs/images/volodocs-iis-application-pool.png deleted file mode 100644 index 28ccfa5c42..0000000000 Binary files a/docs/cs/images/volodocs-iis-application-pool.png and /dev/null differ diff --git a/docs/cs/images/widget-basic-files.png b/docs/cs/images/widget-basic-files.png deleted file mode 100644 index c692abd9e0..0000000000 Binary files a/docs/cs/images/widget-basic-files.png and /dev/null differ diff --git a/docs/docs-langs.json b/docs/docs-langs.json index 3986dc8618..b33b751207 100644 --- a/docs/docs-langs.json +++ b/docs/docs-langs.json @@ -4,16 +4,6 @@ "DisplayName" : "English", "Code" : "en", "IsDefault": true - }, - { - "DisplayName" : "Português", - "Code" : "pt-BR", - "IsDefault": false - }, - { - "DisplayName" : "简体中文", - "Code" : "zh-Hans", - "IsDefault": false } ] } diff --git a/docs/en/API/API-Versioning.md b/docs/en/API/API-Versioning.md deleted file mode 100644 index 741eafbb75..0000000000 --- a/docs/en/API/API-Versioning.md +++ /dev/null @@ -1,344 +0,0 @@ -# API Versioning System - -ABP Framework integrates the [ASPNET-API-Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki) feature and adapts to C# and JavaScript Static Client Proxies and [Auto API Controller](../API/Auto-API-Controllers.md). - - -## Enable API Versioning - -```cs -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} -``` - -## C# and JavaScript Static Client Proxies - -This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), we suggest to use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String). - -### Example - -**Application Services:** -```cs -public interface IBookAppService : IApplicationService -{ - Task GetAsync(); -} - -public interface IBookV2AppService : IApplicationService -{ - Task GetAsync(); - - Task GetAsync(string isbn); -} -``` - -**HttpApi Controllers:** -```cs -[Area(BookStoreRemoteServiceConsts.ModuleName)] -[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] -[ApiVersion("1.0", Deprecated = true)] -[ApiController] -[ControllerName("Book")] -[Route("api/BookStore/Book")] -public class BookController : BookStoreController, IBookAppService -{ - private readonly IBookAppService _bookAppService; - - public BookController(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - [HttpGet] - public async Task GetAsync() - { - return await _bookAppService.GetAsync(); - } -} - -[Area(BookStoreRemoteServiceConsts.ModuleName)] -[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] -[ApiVersion("2.0")] -[ApiController] -[ControllerName("Book")] -[Route("api/BookStore/Book")] -public class BookV2Controller : BookStoreController, IBookV2AppService -{ - private readonly IBookV2AppService _bookAppService; - - public BookV2Controller(IBookV2AppService bookAppService) - { - _bookAppService = bookAppService; - } - - [HttpGet] - public async Task GetAsync() - { - return await _bookAppService.GetAsync(); - } - - [HttpGet] - [Route("{isbn}")] - public async Task GetAsync(string isbn) - { - return await _bookAppService.GetAsync(isbn); - } -} -``` - -**Generated CS and JS proxies:** - -```cs -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IBookAppService), typeof(BookClientProxy))] -public partial class BookClientProxy : ClientProxyBase, IBookAppService -{ - public virtual async Task GetAsync() - { - return await RequestAsync(nameof(GetAsync)); - } -} - -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IBookV2AppService), typeof(BookV2ClientProxy))] -public partial class BookV2ClientProxy : ClientProxyBase, IBookV2AppService -{ - public virtual async Task GetAsync() - { - return await RequestAsync(nameof(GetAsync)); - } - - public virtual async Task GetAsync(string isbn) - { - return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue - { - { typeof(string), isbn } - }); - } -} -``` - - -```js -// controller bookStore.books.book - -(function(){ - -abp.utils.createNamespace(window, 'bookStore.books.book'); - -bookStore.books.book.get = function(api_version, ajaxParams) { - var api_version = api_version ? api_version : '1.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -})(); - -// controller bookStore.books.bookV2 - -(function(){ - -abp.utils.createNamespace(window, 'bookStore.books.bookV2'); - -bookStore.books.bookV2.get = function(api_version, ajaxParams) { - var api_version = api_version ? api_version : '2.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams) { - var api_version = api_version ? api_version : '2.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book/' + isbn + '' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -})(); -``` - - -## Changing version manually - -If an application service class supports multiple versions. You can inject `ICurrentApiVersionInfo` to switch versions in C#. - -```cs -var currentApiVersionInfo = _abpApplication.ServiceProvider.GetRequiredService(); -var bookV4AppService = _abpApplication.ServiceProvider.GetRequiredService(); -using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.0"))) -{ - book = await bookV4AppService.GetAsync(); - logger.LogWarning(book.Title); - logger.LogWarning(book.ISBN); -} - -using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.1"))) -{ - book = await bookV4AppService.GetAsync(); - logger.LogWarning(book.Title); - logger.LogWarning(book.ISBN); -} -``` - -We have made a default version in the JS proxy. Of course, you can also manually change the version. - -```js - -bookStore.books.bookV4.get("4.0") // Manually change the version. -//Title: Mastering ABP Framework V4.0 - -bookStore.books.bookV4.get() // The latest supported version is used by default. -//Title: Mastering ABP Framework V4.1 -``` - -## Auto API Controller - -```cs -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - //2.0 Version - options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => - { - opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v2.TodoAppService).Namespace; - opts.ApiVersions.Add(new ApiVersion(2, 0)); - }); - - //1.0 Compatibility version - options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => - { - opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v1.TodoAppService).Namespace; - opts.ApiVersions.Add(new ApiVersion(1, 0)); - }); - }); -} - -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var preActions = context.Services.GetPreConfigureActions(); - Configure(options => - { - preActions.Configure(options); - }); - - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - - options.ConfigureAbp(preActions.Configure()); - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} -``` - -## Swagger/VersionedApiExplorer - -```cs - -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - }).AddApiExplorer(options => { - // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service - // note: the specified format code will format the version as "'v'major[.minor][-status]" - options.GroupNameFormat = "'v'VVV"; - - // note: this option is only necessary when versioning by url segment. the SubstitutionFormat - // can also be used to control the format of the API version in route templates - options.SubstituteApiVersionInUrl = true; - }); - - context.Services.AddTransient, ConfigureSwaggerOptions>(); - - context.Services.AddAbpSwaggerGen( - options => - { - // add a custom operation filter which sets default values - options.OperationFilter(); - - options.CustomSchemaIds(type => type.FullName); - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} - -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseErrorPage(); - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseAbpRequestLocalization(); - - app.UseSwagger(); - app.UseAbpSwaggerUI( - options => - { - var provider = app.ApplicationServices.GetRequiredService(); - // build a swagger endpoint for each discovered API version - foreach (var description in provider.ApiVersionDescriptions) - { - options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); - } - }); - - app.UseConfiguredEndpoints(); -} -``` - -## Custom multi-version API controller - -ABP Framework will not affect to your APIs, you can freely implement your APIs according to the Microsoft's documentation. - -Further information, see https://github.com/dotnet/aspnet-api-versioning/wiki - - -## Sample source code - -Follow the link below to get the sample's complete source-code - -https://github.com/abpframework/abp-samples/tree/master/Api-Versioning diff --git a/docs/en/API/Application-Configuration.md b/docs/en/API/Application-Configuration.md deleted file mode 100644 index 89463f3077..0000000000 --- a/docs/en/API/Application-Configuration.md +++ /dev/null @@ -1,70 +0,0 @@ -# Application Configuration Endpoint - -ABP Framework provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, is the list of some fundamental information at this endpoint: - -* Granted [policies](../Authorization.md) (permissions) for the current user. -* [Setting](../Settings.md) values for the current user. -* Info about the [current user](../CurrentUser.md) (like id and user name). -* Info about the current [tenant](../Multi-Tenancy.md) (like id and name). -* [Time zone](../Timing.md) information for the current user and the [clock](../Timing.md) type of the application. - -> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint. - -## HTTP API - -If you navigate to the `/api/abp/application-configuration` URL of an ABP Framework based web application or HTTP Service, you can access the configuration as a JSON object. This endpoint is useful to create the client of your application. - -## Script - -For ASP.NET Core MVC (Razor Pages) applications, the same configuration values are also available on the JavaScript side. `/Abp/ApplicationConfigurationScript` is the URL of the script that is auto-generated based on the HTTP API above. - -See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI. - -Other UI types provide services native to the related platform. For example, see the [Angular UI settings documentation](../UI/Angular/Settings.md) to learn how to use the setting values exposes by this endpoint. - -## Extending the Endpoint - -The **application-configuration** endpoint contains some useful information about the application, such as _localization values_, _current user information_, _granted permissions_, etc. Even most of the time these provided values are sufficient to use in your application to perform common requirements such as getting the logged-in user's ID or its granted permissions. You may still want to extend this endpoint and provide additional information for your application/service. At that point, you can use the `IApplicationConfigurationContributor` endpoint. - -### IApplicationConfigurationContributor - -`IApplicationConfigurationContributor` is the interface that should be implemented to add additional information to the **application-configuration** endpoint. - -**Example: Setting the deployment version** - -```csharp -using System.Threading.Tasks; -using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; -using Volo.Abp.Data; - -namespace Acme.BookStore.Web -{ - public class MyApplicationConfigurationContributor : IApplicationConfigurationContributor - { - public Task ContributeAsync(ApplicationConfigurationContributorContext context) - { - //for simplicity, it's a static number, you can inject any service to this class and perform your logic... - var deploymentVersion = "v1.0.0"; - - //setting the deploymentVersion - context.ApplicationConfiguration.SetProperty("deploymentVersion", deploymentVersion); - - return Task.CompletedTask; - } - } -} -``` - -Add your contributor instance to the `AbpApplicationConfigurationOptions` - -```csharp -Configure(options => -{ - options.Contributors.AddIfNotContains(new MyApplicationConfigurationContributor()); -}); -``` - -* `IApplicationConfigurationContributor` defines the `ContributeAsync` method to extend the **application-configuration** endpoint with the specified additional data. -* You can inject services and perform any logic needed to extend the endpoint as you wish. - -> Application configuration contributors are executed as a part of the application configuration initialization process. diff --git a/docs/en/API/Application-Localization.md b/docs/en/API/Application-Localization.md deleted file mode 100644 index adf26c8fa8..0000000000 --- a/docs/en/API/Application-Localization.md +++ /dev/null @@ -1,33 +0,0 @@ -# Application Localization Endpoint - -ABP Framework provides a pre-built and standard endpoint that returns all the [localization](../Localization.md) resources and texts defined in the server. - -> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint. - -## HTTP API - -`/api/abp/application-localization` is the main URL of the HTTP API that returns the localization data as a JSON string. I accepts the following query string parameters: - -* `cultureName` (required): A culture code to get the localization data, like `en` or `en-US`. -* `onlyDynamics` (optional, default: `false`): Can be set to `true` to only get the dynamically defined localization resources and texts. If your client-side application shares the same localization resources with the server (like ABP's Blazor and MVC UIs), you can set `onlyDynamics` to `true`. - -**Example request:** - -```` -/api/abp/application-localization?cultureName=en -```` - -## Script - -For [ASP.NET Core MVC (Razor Pages)](../UI/AspNetCore/Overall.md) applications, the same localization data is also available on the JavaScript side. `/Abp/ApplicationLocalizationScript` is the URL of the script that is auto-generated based on the HTTP API above. - -**Example request:** - -```` -/Abp/ApplicationLocalizationScript?cultureName=en -```` - -See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI. - -Other UI types provide services native to the related platform. For example, see the [Angular UI localization documentation](../UI/Angular/Localization.md) to learn how to use the localization values exposes by this endpoint. - diff --git a/docs/en/API/Auto-API-Controllers.md b/docs/en/API/Auto-API-Controllers.md deleted file mode 100644 index ee7c068bec..0000000000 --- a/docs/en/API/Auto-API-Controllers.md +++ /dev/null @@ -1,219 +0,0 @@ -# Auto API Controllers - -Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. - -ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. - -## Configuration - -Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). - -![bookstore-apis](../images/bookstore-apis.png) - -### Examples - -Some example method names and the corresponding routes calculated by convention: - -| Service Method Name | HTTP Method | Route | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP uses a naming convention while determining the HTTP method for a service method (action): - -- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. -- **Put**: Used if the method name starts with 'Put' or 'Update'. -- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. -- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. -- **Patch**: Used if the method name starts with 'Patch'. -- Otherwise, **Post** is used **by default**. - -If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. - -### Route - -Route is calculated based on some conventions: - -* It always starts with '**/api**'. -* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. - -* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **kebab-case**. If your application service class name is 'ReadingBookAppService' then it becomes only '/reading-book'. - * If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. -* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. -* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; - * Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. - * Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. - * Converting the result to **kebab-case**. - * If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. - * Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. -* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). - -#### Customizing the Route Calculation - -`IConventionalRouteBuilder` is used to build the route. It is implemented by the `ConventionalRouteBuilder` by default and works as explained above. You can replace/override this service to customize the route calculation strategy. - -#### Version 3.x Style Route Calculation - -The route calculation was different before the version 4.0. It was using camelCase conventions, while the ABP Framework version 4.0+ uses kebab-case. If you use the old route calculation strategy, follow one of the approaches; - -* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: - -````csharp -options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.UseV3UrlStyle = true; - }); -```` - -This approach effects only the controllers for the `BookStoreApplicationModule`. - -* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: - -```csharp -Configure(options => -{ - options.UseV3UrlStyle = true; -}); -``` - -Setting it globally effects all the modules in a modular application. - -## Service Selection - -Creating conventional HTTP API controllers are not unique to application services actually. - -### IRemoteService Interface - -If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. - -### RemoteService Attribute - -`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate Option - -You can further filter classes to become an API controller by providing the `TypePredicate` option: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. - -## API Explorer - -API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. - -API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. - -## Replace or Remove Controllers. - -In addition to [Overriding a Controller](../Customizing-Application-Modules-Overriding-Services.md#example-overriding-a-controller), you can also use a completely independent **Controller** to replace the controller in the framework or module. - -They have the same [route](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-7.0), but can have **different** input and output parameters. - -### Replace built-in AbpApplicationConfigurationController - -The `ReplaceControllersAttribute` indicates the replaced controller type. - -````csharp -[ReplaceControllers(typeof(AbpApplicationConfigurationController))] -[Area("abp")] -[RemoteService(Name = "abp")] -public class ReplaceBuiltInController : AbpController -{ - [HttpGet("api/abp/application-configuration")] - public virtual Task GetAsync(MyApplicationConfigurationRequestOptions options) - { - return Task.FromResult(new MyApplicationConfigurationDto()); - } -} - -public class MyApplicationConfigurationRequestOptions : ApplicationConfigurationRequestOptions -{ - -} - -public class MyApplicationConfigurationDto : ApplicationConfigurationDto -{ - -} -```` - -### Remove controller - -Configure `ControllersToRemove` of `AbpAspNetCoreMvcOptions` to remove the controllers. - -````csharp -services.Configure(options => -{ - options.ControllersToRemove.Add(typeof(AbpLanguagesController)); -}); -```` -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/auto-api-controllers) \ No newline at end of file diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md deleted file mode 100644 index de9b87e206..0000000000 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ /dev/null @@ -1,209 +0,0 @@ -# Dynamic C# API Client Proxies - -ABP can dynamically create C# API client proxies to call your remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level details to call remote services and get results. - -Dynamic C# proxies automatically handle the following stuff for you; - -* Maps C# **method calls** to remote server **HTTP calls** by considering the HTTP method, route, query string parameters, request payload and other details. -* **Authenticates** the HTTP Client by adding access token to the HTTP header. -* **Serializes** to and deserialize from JSON. -* Handles HTTP API **versioning**. -* Add **correlation id**, current **tenant** id and the current **culture** to the request. -* Properly **handles the error messages** sent by the server and throws proper exceptions. - -This system can be used by any type of .NET client to consume your HTTP APIs. - -## Static vs Dynamic Client Proxies - -ABP provides **two types** of client proxy generation system. This document explains the **dynamic client proxies**, which generates client-side proxies on runtime. You can also see the [Static C# API Client Proxies](Static-CSharp-API-Clients.md) documentation to learn how to generate proxies on development time. - -Development-time (static) client proxy generation has a **performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**. - -## Service Interface - -Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project, typically in the `Application.Contracts` project if you've created your solution using the startup templates. - -Example: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -> Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. - -Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. - -## Client Proxy Generation - -> The startup templates already comes pre-configured for the client proxy generation, in the `HttpApi.Client` project. - -If you're not using a startup template, then execute the following command in the folder that contains the .csproj file of your client project: - -```` -abp add-package Volo.Abp.Http.Client -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](../CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Http.Client). - -Now, it's ready to create the client proxies. Example: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //used to create client proxies - typeof(BookStoreApplicationContractsModule) //contains the application service interfaces - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Create dynamic client proxies - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly - ); - } -} -```` - -`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. - -### Endpoint Configuration - -`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. The simplest configuration is shown below: - -```json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -``` - -See the "AbpRemoteServiceOptions" section below for more detailed configuration. - -## Usage - -It's straightforward to use. Just inject the service interface in the client application code: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. - -### IHttpClientProxy Interface - -While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy` interface. - -## Configuration - -### AbpRemoteServiceOptions - -`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can configure it in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) to set or override it. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### Multiple Remote Service Endpoints - -The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly, - remoteServiceConfigurationName: "BookStore" -); -```` - -`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. - -### As Default Services - -When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly, - asDefaultServices: false -); -```` - -Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. - -> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies. See the *IHttpClientProxy Interface* section above. - -### Retry/Failure Logic & Polly Integration - -If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class. - -**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to re-try 3 times on a failure** - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => - { - clientBuilder.AddTransientHttpErrorPolicy(policyBuilder => - policyBuilder.WaitAndRetryAsync( - 3, - i => TimeSpan.FromSeconds(Math.Pow(2, i)) - ) - ); - }); - }); -} -```` - -This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method. - -## See Also - -* [Static C# Client Proxies](Static-CSharp-API-Clients.md) diff --git a/docs/en/API/Static-CSharp-API-Clients.md b/docs/en/API/Static-CSharp-API-Clients.md deleted file mode 100644 index 5bcbc252c7..0000000000 --- a/docs/en/API/Static-CSharp-API-Clients.md +++ /dev/null @@ -1,275 +0,0 @@ -# Static C# API Client Proxies - -ABP can create C# API client proxy code to call your remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level details to call remote services and get results. - -Static C# proxies automatically handle the following stuff for you; - -* Maps C# **method calls** to remote server **HTTP calls** by considering the HTTP method, route, query string parameters, request payload and other details. -* **Authenticates** the HTTP Client by adding access token to the HTTP header. -* **Serializes** to and deserialize from JSON. -* Handles HTTP API **versioning**. -* Add **correlation id**, current **tenant** id and the current **culture** to the request. -* Properly **handles the error messages** sent by the server and throws proper exceptions. - -This system can be used by any type of .NET client to consume your HTTP APIs. - -## Static vs Dynamic Client Proxies - -ABP provides **two types** of client proxy generation system. This document explains the **static client proxies**, which generates client-side code in your development time. You can also see the [Dynamic C# API Client Proxies](Dynamic-CSharp-API-Clients.md) documentation to learn how to use proxies generated on runtime. - -Development-time (static) client proxy generation has a **performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**. - -## Service Interface - -Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project, typically in the `Application.Contracts` project if you've created your solution using the startup templates. - -Example: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -> Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. - -Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. - -## With Contracts or Without Contracts - -`Without Contracts` depending on target service's `application.contracts` package, so they can reuse the DTOs and other related classes. However, that can be a problem when we want to create fully independently developed and deployed microservices. We want to use the static proxy generation even without depending target service's application.contracts package. - -`With Contracts` generate all the `classes/enums/other` types in the client side (including application service interfaces) , This is also the default behavior of the `generate-proxy` command. - -## Client Proxy Generation - -First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: - -```` -Install-Package Volo.Abp.Http.Client -```` - -Then add `AbpHttpClientModule` dependency to your module: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //add the dependency -public class MyClientAppModule : AbpModule -{ -} -```` - -Now, it's ready to configure the application for the static client proxy generation. - -### With Contracts Example - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //used to create client proxies - typeof(AbpVirtualFileSystemModule) //virtual file system -)] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - // Prepare for static client proxy generation - context.Services.AddStaticHttpClientProxies( - typeof(MyClientAppModule).Assembly - ); - - // Include the generated app-generate-proxy.json in the virtual file system - Configure(options => - { - options.FileSets.AddEmbedded(); - }); - } -} -```` - -### Without Contracts Example - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //used to create client proxies - typeof(AbpVirtualFileSystemModule), //virtual file system - typeof(BookStoreApplicationContractsModule) //contains the application service interfaces -)] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - // Prepare for static client proxy generation - context.Services.AddStaticHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly - ); - - // Include the generated app-generate-proxy.json in the virtual file system - Configure(options => - { - options.FileSets.AddEmbedded(); - }); - } -} -```` - -`AddStaticHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, and prepares for static client proxy generation. - -> The [application startup template](../Startup-Templates/Application.md) comes pre-configured for the **dynamic** client proxy generation, in the `HttpApi.Client` project. If you want to switch to the **static** client proxies, change `context.Services.AddHttpClientProxies` to `context.Services.AddStaticHttpClientProxies` in the module class of your `HttpApi.Client` project. - -### Endpoint Configuration - -`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. The simplest configuration is shown below: - -```json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -``` - -See the *AbpRemoteServiceOptions* section below for more detailed configuration. - -### Code Generation - -Server side must be up and running while generating the client proxy code. So, run your application that serves the HTTP APIs on the `BaseUrl` that is configured like explained in the *Endpoint Configuration* section. - -Open a command-line terminal in the root folder of your client project (`.csproj`) and type the following command: - -#### With Contracts - -````bash -abp generate-proxy -t csharp -u http://localhost:53929/ -```` - -> If you haven't installed yet, you should install the [ABP CLI](../CLI.md). - -This command should generate the following files under the `ClientProxies` folder: - -![generated-static-client-proxies](../images/generated-static-client-proxies-with-contracts.png) - -* `BookClientProxy.Generated.cs` is the actual generated proxy class in this example. `BookClientProxy` is a `partial` class * where you can write your custom code (ABP won't override it). -* `IBookAppService.cs` is the app service. -* `BookDto.cs` is the Dto class which uses by app service. -* `app-generate-proxy.json` contains information about the remote HTTP endpoint, so ABP can properly perform HTTP requests. This file must be configured as an embedded resource in your project, so that it can be found by the virtual file system. - -#### Without Contracts - -````bash -abp generate-proxy -t csharp -u http://localhost:53929/ --without-contracts -```` - -This command should generate the following files under the `ClientProxies` folder: - -![generated-static-client-proxies](../images/generated-static-client-proxies-without-contracts.png) - -* `BookClientProxy.Generated.cs` is the actual generated proxy class in this example. `BookClientProxy` is a `partial` class where you can write your custom code (ABP won't override it). -* `app-generate-proxy.json` contains information about the remote HTTP endpoint, so ABP can properly perform HTTP requests. This file must be configured as an embedded resource in your project, so that it can be found by the virtual file system. - -> `generate-proxy` command generates proxies for only the APIs you've defined in your application. If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../CLI.md) documentation for other options. - -## Usage - -It's straightforward to use the client proxies. Just inject the service interface in the client application code: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoItAsync() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -This sample injects the `IBookAppService` service interface defined above. The static client proxy implementation makes an HTTP call whenever a service method is called by the client. - -## Configuration - -### AbpRemoteServiceOptions - -`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can configure it in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) to set or override it. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### Multiple Remote Service Endpoints - -The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddStaticHttpClientProxies` method can get an additional parameter for the remote service name. Example: - -````csharp -context.Services.AddStaticHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly, - remoteServiceConfigurationName: "BookStore" -); -```` - -`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. - -### Retry/Failure Logic & Polly Integration - -If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class. - -**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to re-try 3 times on a failure** - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => - { - clientBuilder.AddTransientHttpErrorPolicy(policyBuilder => - policyBuilder.WaitAndRetryAsync( - 3, - i => TimeSpan.FromSeconds(Math.Pow(2, i)) - ) - ); - }); - }); -} -```` - -This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method. - -## See Also - -* [Dynamic C# Client Proxies](Dynamic-CSharp-API-Clients.md) diff --git a/docs/en/API/Swagger-Integration.md b/docs/en/API/Swagger-Integration.md deleted file mode 100644 index 395b1830d7..0000000000 --- a/docs/en/API/Swagger-Integration.md +++ /dev/null @@ -1,194 +0,0 @@ -# Swagger Integration - -[Swagger (OpenAPI)](https://swagger.io/) is a language-agnostic specification for describing REST APIs. It allows both computers and humans to understand the capabilities of a REST API without direct access to the source code. Its main goals are to: - -- Minimize the amount of work needed to connect decoupled services. -- Reduce the amount of time needed to accurately document a service. - -ABP Framework offers a prebuilt module for full Swagger integration with small configurations. - -## Installation - -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. - -If installation is needed, it is suggested to use the [ABP CLI](../CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the `Web` or `HttpApi.Host` project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Swashbuckle -``` - -> If you haven't done it yet, you first need to install the [ABP CLI](../CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Swashbuckle). - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Swashbuckle](https://www.nuget.org/packages/Volo.Abp.Swashbuckle) NuGet package to your `Web` or `HttpApi.Host` project: - - `Install-Package Volo.Abp.Swashbuckle` - -2. Add the `AbpSwashbuckleModule` to the dependency list of your module: - - ```csharp - [DependsOn( - //...other dependencies - typeof(AbpSwashbuckleModule) // <-- Add module dependency like that - )] - public class YourModule : AbpModule - { - } - ``` - -## Configuration - -First, we need to use `AddAbpSwaggerGen` extension to configure Swagger in `ConfigureServices` method of our module: - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var services = context.Services; - - //... other configurations. - - services.AddAbpSwaggerGen( - options => - { - options.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); - options.DocInclusionPredicate((docName, description) => true); - options.CustomSchemaIds(type => type.FullName); - } - ); -} -``` - -Then we can use Swagger UI by calling `UseAbpSwaggerUI` method in the `OnApplicationInitialization` method of our module: - -```csharp -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - var app = context.GetApplicationBuilder(); - - //... other configurations. - - app.UseStaticFiles(); - - app.UseSwagger(); - - app.UseAbpSwaggerUI(options => - { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API"); - }); - - //... other configurations. -} -``` - -### Hide ABP Endpoints on Swagger UI - -If you want to hide ABP's default endpoints, call the `HideAbpEndpoints` method in your Swagger configuration as shown in the following example: - -```csharp -services.AddAbpSwaggerGen( - options => - { - //... other options - - //Hides ABP Related endpoints on Swagger UI - options.HideAbpEndpoints(); - } -) -``` - -## Using Swagger with OAUTH - -For non MVC/Tiered applications, we need to configure Swagger with OAUTH to handle authorization. - -> ABP Framework uses OpenIddict by default. To get more information about OpenIddict, check this [documentation](../Modules/OpenIddict.md). - -To do that, we need to use `AddAbpSwaggerGenWithOAuth` extension to configure Swagger with OAuth issuer and scopes in `ConfigureServices` method of our module: - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var services = contex.Services; - - //... other configarations. - - services.AddAbpSwaggerGenWithOAuth( - "https://localhost:44341", // authority issuer - new Dictionary // - { // scopes - {"Test", "Test API"} // - }, // - options => - { - options.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); - options.DocInclusionPredicate((docName, description) => true); - options.CustomSchemaIds(type => type.FullName); - } - ); -} -``` - -Then we can use Swagger UI by calling `UseAbpSwaggerUI` method in the `OnApplicationInitialization` method of our module: - -```csharp -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - var app = context.GetApplicationBuilder(); - - //... other configurations. - - app.UseAbpSwaggerUI(options => - { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API"); - - var configuration = context.ServiceProvider.GetRequiredService(); - options.OAuthClientId("Test_Swagger"); // clientId - options.OAuthClientSecret("1q2w3e*"); // clientSecret - }); - - //... other configurations. -} -``` - -> Do not forget to set `OAuthClientId` and `OAuthClientSecret`. - -## Using Swagger with OIDC - -You may also want to configure swagger using **OpenIdConnect** instead of OAUTH. This is especially useful when you need to configure different metadata address than the issuer in cases such as when you deploy your application to kubernetes cluster or docker. In these cases, metadata address will be used in sign-in process to reach the valid authentication server discovery endpoint over the internet and use the internal network to validate the obtained token. - -To do that, we need to use `AddAbpSwaggerGenWithOidc` extension to configure Swagger with OAuth issuer and scopes in `ConfigureServices` method of our module: - -```csharp -context.Services.AddAbpSwaggerGenWithOidc( - configuration["AuthServer:Authority"], - scopes: new[] { "SwaggerDemo" }, - // "authorization_code" - flows: new[] { AbpSwaggerOidcFlows.AuthorizationCode }, - // When deployed on K8s, should be metadata URL of the reachable DNS over internet like https://myauthserver.company.com - discoveryEndpoint: configuration["AuthServer:Authority"], - options => - { - options.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerDemo API", Version = "v1" }); - options.DocInclusionPredicate((docName, description) => true); - options.CustomSchemaIds(type => type.FullName); - }); -``` - -The `flows` is a list of default oidc flows that is supported by the oidc-provider (authserver). You can see the default supported flows below: - -- `AbpSwaggerOidcFlows.AuthorizationCode`: The `"authorization_code"` flow is the **default and suggested** flow. **Doesn't require a client secret** when even there is a field for it. -- `AbpSwaggerOidcFlows.Implicit`: The deprecated `"implicit"` flow that was used for javascript applications. -- `AbpSwaggerOidcFlows.Password`: The legacy `password` flow which is also known as Resource Ownder Password flow. You need to provide a user name, password and client secret for it. -- `AbpSwaggerOidcFlows.ClientCredentials`: The `"client_credentials"` flow that is used for server to server interactions. - -You can define one or many flows which will be shown in the Authorize modal. You can set it **null which will use the default "authorization_code"** flow. - -The `discoveryEndpoint` is the reachable openid-provider endpoint for the `.well-known/openid-configuration`. You can set it to **null which will use default AuthServer:Authority** appsettings configuration. If you are deploying your applications to a kubernetes cluster or docker swarm, you should to set the `discoveryEndpoint` as real DNS that should be reachable over the internet. - -> If are having problems with seeing the authorization modal, check the browser console logs and make sure you have a correct and reachable `discoveryEndpoint` diff --git a/docs/en/Ambient-Context-Pattern.md b/docs/en/Ambient-Context-Pattern.md deleted file mode 100644 index d00a721ce2..0000000000 --- a/docs/en/Ambient-Context-Pattern.md +++ /dev/null @@ -1,3 +0,0 @@ -## Ambient Context Pattern - -TODO \ No newline at end of file diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md deleted file mode 100644 index 0bd71f08c9..0000000000 --- a/docs/en/Application-Services.md +++ /dev/null @@ -1,568 +0,0 @@ -# Application Services - -Application services are used to implement the **use cases** of an application. They are used to **expose domain logic to the presentation layer**. - -An Application Service is called from the presentation layer (optionally) with a **DTO ([Data Transfer Object](Data-Transfer-Objects.md))** as the parameter. It uses domain objects to **perform some specific business logic** and (optionally) returns a DTO back to the presentation layer. Thus, the presentation layer is completely **isolated** from domain layer. - -## Example - -### Book Entity - -Assume that you have a `Book` entity (actually, an aggregate root) defined as shown below: - -````csharp -public class Book : AggregateRoot -{ - public const int MaxNameLength = 128; - - public virtual string Name { get; protected set; } - - public virtual BookType Type { get; set; } - - public virtual float? Price { get; set; } - - protected Book() - { - - } - - public Book(Guid id, [NotNull] string name, BookType type, float? price = 0) - { - Id = id; - Name = CheckName(name); - Type = type; - Price = price; - } - - public virtual void ChangeName([NotNull] string name) - { - Name = CheckName(name); - } - - private static string CheckName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException( - $"name can not be empty or white space!"); - } - - if (name.Length > MaxNameLength) - { - throw new ArgumentException( - $"name can not be longer than {MaxNameLength} chars!"); - } - - return name; - } -} -```` - -* `Book` entity has a `MaxNameLength` that defines the maximum length of the `Name` property. -* `Book` constructor and `ChangeName` method to ensure that the `Name` is always a valid value. Notice that `Name`'s setter is not `public`. - -> ABP does not force you to design your entities like that. It just can have public get/set for all properties. It's your decision to fully implement DDD practices. - -### IBookAppService Interface - -In ABP, an application service should implement the `IApplicationService` interface. It's good to create an interface for each application service: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task CreateAsync(CreateBookDto input); -} -```` - -A Create method will be implemented as the example. `CreateBookDto` is defined like that: - -````csharp -public class CreateBookDto -{ - [Required] - [StringLength(Book.MaxNameLength)] - public string Name { get; set; } - - public BookType Type { get; set; } - - public float? Price { get; set; } -} -```` - -> See [data transfer objects document](Data-Transfer-Objects.md) for more about DTOs. - -### BookAppService (Implementation) - -````csharp -public class BookAppService : ApplicationService, IBookAppService -{ - private readonly IRepository _bookRepository; - - public BookAppService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task CreateAsync(CreateBookDto input) - { - var book = new Book( - GuidGenerator.Create(), - input.Name, - input.Type, - input.Price - ); - - await _bookRepository.InsertAsync(book); - } -} -```` - -* `BookAppService` inherits from the `ApplicationService` base class. It's not required, but the `ApplicationService` class provides helpful properties for common application service requirements like `GuidGenerator` used in this service. If we didn't inherit from it, we would need to inject the `IGuidGenerator` service manually (see [guid generation](Guid-Generation.md) document). -* `BookAppService` implements the `IBookAppService` as expected. -* `BookAppService` [injects](Dependency-Injection.md) `IRepository` (see [repositories](Repositories.md)) and uses it inside the `CreateAsync` method to insert a new entity to the database. -* `CreateAsync` uses the constructor of the `Book` entity to create a new book from the properties of given `input`. - -## Data Transfer Objects - -Application services get and return DTOs instead of entities. ABP does not force this rule. However, exposing entities to the presentation layer (or to remote clients) has significant problems and is not suggested. - -See the [DTO documentation](Data-Transfer-Objects.md) for more. - -## Object to Object Mapping - -The `CreateAsync` method above manually creates a `Book` entity from given `CreateBookDto` object, because the `Book` entity enforces it (we designed it like that). - -However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier. - -Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default. - -Let's create another method to get a book. First, define the method in the `IBookAppService` interface: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task CreateAsync(CreateBookDto input); - - Task GetAsync(Guid id); //New method -} -```` - -`BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below: - -````csharp -public class BookDto -{ - public Guid Id { get; set; } - - public string Name { get; set; } - - public BookType Type { get; set; } - - public float? Price { get; set; } -} -```` - -AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - } -} -```` - -You should then register profiles using the `AbpAutoMapperOptions`: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Add all mappings defined in the assembly of the MyModule class - options.AddMaps(); - }); - } -} -```` - -`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html). - -Then you can implement the `GetAsync` method as shown below: - -````csharp -public async Task GetAsync(Guid id) -{ - var book = await _bookRepository.GetAsync(id); - return ObjectMapper.Map(book); -} -```` - -See the [object to object mapping document](Object-To-Object-Mapping.md) for more. - -## Validation - -Inputs of application service methods are automatically validated (like ASP.NET Core controller actions). You can use the standard data annotation attributes or a custom validation method to perform the validation. ABP also ensures that the input is not null. - -See the [validation document](Validation.md) for more. - -## Authorization - -It's possible to use declarative and imperative authorization for application service methods. - -See the [authorization document](Authorization.md) for more. - -## CRUD Application Services - -If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from the `CrudAppService`. - -### Example - -Create an `IBookAppService` interface inheriting from the `ICrudAppService` interface. - -````csharp -public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book -{ -} -```` - -`ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface). - -> Creating an interface for an application service is good practice, but not required by the ABP Framework. You can skip the interface part. - -`ICrudAppService` declares the following methods: - -````csharp -public interface ICrudAppService< - TEntityDto, - in TKey, - in TGetListInput, - in TCreateInput, - in TUpdateInput> - : IApplicationService - where TEntityDto : IEntityDto -{ - Task GetAsync(TKey id); - - Task> GetListAsync(TGetListInput input); - - Task CreateAsync(TCreateInput input); - - Task UpdateAsync(TKey id, TUpdateInput input); - - Task DeleteAsync(TKey id); -} -```` - -DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`: - -````csharp -public class BookDto : AuditedEntityDto -{ - public string Name { get; set; } - - public BookType Type { get; set; } - - public float Price { get; set; } -} - -public class CreateUpdateBookDto -{ - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public float Price { get; set; } -} -```` - -[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class. - -```csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - CreateMap(); - } -} -``` - -* `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well. - -And finally, the `BookAppService` implementation is very simple: - -````csharp -public class BookAppService : - CrudAppService, - IBookAppService -{ - public BookAppService(IRepository repository) - : base(repository) - { - } -} -```` - -`CrudAppService` implements all methods declared in the `ICrudAppService` interface. You can then add your own custom methods or override and customize base methods. - -> `CrudAppService` has different versions gets different number of generic arguments. Use the one suitable for you. - -### AbstractKeyCrudAppService - -`CrudAppService` requires to have an Id property as the primary key of your entity. If you are using composite keys then you can not utilize it. - -`AbstractKeyCrudAppService` implements the same `ICrudAppService` interface, but this time without making assumption about your primary key. - -#### Example - -Assume that you have a `District` entity with `CityId` and `Name` as a composite primary key. Using `AbstractKeyCrudAppService` requires to implement `DeleteByIdAsync` and `GetEntityByIdAsync` methods yourself: - -````csharp -public class DistrictAppService - : AbstractKeyCrudAppService -{ - public DistrictAppService(IRepository repository) - : base(repository) - { - } - - protected async override Task DeleteByIdAsync(DistrictKey id) - { - await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); - } - - protected async override Task GetEntityByIdAsync(DistrictKey id) - { - var queryable = await Repository.GetQueryableAsync(); - return await AsyncQueryableExecuter.FirstOrDefaultAsync( - queryable.Where(d => d.CityId == id.CityId && d.Name == id.Name) - ); - } -} -```` - -This implementation requires you to create a class that represents your composite key: - -````csharp -public class DistrictKey -{ - public Guid CityId { get; set; } - - public string Name { get; set; } -} -```` - -### Authorization (for CRUD App Services) - -There are two ways of authorizing the base application service methods; - -1. You can set the policy properties (xxxPolicyName) in the constructor of your service. Example: - -```csharp -public class MyPeopleAppService : CrudAppService -{ - public MyPeopleAppService(IRepository repository) - : base(repository) - { - GetPolicyName = "..."; - GetListPolicyName = "..."; - CreatePolicyName = "..."; - UpdatePolicyName = "..."; - DeletePolicyName = "..."; - } -} -``` - -`CreatePolicyName` is checked by the `CreateAsync` method and so on... You should specify a policy (permission) name defined in your application. - -2. You can override the check methods (CheckXxxPolicyAsync) in your service. Example: - -```csharp -public class MyPeopleAppService : CrudAppService -{ - public MyPeopleAppService(IRepository repository) - : base(repository) - { - } - - protected async override Task CheckDeletePolicyAsync() - { - await AuthorizationService.CheckAsync("..."); - } -} -``` - -You can perform any logic in the `CheckDeletePolicyAsync` method. It is expected to throw an `AbpAuthorizationException` in any unauthorized case, like `AuthorizationService.CheckAsync` already does. - -### Base Properties & Methods - -CRUD application service base class provides many useful base methods that **you can override** to customize it based on your requirements. - -#### CRUD Methods - -These are the essential CRUD methods. You can override any of them to completely customize the operation. Here, the definitions of the methods: - -````csharp -Task GetAsync(TKey id); -Task> GetListAsync(TGetListInput input); -Task CreateAsync(TCreateInput input); -Task UpdateAsync(TKey id, TUpdateInput input); -Task DeleteAsync(TKey id); -```` - -#### Querying - -These methods are low level methods that can control how to query entities from the database. - -* `CreateFilteredQuery` can be overridden to create an `IQueryable` that is filtered by the given input. If your `TGetListInput` class contains any filter, it is proper to override this method and filter the query. It returns the (unfiltered) repository (which is already `IQueryable`) by default. -* `ApplyPaging` is used to make paging on the query. If your `TGetListInput` already implements `IPagedResultRequest`, you don't need to override this since the ABP Framework automatically understands it and performs the paging. -* `ApplySorting` is used to sort (order by...) the query. If your `TGetListInput` already implements the `ISortedResultRequest`, ABP Framework automatically sorts the query. If not, it fallbacks to the `ApplyDefaultSorting` which tries to sort by creation time, if your entity implements the standard `IHasCreationTime` interface. -* `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default. -* `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default. - -#### Object to Object Mapping - -These methods are used to convert Entities to DTOs and vice verse. They use the [IObjectMapper](Object-To-Object-Mapping.md) by default. - -* `MapToGetOutputDtoAsync` is used to map the entity to the DTO returned from the `GetAsync`, `CreateAsync` and `UpdateAsync` methods. Alternatively, you can override the `MapToGetOutputDto` if you don't need to perform any async operation. -* `MapToGetListOutputDtosAsync` is used to map a list of entities to a list of DTOs returned from the `GetListAsync` method. It uses the `MapToGetListOutputDtoAsync` to map each entity in the list. You can override one of them based on your case. Alternatively, you can override the `MapToGetListOutputDto` if you don't need to perform any async operation. -* `MapToEntityAsync` method has two overloads; - * `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`. - * `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`. - -## Miscellaneous - -### Working with Streams - -`Stream` object itself is not serializable. So, you may have problems if you directly use `Stream` as the parameter or the return value for your application service. ABP Framework provides a special type, `IRemoteStreamContent` to be used to get or return streams in the application services. - -**Example: Application Service Interface that can be used to get and return streams** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Content; - -namespace MyProject.Test -{ - public interface ITestAppService : IApplicationService - { - Task Upload(Guid id, IRemoteStreamContent streamContent); - Task Download(Guid id); - - Task CreateFile(CreateFileInput input); - Task CreateMultipleFile(CreateMultipleFileInput input); - } - - public class CreateFileInput - { - public Guid Id { get; set; } - - public IRemoteStreamContent Content { get; set; } - } - - public class CreateMultipleFileInput - { - public Guid Id { get; set; } - - public IEnumerable Contents { get; set; } - } -} -```` - -**You need to configure `AbpAspNetCoreMvcOptions` to add DTO class to `FormBodyBindingIgnoredTypes` to use `IRemoteStreamContent` in** **DTO ([Data Transfer Object](Data-Transfer-Objects.md))** - -````csharp -Configure(options => -{ - options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput)); - options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput)); -}); -```` - -**Example: Application Service Implementation that can be used to get and return streams** - -````csharp -using System; -using System.IO; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Application.Services; -using Volo.Abp.Content; - -namespace MyProject.Test -{ - public class TestAppService : ApplicationService, ITestAppService - { - public Task Download(Guid id) - { - var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.OpenOrCreate); - return Task.FromResult( - (IRemoteStreamContent) new RemoteStreamContent(fs) { - ContentType = "application/octet-stream" - } - ); - } - - public async Task Upload(Guid id, IRemoteStreamContent streamContent) - { - using (var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.Create)) - { - await streamContent.GetStream().CopyToAsync(fs); - await fs.FlushAsync(); - } - } - - public async Task CreateFileAsync(CreateFileInput input) - { - using (var fs = new FileStream("C:\\Temp\\" + input.Id + ".blob", FileMode.Create)) - { - await input.Content.GetStream().CopyToAsync(fs); - await fs.FlushAsync(); - } - } - - public async Task CreateMultipleFileAsync(CreateMultipleFileInput input) - { - using (var fs = new FileStream("C:\\Temp\\" + input.Id + ".blob", FileMode.Append)) - { - foreach (var content in input.Contents) - { - await content.GetStream().CopyToAsync(fs); - } - await fs.FlushAsync(); - } - } - } -} -```` - -`IRemoteStreamContent` is compatible with the [Auto API Controller](API/Auto-API-Controllers.md) and [Dynamic C# HTTP Proxy](API/Dynamic-CSharp-API-Clients.md) systems. - -## Lifetime - -Lifetime of application services are [transient](Dependency-Injection.md) and they are automatically registered to the dependency injection system. - -## See Also - -* [Video tutorial Part-1](https://abp.io/video-courses/essentials/application-services-part-1) -* [Video tutorial Part-2](https://abp.io/video-courses/essentials/application-services-part-2) \ No newline at end of file diff --git a/docs/en/Application-Startup.md b/docs/en/Application-Startup.md deleted file mode 100644 index afa13fd050..0000000000 --- a/docs/en/Application-Startup.md +++ /dev/null @@ -1,320 +0,0 @@ -## ABP Application Startup - -You typically use the [ABP CLI](CLI.md)'s `abp new` command to [get started](Getting-Started.md) with one of the pre-built [startup solution templates](Startup-Templates/Index.md). When you do that, you generally don't need to know the details of how the ABP Framework is integrated with your application or how it is configured and initialized. The startup template also comes with the fundamental ABP packages and [application modules](Modules/Index) are pre-installed and configured for you. - -> It is always suggested to [get started with a startup template](Getting-Started.md) and modify it for your requirements. Read this document only if you want to understand the details or if you need to modify how the ABP Framework starts. - -While the ABP Framework has a lot of features and integrations, it is built as a lightweight and modular framework. It consists of [hundreds of NuGet and NPM packages](https://abp.io/packages), so you can only use the features you need. If you follow the [Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application](Getting-Started-AspNetCore-Application.md) document, you'll see how easy it is to install the ABP Framework into an empty ASP.NET Core project from scratch. You only need to install a single NuGet package and make a few small changes. - -This document is for who wants to better understand how the ABP Framework is initialized and configured on startup. - -## Installing to a Console Application - -A .NET Console application is the minimalist .NET application. So, it is best to show the installing of the ABP Framework to a console application as a minimalist example. - -If you [create a new console application with Visual Studio](https://learn.microsoft.com/en-us/dotnet/core/tutorials/with-visual-studio) (for .NET 8.0 or later), you will see the following solution structure (I named the solution as `MyConsoleDemo`): - -![app-startup-console-initial](images/app-startup-console-initial.png) - -This example uses the [top level statements](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/top-level-statements), so it consists of only a single line of code. - -The first step is to install the [Volo.Abp.Core](https://www.nuget.org/packages/Volo.Abp.Core) NuGet package, which is the most core NuGet package of the ABP framework. You can install it using the ABP CLI. Execute the following command in the folder of the .csproj file that you want to install the package on: - -````bash -abp add-package Volo.Abp.Core -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Core). - -Alternatively, you can use a command-line terminal in the root folder of the project (the folder containing the `MyConsoleDemo.csproj` file, for this example): - -````bash -dotnet add package Volo.Abp.Core -```` - -After adding the NuGet package, we should create a root [module class](Module-Development-Basics.md) for our application. We can create the following class in the project: - -````csharp -using Volo.Abp.Modularity; - -namespace MyConsoleDemo -{ - public class MyConsoleDemoModule : AbpModule - { - } -} -```` - -This is an empty class deriving from the `AbpModule` class. It is the main class that you will control your application's dependencies with, and implement your configuration and startup/shutdown logic. For more information, please check the [Modularity](Module-Development-Basics.md) document. - -As the second and the last step, change the `Program.cs` as shown in the following code block: - -````csharp -using MyConsoleDemo; -using Volo.Abp; - -// 1: Create the ABP application container -using var application = await AbpApplicationFactory.CreateAsync(); - -// 2: Initialize/start the ABP Framework (and all the modules) -await application.InitializeAsync(); - -Console.WriteLine("ABP Framework has been started..."); - -// 3: Stop the ABP Framework (and all the modules) -await application.ShutdownAsync(); -```` - -That's all. Now, ABP Framework is installed, integrated, started and stopped in your application. From now, you can install [ABP packages](https://abp.io/packages) to your application whenever you need them. - -## Installing a Framework Package - -If you want to send emails from your .NET application, you can use .NET's standard [SmtpClient class](https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient). ABP also provides an `IEmailSender` service that simplifies [sending emails](Emailing.md) and configuring the email settings in a central place. If you want to use it, you should install the [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) NuGet package to your project: - -````bash -dotnet add package Volo.Abp.Emailing -```` - -Once you add a new ABP package/module, you also need to specify the module dependency from your module class. So, change the `MyConsoleDemoModule` class as shown below: - -````csharp -using Volo.Abp.Emailing; -using Volo.Abp.Modularity; - -namespace MyConsoleDemo -{ - [DependsOn(typeof(AbpEmailingModule))] // Added the module dependency - public class MyConsoleDemoModule : AbpModule - { - } -} -```` - -I've just added a `[DependsOn]` attribute to declare that I want to use the ABP Emailing Module (`AbpEmailingModule`). Now, I can use the `IEmailSender` service in my `Program.cs`: - -````csharp -using Microsoft.Extensions.DependencyInjection; -using MyConsoleDemo; -using Volo.Abp; -using Volo.Abp.Emailing; - -using var application = await AbpApplicationFactory.CreateAsync(); -await application.InitializeAsync(); - -// Sending emails using the IEmailSender service -var emailsender = application.ServiceProvider.GetRequiredService(); -await emailsender.SendAsync( - to: "info@acme.com", - subject: "Hello World", - body: "My message body..." -); - -await application.ShutdownAsync(); -```` - -> If you run that application, you get a runtime error indicating that the email sending settings haven't been done yet. You can check the [Email Sending document](Emailing.md) to learn how to configure it. - -That's all. Install an ABP NuGet package, add the module dependency (using the `[DependsOn]` attribute) and use any service inside the NuGet package. - -The [ABP CLI](CLI.md) already has a special command to perform the addition of an ABP NuGet and also adding the `[DependsOn]` attribute to your module class for you with a single command: - -````bash -abp add-package Volo.Abp.Emailing -```` - -We suggest you to use the `abp add-package` command instead of manually doing it. - -## AbpApplicationFactory - -`AbpApplicationFactory` is the main class that creates an ABP application container. It provides a single static `CreateAsync` (and `Create` if you can't use asynchronous programming) method with multiple overloads. Let's investigate these overloads to understand where you can use them. - -The first overload gets a generic module class parameter as we've used before in this document: - -````csharp -AbpApplicationFactory.CreateAsync(); -```` - -The generic class parameter should be the root module class of your application. All the other modules are resolved as dependencies of that module. - -The second overload gets the module class as a `Type` parameter, instead of the generic parameter. So, the previous code block could be re-written as shown below: - -````csharp -AbpApplicationFactory.CreateAsync(typeof(MyConsoleDemoModule)); -```` - -Both overloads work exactly the same. So, you can use the second one if you don't know the module class type on development time and you (somehow) calculate it on runtime. - -If you use one of the methods above, ABP creates an internal service collection (`IServiceCollection`) and an internal service provider (`IServiceProvider`) to setup the [dependency injection](Dependency-Injection.md) system internally. Notice that we've used the `application.ServiceProvider` property in the *Installing a Framework Package* section to resolve the `IEmailSender` service from the dependency injection system. - -The next overload gets an `IServiceCollection` parameter from you to allow you to setup the dependency injection system yourself, or integrate to another framework (like ASP.NET Core) that also sets up the dependency injection system internally. - -We can change the `Program.cs` as shown below to externally manage the dependency injection setup: - -````csharp -using Microsoft.Extensions.DependencyInjection; -using MyConsoleDemo; -using Volo.Abp; - -// 1: Manually created the IServiceCollection -IServiceCollection services = new ServiceCollection(); - -// 2: Pass the IServiceCollection externally to the ABP Framework -using var application = await AbpApplicationFactory - .CreateAsync(services); - -// 3: Manually built the IServiceProvider object -IServiceProvider serviceProvider = services.BuildServiceProvider(); - -// 4: Pass the IServiceProvider externally to the ABP Framework -await application.InitializeAsync(serviceProvider); - -Console.WriteLine("ABP Framework has been started..."); - -await application.ShutdownAsync(); -```` - -In this example, we've used .NET's standard dependency injection container. The `services.BuildServiceProvider()` call creates the standard container. However, ABP provides an alternative extension method, `BuildServiceProviderFromFactory()`, that properly works even if you are using another dependency injection container: - -````csharp -IServiceProvider serviceProvider = services.BuildServiceProviderFromFactory(); -```` - -> You can check the [Autofac Integration](Autofac-Integration.md) document if you want to learn how you can integrate the [Autofac](https://autofac.org/) dependency injection container with the ABP Framework. - -Finally, the `CreateAsync` method has a last overload that takes the module class name as a `Type` parameter and a `IServiceCollection` object. So, we could re-write the last `CreateAsync` method usage as in the following code block: - -````csharp -using var application = await AbpApplicationFactory - .CreateAsync(typeof(MyConsoleDemoModule), services); -```` - -> All of the `CreateAsync` method overloads have `Create` counterparts. If your application type can not utilize asynchronous programming (that means you can't use the `await` keyword), then you can use the `Create` method instead of the `CreateAsync` method. - -### AbpApplicationCreationOptions - -All of the `CreateAsync` overloads can get an optional `Action` parameter to configure the options that are used on the application creation. See the following example: - -````csharp -using var application = await AbpApplicationFactory - .CreateAsync(options => - { - options.ApplicationName = "MyApp"; - }); -```` - -We've passed a lambda method to configure the `ApplicationName` option. Here's a list of all standard options: - -* `ApplicationName`: A human-readable name for the application. It is a unique value for an application. -* `Configuration`: Can be used to setup the [application configuration](Configuration.md) when it is not provided by the hosting system. It is not needed for ASP.NET Core and other .NET hosted applications. However, if you've used `AbpApplicationFactory` with an internal service provider, you can use this option to configure how the application configuration is built. -* `Environment`: Environment name for the application. -* `PlugInSources`: A list of plugin sources. See the [Plug-In Modules documentation](PlugIn-Modules) to learn how to work with plugins. -* `Services`: The `IServiceCollection` object that can be used to register service dependencies. You generally don't need that, because you configure your services in your [module class](Module-Development-Basics.md). However, it can be used while writing extension methods for the `AbpApplicationCreationOptions` class. - -#### The ApplicationName option - -As defined above, the `ApplicationName` option is a human-readable name for the application. It is a unique value for an application. - -`ApplicationName` is used by the ABP Framework in several places to distinguish the application. For example, the [audit logging](Audit-Logging.md) system saves the `ApplicationName` in each audit log record written by the related application, so you can understand which application has created the audit log entry. So, if your system consists of multiple applications (like a microservice solution) that are saving audit logs to a single point, you should be sure that each application has a different `ApplicationName`. - -The `ApplicationName` property's value is set automatically from the **entry assembly's name** (generally, the project name in a .NET solution) by default, which is proper for most cases, since each application typically has a unique entry assembly name. - -There are two ways to set the application name to a different value. In this first approach, you can set the `ApplicationName` property in your application's [configuration](Configuration.md). The easiest way is to add an `ApplicationName` field to your `appsettings.json` file: - -````json -{ - "ApplicationName": "Services.Ordering" -} -```` - -Alternatively, you can set `AbpApplicationCreationOptions.ApplicationName` while creating the ABP application. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `ApplicationName` option as shown below: - -````csharp -await builder.AddApplicationAsync(options => -{ - options.ApplicationName = "Services.Ordering"; -}); -```` - -#### IApplicationInfoAccessor - -If you need to access the `ApplicationName` later in your solution, you can inject the `IApplicationInfoAccessor` service and get the value from its `ApplicationName` property. - -`IApplicationInfoAccessor` also provides an `InstanceId` value, that is a random GUID value that is generated when your application starts. You can use that value to distinguish application instances from each other. - -## IAbpApplication - -`AbpApplicationFactory` returns an `IAbpApplication` object from its `CreateAsync` (or `Create`) method. `IAbpApplication` is the main container for an ABP application. It is also registered to the [dependency injection](Dependency-Injection.md) system, so you can inject `IAbpApplication` in your services to use its properties and methods. - -Here's a list of `IAbpApplication` properties you may want to know: - -* `StartupModuleType`: Gets the root module of the application that was used while creating the application container (on the `AbpApplicationFactory.CreateAsync` method). -* `Services`: A list of all service registrations (the `IServiceCollection` object). You can not add new services to this collection after application initialization (you can actually add, but it won't have any effect). -* `ServiceProvider`: A reference to the root service provider used by the application. This can not be used before initializing the application. If you need to resolve non-singleton services from that `IServiceProvider` object, always create a new service scope and dispose it after usage. Otherwise, your application will have memory leak problems. See the *Releasing/Disposing Services* section of the [dependency injection](Dependency-Injection.md) document for more information about service scopes. -* `Modules`: A read-only list of all the modules loaded into the current application. Alternatively, you can inject the `IModuleContainer` service if you need to access the module list in your application code. - -The `IAbpApplication` interface extends the `IApplicationInfoAccessor` interface, so you can get the `ApplicationName` and `InstanceId` values from it. However, if you only need to access these properties, inject and use the `IApplicationInfoAccessor` service instead. - -`IAbpApplication` is disposable. Always dispose of it before exiting your application. - -## IAbpHostEnvironment - -Sometimes, while creating an application, we need to get the current hosting environment and take actions according to that. In such cases, we can use some services such as [IWebHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-8.0) or [IWebAssemblyHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) provided by .NET, in the final application. - -However, we can not use these services in a class library, which is used by the final application. ABP Framework provides the `IAbpHostEnvironment` service, which allows you to get the current environment name whenever you want. `IAbpHostEnvironment` is used by the ABP Framework in several places to perform specific actions by the environment. For example, ABP Framework reduces the cache duration on the **Development** environment for some services. - -`IAbpHostEnvironment` obtains the current environment name by the following order: - -1. Gets and sets the environment name if it's specified in the `AbpApplicationCreationOptions`. -2. Tries to obtain the environment name from the `IWebHostEnvironment` or `IWebAssemblyHostEnvironment` services for ASP.NET Core & Blazor WASM applications if the environment name isn't specified in the `AbpApplicationCreationOptions`. -3. Sets the environment name as **Production**, if the environment name is not specified or can not be obtained from the services. - -You can configure the `AbpApplicationCreationOptions` [options class](Options.md) while creating the ABP application and set an environment name to its `Environment` property. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `Environment` option as shown below: - -```csharp -await builder.AddApplicationAsync(options => -{ - options.Environment = Environments.Staging; //or directly set as "Staging" -}); -``` - -Then, whenever you need to get the current environment name or check the environment, you can use the `IAbpHostEnvironment` interface: - -```csharp -public class MyDemoService -{ - private readonly IAbpHostEnvironment _abpHostEnvironment; - - public MyDemoService(IAbpHostEnvironment abpHostEnvironment) - { - _abpHostEnvironment = abpHostEnvironment; - } - - public void MyMethod() - { - var environmentName = _abpHostEnvironment.EnvironmentName; - - if (_abpHostEnvironment.IsDevelopment()) { /* ... */ } - - if (_abpHostEnvironment.IsStaging()) { /* ... */ } - - if (_abpHostEnvironment.IsProduction()) { /* ... */ } - - if (_abpHostEnvironment.IsEnvironment("custom-environment")) { /* ... */ } - } -} -``` - -## .NET Generic Host & ASP.NET Core Integrations - -`AbpApplicationFactory` can create a standalone ABP application container without any external dependency. However, in most cases, you will want to integrate it with [.NET's generic host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host) or ASP.NET Core. For such usages, ABP provides built-in extension methods to easily create an ABP application container that is well-integrated to these systems. - -The [Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application](Getting-Started-AspNetCore-Application.md) document clearly explains how you can create an ABP application container in an ASP.NET Core application. - -You can also [create a console application](Startup-Templates/Console) to see how it is integrated with .NET Generic Host. - -> Most of the times, you will directly create ABP applications using the ABP CLI's `new` command. So, you don't need to care about these integration details. - -## See Also - -* [Dependency injection](Dependency-Injection.md) -* [Modularity](Module-Development-Basics.md) diff --git a/docs/en/Apps/VoloDocs.md b/docs/en/Apps/VoloDocs.md deleted file mode 100644 index 06d8d0bf66..0000000000 --- a/docs/en/Apps/VoloDocs.md +++ /dev/null @@ -1,183 +0,0 @@ -# VoloDocs - -## What is VoloDocs? - -VoloDocs is a cross-platform web application that allows you to easily create beautiful documentation and build developer communities. It simplifies software documentation with the help of GitHub integration. You use the power of GitHub for versioning, hosting of your docs. You let your users to edit a document. - -## Main Features - -- Serves documents from your GitHub repository. -- Supports Markdown / HTML document formatting. -- Supports versioning (integrated to GitHub releases). -- Supports multiple projects. -- Allows users to edit a document on GitHub. -- Cross-platform; deployable to Windows / Linux / macOS. - -## GitHub Repository - -It's free & open-source. You can browse VoloDocs source-code and contribute on GitHub: - -https://github.com/abpframework/abp/tree/master/modules/docs - -## Download - -You can download the VoloDocs release from the following links: - -https://apps.abp.io/VoloDocs/VoloDocs.win-x64.zip - **Windows 64 bit** - -https://apps.abp.io/VoloDocs/VoloDocs.win-x86.zip - **Windows 32 bit** - -https://apps.abp.io/VoloDocs/VoloDocs.osx-x64.zip - **MacOS** - -https://apps.abp.io/VoloDocs/VoloDocs.linux-x64.zip - **Linux** - -Notice that, all installations are self-contained deployments. It means all the required third-party dependencies along with the version of .NET Core is included. So you don't need to install any .NET Core SDK / Runtime. - -## Folder Structure - -When you extract the `VoloDocs.*.zip` file, you will see a `Web` folder and a `Migrator` folder. The `Web` folder contains the website files and `Migrator` contains the application to build your database. Before publishing your website, you need to create a new database or update your existing database to the latest. If this is the first time you install VoloDocs, `Migrator` will create a new database for you, otherwise it updates to the latest version. The only setting you need to configure, is the `ConnectionString` which is located in the `appsettings.json` file. See the next section to learn how to configure your VoloDocs application. - -## Steps by Step Deployment - -- ### Database Migration - - To update your existing database or create your initial database, go to `Migrator` folder in your VoloDocs directory. - - Open `appsettings.json` in your text editor and set your database connection string. If you don't know how to write the connection string for your database system, you can check out https://www.connectionstrings.com/. - - After you set your connection string, run `Migrate.bat` for Windows platform and `VoloDocs.Migrator` for other operating systems. That's it now configure your website. - -- ### Configuring Website - - Go to `Web` folder in your VoloDocs directory. Open `appsettings.json` in your text editor. Set your connection string (same as in the `Migrator`'s `appsettings.json`). That's it! Now you can publish your website. - - If you want to run - -- ### Deploying Website - - In the previous step, you created or updated your database. Ensure that your database exists on the specified connection string. - - - #### Deploying to IIS - - - Move `Web` folder to your `wwwroot ` folder. - - Rename `Web` folder to `VoloDocs` (Now you have `C:\inetpub\wwwroot\VoloDocs`).![Add IIS Website](../images/volodocs-iis-add-website.png) - - The `VoloDocs` application pool is being created automatically. Open **Application Pools** and double click `VoloDocs` application pool and set - - **.NET CLR version**: `No Managed Code` - - **Managed pipeline mode**: `Integrated` - - ![Add IIS Website](../images/volodocs-iis-application-pool.png) - - - - - If you get the below error, it means don't have the hosting bundle installed on the server. See [this document](https://docs.microsoft.com/aspnet/core/host-and-deploy/iis/#install-the-net-core-hosting-bundle) to learn how to install it or [download Hosting Bundle](https://www.microsoft.com/net/permalink/dotnetcore-current-windows-runtime-bundle-installer) and run on your server. - - ``` - Handler "aspNetCore" has a bad module "AspNetCoreModuleV2" in its module list using IIS - ``` - - - Further information about hosting VoloDocs check out [Microsoft's official document for hosting ASP.NET Core application on IIS](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis). - - - #### Deploying to Azure - - Microsoft has a good document on how to deploy your ASP.NET Core web app to Azure App Service. We recommend you to read this document https://docs.microsoft.com/en-us/azure/app-service/app-service-web-get-started-dotnet. - - - #### Running the Application From Command Line - - Alternatively you can run the application from command line, navigate to `VoloDocs\Web` folder and run `VoloDocs.Web.exe` for Windows or `VoloDocs.Web` for MacOS / Linux. - -- ### First Run - - To start the website, navigate to your address (as configured in the previous section). - - When you first open the website, you need to create a project. - - #### Creating a Project - - Go to the following address to create project - - - `http:///Account/Login?returnUrl=/Docs/Admin/Projects` - - ##### Default credentials - - To login the admin side, use the following credentials: - - * **Username**: `admin` - - * **Password**: `1q2w3E*` - - ##### An example project definition - - Here's a sample project information that uses GitHub source. - - We will configure the VoloDocs to show ABP Framework's documentation that's stored in GitHub. - - Here's the link to ABP Framework GitHub docs folder: - - https://github.com/abpframework/abp/tree/master/docs/en - - - - * **Name**: `ABP Framework` - - * **Short name**: `abp` - - * **Format**: `markdown` - - * **Default document name**: `Index` - - * **Navigation document name**: `docs-nav.json` ([see the sample navigation](https://github.com/abpframework/abp/blob/master/docs/en/docs-nav.json)) - - * **Parameters Document Name**: `docs-params.json` ([see the sample parameters](https://github.com/abpframework/abp/blob/dev/docs/en/docs-params.json)) - - * **Minimum version**: *leave empty* *(hides the previous versions)* - - * **Main web site URL**: `/` - - * **Latest version branch name**: leave empty () - - * **GitHub root URL**: `https://github.com/abpframework/abp/tree/{version}/docs/` - - * **GitHub access token**: [see how to retrieve GitHub access token](#retrieving-github-access-token) - - * **GitHub user agent**: [see how to learn your GitHub username](#learn-your-github-username) - - * **GitHub version provider source**: `Releases` (other option is `Branches`) - - * **Version branch prefix**: leave empty () - - ![Creating a new project](../images/docs-create-project-v4.4.0.png) - - ##### Retrieving GitHub Access Token - - To create a personal access token in GitHub, you need to visit the **Settings** of the user account and under **Developer settings** you will find **Personal access tokens**. Select **Generate new token**, enter in a name as the Token description and enable the repo checkbox. Alternatively, to enter generate new token, browse to https://github.com/settings/tokens/new. - - ###### Generate Token for Public Repositories - - To access public repositories, check `public_repo` under the `repo` section. This will enable VoloDocs to access your public GitHub repositories. Click `Generate Token` button on the bottom of the page. - - ![Retrieve GitHub Access Token for Public Repo](../images/github-access-token-public-repo.jpg) - - ###### Generate Token for Private Repositories - - To access public repositories, check all items under the `repo` section. This will enable VoloDocs to access your private GitHub repositories. Click `Generate Token` button on the bottom of the page. - - ![Retrieve GitHub Access Token for Private Repo](../images/github-access-token-private-repo.jpg) - - ###### Learn Your GitHub Username - - To learn your GitHub username, click on your profile picture on the top-right corner of the GitHub page. You will see your username right after the text "Signed in as ..." - - ![Your GitHub Username](../images/github-myusername.jpg) - - - -After you save the project, go to root website address and you will see your documentation. - -`http:///documents` - -### Any Issues? - -If you encounter any problem or issues about installation, usage or report a bug, follow the link: - -https://github.com/abpframework/abp/issues/new - diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md deleted file mode 100644 index e337b78a6d..0000000000 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ /dev/null @@ -1,731 +0,0 @@ -# Migrating from ASP.NET Boilerplate to the ABP Framework - -ABP Framework is **the successor** of the open source [ASP.NET Boilerplate](https://aspnetboilerplate.com/) framework. This guide aims to help you to **migrate your existing solutions** (you developed with the ASP.NET Boilerplate framework) to the ABP Framework. - -## Introduction - -**ASP.NET Boilerplate** is being **actively developed** [since 2013](https://github.com/aspnetboilerplate/aspnetboilerplate/graphs/contributors). It is loved, used and contributed by the community. It started as a side project of [a developer](http://halilibrahimkalkan.com/), but now it is officially maintained and improved by the company [Volosoft](https://volosoft.com/) in addition to the great community support. - -ABP Framework has the same goal of the ASP.NET Boilerplate framework: **Don't Repeat Yourself**! It provides infrastructure, tools and startup templates to make a developer's life easier while developing enterprise software solutions. - -See [the introduction blog post](https://blog.abp.io/abp/Abp-vNext-Announcement) if you wonder why we needed to re-write the ASP.NET Boilerplate framework. - -### Should I Migrate? - -No, you don't have to! - -* ASP.NET Boilerplate is still in active development and maintenance. -* It also works on the latest ASP.NET Core and related libraries and tools. It is up to date. - -However, if you want to take the advantage of the new ABP Framework [features](https://abp.io/features) and the new architecture opportunities (like support for NoSQL databases, microservice compatibility, advanced modularity), you can use this document as a guide. - -### What About the ASP.NET Zero? - -[ASP.NET Zero](https://aspnetzero.com/) is a commercial product developed by the core ASP.NET Boilerplate team, on top of the ASP.NET Boilerplate framework. It provides pre-built application [features](https://aspnetzero.com/Features), code generation tooling and a nice looking modern UI. It is trusted and used by thousands of companies from all around the World. - -We have created the [ABP Commercial](https://commercial.abp.io/) as an alternative to the ASP.NET Zero. ABP Commercial is more modular and upgradeable compared to the ASP.NET Zero. It currently has less features compared to ASP.NET Zero, but the gap will be closed by the time (it also has some features don't exist in the ASP.NET Zero). - -We think ASP.NET Zero is still a good choice while starting a new application. It is production ready and mature solution delivered as a full source code. It is being actively developed and we are constantly adding new features. - -We don't suggest to migrate your ASP.NET Zero based solution to the ABP Commercial if; - -* Your ASP.NET Zero solution is mature and it is in maintenance rather than a rapid development. -* You don't have enough development time to perform the migration. -* A monolithic solution fits in your business. -* You've customized existing ASP.NET Zero features too much based on your requirements. - -We also suggest you to compare the features of two products based on your needs. - -If you have an ASP.NET Zero based solution and want to migrate to the ABP Commercial, this guide will also help you. - -### ASP.NET MVC 5.x Projects - -The ABP Framework doesn't support ASP.NET MVC 5.x, it only works with ASP.NET Core. So, if you migrate your ASP.NET MVC 5.x based projects, you will also deal with the .NET Core migration. - -## The Migration Progress - -We've designed the ABP Framework by **getting the best parts** of the ASP.NET Boilerplate framework, so it will be familiar to you if you've developed ASP.NET Boilerplate based applications. - -In the ASP.NET Boilerplate, we have not worked much on the UI side, but used some free themes (we've used [metronic theme](https://keenthemes.com/metronic/) for ASP.NET Zero on the other side). In the ABP Framework, we worked a lot on the UI side (especially for the MVC / Razor Pages UI, because Angular already has a good modular system of its own). So, the **most challenging part** of the migration will be the **User Interface** of your solution. - -ABP Framework is (and ASP.NET Boilerplate was) designed based on the [Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) patterns & principles and the startup templates are layered based on the DDD layers. So, this guide respects to that layering model and explains the migration layer by layer. - -## Creating the Solution - -First step of the migration is to create a new solution. We suggest you to create a fresh new project using [the startup templates](https://abp.io/get-started) (see [this document](https://docs.abp.io/en/commercial/latest/getting-started) for the ABP Commercial). - -After creating the project and running the application, you can copy your code from your existing solution to the new solution step by step, layer by layer. - -### About Pre-Built Modules - -The startup projects for the ABP Framework use the [pre-built modules](https://docs.abp.io/en/abp/latest/Modules/Index) (not all of them, but the essentials) and themes as NuGet/NPM packages. So, you don't see the source code of the modules/themes in your solution. This has an advantage that you can easily update these packages when a new version is released. However, you can not easily customize them as their source code in your hands. - -We suggest to continue to use these modules as package references, in this way you can get new features easily (see [abp update command](https://docs.abp.io/en/abp/latest/CLI#update)). In this case, you have a few options to customize or extend the functionality of the used modules; - -* You can create your own entity and share the same database table with an entity in a used module. An example of this is the `AppUser` entity comes in the startup template. -* You can [replace](https://docs.abp.io/en/abp/latest/Dependency-Injection#replace-a-service) a domain service, application service, controller, page model or other types of services with your own implementation. We suggest you to inherit from the existing implementation and override the method you need. -* You can replace a `.cshtml` view, page, view component, partial view... with your own one using the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). -* You can override javascript, css, image or any other type of static files using the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). - -More extend/customization options will be developed and documented by the time. However, if you need to fully change the module implementation, it is best to add the [source code](https://github.com/abpframework/abp/tree/dev/modules) of the related module into your own solution and remove the package dependencies. - -The source code of the modules and the themes are [MIT](https://opensource.org/licenses/MIT) licensed, you can fully own and customize it without any limitation (for the ABP Commercial, you can download the source code of a [module](https://commercial.abp.io/modules)/[theme](https://commercial.abp.io/themes) if you have a [license](https://commercial.abp.io/pricing) type that includes the source code). - -## The Domain Layer - -Most of your domain layer code will remain same, while you need to perform some minor changes in your domain objects. - -### Aggregate Roots & Entities - -The ABP Framework and the ASP.NET Boilerplate both have the `IEntity` and `IEntity` interfaces and `Entity` and `Entity` base classes to define entities but they have some differences. - -If you have an entity in the ASP.NET Boilerplate application like that: - -````csharp -public class Person : Entity //Default PK is int for the ASP.NET Boilerplate -{ - ... -} -```` - -Then your primary key (the `Id` property in the base class) is `int` which is the **default primary key** (PK) type for the ASP.NET Boilerplate. If you want to set another type of PK, you need to explicitly declare it: - -````csharp -public class Person : Entity //Set explicit PK in the ASP.NET Boilerplate -{ - ... -} -```` - -ABP Framework behaves differently and expects to **always explicitly set** the PK type: - -````csharp -public class Person : Entity //Set explicit PK in the ASP.NET Boilerplate -{ - ... -} -```` - -`Id` property (and the corresponding PK in the database) will be `Guid` in this case. - -#### Composite Primary Keys - -ABP Framework also has a non-generic `Entity` base class, but this time it has no `Id` property. Its purpose is to allow you to create entities with composite PKs. See [the documentation](https://docs.abp.io/en/abp/latest/Entities#entities-with-composite-keys) to learn more about the composite PKs. - -#### Aggregate Root - -It is best practice now to use the `AggregateRoot` base class instead of `Entity` for aggregate root entities. See [the documentation](https://docs.abp.io/en/abp/latest/Entities#aggregateroot-class) to learn more about the aggregate roots. - -In opposite to the ASP.NET Boilerplate, the ABP Framework creates default repositories (`IRepository`) **only for the aggregate roots**. It doesn't create for other types derived from the `Entity`. - -If you still want to create default repositories for all entity types, find the *YourProjectName*EntityFrameworkCoreModule class in your solution and change `options.AddDefaultRepositories()` to `options.AddDefaultRepositories(includeAllEntities: true)` (it may be already like that for the application startup template). - -#### Migrating the Existing Entities - -We suggest & use the GUID as the PK type for all the ABP Framework modules. However, you can continue to use your existing PK types to migrate your database tables easier. - -The challenging part will be the primary keys of the ASP.NET Boilerplate related entities, like Users, Roles, Tenants, Settings... etc. Our suggestion is to copy data from existing database to the new database tables using a tool or in a manual way (be careful about the foreign key values). - -#### Documentation - -See the documentation for details on the entities: - -* [ASP.NET Boilerplate - Entity documentation](https://aspnetboilerplate.com/Pages/Documents/Entities) -* [ABP Framework - Entity documentation](https://docs.abp.io/en/abp/latest/Entities) - -### Repositories - -> ABP Framework creates default repositories (`IRepository`) **only for the aggregate roots**. It doesn't create for other types derived from the `Entity`. See the "Aggregate Root" section above for more information. - -The ABP Framework and the ASP.NET Boilerplate both have the default generic repository system, but has some differences. - -#### Injecting the Repositories - -In the ASP.NET Boilerplate, there are two default repository interfaces you can directly inject and use: - -* `IRepository` (e.g. `IRepository`) is used for entities with `int` primary key (PK) which is the default PK type. -* `IRepository` (e.g. `IRepository`) is used for entities with other types of PKs. - -ABP Framework doesn't have a default PK type, so you need to **explicitly declare the PK type** of your entity, like `IRepository` or `IRepository`. - -ABP Framework also has the `IRepository` (without PK), but it is mostly used when your entity has a composite PK (because this repository has no methods work with the `Id` property). See [the documentation](https://docs.abp.io/en/abp/latest/Entities#entities-with-composite-keys) to learn more about the **composite PKs**. - -#### Restricted Repositories - -ABP Framework additionally provides a few repository interfaces: - -* `IBasicRepository` has the same methods with the `IRepository` except it doesn't have `IQueryable` support. It can be useful if you don't want to expose complex querying code to the application layer. In this case, you typically want to create custom repositories to encapsulate the querying logic. It is also useful for database providers those don't support `IQueryable`. -* `IReadOnlyRepository` has the methods get data from the database, but doesn't contain any method change the database. -* `IReadOnlyBasicRepository` is similar to the read only repository but also doesn't support `IQueryable`. - -All the interfaces also have versions without `TKey` (like ``IReadOnlyRepository`) those can be used for composite PKs just like explained above. - -#### GetAll() vs IQueryable - -ASP.NET Boilerplate's repository has a `GetAll()` method that is used to obtain an `IQueryable` object to execute LINQ on it. An example application service calls the `GetAll()` method: - -````csharp -public class PersonAppService : ApplicationService, IPersonAppService -{ - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task DoIt() - { - var people = await _personRepository - .GetAll() //GetAll() returns IQueryable - .Where(p => p.BirthYear > 2000) //Use LINQ extension methods - .ToListAsync(); - } -} -```` - -ABP Framework's repository have `GetQueryableAsync` instead: - -````csharp -public class PersonAppService : ApplicationService, IPersonAppService -{ - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task DoIt() - { - var queryable = await _personRepository.GetQueryableAsync(); - var people = await queryable - .Where(p => p.BirthYear > 2000) //Use LINQ extension methods - .ToListAsync(); - } -} -```` - -> Note that in order to use the async LINQ extension methods (like `ToListAsync` here), you may need to depend on the database provider (like EF Core) since these methods are defined in the database provider package, they are not standard LINQ methods. See the [repository document](Repositories.md) for alternative approaches for async query execution. - -#### FirstOrDefault(predicate), Single()... Methods - -ABP Framework repository has not such methods get predicate (expression) since the repository itself is `IQueryable` and all these methods are already standard LINQ extension methods those can be directly used. - -However, it provides the following methods those can be used to query a single entity by its Id: - -* `FindAsync(id)` returns the entity or null if not found. -* `GetAsync(id)` method returns the entity or throws an `EntityNotFoundException` (which causes HTTP 404 status code) if not found. - -#### Sync vs Async - -ABP Framework repository has no sync methods (like `Insert`). All the methods are async (like `InsertAsync`). So, if your application has sync repository method usages, convert them to async versions. - -In general, ABP Framework forces you to completely use async everywhere, because mixing async & sync methods is not a recommended approach. - -#### Documentation - -See the documentation for details on the repositories: - -* [ASP.NET Boilerplate - Repository documentation](https://aspnetboilerplate.com/Pages/Documents/Repositories) -* [ABP Framework - Repository documentation](https://docs.abp.io/en/abp/latest/Repositories) - -### Domain Services - -Your domain service logic mostly remains same on the migration. ABP Framework also defines the base `DomainService` class and the `IDomainService` interface just works like the ASP.NET Boilerplate. - -## The Application Layer - -Your application service logic remains similar on the migration. ABP Framework also defines the base `ApplicationService` class and the `IApplicationService` interface just works like the ASP.NET Boilerplate, but there are some differences in details. - -### Declarative Authorization - -ASP.NET Boilerplate has `AbpAuthorize` and `AbpMvcAuthorize` attributes for declarative authorization. Example usage: - -````csharp -[AbpAuthorize("MyUserDeletionPermissionName")] -public async Task DeleteUserAsync(...) -{ - ... -} -```` - -ABP Framework doesn't has such a custom attribute. It uses the standard `Authorize` attribute in all layers. - -````csharp -[Authorize("MyUserDeletionPermissionName")] -public async Task DeleteUserAsync(...) -{ - ... -} -```` - -This is possible with the better integration to the Microsoft Authorization Extensions libraries. See the Authorization section below for more information about the authorization system. - -### CrudAppService and AsyncCrudAppService Classes - -ASP.NET Boilerplate has `CrudAppService` (with sync service methods) and `AsyncCrudAppService` (with async service methods) classes. - -ABP Framework only has the `CrudAppService` which actually has only the async methods (instead of sync methods). - -ABP Framework's `CrudAppService` method signatures are slightly different than the old one. For example, old update method signature was ` Task UpdateAsync(TUpdateInput input) ` while the new one is ` Task UpdateAsync(TKey id, TUpdateInput input) `. The main difference is that it gets the Id of the updating entity as a separate parameter instead of including in the input DTO. - -### Data Transfer Objects (DTOs) - -There are similar base DTO classes (like `EntityDto`) in the ABP Framework too. So, you can find the corresponding DTO base class if you need. - -#### Validation - -You can continue to use the data annotation attributes to validate your DTOs just like in the ASP.NET Boilerplate. - -ABP Framework doesn't include the ` ICustomValidate ` that does exists in the ASP.NET Boilerplate. Instead, you should implement the standard `IValidatableObject` interface for your custom validation logic. - -## The Infrastructure Layer - -### Namespaces - -ASP.NET Boilerplate uses the `Abp.*` namespaces while the ABP Framework uses the `Volo.Abp.*` namespaces for the framework and pre-built fundamental modules. - -In addition, there are also some pre-built application modules (like docs and blog modules) those are using the `Volo.*` namespaces (like `Volo.Blogging.*` and `Volo.Docs.*`). We consider these modules as standalone open source products developed by Volosoft rather than add-ons or generic modules completing the ABP Framework and used in the applications. We've developed them as a module to make them re-usable as a part of a bigger solution. - -### Module System - -Both of the ASP.NET Boilerplate and the ABP Framework have the `AbpModule` while they are a bit different. - -ASP.NET Boilerplate's `AbpModule` class has `PreInitialize`, `Initialize` and `PostInitialize` methods you can override and configure the framework and the depended modules. You can also register and resolve dependencies in these methods. - -ABP Framework's `AbpModule` class has the `ConfigureServices` and `OnApplicationInitialization` methods (and their Pre and Post versions). It is similar to ASP.NET Core's Startup class. You configure other services and register dependencies in the `ConfigureServices`. However, you can now resolve dependencies in that point. You can resolve dependencies and configure the ASP.NET Core pipeline in the `OnApplicationInitialization` method while you can not register dependencies here. So, the new module classes separate dependency registration phase from dependency resolution phase since it follows the ASP.NET Core's approach. - -### Dependency Injection - -#### The DI Framework - -ASP.NET Boilerplate is using the [Castle Windsor](http://www.castleproject.org/projects/windsor/) as the dependency injection framework. This is a fundamental dependency of the ASP.NET Boilerplate framework. We've got a lot of feedback to make the ASP.NET Boilerplate DI framework agnostic, but it was not so easy because of the design. - -ABP Framework is dependency injection framework independent since it uses Microsoft's [Dependency Injection Extensions](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) library as an abstraction. None of the ABP Framework or module packages depends on any specific library. - -However, ABP Framework doesn't use the Microsoft's base DI library because it has some missing features ABP Framework needs to: Property Injection and Interception. All the startup templates and the samples are using the [Autofac](https://autofac.org/) as the DI library and it is the only [officially integrated](Autofac-Integration.md) library to the ABP Framework. We suggest you to use the Autofac with the ABP Framework if you have not a good reason. If you have a good reason, please create an [issue](https://github.com/abpframework/abp/issues/new) on GitHub to request it or just implement it and send a pull request :) - -#### Registering the Dependencies - -Registering the dependencies are similar and mostly handled by the framework conventionally (like repositories, application services, controllers... etc). Implement the same `ITransientDependency`, `ISingletonDependency` and `IScopedDependency` interfaces for the services not registered by conventions. - -When you need to manually register dependencies, use the `context.Services` in the `ConfigureServices` method of your module. Example: - -````csharp -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Register an instance as singleton - context.Services.AddSingleton(new TaxCalculator(taxRatio: 0.18)); - - //Register a factory method that resolves from IServiceProvider - context.Services.AddScoped( - sp => sp.GetRequiredService() - ); - } -} -```` - -See the ABP Framework [dependency injection document](https://docs.abp.io/en/abp/latest/Dependency-Injection) for details. - -### Configuration vs Options System - -ASP.NET Boilerplate has its own configuration system to configure the framework and the modules. For example, you could disable the audit logging in the `Initialize` method of your [module](https://aspnetboilerplate.com/Pages/Documents/Module-System): - -````csharp -public override void Initialize() -{ - Configuration.Auditing.IsEnabled = false; -} -```` - -ABP Framework uses [the options pattern](Options.md) to configure the framework and the modules. You typically configure the options in the `ConfigureServices` method of your [module](Module-Development-Basics.md): - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.IsEnabled = false; - }); -} -```` - -Instead of a central configuration object, there are separated option classes for every module and feature those are defined in the related documents. - -### IAbpSession vs ICurrentUser and ICurrentTenant - -ASP.NET Boilerplate's `IAbpSession` service is used to obtain the current user and tenant information, like ` UserId ` and `TenantId`. - -ABP Framework doesn't have the same service. Instead, use `ICurrentUser` and `ICurrentTenant` services. These services are defined as base properties in some common classes (like `ApplicationService` and `AbpController`), so you generally don't need to manually inject them. They also have much properties compared to the `IAbpSession`. - -### Authorization - -ABP Framework extends the [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing the authorization system to be usable in the [application services](Application-Services.md) too. - -#### AbpAuthorize vs Authorize - -Use the standard `[Authorize]` and `[AllowAnonymous]` attributes instead of ASP.NET Boilerplate's custom `[AbpAuthorize]` and `[AbpAllowAnonymous]` attributes. - -#### IPermissionChecker vs IAuthorizationService - -Use the standard `IAuthorizationService` to check permissions instead of the ASP.NET Boilerplate's `IPermissionChecker` service. While `IPermissionChecker` also exists in the ABP Framework, it is used to explicitly use the permissions. Using `IAuthorizationService` is the recommended way since it covers other type of policy checks too. - -#### AuthorizationProvider vs PermissionDefinitionProvider - -You inherit from the `AuthorizationProvider` in the ASP.NET Boilerplate to define your permissions. ABP Framework replaces it by the `PermissionDefinitionProvider` base class. So, define your permissions by inheriting from the `PermissionDefinitionProvider` class. - -### Unit of Work - -Unit of work system has been designed to work seamlessly. For most of the cases, you don't need to change anything. - -`UnitOfWork` attribute of the ABP Framework doesn't have the `ScopeOption` (type of `TransactionScopeOption`) property. Instead, use `IUnitOfWorkManager.Begin()` method with `requiresNew = true` to create an independent inner transaction in a transaction scope. - -#### Data Filters - -ASP.NET Boilerplate implements the data filtering system as a part of the unit of work. ABP Framework has a separate `IDataFilter` service. - -See the [data filtering document](Data-Filtering.md) to learn how to enable/disable a filter. - -See [the UOW documentation](Unit-Of-Work.md) for more about the UOW system. - -### Multi-Tenancy - -#### IMustHaveTenant & IMayHaveTenant vs IMultiTenant - -ASP.NET Boilerplate defines `IMustHaveTenant` and `IMayHaveTenant` interfaces to implement them for your entities. In this way, your entities are automatically filtered according to the current tenant. Because of the design, there was a problem: You had to create a "Default" tenant in the database with "1" as the Id if you want to create a non multi-tenant application (this "Default" tenant was used as the single tenant). - -ABP Framework has a single interface for multi-tenant entities: `IMultiTenant` which defines a nullable `TenantId` property of type `Guid`. If your application is not multi-tenant, then your entities will have null TenantId (instead of a default one). - -On the migration, you need to change the TenantId field type and replace these interfaces with the `IMultiTenant` - -#### Switch Between Tenants - -In some cases you might need to switch to a tenant for a code scope and work with the tenant's data in this scope. - -In ASP.NET Boilerplate, it is done using the `IUnitOfWorkManager` service: - -````csharp -public async Task> GetProducts(int tenantId) -{ - using (_unitOfWorkManager.Current.SetTenantId(tenantId)) - { - return await _productRepository.GetAllListAsync(); - } -} -```` - -In the ABP Framework it is done with the `ICurrentTenant` service: - -````csharp -public async Task> GetProducts(Guid tenantId) -{ - using (_currentTenant.Change(tenantId)) - { - return await _productRepository.GetListAsync(); - } -} -```` - -Pass `null` to the `Change` method to switch to the host side. - -### Caching - -ASP.NET Boilerplate has its [own distributed caching abstraction](https://aspnetboilerplate.com/Pages/Documents/Caching) which has in-memory and Redis implementations. You typically inject the `ICacheManager` service and use its `GetCache(...)` method to obtain a cache, then get and set objects in the cache. - -ABP Framework uses and extends ASP.NET Core's [distributed caching abstraction](Caching.md). It defines the `IDistributedCache` services to inject a cache and get/set objects. - -### Logging - -ASP.NET Boilerplate uses Castle Windsor's [logging facility](https://github.com/castleproject/Windsor/blob/master/docs/logging-facility.md) as an abstraction and supports multiple logging providers including Log4Net (the default one comes with the startup projects) and Serilog. You typically property-inject the logger: - -````csharp -using Castle.Core.Logging; //1: Import Logging namespace - -public class TaskAppService : ITaskAppService -{ - //2: Getting a logger using property injection - public ILogger Logger { get; set; } - - public TaskAppService() - { - //3: Do not write logs if no Logger supplied. - Logger = NullLogger.Instance; - } - - public void CreateTask(CreateTaskInput input) - { - //4: Write logs - Logger.Info("Creating a new task with description: " + input.Description); - //... - } -} -```` - -ABP Framework depends on Microsoft's [logging extensions](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging) library which is also an abstraction and there are many providers implement it. Startup templates are using the Serilog as the pre-configured logging libary while it is easy to change in your project. The usage pattern is similar: - -````csharp -//1: Import the Logging namespaces -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -public class TaskAppService : ITaskAppService -{ - //2: Getting a logger using property injection - public ILogger Logger { get; set; } - - public TaskAppService() - { - //3: Do not write logs if no Logger supplied. - Logger = NullLogger.Instance; - } - - public void CreateTask(CreateTaskInput input) - { - //4: Write logs - Logger.Info("Creating a new task with description: " + input.Description); - //... - } -} -```` - -You inject the `ILogger` instead of the `ILogger`. - -### Object to Object Mapping - -#### IObjectMapper Service - -ASP.NET Boilerplate defines an `IObjectMapper` service ([see](https://aspnetboilerplate.com/Pages/Documents/Object-To-Object-Mapping)) and has an integration to the [AutoMapper](https://automapper.org/) library. - -Example usage: Create a `User` object with the given `CreateUserInput` object: - -````csharp -public void CreateUser(CreateUserInput input) -{ - var user = ObjectMapper.Map(input); - ... -} -```` - -Example: Update an existing `User` properties with the given `UpdateUserInput` object: - -````csharp -public async Task UpdateUserAsync(Guid id, UpdateUserInput input) -{ - var user = await _userRepository.GetAsync(id); - ObjectMapper.Map(input, user); -} -```` - -ABP Framework has the same `IObjectMapper` service ([see](Object-To-Object-Mapping.md)) and the AutoMapper integration with a slightly different mapping methods. - -Example usage: Create a `User` object with the given `CreateUserInput` object: - -````csharp -public void CreateUser(CreateUserInput input) -{ - var user = ObjectMapper.Map(input); -} -```` - -This time you need to explicitly declare the source type and target type (while ASP.NET Boilerplate was requiring only the target type). - -Example: Update an existing `User` properties with the given `UpdateUserInput` object: - -````csharp -public async Task UpdateUserAsync(Guid id, UpdateUserInput input) -{ - var user = await _userRepository.GetAsync(id); - ObjectMapper.Map(input, user); -} -```` - -Again, ABP Framework expects to explicitly set the source and target types. - -#### AutoMapper Integration - -##### Auto Mapping Attributes - -ASP.NET Boilerplate has `AutoMapTo`, `AutoMapFrom` and `AutoMap` attributes to automatically create mappings for the declared types. Example: - -````csharp -[AutoMapTo(typeof(User))] -public class CreateUserInput -{ - public string Name { get; set; } - public string Surname { get; set; } - ... -} -```` - -ABP Framework has no such attributes, because AutoMapper as a [similar attribute](https://automapper.readthedocs.io/en/latest/Attribute-mapping.html) now. You need to switch to AutoMapper's attribute. - -##### Mapping Definitions - -ABP Framework follows AutoMapper principles closely. You can define classes derived from the `Profile` class to define your mappings. - -##### Configuration Validation - -Configuration validation is a best practice for the AutoMapper to maintain your mapping configuration in a safe way. - -See [the documentation](Object-To-Object-Mapping.md) for more information related to the object mapping. - -### Setting Management - -#### Defining the Settings - -In an ASP.NET Boilerplate based application, you create a class deriving from the `SettingProvider` class, implement the `GetSettingDefinitions` method and add your class to the `Configuration.Settings.Providers` list. - -In the ABP Framework, you need to derive your class from the `SettingDefinitionProvider` and implement the `Define` method. You don't need to register your class since the ABP Framework automatically discovers it. - -#### Getting the Setting Values - -ASP.NET Boilerplate provides the `ISettingManager` to read the setting values in the server side and `abp.setting.get(...)` method in the JavaScript side. - -ABP Framework has the `ISettingProvider` service to read the setting values in the server side and `abp.setting.get(...)` method in the JavaScript side. - -#### Setting the Setting Values - -For ASP.NET Boilerplate, you use the same `ISettingManager` service to change the setting values. - -ABP Framework separates it and provides the setting management module (pre-added to the startup projects) which has the ` ISettingManager ` to change the setting values. This separation was introduced to support tiered deployment scenarios (where `ISettingProvider` can also work in the client application while `ISettingManager ` can also work in the server (API) side). - -### Clock - -ASP.NET Boilerplate has a static `Clock` service ([see](https://aspnetboilerplate.com/Pages/Documents/Timing)) which is used to abstract the `DateTime` kind, so you can easily switch between Local and UTC times. You don't inject it, but just use the `Clock.Now` static method to obtain the current time. - -ABP Framework has the `IClock` service ([see](Timing.md)) which has a similar goal, but now you need to inject it whenever you need it. - -### Event Bus - -ASP.NET Boilerplate has an in-process event bus system. You typically inject the `IEventBus` (or use the static instance `EventBus.Default`) to trigger an event. It automatically triggers events for entity changes (like `EntityCreatingEventData` and `EntityUpdatedEventData`). You create a class by implementing the `IEventHandler` interface. - -ABP Framework separates the event bus into two services: `ILocalEventBus` and `IDistributedEventBus`. - -The local event bus is similar to the event bus of the ASP.NET Boilerplate while the distributed event bus is new feature introduced in the ABP Framework. - -So, to migrate your code; - -* Use the `ILocalEventBus` instead of the `IEventBus`. -* Implement the `ILocalEventHandler` instead of the `IEventHandler`. - -> Note that ABP Framework has also an `IEventBus` interface, but it does exists to be a common interface for the local and distributed event bus. It is not injected and directly used. - -### Feature Management - -Feature system is used in multi-tenant applications to define features of your application check if given feature is available for the current tenant. - -#### Defining Features - -In the ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Feature-Management)), you create a class inheriting from the `FeatureProvider`, override the `SetFeatures` method and add your class to the `Configuration.Features.Providers` list. - -In the ABP Framework ([see](Features.md)), you derive your class from the `FeatureDefinitionProvider` and override the `Define` method. No need to add your class to the configuration, it is automatically discovered by the framework. - -#### Checking Features - -You can continue to use the `RequiresFeature` attribute and `IFeatureChecker` service to check if a feature is enabled for the current tenant. - -#### Changing the Feature Values - -In the ABP Framework you use the `IFeatureManager` to change a feature value for a tenant. - -### Audit Logging - -The ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Audit-Logging)) and the ABP Framework ([see](Audit-Logging.md)) has similar audit logging systems. ABP Framework requires to add `UseAuditing()` middleware to the ASP.NET Core pipeline, which is already added in the startup templates. So, most of the times it will be work out of the box. - -### Localization - -ASP.NET Boilerplate supports XML and JSON files to define the localization key-values for the UI ([see](https://aspnetboilerplate.com/Pages/Documents/Localization)). ABP Framework only supports the JSON formatter localization files ([see](Localization.md)). So, you need to convert your XML file to JSON. - -The ASP.NET Boilerplate has its own the `ILocalizationManager` service to be injected and used for the localization in the server side. - -The ABP Framework uses [Microsoft localization extension](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) library, so it is completely integrated to ASP.NET Core. You use the `IStringLocalizer` service to get a localized text. Example: - -````csharp -public class MyService -{ - private readonly IStringLocalizer _localizer; - - public MyService(IStringLocalizer localizer) - { - _localizer = localizer; - } - - public void Foo() - { - var str = _localizer["HelloWorld"]; //Get a localized text - } -} -```` - -So, you need to replace `ILocalizationManager` usage by the `IStringLocalizer`. - -It also provides API used in the client side: - -````js -var testResource = abp.localization.getResource('Test'); -var str = testResource('HelloWorld'); -```` - -It was like `abp.localization.localize(...)` in the ASP.NET Boilerplate. - -### Navigation vs Menu - -In ASP.NET Boilerplate you create a class deriving from the `NavigationProvider` to define your menu elements. Menu items has `requiredPermissionName` attributes to restrict access to a menu element. Menu items were static and your class is executed only one time. - -In the ABP Framework you need to create a class implements the `IMenuContributor` interface. Your class is executed whenever the menu needs to be rendered. So, you can conditionally add menu items. - -As an example, this is the menu contributor of the tenant management module: - -````csharp -public class AbpTenantManagementWebMainMenuContributor : IMenuContributor -{ - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - //Add items only to the main menu - if (context.Menu.Name != StandardMenus.Main) - { - return; - } - - //Get the standard administration menu item - var administrationMenu = context.Menu.GetAdministration(); - - //Resolve some needed services from the DI container - var l = context.GetLocalizer(); - - var tenantManagementMenuItem = new ApplicationMenuItem( - TenantManagementMenuNames.GroupName, - l["Menu:TenantManagement"], - icon: "fa fa-users"); - - administrationMenu.AddItem(tenantManagementMenuItem); - - //Conditionally add the "Tenants" menu item based on the permission - if (await context.IsGrantedAsync(TenantManagementPermissions.Tenants.Default)) - { - tenantManagementMenuItem.AddItem( - new ApplicationMenuItem( - TenantManagementMenuNames.Tenants, - l["Tenants"], - url: "/TenantManagement/Tenants")); - } - } -} -```` - -So, you need to check permission using the `IAuthorizationService` if you want to show a menu item only when the user has the related permission. - -> Navigation/Menu system is only for ASP.NET Core MVC / Razor Pages applications. Angular applications has a different system implemented in the startup templates. - -## Missing Features - -The following features are not present for the ABP Framework. Here, a list of some major missing features (and the related issue for that feature waiting on the ABP Framework GitHub repository): - -* [Multi-Lingual Entities](https://aspnetboilerplate.com/Pages/Documents/Multi-Lingual-Entities) ([#1754](https://github.com/abpframework/abp/issues/1754)) -* [Real time notification system](https://aspnetboilerplate.com/Pages/Documents/Notification-System) ([#633](https://github.com/abpframework/abp/issues/633)) -* [NHibernate Integration](https://aspnetboilerplate.com/Pages/Documents/NHibernate-Integration) ([#339](https://github.com/abpframework/abp/issues/339)) - We don't intent to work on this, but any community contribution welcome. - -Some of these features will eventually be implemented. However, you can implement them yourself if they are important for you. If you want, you can [contribute](Contribution/Index.md) to the framework, it is appreciated. diff --git a/docs/en/AspNetCore/Auto-API-Controllers.md b/docs/en/AspNetCore/Auto-API-Controllers.md deleted file mode 100644 index 52db2bde06..0000000000 --- a/docs/en/AspNetCore/Auto-API-Controllers.md +++ /dev/null @@ -1,3 +0,0 @@ -This document has moved. - -[Click to navigate to Auto API Controllers document](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Bundling-Minification.md b/docs/en/AspNetCore/Bundling-Minification.md deleted file mode 100644 index a67fbe429d..0000000000 --- a/docs/en/AspNetCore/Bundling-Minification.md +++ /dev/null @@ -1,4 +0,0 @@ - -This document has moved. - -[Click to navigate to ASP.NET Core MVC Bundling & Minification document](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Client-Side-Package-Management.md b/docs/en/AspNetCore/Client-Side-Package-Management.md deleted file mode 100644 index 780633de50..0000000000 --- a/docs/en/AspNetCore/Client-Side-Package-Management.md +++ /dev/null @@ -1,4 +0,0 @@ - -This document has moved. - -[Click to navigate to ASP.NET Core MVC Client Side Package Management document](../UI/AspNetCore/Client-Side-Package-Management.md) diff --git a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md deleted file mode 100644 index 4ca26e8220..0000000000 --- a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md +++ /dev/null @@ -1,3 +0,0 @@ -This document has moved. - -[Click to navigate to Dynamic C# API Clients document](../API/Dynamic-CSharp-API-Clients.md) diff --git a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md deleted file mode 100644 index 35382845ce..0000000000 --- a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ /dev/null @@ -1,3 +0,0 @@ -This document has moved. - -[Click to navigate to Dynamic Forms document](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/Index.md b/docs/en/AspNetCore/Tag-Helpers/Index.md deleted file mode 100644 index c15c645feb..0000000000 --- a/docs/en/AspNetCore/Tag-Helpers/Index.md +++ /dev/null @@ -1,3 +0,0 @@ -This document has moved. - -[Click to navigate to ABP Tag Helpers document](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/en/AspNetCore/Theming.md b/docs/en/AspNetCore/Theming.md deleted file mode 100644 index 716cf5fa9e..0000000000 --- a/docs/en/AspNetCore/Theming.md +++ /dev/null @@ -1,4 +0,0 @@ - -This document has moved. - -[Click to navigate to Theming document](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Widgets.md b/docs/en/AspNetCore/Widgets.md deleted file mode 100644 index 562678c1d8..0000000000 --- a/docs/en/AspNetCore/Widgets.md +++ /dev/null @@ -1,4 +0,0 @@ - -This document has moved. - -[Click to navigate to Widgets document](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/Aspect-Oriented-Programming.md b/docs/en/Aspect-Oriented-Programming.md deleted file mode 100644 index 76a23daa8f..0000000000 --- a/docs/en/Aspect-Oriented-Programming.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic Proxying / Interceptors - -TODO \ No newline at end of file diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md deleted file mode 100644 index ffacc1b10f..0000000000 --- a/docs/en/Audit-Logging.md +++ /dev/null @@ -1,391 +0,0 @@ -# Audit Logging - -[Wikipedia](https://en.wikipedia.org/wiki/Audit_trail): "*An audit trail (also called **audit log**) is a security-relevant chronological record, set of records, and/or destination and source of records that provide documentary evidence of the sequence of activities that have affected at any time a specific operation, procedure, or event*". - -ABP Framework provides an **extensible audit logging system** that automates the audit logging by **convention** and provides **configuration** points to control the level of the audit logs. - -An **audit log object** (see the Audit Log Object section below) is typically created & saved per web request. It includes; - -* **Request & response details** (like URL, Http method, Browser info, HTTP status code... etc.). -* **Performed actions** (controller actions and application service method calls with their parameters). -* **Entity changes** occurred in the web request. -* **Exception** information (if there was an error while executing the request). -* **Request duration** (to measure the performance of the application). - -> [Startup templates](Startup-Templates/Index.md) are configured for the audit logging system which is suitable for most of the applications. Use this document for a detailed control over the audit log system. - -### Database Provider Support - -* Fully supported by the [Entity Framework Core](Entity-Framework-Core.md) provider. -* Entity change logging is not supported by the [MongoDB](MongoDB.md) provider. Other features work as expected. - -## UseAuditing() - -`UseAuditing()` middleware should be added to the ASP.NET Core request pipeline in order to create and save the audit logs. If you've created your applications using [the startup templates](Startup-Templates/Index.md), it is already added. - -## AbpAuditingOptions - -`AbpAuditingOptions` is the main [options object](Options.md) to configure the audit log system. You can configure it in the `ConfigureServices` method of your [module](Module-Development-Basics.md): - -````csharp -Configure(options => -{ - options.IsEnabled = false; //Disables the auditing system -}); -```` - -Here, a list of the options you can configure: - -* `IsEnabled` (default: `true`): A root switch to enable or disable the auditing system. Other options is not used if this value is `false`. -* `HideErrors` (default: `true`): Audit log system hides and write regular [logs](Logging.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors. -* `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users. -* `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging). -* `IsEnabledForIntegrationService` (default: `false`): Audit Logging is disabled for [integration services](Integration-Services.md) by default. Set this property as `true` to enable it. -* `IsEnabledForGetRequests` (default: `false`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET requests. -* `DisableLogActionInfo` (default: `false`):If you set to true, Will no longer log `AuditLogActionInfo`. -* `ApplicationName`: If multiple applications are saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. If you don't set, it will set from the `IApplicationInfoAccessor.ApplicationName` value, which is the entry assembly name by default. -* `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters. -* `EntityHistorySelectors`: A list of selectors those are used to determine if an entity type is selected for saving the entity change. See the section below for details. -* `SaveEntityHistoryWhenNavigationChanges` (default: `true`): If you set to true, it will save entity changes to audit log when any navigation property changes. -* `Contributors`: A list of `AuditLogContributor` implementations. A contributor is a way of extending the audit log system. See the "Audit Log Contributors" section below. -* `AlwaysLogSelectors`: A list of selectors to save the audit logs for the matched criteria. - -### Entity History Selectors - -Saving all changes of all your entities would require a lot of database space. For this reason, **audit log system doesn't save any change for the entities unless you explicitly configure it**. - -To save all changes of all entities, simply use the `AddAllEntities()` extension method. - -````csharp -Configure(options => -{ - options.EntityHistorySelectors.AddAllEntities(); -}); -```` - -`options.EntityHistorySelectors` actually a list of type predicate. You can write a lambda expression to define your filter. - -The example selector below does the same of the `AddAllEntities()` extension method defined above: - -````csharp -Configure(options => -{ - options.EntityHistorySelectors.Add( - new NamedTypeSelector( - "MySelectorName", - type => - { - if (typeof(IEntity).IsAssignableFrom(type)) - { - return true; - } - else - { - return false; - } - } - ) - ); -}); -```` - -The condition `typeof(IEntity).IsAssignableFrom(type)` will be `true` for any class implements the `IEntity` interface (this is technically all the entities in your application). You can conditionally check and return `true` or `false` based on your preference. - -`options.EntityHistorySelectors` is a flexible and dynamic way of selecting the entities for audit logging. Another way is to use the `Audited` and `DisableAuditing` attributes per entity. - -## AbpAspNetCoreAuditingOptions - -`AbpAspNetCoreAuditingOptions` is the [options object](Options.md) to configure audit logging in the ASP.NET Core layer. You can configure it in the `ConfigureServices` method of your [module](Module-Development-Basics.md): - -````csharp -Configure(options => -{ - options.IgnoredUrls.Add("/products"); -}); -```` - -`IgnoredUrls` is the only option. It is a list of ignored URLs prefixes. In the preceding example, all URLs starting with `/products` will be ignored for audit logging. - -## Enabling/Disabling Audit Logging for Services - -### Enable/Disable for Controllers & Actions - -All the controller actions are logged by default (see `IsEnabledForGetRequests` above for GET requests). - -You can use the `[DisableAuditing]` to disable it for a specific controller type: - -````csharp -[DisableAuditing] -public class HomeController : AbpController -{ - //... -} -```` - -Use `[DisableAuditing]` for any action to control it in the action level: - -````csharp -public class HomeController : AbpController -{ - [DisableAuditing] - public async Task Home() - { - //... - } - - public async Task OtherActionLogged() - { - //... - } -} -```` - -### Enable/Disable for Application Services & Methods - -[Application service](Application-Services.md) method calls also included into the audit log by default. You can use the `[DisableAuditing]` in service or method level. - -#### Enable/Disable for Other Services - -Action audit logging can be enabled for any type of class (registered to and resolved from the [dependency injection](Dependency-Injection.md)) while it is only enabled for the controllers and the application services by default. - -Use `[Audited]` and `[DisableAuditing]` for any class or method that need to be audit logged. In addition, your class can (directly or inherently) implement the `IAuditingEnabled` interface to enable the audit logging for that class by default. - -### Enable/Disable for Entities & Properties - -An entity is ignored on entity change audit logging in the following cases; - -* If you add an entity type to the `AbpAuditingOptions.IgnoredTypes` (as explained before), it is completely ignored in the audit logging system. -* If the object is not an [entity](Entities.md) (not implements `IEntity` directly or inherently - All entities implement this interface by default). -* If entity type is not public. - -Otherwise, you can use `Audited` to enable entity change audit logging for an entity: - -````csharp -[Audited] -public class MyEntity : Entity -{ - //... -} -```` - -Or disable it for an entity: - -````csharp -[DisableAuditing] -public class MyEntity : Entity -{ - //... -} -```` - -Disabling audit logging can be necessary only if the entity is being selected by the `AbpAuditingOptions.EntityHistorySelectors` that explained before. - -You can disable auditing only some properties of your entities for a detailed control over the audit logging: - -````csharp -[Audited] -public class MyUser : Entity -{ - public string Name { get; set; } - - public string Email { get; set; } - - [DisableAuditing] //Ignore the Passoword on audit logging - public string Password { get; set; } -} -```` - -Audit log system will save changes for the `MyUser` entity while it ignores the `Password` property which can be dangerous to save for security purposes. - -In some cases, you may want to save a few properties but ignore all others. Writing `[DisableAuditing]` for all the other properties would be tedious. In such cases, use `[Audited]` only for the desired properties and mark the entity with the `[DisableAuditing]` attribute: - -````csharp -[DisableAuditing] -public class MyUser : Entity -{ - [Audited] //Only log the Name change - public string Name { get; set; } - - public string Email { get; set; } - - public string Password { get; set; } -} -```` - -## IAuditingStore - -`IAuditingStore` is an interface that is used to save the audit log objects (explained below) by the ABP Framework. If you need to save the audit log objects to a custom data store, you can implement the `IAuditingStore` in your own application and replace using the [dependency injection system](Dependency-Injection.md). - -`SimpleLogAuditingStore` is used if no audit store was registered. It simply writes the audit object to the standard [logging system](Logging.md). - -[The Audit Logging Module](Modules/Audit-Logging.md) has been configured in [the startup templates](Startup-Templates/Index.md) saves audit log objects to a database (it supports multiple database providers). So, most of the times you don't care about how `IAuditingStore` was implemented and used. - -## Audit Log Object - -An **audit log object** is created for each **web request** by default. An audit log object can be represented by the following relation diagram: - -![**auditlog-object-diagram**](images/auditlog-object-diagram.png) - -* **AuditLogInfo**: The root object with the following properties: - * `ApplicationName`: When you save audit logs of different applications to the same database, this property is used to distinguish the logs of the applications. - * `UserId`: Id of the current user, if the user has logged in. - * `UserName`: User name of the current user, if the user has logged in (this value is here to not depend on the identity module/system for lookup). - * `TenantId`: Id of the current tenant, for a multi-tenant application. - * `TenantName`: Name of the current tenant, for a multi-tenant application. - * `ExecutionTime`: The time when this audit log object has been created. - * `ExecutionDuration`: Total execution duration of the request, in milliseconds. This can be used to observe the performance of the application. - * `ClientId`: Id of the current client, if the client has been authenticated. A client is generally a 3rd-party application using the system over an HTTP API. - * `ClientName`: Name of the current client, if available. - * `ClientIpAddress`: IP address of the client/user device. - * `CorrelationId`: Current [Correlation Id](CorrelationId.md). Correlation Id is used to relate the audit logs written by different applications (or microservices) in a single logical operation. - * `BrowserInfo`: Browser name/version info of the current user, if available. - * `HttpMethod`: HTTP method of the current request (GET, POST, PUT, DELETE... etc.). - * `HttpStatusCode`: HTTP response status code for this request. - * `Url`: URL of the request. -* **AuditLogActionInfo**: An audit log action is typically a controller action or an [application service](Application-Services.md) method call during the web request. One audit log may contain multiple actions. An action object has the following properties: - * `ServiceName`: Name of the executed controller/service. - * `MethodName`: Name of the executed method of the controller/service. - * `Parameters`: A JSON formatted text representing the parameters passed to the method. - * `ExecutionTime`: The time when this method was executed. - * `ExecutionDuration`: Duration of the method execution, in milliseconds. This can be used to observe the performance of the method. -* **EntityChangeInfo**: Represents a change of an entity in this web request. An audit log may contain zero or more entity changes. An entity change has the following properties: - * `ChangeTime`: The time when the entity was changed. - * `ChangeType`: An enum with the following fields: `Created` (0), `Updated` (1) and `Deleted` (2). - * `EntityId`: Id of the entity that was changed. - * `EntityTenantId`: Id of the tenant this entity belongs to. - * `EntityTypeFullName`: Type (class) name of the entity with full namespace (like *Acme.BookStore.Book* for the Book entity). -* **EntityPropertyChangeInfo**: Represents a change of a property of an entity. An entity change info (explained above) may contain one or more property change with the following properties: - * `NewValue`: New value of the property. It is `null` if the entity was deleted. - * `OriginalValue`: Old/original value before the change. It is `null` if the entity was newly created. - * `PropertyName`: The name of the property on the entity class. - * `PropertyTypeFullName`: Type (class) name of the property with full namespace. -* **Exception**: An audit log object may contain zero or more exception. In this way, you can get a report of the failed requests. -* **Comment**: An arbitrary string value to add custom messages to the audit log entry. An audit log object may contain zero or more comments. - -In addition to the standard properties explained above, `AuditLogInfo`, `AuditLogActionInfo` and `EntityChangeInfo` objects implement the `IHasExtraProperties` interface, so you can add custom properties to these objects. - -## Audit Log Contributors - -You can extend the auditing system by creating a class that is derived from the `AuditLogContributor` class which defines the `PreContribute` and the `PostContribute` methods. - -The only pre-built contributor is the `AspNetCoreAuditLogContributor` class which sets the related properties for an HTTP request. - -A contributor can set properties and collections of the `AuditLogInfo` class to add more information. - -Example: - -````csharp -public class MyAuditLogContributor : AuditLogContributor -{ - public override void PreContribute(AuditLogContributionContext context) - { - var currentUser = context.ServiceProvider.GetRequiredService(); - context.AuditInfo.SetProperty( - "MyCustomClaimValue", - currentUser.FindClaimValue("MyCustomClaim") - ); - } - - public override void PostContribute(AuditLogContributionContext context) - { - context.AuditInfo.Comments.Add("Some comment..."); - } -} -```` - -* `context.ServiceProvider` can be used to resolve services from the [dependency injection](Dependency-Injection.md). -* `context.AuditInfo` can be used to access to the current audit log object to manipulate it. - -After creating such a contributor, you must add it to the `AbpAuditingOptions.Contributors` list: - -````csharp -Configure(options => -{ - options.Contributors.Add(new MyAuditLogContributor()); -}); -```` - -## IAuditLogScope & IAuditingManager - -This section explains the `IAuditLogScope` & `IAuditingManager` services for advanced use cases. - -An **audit log scope** is an [ambient scope](Ambient-Context-Pattern.md) that **builds** and **saves** an audit log object (explained before). By default, an audit log scope is created for a web request by the Audit Log Middleware (see `UseAuditing()` section above). - -### Access to the Current Audit Log Scope - -Audit log contributors, was explained above, is a global way of manipulating the audit log object. It is good if you can get a value from a service. - -If you need to manipulate the audit log object in an arbitrary point of your application, you can access to the current audit log scope and get the current audit log object (independent of how the scope is managed). Example: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAuditingManager _auditingManager; - - public MyService(IAuditingManager auditingManager) - { - _auditingManager = auditingManager; - } - - public async Task DoItAsync() - { - var currentAuditLogScope = _auditingManager.Current; - if (currentAuditLogScope != null) - { - currentAuditLogScope.Log.Comments.Add( - "Executed the MyService.DoItAsync method :)" - ); - - currentAuditLogScope.Log.SetProperty("MyCustomProperty", 42); - } - } -} -```` - -Always check if `_auditingManager.Current` is null or not, because it is controlled in an outer scope and you can't know if an audit log scope was created before calling your method. - -### Manually Create an Audit Log Scope - -You rarely need to create a manual audit log scope, but if you need, you can create an audit log scope using the `IAuditingManager` as like in the following example: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAuditingManager _auditingManager; - - public MyService(IAuditingManager auditingManager) - { - _auditingManager = auditingManager; - } - - public async Task DoItAsync() - { - using (var auditingScope = _auditingManager.BeginScope()) - { - try - { - //Call other services... - } - catch (Exception ex) - { - //Add exceptions - _auditingManager.Current.Log.Exceptions.Add(ex); - throw; - } - finally - { - //Always save the log - await auditingScope.SaveAsync(); - } - } - } -} -```` - -You can call other services, they may call others, they may change entities and so on. All these interactions are saved as a single audit log object in the finally block. - -## The Audit Logging Module - -The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. It supports multiple database providers. This module is added to the startup templates by default. - -See [the Audit Logging Module document](Modules/Audit-Logging.md) for more about it. diff --git a/docs/en/Authentication/Social-External-Logins.md b/docs/en/Authentication/Social-External-Logins.md deleted file mode 100644 index e80e53d4bd..0000000000 --- a/docs/en/Authentication/Social-External-Logins.md +++ /dev/null @@ -1,3 +0,0 @@ -# Social/External Logins - -> This document has been moved. See the [Account Module](../Modules/Account.md) documentation. \ No newline at end of file diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md deleted file mode 100644 index 37e50279e9..0000000000 --- a/docs/en/Authorization.md +++ /dev/null @@ -1,476 +0,0 @@ -# Authorization - -Authorization is used to check if a user is allowed to perform some specific operations in the application. - -ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](Application-Services.md)** too. - -So, all the ASP.NET Core authorization features and the documentation are valid in an ABP based application. This document focuses on the features that are added on top of ASP.NET Core authorization features. - -## Authorize Attribute - -ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple) attribute that can be used for an action, a controller or a page. ABP allows you to use the same attribute for an [application service](Application-Services.md) too. - -Example: - -```csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - [Authorize] - public class AuthorAppService : ApplicationService, IAuthorAppService - { - public Task> GetListAsync() - { - ... - } - - [AllowAnonymous] - public Task GetAsync(Guid id) - { - ... - } - - [Authorize("BookStore_Author_Create")] - public Task CreateAsync(CreateAuthorDto input) - { - ... - } - } -} - -``` - -- `Authorize` attribute forces the user to login into the application in order to use the `AuthorAppService` methods. So, `GetListAsync` method is only available to the authenticated users. -- `AllowAnonymous` suppresses the authentication. So, `GetAsync` method is available to everyone including unauthorized users. -- `[Authorize("BookStore_Author_Create")]` defines a policy (see [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies)) that is checked to authorize the current user. - -"BookStore_Author_Create" is an arbitrary policy name. If you declare an attribute like that, ASP.NET Core authorization system expects a policy to be defined before. - -You can, of course, implement your policies as stated in the ASP.NET Core documentation. But for simple true/false conditions like a policy was granted to a user or not, ABP defines the permission system which will be explained in the next section. - -## Permission System - -A permission is a simple policy that is granted or prohibited for a particular user, role or client. - -### Defining Permissions - -To define permissions, create a class inheriting from the `PermissionDefinitionProvider` as shown below: - -```csharp -using Volo.Abp.Authorization.Permissions; - -namespace Acme.BookStore.Permissions -{ - public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider - { - public override void Define(IPermissionDefinitionContext context) - { - var myGroup = context.AddGroup("BookStore"); - - myGroup.AddPermission("BookStore_Author_Create"); - } - } -} -``` - -> ABP automatically discovers this class. No additional configuration required! - -> You typically define this class inside the `Application.Contracts` project of your [application](Startup-Templates/Application.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with. - -In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group. - -When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role: - -![authorization-new-permission-ui](images/authorization-new-permission-ui.png) - -- The "BookStore" group is shown as a new tab on the left side. -- "BookStore_Author_Create" on the right side is the permission name. You can grant or prohibit it for the role. - -When you save the dialog, it is saved to the database and used in the authorization system. - -> The screen above is available when you have installed the identity module, which is basically used for user and role management. Startup templates come with the identity module pre-installed. - -#### Localizing the Permission Name - -"BookStore_Author_Create" is not a good permission name for the UI. Fortunately, `AddPermission` and `AddGroup` methods can take `LocalizableString` as second parameters: - -```csharp -var myGroup = context.AddGroup( - "BookStore", - LocalizableString.Create("BookStore") -); - -myGroup.AddPermission( - "BookStore_Author_Create", - LocalizableString.Create("Permission:BookStore_Author_Create") -); -``` - -Then you can define texts for "BookStore" and "Permission:BookStore_Author_Create" keys in the localization file: - -```json -"BookStore": "Book Store", -"Permission:BookStore_Author_Create": "Creating a new author" -``` - -> For more information, see the [localization document](Localization.md) on the localization system. - -The localized UI will be as seen below: - -![authorization-new-permission-ui-localized](images/authorization-new-permission-ui-localized.png) - -#### Multi-Tenancy - -ABP supports [multi-tenancy](Multi-Tenancy.md) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below: - -- **Host**: The permission is available only for the host side. -- **Tenant**: The permission is available only for the tenant side. -- **Both** (default): The permission is available both for tenant and host sides. - -> If your application is not multi-tenant, you can ignore this option. - -To set the multi-tenancy side option, pass to the third parameter of the `AddPermission` method: - -```csharp -myGroup.AddPermission( - "BookStore_Author_Create", - LocalizableString.Create("Permission:BookStore_Author_Create"), - multiTenancySide: MultiTenancySides.Tenant //set multi-tenancy side! -); -``` - -#### Enable/Disable Permissions - -A permission is enabled by default. It is possible to disable a permission. A disabled permission will be prohibited for everyone. You can still check for the permission, but it will always return prohibited. - -Example definition: - -````csharp -myGroup.AddPermission("Author_Management", isEnabled: false); -```` - -You normally don't need to define a disabled permission (unless you temporary want disable a feature of your application). However, you may want to disable a permission defined in a depended module. In this way you can disable the related application functionality. See the "*Changing Permission Definitions of a Depended Module*" section below for an example usage. - -> Note: Checking an undefined permission will throw an exception while a disabled permission check simply returns prohibited (false). - -#### Child Permissions - -A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted. - -Example definition: - -```csharp -var authorManagement = myGroup.AddPermission("Author_Management"); -authorManagement.AddChild("Author_Management_Create_Books"); -authorManagement.AddChild("Author_Management_Edit_Books"); -authorManagement.AddChild("Author_Management_Delete_Books"); -``` - -The result on the UI is shown below (you probably want to localize permissions for your application): - -![authorization-new-permission-ui-hierarcy](images/authorization-new-permission-ui-hierarcy.png) - -For the example code, it is assumed that a role/user with "Author_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below: - -```csharp -[Authorize("Author_Management")] -public class AuthorAppService : ApplicationService, IAuthorAppService -{ - public Task> GetListAsync() - { - ... - } - - public Task GetAsync(Guid id) - { - ... - } - - [Authorize("Author_Management_Create_Books")] - public Task CreateAsync(CreateAuthorDto input) - { - ... - } - - [Authorize("Author_Management_Edit_Books")] - public Task UpdateAsync(CreateAuthorDto input) - { - ... - } - - [Authorize("Author_Management_Delete_Books")] - public Task DeleteAsync(CreateAuthorDto input) - { - ... - } -} -``` - -- `GetListAsync` and `GetAsync` will be available to users if they have `Author_Management` permission is granted. -- Other methods require additional permissions. - -### Overriding a Permission by a Custom Policy - -If you define and register a policy to the ASP.NET Core authorization system with the same name of a permission, your policy will override the existing permission. This is a powerful way to extend the authorization for a pre-built module that you are using in your application. - -See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) document to learn how to define a custom policy. - -### Changing Permission Definitions of a Depended Module - -A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. - -Example: - -````csharp -context - .GetPermissionOrNull(IdentityPermissions.Roles.Delete) - .IsEnabled = false; -```` - -When you write this code inside your permission definition provider, it finds the "role deletion" permission of the [Identity Module](Modules/Identity.md) and disabled the permission, so no one can delete a role on the application. - -> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined. - -### Permission Depending on a Condition - -You may want to disable a permission based on a condition. Disabled permissions are not visible on the UI and always returns `prohibited` when you check them. There are two built-in conditional dependencies for a permission definition; - -* A permission can be automatically disabled if a [Feature](Features.md) was disabled. -* A permission can be automatically disabled if a [Global Feature](Global-Features.md) was disabled. - -In addition, you can create your custom extensions. - -#### Depending on a Features - -Use the `RequireFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled: - -````csharp -myGroup.AddPermission("Book_Creation") - .RequireFeatures("BookManagement"); -```` - -#### Depending on a Global Feature - -Use the `RequireGlobalFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled: - -````csharp -myGroup.AddPermission("Book_Creation") - .RequireGlobalFeatures("BookManagement"); -```` - -#### Creating a Custom Permission Dependency - -`PermissionDefinition` supports state check, Please refer to [Simple State Checker's documentation](SimpleStateChecker.md) - -## IAuthorizationService - -ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization. - -Example: - -```csharp -public async Task CreateAsync(CreateAuthorDto input) -{ - var result = await AuthorizationService - .AuthorizeAsync("Author_Management_Create_Books"); - if (result.Succeeded == false) - { - //throw exception - throw new AbpAuthorizationException("..."); - } - - //continue to the normal flow... -} -``` - -> `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](Dependency-Injection.md) it into your class. - -Since this is a typical code block, ABP provides extension methods to simplify it. - -Example: - -```csharp -public async Task CreateAsync(CreateAuthorDto input) -{ - await AuthorizationService.CheckAsync("Author_Management_Create_Books"); - - //continue to the normal flow... -} -``` - -`CheckAsync` extension method throws `AbpAuthorizationException` if the current user/client is not granted for the given permission. There is also `IsGrantedAsync` extension method that returns `true` or `false`. - -`IAuthorizationService` has some overloads for the `AuthorizeAsync` method. These are explained in the [ASP.NET Core authorization documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). - -> Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check. - -## Check a Permission in JavaScript - -See the following documents to learn how to re-use the authorization system on the client side: - -* [ASP.NET Core MVC / Razor Pages UI: Authorization](UI/AspNetCore/JavaScript-API/Auth.md) -* [Angular UI Authorization](UI/Angular/Permission-Management.md) -* [Blazor UI Authorization](UI/Blazor/Authorization.md) - -## Permission Management - -Permission management is normally done by an admin user using the permission management modal: - -![authorization-new-permission-ui-localized](images/authorization-new-permission-ui-localized.png) - -If you need to manage permissions by code, inject the `IPermissionManager` and use as shown below: - -```csharp -public class MyService : ITransientDependency -{ - private readonly IPermissionManager _permissionManager; - - public MyService(IPermissionManager permissionManager) - { - _permissionManager = permissionManager; - } - - public async Task GrantPermissionForUserAsync(Guid userId, string permissionName) - { - await _permissionManager.SetForUserAsync(userId, permissionName, true); - } - - public async Task ProhibitPermissionForUserAsync(Guid userId, string permissionName) - { - await _permissionManager.SetForUserAsync(userId, permissionName, false); - } -} -``` - -`SetForUserAsync` sets the value (true/false) for a permission of a user. There are more extension methods like `SetForRoleAsync` and `SetForClientAsync`. - -`IPermissionManager` is defined by the permission management module. See the [permission management module documentation](Modules/Permission-Management.md) for more information. - -## Advanced Topics - -### Permission Value Providers - -Permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers: - -- `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. -- `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. -- `ClientPermissionValueProvider` checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the `AbpClaimTypes.ClientId` static property. - -You can extend the permission checking system by defining your own permission value provider. - -Example: - -```csharp -public class SystemAdminPermissionValueProvider : PermissionValueProvider -{ - public SystemAdminPermissionValueProvider(IPermissionStore permissionStore) - : base(permissionStore) - { - } - - public override string Name => "SystemAdmin"; - - public async override Task - CheckAsync(PermissionValueCheckContext context) - { - if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") - { - return PermissionGrantResult.Granted; - } - - return PermissionGrantResult.Undefined; - } -} -``` - -This provider allows for all permissions to a user with a `User_Type` claim that has `SystemAdmin` value. It is common to use current claims and `IPermissionStore` in a permission value provider. - -A permission value provider should return one of the following values from the `CheckAsync` method: - -- `PermissionGrantResult.Granted` is returned to grant the user for the permission. If any of the providers return `Granted`, the result will be `Granted`, if no other provider returns `Prohibited`. -- `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. -- `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. - -Once a provider is defined, it should be added to the `AbpPermissionOptions` as shown below: - -```csharp -Configure(options => -{ - options.ValueProviders.Add(); -}); -``` - -### Permission Store - -`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](Modules/Permission-Management.md) for more information - -### AlwaysAllowAuthorizationService - -`AlwaysAllowAuthorizationService` is a class that is used to bypass the authorization service. It is generally used in integration tests where you may want to disable the authorization system. - -Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](Dependency-Injection.md) system: - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.AddAlwaysAllowAuthorization(); -} -``` - -This is already done for the startup template integration tests. - -### Claims Principal Factory - -Claims are important elements of authentication and authorization. ABP uses the `IAbpClaimsPrincipalFactory` service to create claims on authentication. This service was designed as extensible. If you need to add your custom claims to the authentication ticket, you can implement the `IAbpClaimsPrincipalContributor` in your application. - -**Example: Add a `SocialSecurityNumber` claim and get it:** - -```csharp -public class SocialSecurityNumberClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency -{ - public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context) - { - var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); - var userId = identity?.FindUserId(); - if (userId.HasValue) - { - var userService = context.ServiceProvider.GetRequiredService(); //Your custom service - var socialSecurityNumber = await userService.GetSocialSecurityNumberAsync(userId.Value); - if (socialSecurityNumber != null) - { - identity.AddClaim(new Claim("SocialSecurityNumber", socialSecurityNumber)); - } - } - } -} - - -public static class CurrentUserExtensions -{ - public static string GetSocialSecurityNumber(this ICurrentUser currentUser) - { - return currentUser.FindClaimValue("SocialSecurityNumber"); - } -} -``` - -> If you use Identity Server please add your claims to `RequestedClaims` of `AbpClaimsServiceOptions`. - -```csharp -Configure(options => -{ - options.RequestedClaims.AddRange(new[]{ "SocialSecurityNumber" }); -}); -``` - -## See Also - -* [Permission Management Module](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](UI/AspNetCore/JavaScript-API/Auth.md) -* [Permission Management in Angular UI](UI/Angular/Permission-Management.md) -* [Video tutorial](https://abp.io/video-courses/essentials/authorization) \ No newline at end of file diff --git a/docs/en/Autofac-Integration.md b/docs/en/Autofac-Integration.md deleted file mode 100644 index 43da50299b..0000000000 --- a/docs/en/Autofac-Integration.md +++ /dev/null @@ -1,100 +0,0 @@ -# Autofac Integration - -[Autofac](https://autofac.org/) is one of the most used dependency injection frameworks for .NET. It provides advanced features compared to .Net Core's standard DI library, like dynamic proxying and property injection. - -## Install Autofac Integration - -> All the [startup templates](Startup-Templates/Index.md) and samples are Autofac integrated. So, most of the time you don't need to manually install this package. - -If you're not using a startup template, you can use the [ABP CLI](CLI.md) to install it to your project. Execute the following command in the folder that contains the .csproj file of your project (suggested to add it to the executable/web project): - -````bash -abp add-package Volo.Abp.Autofac -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Autofac). -> - -Finally, configure `AbpApplicationCreationOptions` to replace default dependency injection services by Autofac. It depends on the application type. - -### ASP.NET Core Application - -Call `UseAutofac()` in the **Program.cs** file as shown below: - -````csharp -public class Program -{ - public static int Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .UseAutofac(); //Integrate Autofac! -} -```` - -If you are using the static `WebApplication` class, you can call the `UseAutofac()` extension method as shown below: - -````csharp -public class Program -{ - public async static Task Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - builder.Host.UseAutofac(); // Integrate Autofac! - await builder.AddApplicationAsync(); - var app = builder.Build(); - await app.InitializeApplicationAsync(); - await app.RunAsync(); - } -} -```` - -### Console Application - -Call `UseAutofac()` method in the `AbpApplicationFactory.Create` options as shown below: - -````csharp -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); //Autofac integration - })) - { - //... - } - } - } -} -```` - -## Using the Autofac Registration API - -If you want to use Autofac's advanced [registration API](https://autofac.readthedocs.io/en/latest/register/registration.html), you need to access the `ContainerBuilder` object. [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package defines the `IServiceCollection.GetContainerBuilder()` extension method to obtain the `ContainerBuilder` object. - -**Example: Get the `ContainerBuilder` object in the `ConfigureServices` method of your [module class](Module-Development-Basics.md)** - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var containerBuilder = context.Services.GetContainerBuilder(); - containerBuilder.RegisterType(); // Using Autofac's registration API -} -```` - -> You should install the [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to the project that you want to use the Autofac API. diff --git a/docs/en/Background-Jobs-Hangfire.md b/docs/en/Background-Jobs-Hangfire.md deleted file mode 100644 index 69903e1c85..0000000000 --- a/docs/en/Background-Jobs-Hangfire.md +++ /dev/null @@ -1,156 +0,0 @@ -# Hangfire Background Job Manager - -[Hangfire](https://www.hangfire.io/) is an advanced background job manager. You can integrate Hangfire with the ABP Framework to use it instead of the [default background job manager](Background-Jobs.md). In this way, you can use the same background job API for Hangfire and your code will be independent of Hangfire. If you like, you can directly use Hangfire's API, too. - -> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the Hangfire integration. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.BackgroundJobs.HangFire -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.BackgroundJobs.HangFire). - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.BackgroundJobs.HangFire](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.HangFire) NuGet package to your project: - - ```` - Install-Package Volo.Abp.BackgroundJobs.HangFire - ```` - -2. Add the `AbpBackgroundJobsHangfireModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsHangfireModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## Configuration - -You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package). - -After you have installed these NuGet packages, you need to configure your project to use Hangfire. - -1.First, we change the `Module` class (example: `HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method: - -````csharp - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - var hostingEnvironment = context.Services.GetHostingEnvironment(); - - //... other configurations. - - ConfigureHangfire(context, configuration); - } - - private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) - { - context.Services.AddHangfire(config => - { - config.UseSqlServerStorage(configuration.GetConnectionString("Default")); - }); - } -```` - -> You have to configure a storage for Hangfire. - -2. If you want to use hangfire's dashboard, you can add `UseAbpHangfireDashboard` call in the `OnApplicationInitialization` method in `Module` class: - -````csharp - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - - // ... others - - app.UseAbpHangfireDashboard(); //should add to the request pipeline before the app.UseConfiguredEndpoints() - app.UseConfiguredEndpoints(); - } -```` - -### Specifying Queue - -You can use the [`QueueAttribute`](https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html) to specify the queue: - -````csharp -using System.Threading.Tasks; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; - -namespace MyProject -{ - [Queue("alpha")] - public class EmailSendingJob - : AsyncBackgroundJob, ITransientDependency - { - private readonly IEmailSender _emailSender; - - public EmailSendingJob(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public override async Task ExecuteAsync(EmailSendingArgs args) - { - await _emailSender.SendAsync( - args.EmailAddress, - args.Subject, - args.Body - ); - } - } -} -```` - -### Dashboard Authorization - -Hangfire Dashboard provides information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is important to restrict access to the Dashboard. -To make it secure by default, only local requests are allowed, however you can change this by following the [official documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html) of Hangfire. - -You can integrate the Hangfire dashboard to [ABP authorization system](Authorization.md) using the **AbpHangfireAuthorizationFilter** -class. This class is defined in the `Volo.Abp.Hangfire` package. The following example, checks if the current user is logged in to the application: - -```csharp -app.UseAbpHangfireDashboard("/hangfire", options => -{ - options.AsyncAuthorization = new[] { new AbpHangfireAuthorizationFilter() }; -}); -``` - -* `AbpHangfireAuthorizationFilter` is an implementation of an authorization filter. - -#### AbpHangfireAuthorizationFilter - -`AbpHangfireAuthorizationFilter` class has the following fields: - -* **`enableTenant` (`bool`, default: `false`):** Enables/disables accessing the Hangfire dashboard on tenant users. -* **`requiredPermissionName` (`string`, default: `null`):** Hangfire dashboard is accessible only if the current user has the specified permission. In this case, if we specify a permission name, we don't need to set `enableTenant` `true` because the permission system already does it. - -If you want to require an additional permission, you can pass it into the constructor as below: - -```csharp -app.UseAbpHangfireDashboard("/hangfire", options => -{ - options.AsyncAuthorization = new[] { new AbpHangfireAuthorizationFilter(requiredPermissionName: "MyHangFireDashboardPermissionName") }; -}); -``` - -**Important**: `UseAbpHangfireDashboard` should be called after the authentication and authorization middlewares in your `Startup` class (probably at the last line). Otherwise, -authorization will always fail! diff --git a/docs/en/Background-Jobs-Quartz.md b/docs/en/Background-Jobs-Quartz.md deleted file mode 100644 index 4c1441da7f..0000000000 --- a/docs/en/Background-Jobs-Quartz.md +++ /dev/null @@ -1,159 +0,0 @@ -# Quartz Background Job Manager - -[Quartz](https://www.quartz-scheduler.net/) is an advanced background job manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background job manager](Background-Jobs.md). In this way, you can use the same background job API for Quartz and your code will be independent of Quartz. If you like, you can directly use Quartz's API, too. - -> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the Quartz integration. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.BackgroundJobs.Quartz -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.BackgroundJobs.Quartz). - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet package to your project: - - ```` - Install-Package Volo.Abp.BackgroundJobs.Quartz - ```` - -2. Add the `AbpBackgroundJobsQuartzModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## Configuration - -Quartz is a very configurable library,and the ABP framework provides `AbpQuartzOptions` for this. You can use the `PreConfigure` method in your module class to pre-configure this option. ABP will use it when initializing the Quartz module. For example: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - PreConfigure(options => - { - options.Properties = new NameValueCollection - { - ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp", - ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", - ["quartz.jobStore.tablePrefix"] = "QRTZ_", - ["quartz.serializer.type"] = "json", - ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"), - ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer", - ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", - }; - }); - } -} -```` - -Starting from ABP 3.1 version, we have added `Configurator` to `AbpQuartzOptions` to configure Quartz. For example: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - PreConfigure(options => - { - options.Configurator = configure => - { - configure.UsePersistentStore(storeOptions => - { - storeOptions.UseProperties = true; - storeOptions.UseJsonSerializer(); - storeOptions.UseSqlServer(configuration.GetConnectionString("Quartz")); - storeOptions.UseClustering(c => - { - c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); - c.CheckinInterval = TimeSpan.FromSeconds(10); - }); - }); - }; - }); - } -} -```` - -> You can choose the way you favorite to configure Quaratz. - -Quartz stores job and scheduling information **in memory by default**. In the example, we use the pre-configuration of [options pattern](Options.md) to change it to the database. For more configuration of Quartz, please refer to the Quartz's [documentation](https://www.quartz-scheduler.net/). - -## Exception handling - -### Default exception handling strategy - -When an exception occurs in the background job,ABP provide the **default handling strategy** retrying once every 3 seconds, up to 3 times. You can change the retry count and retry interval via `AbpBackgroundJobQuartzOptions` options: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.RetryCount = 1; - options.RetryIntervalMillisecond = 1000; - }); - } -} -``` - -### Customize exception handling strategy - -You can customize the exception handling strategy via `AbpBackgroundJobQuartzOptions` options: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.RetryStrategy = async (retryIndex, executionContext, exception) => - { - // customize exception handling - }; - }); - } -} -``` \ No newline at end of file diff --git a/docs/en/Background-Jobs-RabbitMq.md b/docs/en/Background-Jobs-RabbitMq.md deleted file mode 100644 index 8eec3733fb..0000000000 --- a/docs/en/Background-Jobs-RabbitMq.md +++ /dev/null @@ -1,159 +0,0 @@ -# RabbitMQ Background Job Manager - -RabbitMQ is an industry standard message broker. While it is typically used for inter-process communication (messaging / distributed events), it is pretty useful to store and execute background jobs in FIFO (First In First Out) order. - -ABP Framework provides the [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to use the RabbitMQ for background job execution. - -> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the RabbitMQ integration. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BackgroundJobs.RabbitMQ` package. -* Run `abp add-package Volo.Abp.BackgroundJobs.RabbitMQ` command. - -If you want to do it manually, install the [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to your project and add `[DependsOn(typeof(AbpBackgroundJobsRabbitMqModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -### Default Configuration - -The default configuration automatically connects to the local RabbitMQ server (localhost) with the standard port. **In this case, no configuration needed.** - -### RabbitMQ Connection(s) - -You can configure the RabbitMQ connections using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. - -#### `appsettings.json` file configuration - -This is the simplest way to configure the RabbitMQ connections. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). - -**Example: Configuring the Default RabbitMQ Connection** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123", - "Port": "5672" - } - } - } -} -```` - -You can use any of the [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) properties as the connection properties. See [the RabbitMQ document](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues) to understand these options better. - -Defining multiple connections is allowed. In this case, you can use different connections for different background job types (see the `AbpRabbitMqBackgroundJobOptions` section below). - -**Example: Declare two connections** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - }, - "SecondConnection": { - "HostName": "321.321.321.321" - } - } - } -} -```` - -If you need to connect to the RabbitMQ cluster, you can use the `;` character to separate the host names. - -**Example: Connect to the RabbitMQ cluster** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123;234.234.234.234" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -``` - -#### AbpRabbitMqOptions - -`AbpRabbitMqOptions` class can be used to configure the connection strings for the RabbitMQ. You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). - -**Example: Configure the connection** - -````csharp -Configure(options => -{ - options.Connections.Default.UserName = "user"; - options.Connections.Default.Password = "pass"; - options.Connections.Default.HostName = "123.123.123.123"; - options.Connections.Default.Port = 5672; -}); -```` - -Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. - -### AbpRabbitMqBackgroundJobOptions - -#### Job Queue Names - -By default, each job type uses a separate queue. Queue names are calculated by combining a standard prefix and the job name. Default prefix is `AbpBackgroundJobs.` So, if the job name is `EmailSending` then the queue name in the RabbitMQ becomes `AbpBackgroundJobs.EmailSending` - -> Use `BackgroundJobName` attribute on the background **job argument** class to specify the job name. Otherwise, the job name will be the full name (with namespace) of the job class. - -#### Job Connections - -By default, all the job types use the `Default` RabbitMQ connection. - -#### Customization - -`AbpRabbitMqBackgroundJobOptions` can be used to customize the queue names and the connections used by the jobs. - -**Example:** - -````csharp -Configure(options => -{ - options.DefaultQueueNamePrefix = "my_app_jobs."; - options.DefaultDelayedQueueNamePrefix = "my_app_jobs.delayed" - options.PrefetchCount = 1; - options.JobQueues[typeof(EmailSendingArgs)] = - new JobQueueConfiguration( - typeof(EmailSendingArgs), - queueName: "my_app_jobs.emails", - connectionName: "SecondConnection", - delayedQueueName:"my_app_jobs.emails.delayed" - ); -}); -```` - -* This example sets the default queue name prefix to `my_app_jobs.` and default delayed queue name prefix to `my_app_jobs.delayed`. If different applications use the same RabbitMQ server, it would be important to use different prefixes for each application to not consume jobs of each other. -* Sets `PrefetchCount` for all queues. -* Also specifies a different connection string for the `EmailSendingArgs`. - -`JobQueueConfiguration` class has some additional options in its constructor; - -* `queueName`: The queue name that is used for this job. The prefix is not added, so you need to specify the full name of the queue. -* `DelayedQueueName`: The delayed queue name that is used for delayed execution of job. The prefix is not added, so you need to specify the full name of the queue. -* `connectionName`: The RabbitMQ connection name (see the connection configuration above). This is optional and the default value is `Default`. -* `durable` (optional, default: `true`). -* `exclusive` (optional, default: `false`). -* `autoDelete` (optional, default: `false`). -* `PrefetchCount` (optional, default: null) - -See the RabbitMQ documentation if you want to understand the `durable`, `exclusive` and `autoDelete` options better, while most of the times the default configuration is what you want. - -## See Also - -* [Background Jobs](Background-Jobs.md) \ No newline at end of file diff --git a/docs/en/Background-Jobs.md b/docs/en/Background-Jobs.md deleted file mode 100644 index c6e19fb48a..0000000000 --- a/docs/en/Background-Jobs.md +++ /dev/null @@ -1,254 +0,0 @@ -# Background Jobs - -## Introduction - -Background jobs are used to queue some tasks to be executed in the background. You may need background jobs for several reasons. Here are some examples: - -- To perform **long-running tasks** without having the users wait. For example, a user presses a 'report' button to start a long-running reporting job. You add this job to the **queue** and send the report's result to your user via email when it's completed. -- To create **re-trying** and **persistent tasks** to **guarantee** that a code will be **successfully executed**. For example, you can send emails in a background job to overcome **temporary failures** and **guarantee** that it eventually will be sent. That way users do not wait while sending emails. - -Background jobs are **persistent** that means they will be **re-tried** and **executed** later even if your application crashes. - -## Abstraction Package - -ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire, RabbitMQ and Quartz integrations. - -`Volo.Abp.BackgroundJobs.Abstractions` NuGet package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration. - -> `Volo.Abp.BackgroundJobs.Abstractions` package is installed to the startup templates by default. - -### Create a Background Job - -A background job is a class that implements the `IBackgroundJob` interface or derives from the `BackgroundJob` class. `TArgs` is a simple plain C# class to store the job data. - -This example is used to send emails in background. First, define a class to store arguments of the background job: - -````csharp -namespace MyProject -{ - public class EmailSendingArgs - { - public string EmailAddress { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - } -} -```` - -Then create a background job class that uses an `EmailSendingArgs` object to send an email: - -````csharp -using System.Threading.Tasks; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; - -namespace MyProject -{ - public class EmailSendingJob - : AsyncBackgroundJob, ITransientDependency - { - private readonly IEmailSender _emailSender; - - public EmailSendingJob(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public override async Task ExecuteAsync(EmailSendingArgs args) - { - await _emailSender.SendAsync( - args.EmailAddress, - args.Subject, - args.Body - ); - } - } -} -```` - -This job simply uses `IEmailSender` to send emails (see [email sending document](Emailing.md)). - -> `AsyncBackgroundJob` is used to create a job needs to perform async calls. You can inherit from `BackgroundJob` and override the `Execute` method if the method doesn't need to perform any async call. - -#### Exception Handling - -A background job should not hide exceptions. If it throws an exception, the background job is automatically re-tried after a calculated waiting time. Hide exceptions only if you don't want to re-run the background job for the current argument. - -#### Cancelling Background Jobs - -If your background task is cancellable, then you can use the standard [Cancellation Token](Cancellation-Token-Provider.md) system to obtain a `CancellationToken` to cancel your job when requested. See the following example that uses the `ICancellationTokenProvider` to obtain the cancellation token: - -```csharp -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Threading; - -namespace MyProject -{ - public class LongRunningJob : AsyncBackgroundJob, ITransientDependency - { - private readonly ICancellationTokenProvider _cancellationTokenProvider; - - public LongRunningJob(ICancellationTokenProvider cancellationTokenProvider) - { - _cancellationTokenProvider = cancellationTokenProvider; - } - - public override async Task ExecuteAsync(LongRunningJobArgs args) - { - foreach (var id in args.Ids) - { - _cancellationTokenProvider.Token.ThrowIfCancellationRequested(); - await ProcessAsync(id); // code omitted for brevity - } - } - } -} -``` - -> A cancellation operation might be needed if the application is shutting down and we don't want to block the application in the background job. This example throws an exception if the cancellation is requested. So, the job will be retried the next time the application starts. If you don't want that, just return from the `ExecuteAsync` method without throwing any exception (you can simply check the `_cancellationTokenProvider.Token.IsCancellationRequested` property). - -#### Job Name - -Each background job has a name. Job names are used in several places. For example, RabbitMQ provider uses job names to determine the RabbitMQ Queue names. - -Job name is determined by the **job argument type**. For the `EmailSendingArgs` example above, the job name is `MyProject.EmailSendingArgs` (full name, including the namespace). You can use the `BackgroundJobName` attribute to set a different job name. - -**Example** - -```csharp -using Volo.Abp.BackgroundJobs; - -namespace MyProject -{ - [BackgroundJobName("emails")] - public class EmailSendingArgs - { - public string EmailAddress { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - } -} -``` - -### Queue a Job Item - -Now, you can queue an email sending job using the `IBackgroundJobManager` service: - -````csharp -public class RegistrationService : ApplicationService -{ - private readonly IBackgroundJobManager _backgroundJobManager; - - public RegistrationService(IBackgroundJobManager backgroundJobManager) - { - _backgroundJobManager = backgroundJobManager; - } - - public async Task RegisterAsync(string userName, string emailAddress, string password) - { - //TODO: Create new user in the database... - - await _backgroundJobManager.EnqueueAsync( - new EmailSendingArgs - { - EmailAddress = emailAddress, - Subject = "You've successfully registered!", - Body = "..." - } - ); - } -} -```` - -Just injected `IBackgroundJobManager` service and used its `EnqueueAsync` method to add a new job to the queue. - -Enqueue method gets some optional arguments to control the background job: - -* **priority** is used to control priority of the job item. It gets an `BackgroundJobPriority` enum which has `Low`, `BelowNormal`, `Normal` (default), `AboveNormal` and `Hight` fields. -* **delay** is used to wait a while (`TimeSpan`) before first try. - -### Disable Job Execution - -You may want to disable background job execution for your application. This is generally needed if you want to execute background jobs in another process and disable it for the current process. - -Use `AbpBackgroundJobOptions` to configure the job execution: - -````csharp -[DependsOn(typeof(AbpBackgroundJobsModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsJobExecutionEnabled = false; //Disables job execution - }); - } -} -```` - -## Default Background Job Manager - -ABP framework includes a simple `IBackgroundJobManager` implementation that; - -- Works as **FIFO** in a **single thread**. -- **Retries** job execution until the job **successfully runs** or **timeouts**. Default timeout is 2 days for a job. Logs all exceptions. -- **Deletes** a job from the store (database) when it's successfully executed. If it's timed out, it sets it as **abandoned** and leaves it in the database. -- **Increasingly waits between retries** for a job. It waits 1 minute for the first retry, 2 minutes for the second retry, 4 minutes for the third retry and so on. -- **Polls** the store for jobs in fixed intervals. It queries jobs, ordering by priority (asc) and then by try count (asc). - -> `Volo.Abp.BackgroundJobs` nuget package contains the default background job manager and it is installed to the startup templates by default. - -### Configuration - -Use `AbpBackgroundJobWorkerOptions` in your [module class](Module-Development-Basics.md) to configure the default background job manager. The example below changes the timeout duration for background jobs: - -````csharp -[DependsOn(typeof(AbpBackgroundJobsModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.DefaultTimeout = 864000; //10 days (as seconds) - }); - } -} -```` - -### Data Store - -The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction to store the jobs. - -Background Jobs module implements `IBackgroundJobStore` using various data access providers. See its own [documentation](Modules/Background-Jobs.md). If you don't want to use this module, you should implement the `IBackgroundJobStore` interface yourself. - -> Background Jobs module is already installed to the startup templates by default and it works based on your ORM/data access choice. - -### Clustered Deployment - -The default background job manager is compatible with [clustered environments](Deployment/Clustered-Environment.md) (where multiple instances of your application run concurrently). It uses a [distributed lock](Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time. - -However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, **please follow the [distributed lock](Distributed-Locking.md) document to configure a provider for your application**, if it is not already configured. - -If you don't want to use a distributed lock provider, you may go with the following options: - -* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs). -* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance. - -## Integrations - -Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations. - -See pre-built job manager alternatives: - -* [Hangfire Background Job Manager](Background-Jobs-Hangfire.md) -* [RabbitMQ Background Job Manager](Background-Jobs-RabbitMq.md) -* [Quartz Background Job Manager](Background-Jobs-Quartz.md) - -## See Also -* [Background Workers](Background-Workers.md) \ No newline at end of file diff --git a/docs/en/Background-Workers-Hangfire.md b/docs/en/Background-Workers-Hangfire.md deleted file mode 100644 index 70c8d0cf95..0000000000 --- a/docs/en/Background-Workers-Hangfire.md +++ /dev/null @@ -1,165 +0,0 @@ -# Hangfire Background Worker Manager - -[Hangfire](https://www.hangfire.io/) is an advanced background jobs and worker manager. You can integrate Hangfire with the ABP Framework to use it instead of the [default background worker manager](Background-Workers.md). - -The major advantage is that you can use the same server farm to manage your Background Jobs and Workers, as well as leverage the advanced scheduling that is available from Hangfire for [Recurring Jobs](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html?highlight=recurring), aka Background Workers. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.BackgroundWorkers.Hangfire -```` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.BackgroundWorkers.Hangfire](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Hangfire) NuGet package to your project: - - ```` - Install-Package Volo.Abp.BackgroundWorkers.Hangfire - ```` - -2. Add the `AbpBackgroundWorkersHangfireModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersHangfireModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -> Hangfire background worker integration provides an adapter `HangfirePeriodicBackgroundWorkerAdapter` to automatically load any `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived classes as `IHangfireBackgroundWorker` instances. This allows you to still to easily switch over to use Hangfire as the background manager even you have existing background workers that are based on the [default background workers implementation](Background-Workers.md). - -## Configuration - -You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package). - -After you have installed these NuGet packages, you need to configure your project to use Hangfire. - -1.First, we change the `Module` class (example: `HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method: - -````csharp - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - var hostingEnvironment = context.Services.GetHostingEnvironment(); - - //... other configarations. - - ConfigureHangfire(context, configuration); - } - - private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) - { - context.Services.AddHangfire(config => - { - config.UseSqlServerStorage(configuration.GetConnectionString("Default")); - }); - } -```` - -> You have to configure a storage for Hangfire. - -2. If you want to use hangfire's dashboard, you can add `UseAbpHangfireDashboard` call in the `OnApplicationInitialization` method in `Module` class - -````csharp - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - - // ... others - - app.UseAbpHangfireDashboard(); //should add to the request pipeline before the app.UseConfiguredEndpoints() - app.UseConfiguredEndpoints(); - } -```` - -## Create a Background Worker - -`HangfireBackgroundWorkerBase` is an easy way to create a background worker. - -```` csharp -public class MyLogWorker : HangfireBackgroundWorkerBase -{ - public MyLogWorker() - { - RecurringJobId = nameof(MyLogWorker); - CronExpression = Cron.Daily(); - } - - public override Task DoWorkAsync(CancellationToken cancellationToken = default) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -* **RecurringJobId** Is an optional parameter, see [Hangfire document](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html) -* **CronExpression** Is a CRON expression, see [CRON expression](https://en.wikipedia.org/wiki/Cron#CRON_expression) - -> You can directly implement the `IHangfireBackgroundWorker`, but `HangfireBackgroundWorkerBase` provides some useful properties like Logger. - -### UnitOfWork - -```csharp -public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker -{ - public MyLogWorker() - { - RecurringJobId = nameof(MyLogWorker); - CronExpression = Cron.Daily(); - } - - public override Task DoWorkAsync(CancellationToken cancellationToken = default) - { - using (var uow = LazyServiceProvider.LazyGetRequiredService().Begin()) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } - } -} -``` - -## Register BackgroundWorkerManager - -After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitializationAsync` method of your module class: - -```` csharp -[DependsOn(typeof(AbpBackgroundWorkersModule))] -public class MyModule : AbpModule -{ - public override async Task OnApplicationInitializationAsync( - ApplicationInitializationContext context) - { - await context.AddBackgroundWorkerAsync(); - } -} -```` - -`context.AddBackgroundWorkerAsync(...)` is a shortcut extension method for the expression below: - -```` csharp -context.ServiceProvider - .GetRequiredService() - .AddAsync( - context - .ServiceProvider - .GetRequiredService() - ); -```` - -So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`. - -While we generally add workers in `OnApplicationInitializationAsync`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. diff --git a/docs/en/Background-Workers-Quartz.md b/docs/en/Background-Workers-Quartz.md deleted file mode 100644 index b3ebf32a91..0000000000 --- a/docs/en/Background-Workers-Quartz.md +++ /dev/null @@ -1,146 +0,0 @@ -# Quartz Background Worker Manager - -[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Workers.md). ABP simply integrates quartz. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.BackgroundWorkers.Quartz -```` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet package to your project: - - ```` - Install-Package Volo.Abp.BackgroundWorkers.Quartz - ```` - -2. Add the `AbpBackgroundWorkersQuartzModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -> Quartz background worker integration provided `QuartzPeriodicBackgroundWorkerAdapter` to adapt `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived class. So, you can still fllow the [background workers document](Background-Workers.md) to define the background worker. - -## Configuration - -See [Configuration](Background-Jobs-Quartz#Configuration). - -## Create a Background Worker - -A background work is a class that derives from the `QuartzBackgroundWorkerBase` base class. for example. A simple worker class is shown below: - -```` csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).StartNow().Build(); - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -We simply implemented the Execute method to write a log. The background worker is a **singleton by default**. If you want, you can also implement a [dependency interface](Dependency-Injection#DependencyInterfaces) to register it as another life cycle. - -> Tips: Add identity to background workers is a best practice,because quartz distinguishes different jobs based on identity. - -## Add to BackgroundWorkerManager - -Default background workers are **automatically** added to the BackgroundWorkerManager when the application is **initialized**. You can set `AutoRegister` property value to `false`,if you want to add it manually: - -```` csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - AutoRegister = false; - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).StartNow().Build(); - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -If you want to globally disable auto add worker, you can global disable via `AbpBackgroundWorkerQuartzOptions` options: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsAutoRegisterEnabled = false; - }); - } -} -``` - -## Advanced topics - -### Customize ScheduleJob - -Assume you have a worker executes every 10 minutes,but because server is unavailable for 30 minutes, 3 executions are missed. You want to execute all missed times after the server is available. You should define your background worker like this: - -```csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).WithSimpleSchedule(s=>s.WithIntervalInMinutes(1).RepeatForever().WithMisfireHandlingInstructionIgnoreMisfires()).Build(); - - ScheduleJob = async scheduler => - { - if (!await scheduler.CheckExists(JobDetail.Key)) - { - await scheduler.ScheduleJob(JobDetail, Trigger); - } - }; - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -``` - -In the example we defined the worker execution interval to be 10 minutes and set `WithMisfireHandlingInstructionIgnoreMisfires`. we customized `ScheduleJob` and add worker to quartz only when the background worker does not exist. - -### More - -Please see Quartz's [documentation](https://www.quartz-scheduler.net/documentation/index.html) for more information. diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md deleted file mode 100644 index 06535cccfc..0000000000 --- a/docs/en/Background-Workers.md +++ /dev/null @@ -1,146 +0,0 @@ -# Background Workers - -## Introduction - -Background workers are simple independent threads in the application running in the background. Generally, they run periodically to perform some tasks. Examples; - -* A background worker can run periodically to **delete old logs**. -* A background worker can run periodically to **determine inactive users** and **send emails** to get users to return to your application. - - -## Create a Background Worker - -A background worker should directly or indirectly implement the `IBackgroundWorker` interface. - -> A background worker is inherently [singleton](Dependency-Injection.md). So, only a single instance of your worker class is instantiated and run. - -### BackgroundWorkerBase - -`BackgroundWorkerBase` is an easy way to create a background worker. - -````csharp -public class MyWorker : BackgroundWorkerBase -{ - public override Task StartAsync(CancellationToken cancellationToken = default) - { - //... - } - - public override Task StopAsync(CancellationToken cancellationToken = default) - { - //... - } -} -```` - -Start your worker in the `StartAsync` (which is called when the application begins) and stop in the `StopAsync` (which is called when the application shuts down). - -> You can directly implement the `IBackgroundWorker`, but `BackgroundWorkerBase` provides some useful properties like `Logger`. - -### AsyncPeriodicBackgroundWorkerBase - -Assume that we want to make a user passive, if the user has not logged in to the application in last 30 days. `AsyncPeriodicBackgroundWorkerBase` class simplifies to create periodic workers, so we will use it for the example below: - -````csharp -public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase -{ - public PassiveUserCheckerWorker( - AbpAsyncTimer timer, - IServiceScopeFactory serviceScopeFactory - ) : base( - timer, - serviceScopeFactory) - { - Timer.Period = 600000; //10 minutes - } - - protected async override Task DoWorkAsync( - PeriodicBackgroundWorkerContext workerContext) - { - Logger.LogInformation("Starting: Setting status of inactive users..."); - - //Resolve dependencies - var userRepository = workerContext - .ServiceProvider - .GetRequiredService(); - - //Do the work - await userRepository.UpdateInactiveUserStatusesAsync(); - - Logger.LogInformation("Completed: Setting status of inactive users..."); - } -} -```` - -* `AsyncPeriodicBackgroundWorkerBase` uses the `AbpAsyncTimer` (a thread-safe timer) object to determine **the period**. We can set its `Period` property in the constructor. -* It required to implement the `DoWorkAsync` method to **execute** the periodic work. -* It is a good practice to **resolve dependencies** from the `PeriodicBackgroundWorkerContext` instead of constructor injection. Because `AsyncPeriodicBackgroundWorkerBase` uses a `IServiceScope` that is **disposed** when your work finishes. -* `AsyncPeriodicBackgroundWorkerBase` **catches and logs exceptions** thrown by the `DoWorkAsync` method. - - -## Register Background Worker - -After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitializationAsync` method of your module class: - -````csharp -[DependsOn(typeof(AbpBackgroundWorkersModule))] -public class MyModule : AbpModule -{ - public override async Task OnApplicationInitializationAsync( - ApplicationInitializationContext context) - { - await context.AddBackgroundWorkerAsync(); - } -} -```` - -`context.AddBackgroundWorkerAsync(...)` is a shortcut extension method for the expression below: - -````csharp -await context.ServiceProvider - .GetRequiredService() - .AddAsync( - context - .ServiceProvider - .GetRequiredService() - ); -```` - -So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`. - -While we generally add workers in `OnApplicationInitializationAsync`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. - -## Options - -`AbpBackgroundWorkerOptions` class is used to [set options](Options.md) for the background workers. Currently, there is only one option: - -* `IsEnabled` (default: true): Used to **enable/disable** the background worker system for your application. - -> See the [Options](Options.md) document to learn how to set options. - -## Making Your Application Always Run - -Background workers only work if your application is running. If you host the background job execution in your web application (this is the default behavior), you should ensure that your web application is configured to always be running. Otherwise, background jobs only work while your application is in use. - -## Running On a Cluster - -Be careful if you run multiple instances of your application simultaneously in a clustered environment. In that case, every application runs the same worker which may create conflicts if your workers are running on the same resources (processing the same data, for example). - -If that's a problem for your workers, you have the following options: - -* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed. -* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers. -* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance. - -## Integrations - -Background worker system is extensible and you can change the default background worker manager with your own implementation or on of the pre-built integrations. - -See pre-built worker manager alternatives: - -* [Quartz Background Worker Manager](Background-Workers-Quartz.md) -* [Hangfire Background Worker Manager](Background-Workers-Hangfire.md) - -## See Also - -* [Background Jobs](Background-Jobs.md) diff --git a/docs/en/Best-Practices/Application-Services.md b/docs/en/Best-Practices/Application-Services.md deleted file mode 100644 index c209f01c5e..0000000000 --- a/docs/en/Best-Practices/Application-Services.md +++ /dev/null @@ -1,236 +0,0 @@ -## Application Services Best Practices & Conventions - -* **Do** create an application service for each **aggregate root**. - -### Application Service Interface - -* **Do** define an `interface` for each application service in the **application contracts** package. -* **Do** inherit from the `IApplicationService` interface. -* **Do** use the `AppService` postfix for the interface name (ex: `IProductAppService`). -* **Do** create DTOs (Data Transfer Objects) for inputs and outputs of the service. -* **Do not** get/return entities for the service methods. -* **Do** define DTOs based on the [DTO best practices](Data-Transfer-Objects.md). - -#### Outputs - -* **Avoid** to define too many output DTOs for same or related entities. Instead, define a **basic** and a **detailed** DTO for an entity. - -##### Basic DTO - -**Do** define a **basic** DTO for an aggregate root. - -- Include all the **primitive properties** directly on the aggregate root. - - Exception: Can **exclude** properties for **security** reasons (like `User.Password`). -- Include all the **sub collections** of the entity where every item in the collection is a simple **relation DTO**. -- Inherit from one of the **extensible entity DTO** classes for aggregate roots (and entities implement the `IHasExtraProperties`). - -Example: - -```c# -[Serializable] -public class IssueDto : ExtensibleFullAuditedEntityDto -{ - public string Title { get; set; } - public string Text { get; set; } - public Guid? MilestoneId { get; set; } - public Collection Labels { get; set; } -} - -[Serializable] -public class IssueLabelDto -{ - public Guid IssueId { get; set; } - public Guid LabelId { get; set; } -} -``` - -##### Detailed DTO - -**Do** define a **detailed** DTO for an entity if it has reference(s) to other aggregate roots. - -* Include all the **primitive properties** directly on the entity. - - Exception-1: Can **exclude** properties for **security** reasons (like `User.Password`). - - Exception-2: **Do** exclude reference properties (like `MilestoneId` in the example above). Will already add details for the reference properties. -* Include a **basic DTO** property for every reference property. -* Include all the **sub collections** of the entity where every item in the collection is the **basic DTO** of the related entity. - -Example: - -````C# -[Serializable] -public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto -{ - public string Title { get; set; } - public string Text { get; set; } - public MilestoneDto Milestone { get; set; } - public Collection Labels { get; set; } -} - -[Serializable] -public class MilestoneDto : ExtensibleEntityDto -{ - public string Name { get; set; } - public bool IsClosed { get; set; } -} - -[Serializable] -public class LabelDto : ExtensibleEntityDto -{ - public string Name { get; set; } - public string Color { get; set; } -} -```` - -#### Inputs - -* **Do not** define any property in an input DTO that is not used in the service class. -* **Do not** share input DTOs between application service methods. -* **Do not** inherit an input DTO class from another one. - * **May** inherit from an abstract base DTO class and share some properties between different DTOs in that way. However, should be very careful in that case because manipulating the base DTO would effect all related DTOs and service methods. Avoid from that as a good practice. - -#### Methods - -* **Do** define service methods as asynchronous with **Async** postfix. -* **Do not** repeat the entity name in the method names. - * Example: Define `GetAsync(...)` instead of `GetProductAsync(...)` in the `IProductAppService`. - -##### Getting A Single Entity - -* **Do** use the `GetAsync` **method name**. -* **Do** get Id with a **primitive** method parameter. -* Return the **detailed DTO**. Example: - -````C# -Task GetAsync(Guid id); -```` - -##### Getting A List Of Entities - -* **Do** use the `GetListAsync` **method name**. -* **Do** get a single DTO argument for **filtering**, **sorting** and **paging** if necessary. - * **Do** implement filters optional where possible. - * **Do** implement sorting & paging properties as optional and provide default values. - * **Do** limit maximum page size (for performance reasons). -* **Do** return a list of **detailed DTO**s. Example: - -````C# -Task> GetListAsync(QuestionListQueryDto queryDto); -```` - -##### Creating A New Entity - -* **Do** use the `CreateAsync` **method name**. -* **Do** get a **specialized input** DTO to create the entity. -* **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed. -* **Do** use **data annotations** for input validation. - * Share constants between domain wherever possible (via constants defined in the **domain shared** package). -* **Do** return **the detailed** DTO for new created entity. -* **Do** only require the **minimum** info to create the entity but provide possibility to set others as optional properties. - -Example **method**: - -````C# -Task CreateAsync(CreateQuestionDto questionDto); -```` - -The related **DTO**: - -````C# -[Serializable] -public class CreateQuestionDto : ExtensibleObject -{ - [Required] - [StringLength(QuestionConsts.MaxTitleLength, - MinimumLength = QuestionConsts.MinTitleLength)] - public string Title { get; set; } - - [StringLength(QuestionConsts.MaxTextLength)] - public string Text { get; set; } //Optional - - public Guid? CategoryId { get; set; } //Optional -} -```` - -##### Updating An Existing Entity - -- **Do** use the `UpdateAsync` **method name**. -- **Do** get a **specialized input** DTO to update the entity. -- **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed. -- **Do** get the Id of the entity as a separated primitive parameter. Do not include to the update DTO. -- **Do** use **data annotations** for input validation. - - Share constants between domain wherever possible (via constants defined in the **domain shared** package). -- **Do** return **the detailed** DTO for the updated entity. - -Example: - -````C# -Task UpdateAsync(Guid id, UpdateQuestionDto updateQuestionDto); -```` - -##### Deleting An Existing Entity - -- **Do** use the `DeleteAsync` **method name**. -- **Do** get Id with a **primitive** method parameter. Example: - -````C# -Task DeleteAsync(Guid id); -```` - -##### Other Methods - -* **Can** define additional methods to perform operations on the entity. Example: - -````C# -Task VoteAsync(Guid id, VoteType type); -```` - -This method votes a question and returns the current score of the question. - -### Application Service Implementation - -* **Do** develop the application layer **completely independent from the web layer**. -* **Do** implement application service interfaces in the **application layer**. - * **Do** use the naming convention. Ex: Create `ProductAppService` class for the `IProductAppService` interface. - * **Do** inherit from the `ApplicationService` base class. -* **Do** make all public methods **virtual**, so developers may inherit and override them. -* **Do not** make **private** methods. Instead make them **protected virtual**, so developers may inherit and override them. - -#### Using Repositories - -* **Do** use the specifically designed repositories (like `IProductRepository`). -* **Do not** use generic repositories (like `IRepository`). - -#### Querying Data - -* **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source. - -#### Extra Properties - -* **Do** use either `MapExtraPropertiesTo` extension method ([see](../Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services. - -#### Manipulating / Deleting Entities - -* **Do** always get all the related entities from repositories to perform the operations on them. -* **Do** call repository's Update/UpdateAsync method after updating an entity. Because, not all database APIs support change tracking & auto update. - -#### Handle files - -* **Do not** use any web components like `IFormFile` or `Stream` in the application services. If you want to serve a file you can use `byte[]`. -* **Do** use a `Controller` to handle file uploading then pass the `byte[]` of the file to the application service method. - -#### Using Other Application Services - -* **Do not** use other application services of the same module/application. Instead; - * Use domain layer to perform the required task. - * Extract a new class and share between the application services to accomplish the code reuse when necessary. But be careful to don't couple two use cases. They may seem similar at the beginning, but may evolve to different directions by time. So, use code sharing carefully. -* **Can** use application services of others only if; - * They are parts of another module / microservice. - * The current module has only reference to the application contracts of the used module. - - -## See Also - -* [Video tutorial Part-1](https://abp.io/video-courses/essentials/application-services-part-1) -* [Video tutorial Part-2](https://abp.io/video-courses/essentials/application-services-part-2) - - diff --git a/docs/en/Best-Practices/Domain-Services.md b/docs/en/Best-Practices/Domain-Services.md deleted file mode 100644 index 89d3125857..0000000000 --- a/docs/en/Best-Practices/Domain-Services.md +++ /dev/null @@ -1,68 +0,0 @@ -## Domain Services Best Practices & Conventions - - - -### Domain Service - -- **Do** define domain services in the **domain layer**. -- **Do not** create interfaces for the domain services **unless** you have a good reason to (like mock and test different implementations). -- **Do** name your domain service with `Manager` suffix. - -For the example of a domain service: -```cs -public class IssueManager : DomainService -{ - //... -} -``` - -### Domain Service Methods - -- **Do not** define `GET` methods. `GET` methods do not change the state of an entity. Hence, use the repository directly in the Application Service instead of Domain Service method. - -- **Do** define methods that only mutates data; changes the state of an entity or an aggregate root. - -- **Do not** define methods with generic names (like `UpdateIssueAsync`). - -- **Do** define methods with self explanatory names (like `AssignToAsync`) that implements the specific domain logic. - - -- **Do** accept valid domain objects as parameters. - -```cs -public async Task AssignToAsync(Issue issue, IdentityUser user) -{ - //... -} -``` - -- **Do** throw `BusinessException` or custom business exception if a validation fails. - - - **Do** use domain error codes with unique code-namespace for exception localization. - -```cs -public async Task AssignToAsync(Issue issue, IdentityUser user) -{ - var openIssueCount = await _issueRepository.GetCountAsync( - i => i.AssignedUserId == user.Id && !i.IsClosed - ); - - if (openIssueCount >= 3) - { - throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); - } - - issue.AssignedUserId = user.Id; -} -``` - -- **Do not** return `DTO`. Return only domain objects when you need. -- **Do not** involve authenticated user logic. Instead, define extra parameter and send the related data of ` CurrentUser` from the Application Service layer. - - - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/domain-services) -* [Domain Services](../Domain-Services.md) -* [Exception Handling](../Exception-Handling.md) \ No newline at end of file diff --git a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md deleted file mode 100644 index 4a6c82c379..0000000000 --- a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md +++ /dev/null @@ -1,197 +0,0 @@ -## Entity Framework Core Integration Best Practices - -> See [Entity Framework Core Integration document](../Entity-Framework-Core.md) for the basics of the EF Core integration. - -- **Do** define a separated `DbContext` interface and class for each module. -- **Do not** rely on lazy loading on the application development. -- **Do not** enable lazy loading for the `DbContext`. - -### DbContext Interface - -- **Do** define an **interface** for the `DbContext` that inherits from `IEfCoreDbContext`. -- **Do** add a `ConnectionStringName` **attribute** to the `DbContext` interface. -- **Do** add `DbSet` **properties** to the `DbContext` interface for only aggregate roots. Example: - -````C# -[ConnectionStringName("AbpIdentity")] -public interface IIdentityDbContext : IEfCoreDbContext -{ - DbSet Users { get; } - DbSet Roles { get; } -} -```` - -* **Do not** define `set;` for the properties in this interface. - -### DbContext class - -* **Do** inherit the `DbContext` from the `AbpDbContext` class. -* **Do** add a `ConnectionStringName` attribute to the `DbContext` class. -* **Do** implement the corresponding `interface` for the `DbContext` class. Example: - -````C# -[ConnectionStringName("AbpIdentity")] -public class IdentityDbContext : AbpDbContext, IIdentityDbContext -{ - public DbSet Users { get; set; } - public DbSet Roles { get; set; } - - public IdentityDbContext(DbContextOptions options) - : base(options) - { - - } - - //code omitted for brevity -} -```` - -### Table Prefix and Schema - -- **Do** add static `TablePrefix` and `Schema` **properties** to the `DbContext` class. Set default value from a constant. Example: - -````C# -public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; -public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema; -```` - - - **Do** always use a short `TablePrefix` value for a module to create **unique table names** in a shared database. `Abp` table prefix is reserved for ABP core modules. - - **Do** set `Schema` to `null` as default. - -### Model Mapping - -- **Do** explicitly **configure all entities** by overriding the `OnModelCreating` method of the `DbContext`. Example: - -````C# -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - builder.ConfigureIdentity(); -} -```` - -- **Do not** configure model directly in the `OnModelCreating` method. Instead, create an **extension method** for `ModelBuilder`. Use Configure*ModuleName* as the method name. Example: - -````C# -public static class IdentityDbContextModelBuilderExtensions -{ - public static void ConfigureIdentity([NotNull] this ModelBuilder builder) - { - Check.NotNull(builder, nameof(builder)); - - builder.Entity(b => - { - b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users", AbpIdentityDbProperties.DbSchema); - b.ConfigureByConvention(); - //code omitted for brevity - }); - - builder.Entity(b => - { - b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "UserClaims", AbpIdentityDbProperties.DbSchema); - b.ConfigureByConvention(); - //code omitted for brevity - }); - - //code omitted for brevity - } -} -```` - -* **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above). - -### Repository Implementation - -- **Do** **inherit** the repository from the `EfCoreRepository` class and implement the corresponding repository interface. Example: - -````C# -public class EfCoreIdentityUserRepository - : EfCoreRepository, IIdentityUserRepository -{ - public EfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -```` - -* **Do** use the `DbContext` interface as the generic parameter, not the class. -* **Do** pass the `cancellationToken` to EF Core using the `GetCancellationToken` helper method. Example: - -````C# -public virtual async Task FindByNormalizedUserNameAsync( - string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default) -{ - return await (await GetDbSetAsync()) - .IncludeDetails(includeDetails) - .FirstOrDefaultAsync( - u => u.NormalizedUserName == normalizedUserName, - GetCancellationToken(cancellationToken) - ); -} -```` - -`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code. - -- **Do** create a `IncludeDetails` **extension method** for the `IQueryable` for each aggregate root which has **sub collections**. Example: - -````C# -public static IQueryable IncludeDetails( - this IQueryable queryable, - bool include = true) -{ - if (!include) - { - return queryable; - } - - return queryable - .Include(x => x.Roles) - .Include(x => x.Logins) - .Include(x => x.Claims) - .Include(x => x.Tokens); -} -```` - -* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see `FindByNormalizedUserNameAsync`). - -- **Do** override `WithDetails` method of the repository for aggregates root which have **sub collections**. Example: - -````C# -public override async Task> WithDetailsAsync() -{ - // Uses the extension method defined above - return (await GetQueryableAsync()).IncludeDetails(); -} -```` - -### Module Class - -- **Do** define a module class for the Entity Framework Core integration package. -- **Do** add `DbContext` to the `IServiceCollection` using the `AddAbpDbContext` method. -- **Do** add implemented repositories to the options for the `AddAbpDbContext` method. Example: - -````C# -[DependsOn( - typeof(AbpIdentityDomainModule), - typeof(AbpEntityFrameworkCoreModule) - )] -public class AbpIdentityEntityFrameworkCoreModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(options => - { - options.AddRepository(); - options.AddRepository(); - }); - } -} -```` - - ## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core) \ No newline at end of file diff --git a/docs/en/Best-Practices/Index.md b/docs/en/Best-Practices/Index.md deleted file mode 100644 index 3cc10a17cc..0000000000 --- a/docs/en/Best-Practices/Index.md +++ /dev/null @@ -1,30 +0,0 @@ -## Module Development Best Practices & Conventions - -### Introduction - -This document describes the **best practices** and **conventions** for those who want to develop **modules** that satisfy the following specifications: - -* Develop modules that conform to the **Domain Driven Design** patterns & best practices. -* Develop modules with **DBMS and ORM independence**. -* Develop modules that can be used as a **remote service / microservice** as well as being compatible with a **monolithic** application. - -Also, this guide is mostly usable for general **application development**. - -### Guides - -* Overall - * [Module Architecture](Module-Architecture.md) -* Domain Layer - * [Entities](Entities.md) - * [Repositories](Repositories.md) - * [Domain Services](Domain-Services.md) -* Application Layer - * [Application Services](Application-Services.md) - * [Data Transfer Objects](Data-Transfer-Objects.md) -* Data Access - * [Entity Framework Core Integration](Entity-Framework-Core-Integration.md) - * [MongoDB Integration](MongoDB-Integration.md) - -## See Also - -* [E-Book: Implementing Domain Driven Design](https://abp.io/books/implementing-domain-driven-design) diff --git a/docs/en/Best-Practices/Module-Architecture.md b/docs/en/Best-Practices/Module-Architecture.md deleted file mode 100644 index b7e0d03db9..0000000000 --- a/docs/en/Best-Practices/Module-Architecture.md +++ /dev/null @@ -1,89 +0,0 @@ -## Module Architecture Best Practices & Conventions - -### Solution Structure - -* **Do** create a separated Visual Studio solution for every module. -* **Do** name the solution as *CompanyName.ModuleName* (for core ABP modules, it's *Volo.Abp.ModuleName*). -* **Do** develop the module as layered, so it has several packages (projects) those are related to each other. - * Every package has its own module definition file and explicitly declares the dependencies for the depended packages/modules. - -### Layers & Packages - -The following diagram shows the packages of a well-layered module and dependencies of those packages between them: - -![module-layers-and-packages](../images/module-layers-and-packages.jpg) - -The ultimate goal is to allow an application to use the module in a flexible manner. Example applications: - -* **A)** A **monolithic** application; - * Adds references to the **Web** and the **Application** packages. - * Adds a reference to one of the **EF Core** or the **MongoDB** packages based on the preference. - * The result; - * The application **can show UI** of the module. - * It hosts the **application** and **domain** layers in the **same process** (that's why it needs to have a reference to a database integration package). - * This application also **serves** the module's **HTTP API** (since it includes the HttpApi package through the Web package). -* **B)** An application that just serves the module as a **microservice**; - * Adds a reference to **HttpApi** and **Application** packages. - * Adds a reference to one of the **EF Core** or the **MongoDB** packages based on the preference. - * The result; - * The application **can not show UI** of the module since it does not have a reference to the Web package. - * It hosts the **application** and **domain** layers in the **same process** (that's why it needs to have a reference to a database integration package). - * This application **serves** the module's **HTTP API** (as the main goal of the application). -* **C)** An application that shows the module **UI** but does not host the application (just uses it as a remote service that is hosted by the application A or B); - * Adds a reference to the **Web** and the **HttpApi.Client** packages. - * Configures the remote endpoint for the HttpApi.Client package. - * The result; - * The application **can show UI** of the module. - * It does not host the application and domain layers of the module in the same process. Instead, uses it as a **remote service**. - * This application also **serves** the module's **HTTP API** (since it includes the HttpApi package through the Web package). -* **D)** A **client** application (or microservice) that just uses the module as a remote service (that is hosted by the application A, B or C); - * Adds a reference to the **HttpApi.Client** package. - * Configures the remote endpoint for the HttpApi.Client package. - * The result; - * The application can use all the functionality of the module as a **remote client**. - * The application is just a client and **can not serve** the **HTTP API** of the module. - * The application is just a client and **can not show** the **UI** of the module. -* **E**) A proxy application that hosts the HTTP API of the module but just forwards all requests to another application (that is hosted by the application A, B or C); - * Adds a reference to the **HttpApi** and **HttpApi.Client** packages. - * Configures the remote endpoint for the HttpApi.Client package. - * The result; - * The application can use all the functionality of the module as a **remote client**. - * This application also **serves** the module's **HTTP API**, but actually works just like a proxy by redirecting all requests (for the module) to another remote server. - -Next section describes the packages in more details. - -#### Domain Layer - -* **Do** divide the domain layer into two projects: - * **Domain.Shared** package, named as *CompanyName.ModuleName.Domain.Shared*, that contains constants, enums and other types those can be safely shared with the all layers of the module. This package can also be shared to 3rd-party clients. It can not contain entities, repositories, domain services or any other business objects. - * **Domain** package, named as *CompanyName.ModuleName.Domain*, that contains entities, repository interfaces, domain service interfaces and their implementations and other domain objects. - * Domain package depends on the **Domain.Shared** package. - -#### Application Layer - -* **Do** divide the application layer into two projects: - * **Application.Contracts** package, named as *CompanyName.ModuleName.Application.Contracts*, that contains application service interfaces and related data transfer objects. - * Application contract package depends on the **Domain.Shared** package. - * **Application** package, named as *CompanyName.ModuleName.Application*, that contains application service implementations. - * Application package depends on the **Domain** and the **Application.Contracts** packages. - -#### Infrastructure Layer - -* **Do** create a separated integration package for each ORM/database integration like Entity Framework Core and MongoDB. - * **Do**, for instance, create a *CompanyName.ModuleName.EntityFrameworkCore* package that abstracts the Entity Framework Core integration. ORM integration packages depend on the **Domain** package. - * **Do not** depend on other layers from the ORM/database integration package. -* **Do** create a separated integration package for each major library that is planned to be replaceable by another library without effecting the other packages. - -#### HTTP Layer - -* **Do** create an **HTTP API** package, named as *CompanyName.ModuleName.HttpApi*, to develop a REST style HTTP API for the module. - * HTTP API package only depends on the **Application.Contracts** package. It does not depend on the Application package. - * **Do** create a Controller for each application service (generally by implementing their interfaces). These controllers uses the application service interfaces to delegate the actions. It just configures routes, HTTP methods and other web related stuffs if needed. -* **Do** create an **HTTP API Client** package, named as *CompanyName.ModuleName.HttpApi.Client*, to provide client services for the HTTP API package. Those client services implement application interfaces as clients to a remote endpoint. - * HTTP API Client package only depends on the **Application.Contracts** package. - * **Do** use dynamic HTTP C# client proxy feature of the ABP framework. - -#### Web Layer - -* **Do** create a **Web** package, named as *CompanyName.ModuleName.Web*, that contains pages, views, scripts, styles, images and other UI components. - * Web package only depends on the **HttpApi** package. \ No newline at end of file diff --git a/docs/en/Best-Practices/Repositories.md b/docs/en/Best-Practices/Repositories.md deleted file mode 100644 index 97cae7c35e..0000000000 --- a/docs/en/Best-Practices/Repositories.md +++ /dev/null @@ -1,75 +0,0 @@ -## Repository Best Practices & Conventions - -### Repository Interfaces - -* **Do** define repository interfaces in the **domain layer**. -* **Do** define a repository interface (like `IIdentityUserRepository`) and create its corresponding implementations for **each aggregate root**. - * **Do** always use the created repository interface from the application code. - * **Do not** use generic repository interfaces (like `IRepository`) from the application code. - * **Do not** use `IQueryable` features in the application code (domain, application... layers). - -For the example aggregate root: - -````C# -public class IdentityUser : AggregateRoot -{ - //... -} -```` - -Define the repository interface as below: - -````C# -public interface IIdentityUserRepository : IBasicRepository -{ - //... -} -```` - -* **Do not** inherit the repository interface from the `IRepository` interface. Because it inherits the `IQueryable` and the repository should not expose `IQueryable` to the application. -* **Do** inherit the repository interface from `IBasicRepository` (as normally) or a lower-featured interface, like `IReadOnlyRepository` (if it's needed). -* **Do not** define repositories for entities those are **not aggregate roots**. - -### Repository Methods - -* **Do** define all repository methods as **asynchronous**. -* **Do** add an **optional** `cancellationToken` parameter to every method of the repository. Example: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - CancellationToken cancellationToken = default -); -```` - -* **Do** add an optional `bool includeDetails = true` parameter (default value is `true`) for every repository method which returns a **single entity**. Example: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default -); -```` - -This parameter will be implemented for ORMs to eager load sub collections of the entity. - -* **Do** add an optional `bool includeDetails = false` parameter (default value is `false`) for every repository method which returns a **list of entities**. Example: - -````C# -Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, - bool includeDetails = false, - CancellationToken cancellationToken = default -); -```` - -* **Do not** create composite classes to combine entities to get from repository with a single method call. Examples: *UserWithRoles*, *UserWithTokens*, *UserWithRolesAndTokens*. Instead, properly use `includeDetails` option to add all details of the entity when needed. -* **Avoid** to create projection classes for entities to get less property of an entity from the repository. Example: Avoid to create BasicUserView class to select a few properties needed for the use case needs. Instead, directly use the aggregate root class. However, there may be some exceptions for this rule, where: - * Performance is so critical for the use case and getting the whole aggregate root highly impacts the performance. - -### See Also - -* [Entity Framework Core Integration](Entity-Framework-Core-Integration.md) -* [MongoDB Integration](MongoDB-Integration.md) -* [Video tutorial](https://abp.io/video-courses/essentials/generic-repositories) \ No newline at end of file diff --git a/docs/en/Best-Practices/images/postgresql-delete-initial-migrations.png b/docs/en/Best-Practices/images/postgresql-delete-initial-migrations.png deleted file mode 100644 index 14788c5fb8..0000000000 Binary files a/docs/en/Best-Practices/images/postgresql-delete-initial-migrations.png and /dev/null differ diff --git a/docs/en/Best-Practices/images/postgresql-update-database.png b/docs/en/Best-Practices/images/postgresql-update-database.png deleted file mode 100644 index 30a5f3abe1..0000000000 Binary files a/docs/en/Best-Practices/images/postgresql-update-database.png and /dev/null differ diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md deleted file mode 100644 index 4060e5ba26..0000000000 --- a/docs/en/Blob-Storing-Aliyun.md +++ /dev/null @@ -1,78 +0,0 @@ -# BLOB Storing Aliyun Provider - -BLOB Storing Aliyun Provider can store BLOBs in [Aliyun Blob storage](https://help.aliyun.com/product/31815.html). - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Aliyun BLOB as the storage provider. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Aliyun` package. -* Run `abp add-package Volo.Abp.BlobStoring.Aliyun` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringAliyunModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the Aliyun storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAliyun(aliyun => - { - aliyun.AccessKeyId = "your aliyun access key id"; - aliyun.AccessKeySecret = "your aliyun access key secret"; - aliyun.Endpoint = "your oss endpoint"; - aliyun.RegionId = "your sts region id"; - aliyun.RoleArn = "the arn of ram role"; - aliyun.RoleSessionName = "the name of the certificate"; - aliyun.Policy = "policy"; - aliyun.DurationSeconds = "expiration date"; - aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = true; - }); - }); -}); -```` - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -### Options - -* **AccessKeyId** ([NotNull]string): AccessKey is the key to access the Alibaba Cloud API. It has full permissions for the account. Please keep it safe! Recommend to follow [Alibaba Cloud security best practicess](https://help.aliyun.com/document_detail/102600.html),Use RAM sub-user AccessKey to call API. -* **AccessKeySecret** ([NotNull]string): Same as above. -* **Endpoint** ([NotNull]string): Endpoint is the external domain name of OSS. See the [document](https://help.aliyun.com/document_detail/31837.html) for details. -* **UseSecurityTokenService** (bool): Use [STS temporary credentials](https://help.aliyun.com/document_detail/100624.html) to access OSS services,default: `false`. -* **RegionId** (string): Access address of STS service. See the [document](https://help.aliyun.com/document_detail/66053.html) for details. -* **RoleArn** ([NotNull]string): STS required role ARN. See the [document](https://help.aliyun.com/document_detail/100624.html) for details. -* **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish. -* **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details. -* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. -* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): - * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. - * Container names Must start and end with lowercase letters and numbers. - * Container names must be from **3** through **63** characters long. -* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aliyun, `AliyunBlobProvider` will try to create it. -* **TemporaryCredentialsCacheKey** (bool): The cache key of STS credentials. - - -## Aliyun Blob Name Calculator - -Aliyun Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: - -* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). -* Appends `tenants/` string if current tenant is not `null`. -* Appends the BLOB name. - -## Other Services - -* `AliyunBlobProvider` is the main service that implements the Aliyun BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AliyunBlobProvider` class). -* `IAliyunBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAliyunBlobNameCalculator` by default. -* `IOssClientFactory` is used create OSS client. It is implemented by the `DefaultOssClientFactory` by default. You can override/replace it,if you want customize. diff --git a/docs/en/Blob-Storing-Aws.md b/docs/en/Blob-Storing-Aws.md deleted file mode 100644 index c69024db28..0000000000 --- a/docs/en/Blob-Storing-Aws.md +++ /dev/null @@ -1,85 +0,0 @@ -# BLOB Storing Aws Provider - -BLOB Storing Aws Provider can store BLOBs in [Amazon Simple Storage Service](https://aws.amazon.com/s3/). - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Aws BLOB as the storage provider. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Aws` package. -* Run `abp add-package Volo.Abp.BlobStoring.Aws` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringAwsModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the Aws storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAws(Aws => - { - Aws.AccessKeyId = "your Aws access key id"; - Aws.SecretAccessKey = "your Aws access key secret"; - Aws.UseCredentials = "set true to use credentials"; - Aws.UseTemporaryCredentials = "set true to use temporary credentials"; - Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; - Aws.ProfileName = "the name of the profile to get credentials from"; - Aws.ProfilesLocation = "the path to the aws credentials file to look at"; - Aws.Region = "the system name of the service"; - Aws.Name = "the name of the federated user"; - Aws.Policy = "policy"; - Aws.DurationSeconds = "expiration date"; - Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = true; - }); - }); -}); - -```` - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -### Options - -* **AccessKeyId** (string): AWS Access Key ID. -* **SecretAccessKey** (string): AWS Secret Access Key. -* **UseCredentials** (bool): Use [credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html) to access AWS services,default : `false`. -* **UseTemporaryCredentials** (bool): Use [temporary credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempSessionToken.html) to access AWS services,default : `false`. -* **UseTemporaryFederatedCredentials** (bool): Use [federated user temporary credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempFederationToken.html) to access AWS services, default : `false`. -* **ProfileName** (string): The [name of the profile](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to get credentials from. -* **ProfilesLocation** (string): The path to the aws credentials file to look at. -* **Region** (string): The system name of the service. -* **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy. -* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using sub-accounts operated OSS,if the value is 0. -* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): - * Bucket names must be between **3** and **63** characters long. - * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). - * Bucket names must begin and end with a letter or number. - * Bucket names must not be formatted as an IP address (for example, 192.168.5.4). - * Bucket names can't begin with **xn--** (for buckets created after February 2020). - * Bucket names must be unique within a partition. - * Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration. -* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aws, `AwsBlobProvider` will try to create it. - -## Aws Blob Name Calculator - -Aws Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: - -* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). -* Appends `tenants/` string if current tenant is not `null`. -* Appends the BLOB name. - -## Other Services - -* `AwsBlobProvider` is the main service that implements the Aws BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AwsBlobProvider` class). -* `IAwsBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAwsBlobNameCalculator` by default. -* `IAmazonS3ClientFactory` is used create OSS client. It is implemented by the `DefaultAmazonS3ClientFactory` by default. You can override/replace it,if you want customize. diff --git a/docs/en/Blob-Storing-Azure.md b/docs/en/Blob-Storing-Azure.md deleted file mode 100644 index 99833e71c0..0000000000 --- a/docs/en/Blob-Storing-Azure.md +++ /dev/null @@ -1,62 +0,0 @@ -# BLOB Storing Azure Provider - -BLOB Storing Azure Provider can store BLOBs in [Azure Blob storage](https://azure.microsoft.com/en-us/services/storage/blobs/). - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Azure BLOB as the storage provider. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BlobStoring.Azure](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Azure) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Azure` package. -* Run `abp add-package Volo.Abp.BlobStoring.Azure` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring.Azure](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Azure) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringAzureModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the azure storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAzure(azure => - { - azure.ConnectionString = "your azure connection string"; - azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = true; - }); - }); -}); -```` - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -### Options - -* **ConnectionString** (string): A connection string includes the authorization information required for your application to access data in an Azure Storage account at runtime using Shared Key authorization. Please refer to Azure documentation: https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string -* **ContainerName** (string): You can specify the container name in azure. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Azure has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): - * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. - * Every dash (-) character must be immediately preceded and followed by a letter or number; consecutive dashes are not permitted in container names. - * All letters in a container name must be **lowercase**. - * Container names must be from **3** through **63** characters long. -* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in azure, `AzureBlobProvider` will try to create it. - - -## Azure Blob Name Calculator - -Azure Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: - -* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). -* Appends `tenants/` string if current tenant is not `null`. -* Appends the BLOB name. - -## Other Services - -* `AzureBlobProvider` is the main service that implements the Azure BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AzureBlobProvider` class). -* `IAzureBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAzureBlobNameCalculator` by default. diff --git a/docs/en/Blob-Storing-Custom-Provider.md b/docs/en/Blob-Storing-Custom-Provider.md deleted file mode 100644 index 0b1255e0b7..0000000000 --- a/docs/en/Blob-Storing-Custom-Provider.md +++ /dev/null @@ -1,177 +0,0 @@ -# BLOB Storing: Creating a Custom Provider - -This document explains how you can create a new storage provider for the BLOB storing system with an example. - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to create a new storage provider. - -## Example Implementation - -The first step is to create a class implements the `IBlobProvider` interface or inherit from the `BlobProviderBase` abstract class. - -````csharp -using System.IO; -using System.Threading.Tasks; -using Volo.Abp.BlobStoring; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency - { - public override Task SaveAsync(BlobProviderSaveArgs args) - { - //TODO... - } - - public override Task DeleteAsync(BlobProviderDeleteArgs args) - { - //TODO... - } - - public override Task ExistsAsync(BlobProviderExistsArgs args) - { - //TODO... - } - - public override Task GetOrNullAsync(BlobProviderGetArgs args) - { - //TODO... - } - } -} -```` - -* `MyCustomBlobProvider` inherits from the `BlobProviderBase` and overrides the `abstract` methods. The actual implementation is up to you. -* Implementing `ITransientDependency` registers this class to the [Dependency Injection](Dependency-Injection.md) system as a transient service. - -> **Notice: Naming conventions are important**. If your class name doesn't end with `BlobProvider`, you must manually register/expose your service for the `IBlobProvider`. - -That's all. Now, you can configure containers (inside the `ConfigureServices` method of your [module](Module-Development-Basics.md)) to use the `MyCustomBlobProvider` class: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.ProviderType = typeof(MyCustomBlobProvider); - }); -}); -```` - -> See the [BLOB Storing document](Blob-Storing.md) if you want to configure a specific container. - -### BlobContainerConfiguration Extension Method - -If you want to provide a simpler configuration, create an extension method for the `BlobContainerConfiguration` class: - -````csharp -public static class MyBlobContainerConfigurationExtensions -{ - public static BlobContainerConfiguration UseMyCustomBlobProvider( - this BlobContainerConfiguration containerConfiguration) - { - containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); - return containerConfiguration; - } -} -```` - -Then you can configure containers easier using the extension method: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMyCustomBlobProvider(); - }); -}); -```` - -### Extra Configuration Options - -`BlobContainerConfiguration` allows to add/remove provider specific configuration objects. If your provider needs to additional configuration, you can create a wrapper class to the `BlobContainerConfiguration` for a type-safe configuration option: - -````csharp - public class MyCustomBlobProviderConfiguration - { - public string MyOption1 - { - get => _containerConfiguration - .GetConfiguration("MyCustomBlobProvider.MyOption1"); - set => _containerConfiguration - .SetConfiguration("MyCustomBlobProvider.MyOption1", value); - } - - private readonly BlobContainerConfiguration _containerConfiguration; - - public MyCustomBlobProviderConfiguration( - BlobContainerConfiguration containerConfiguration) - { - _containerConfiguration = containerConfiguration; - } - } -```` - -Then you can change the `MyBlobContainerConfigurationExtensions` class like that: - -````csharp -public static class MyBlobContainerConfigurationExtensions -{ - public static BlobContainerConfiguration UseMyCustomBlobProvider( - this BlobContainerConfiguration containerConfiguration, - Action configureAction) - { - containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); - - configureAction.Invoke( - new MyCustomBlobProviderConfiguration(containerConfiguration) - ); - - return containerConfiguration; - } - - public static MyCustomBlobProviderConfiguration GetMyCustomBlobProviderConfiguration( - this BlobContainerConfiguration containerConfiguration) - { - return new MyCustomBlobProviderConfiguration(containerConfiguration); - } -} -```` - -* Added an action parameter to the `UseMyCustomBlobProvider` method to allow developers to set the additional options. -* Added a new `GetMyCustomBlobProviderConfiguration` method to be used inside `MyCustomBlobProvider` class to obtain the configured values. - -Then anyone can set the `MyOption1` as shown below: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMyCustomBlobProvider(provider => - { - provider.MyOption1 = "my value"; - }); - }); -}); -```` - -Finally, you can access to the extra options using the `GetMyCustomBlobProviderConfiguration` method: - -````csharp -public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency -{ - public override Task SaveAsync(BlobProviderSaveArgs args) - { - var config = args.Configuration.GetMyCustomBlobProviderConfiguration(); - var value = config.MyOption1; - - //... - } -} -```` - -## Contribute? - -If you create a new provider and you think it can be useful for other developers, please consider to [contribute](Contribution/Index.md) to the ABP Framework on GitHub. diff --git a/docs/en/Blob-Storing-Database.md b/docs/en/Blob-Storing-Database.md deleted file mode 100644 index c14086a2f4..0000000000 --- a/docs/en/Blob-Storing-Database.md +++ /dev/null @@ -1,98 +0,0 @@ -# BLOB Storing Database Provider - -BLOB Storing Database Storage Provider can store BLOBs in a relational or non-relational database. - -There are two database providers implemented; - -* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) package implements for [EF Core](Entity-Framework-Core.md), so it can store BLOBs in [any DBMS supported](https://docs.microsoft.com/en-us/ef/core/providers/) by the EF Core. -* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) package implements for [MongoDB](MongoDB.md). - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a database as the storage provider. - -## Installation - -### Automatic Installation - -If you've created your solution based on the [application startup template](Startup-Templates/Application.md), you can use the `abp add-module` [CLI](CLI.md) command to automatically add related packages to your solution. - -Open a command prompt (terminal) in the folder containing your solution (`.sln`) file and run the following command: - -````bash -abp add-module Volo.Abp.BlobStoring.Database -```` - -This command adds all the NuGet packages to corresponding layers of your solution. If you are using EF Core, it adds necessary configuration, adds a new database migration and updates the database. - -### Manual Installation - -Here, all the NuGet packages defined by this provider; - -* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain.Shared) -* [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) -* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) -* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) - -You can only install Volo.Abp.BlobStoring.Database.EntityFrameworkCore or Volo.Abp.BlobStoring.Database.MongoDB (based on your preference) since they depends on the other packages. - -After installation, add `DepenedsOn` attribute to your related [module](Module-Development-Basics.md). Here, the list of module classes defined by the related NuGet packages listed above: - -* `BlobStoringDatabaseDomainModule` -* `BlobStoringDatabaseDomainSharedModule` -* `BlobStoringDatabaseEntityFrameworkCoreModule` -* `BlobStoringDatabaseMongoDbModule` - -Whenever you add a NuGet package to a project, also add the module class dependency. - -If you are using EF Core, you also need to configure your **Migration DbContext** to add BLOB storage tables to your database schema. Call `builder.ConfigureBlobStoring()` extension method inside the `OnModelCreating` method to include mappings to your DbContext. Then you can use the standard `Add-Migration` and `Update-Database` [commands](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create necessary tables in your database. - -## Configuration - -### Connection String - -If you will use your `Default` connection string, you don't need to any additional configuration. - -If you want to use a separate database for BLOB storage, use the `AbpBlobStoring` as the [connection string](Connection-Strings.md) name in your configuration file (`appsettings.json`). In this case, also read the [EF Core Migrations](Entity-Framework-Core-Migrations.md) document to learn how to create and use a different database for a desired module. - -### Configuring the Containers - -If you are using only the database storage provider, you don't need to manually configure it, since it is automatically done. If you are using multiple storage providers, you may want to configure it. - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the database storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseDatabase(); - }); -}); -```` - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -## Additional Information - -It is expected to use the [BLOB Storing services](Blob-Storing.md) to use the BLOB storing system. However, if you want to work on the database tables/entities, you can use the following information. - -### Entities - -Entities defined for this module: - -* `DatabaseBlobContainer` (aggregate root) represents a container stored in the database. -* `DatabaseBlob` (aggregate root) represents a BLOB in the database. - -See the [entities document](Entities.md) to learn what is an entity and aggregate root. - -### Repositories - -* `IDatabaseBlobContainerRepository` -* `IDatabaseBlobRepository` - -You can also use `IRepository` and `IRepository` to take the power of IQueryable. See the [repository document](Repositories.md) for more. - -### Other Services - -* `DatabaseBlobProvider` is the main service that implements the database BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `DatabaseBlobProvider` class). \ No newline at end of file diff --git a/docs/en/Blob-Storing-File-System.md b/docs/en/Blob-Storing-File-System.md deleted file mode 100644 index 15ecda4ed7..0000000000 --- a/docs/en/Blob-Storing-File-System.md +++ /dev/null @@ -1,59 +0,0 @@ -# BLOB Storing File System Provider - -File System Storage Provider is used to store BLOBs in the local file system as standard files inside a folder. - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use the file system. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.FileSystem` package. -* Run `abp add-package Volo.Abp.BlobStoring.FileSystem` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the File System storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseFileSystem(fileSystem => - { - fileSystem.BasePath = "C:\\my-files"; - }); - }); -}); -```` - -`UseFileSystem` extension method is used to set the File System Provider for a container and configure the file system options. - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -### Options - -* **BasePath** (string): The base folder path to store BLOBs. It is required to set this option. -* **AppendContainerNameToBasePath** (bool; default: `true`): Indicates whether to create a folder with the container name inside the base folder. If you store multiple containers in the same `BaseFolder`, leave this as `true`. Otherwise, you can set it to `false` if you don't like an unnecessarily deeper folder hierarchy. - -## File Path Calculation - -File System Provider organizes BLOB files inside folders and implements some conventions. The full path of a BLOB file is determined by the following rules by default: - -* It starts with the `BasePath` configured as shown above. -* Appends `host` folder if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). -* Appends `tenants/` folder if current tenant is not `null`. -* Appends the container's name if `AppendContainerNameToBasePath` is `true`. If container name contains `/`, this will result with nested folders. -* Appends the BLOB name. If the BLOB name contains `/` it creates folders. If the BLOB name contains `.` it will have a file extension. - -## Extending the File System BLOB Provider - -* `FileSystemBlobProvider` is the main service that implements the File System storage. You can inherit from this class and [override](Customizing-Application-Modules-Overriding-Services.md) methods to customize it. - -* The `IBlobFilePathCalculator` service is used to calculate the file paths. Default implementation is the `DefaultBlobFilePathCalculator`. You can replace/override it if you want to customize the file path calculation. \ No newline at end of file diff --git a/docs/en/Blob-Storing-Minio.md b/docs/en/Blob-Storing-Minio.md deleted file mode 100644 index 6e0d7626b7..0000000000 --- a/docs/en/Blob-Storing-Minio.md +++ /dev/null @@ -1,69 +0,0 @@ -# BLOB Storing Minio Provider - -BLOB Storing Minio Provider can store BLOBs in [MinIO Object storage](https://min.io/). - -> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Minio BLOB as the storage provider. - -## Installation - -Use the ABP CLI to add [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Minio` package. -* Run `abp add-package Volo.Abp.BlobStoring.Minio` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringMinioModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). - -**Example: Configure to use the minio storage provider by default** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMinio(minio => - { - minio.EndPoint = "your minio endPoint"; - minio.AccessKey = "your minio accessKey"; - minio.SecretKey = "your minio secretKey"; - minio.BucketName = "your minio bucketName"; - }); - }); -}); -```` - -> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. - -### Options - -* **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html -* **AccessKey** (string): Access key is the user ID that uniquely identifies your account. -* **SecretKey** (string): Secret key is the password to your account. -* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: - * Bucket names must be between **3** and **63** characters long. - * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). - * Bucket names must begin and end with a letter or number. - * Bucket names must not be formatted as an IP address (for example, 192.168.5.4). - * Bucket names can't begin with **xn--** (for buckets created after February 2020). - * Bucket names must be unique within a partition. - * Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration. -* **WithSSL** (bool): Default value is `false`,Chain to MinIO Client object to use https instead of http. -* **CreateContainerIfNotExists** (bool): Default value is `false`, If a bucket does not exist in minio, `MinioBlobProvider` will try to create it. - - -## Minio Blob Name Calculator - -Minio Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: - -* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). -* Appends `tenants/` string if current tenant is not `null`. -* Appends the BLOB name. - -## Other Services - -* `MinioBlobProvider` is the main service that implements the Minio BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `MinioBlobProvider` class). -* `IMinioBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultMinioBlobNameCalculator` by default. diff --git a/docs/en/Blob-Storing.md b/docs/en/Blob-Storing.md deleted file mode 100644 index 82c2a861ab..0000000000 --- a/docs/en/Blob-Storing.md +++ /dev/null @@ -1,308 +0,0 @@ -# BLOB Storing - -It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. - -A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. - -The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; - -* You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. -* You can then **easily change** your BLOB storage without changing your application code. -* If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. - -ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](Multi-Tenancy.md). - -## BLOB Storage Providers - -The ABP Framework has already the following storage provider implementations: - -* [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files. -* [Database](Blob-Storing-Database.md): Stores BLOBs in a database. -* [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/). -* [Aliyun](Blob-Storing-Aliyun.md): Stores BLOBs on the [Aliyun Storage Service](https://help.aliyun.com/product/31815.html). -* [Minio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/). -* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/). - -More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework. - -Multiple providers **can be used together** by the help of the **container system**, where each container can uses a different provider. - -> BLOB storing system can not work unless you **configure a storage provider**. Refer to the linked documents for the storage provider configurations. - -## Installation - -[Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) is the main package that defines the BLOB storing services. You can use this package to use the BLOB Storing system without depending a specific storage provider. - -Use the ABP CLI to add this package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring` package. -* Run `abp add-package Volo.Abp.BlobStoring` command. - -If you want to do it manually, install the [Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## The IBlobContainer - -`IBlobContainer` is the main interface to store and read BLOBs. Your application may have multiple containers and each container can be separately configured. But, there is a **default container** that can be simply used by [injecting](Dependency-Injection.md) the `IBlobContainer`. - -**Example: Simply save and read bytes of a named BLOB** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.BlobStoring; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IBlobContainer _blobContainer; - - public MyService(IBlobContainer blobContainer) - { - _blobContainer = blobContainer; - } - - public async Task SaveBytesAsync(byte[] bytes) - { - await _blobContainer.SaveAsync("my-blob-1", bytes); - } - - public async Task GetBytesAsync() - { - return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); - } - } -} -```` - -This service saves the given bytes with the `my-blob-1` name and then gets the previously saved bytes with the same name. - -> A BLOB is a named object and **each BLOB should have a unique name**, which is an arbitrary string. - -`IBlobContainer` can work with `Stream` and `byte[]` objects, which will be detailed in the next sections. - -### Saving BLOBs - -`SaveAsync` method is used to save a new BLOB or replace an existing BLOB. It can save a `Stream` by default, but there is a shortcut extension method to save byte arrays. - -`SaveAsync` gets the following parameters: - -* **name** (string): Unique name of the BLOB. -* **stream** (Stream) or **bytes** (byte[]): The stream to read the BLOB content or a byte array. -* **overrideExisting** (bool): Set `true` to replace the BLOB content if it does already exists. Default value is `false` and throws `BlobAlreadyExistsException` if there is already a BLOB in the container with the same name. - -### Reading/Getting BLOBs - -* `GetAsync`: Only gets a BLOB name and returns a `Stream` object that can be used to read the BLOB content. Always **dispose the stream** after using it. This method throws exception, if it can not find the BLOB with the given name. -* `GetOrNullAsync`: In opposite to the `GetAsync` method, this one returns `null` if there is no BLOB found with the given name. -* `GetAllBytesAsync`: Returns a `byte[]` instead of a `Stream`. Still throws exception if can not find the BLOB with the given name. -* `GetAllBytesOrNullAsync`: In opposite to the `GetAllBytesAsync` method, this one returns `null` if there is no BLOB found with the given name. - -### Deleting BLOBs - -`DeleteAsync` method gets a BLOB name and deletes the BLOB data. It doesn't throw any exception if given BLOB was not found. Instead, it returns a `bool` indicating that the BLOB was actually deleted or not, if you care about it. - -### Other Methods - -* `ExistsAsync` method simply checks if there is a BLOB in the container with the given name. - -### About Naming the BLOBs - -There is not a rule for naming the BLOBs. A BLOB name is just a string that is unique per container (and per tenant - see the "*Multi-Tenancy*" section). However, different storage providers may conventionally implement some practices. For example, the [File System Provider](Blob-Storing-File-System.md) use directory separators (`/`) and file extensions in your BLOB name (if your BLOB name is `images/common/x.png` then it is saved as `x.png` in the `images/common` folder inside the root container folder). - -## Typed IBlobContainer - -Typed BLOB container system is a way of creating and managing **multiple containers** in an application; - -* **Each container is separately stored**. That means the BLOB names should be unique in a container and two BLOBs with the same name can live in different containers without effecting each other. -* **Each container can be separately configured**, so each container can use a different storage provider based on your configuration. - -To create a typed container, you need to create a simple class decorated with the `BlobContainerName` attribute: - -````csharp -using Volo.Abp.BlobStoring; - -namespace AbpDemo -{ - [BlobContainerName("profile-pictures")] - public class ProfilePictureContainer - { - - } -} -```` - -> If you don't use the `BlobContainerName` attribute, ABP Framework uses the full name of the class (with namespace), but it is always recommended to use a container name which is stable and does not change even if you rename the class. - -Once you create the container class, you can inject `IBlobContainer` for your container type. - -**Example: An [application service](Application-Services.md) to save and read profile picture of the [current user](CurrentUser.md)** - -````csharp -[Authorize] -public class ProfileAppService : ApplicationService -{ - private readonly IBlobContainer _blobContainer; - - public ProfileAppService(IBlobContainer blobContainer) - { - _blobContainer = blobContainer; - } - - public async Task SaveProfilePictureAsync(byte[] bytes) - { - var blobName = CurrentUser.GetId().ToString(); - await _blobContainer.SaveAsync(blobName, bytes); - } - - public async Task GetProfilePictureAsync() - { - var blobName = CurrentUser.GetId().ToString(); - return await _blobContainer.GetAllBytesOrNullAsync(blobName); - } -} -```` - -`IBlobContainer` has the same methods with the `IBlobContainer`. - -> It is a good practice to **always use a typed container while developing re-usable modules**, so the final application can configure the provider for your container without effecting the other containers. - -### The Default Container - -If you don't use the generic argument and directly inject the `IBlobContainer` (as explained before), you get the default container. Another way of injecting the default container is using `IBlobContainer`, which returns exactly the same container. - -The name of the default container is `default`. - -### Named Containers - -Typed containers are just shortcuts for named containers. You can inject and use the `IBlobContainerFactory` to get a BLOB container by its name: - -````csharp -public class ProfileAppService : ApplicationService -{ - private readonly IBlobContainer _blobContainer; - - public ProfileAppService(IBlobContainerFactory blobContainerFactory) - { - _blobContainer = blobContainerFactory.Create("profile-pictures"); - } - - //... -} -```` - -## IBlobContainerFactory - -`IBlobContainerFactory` is the service that is used to create the BLOB containers. One example was shown above. - -**Example: Create a container by name** - -````csharp -var blobContainer = blobContainerFactory.Create("profile-pictures"); -```` - -**Example: Create a container by type** - -````csharp -var blobContainer = blobContainerFactory.Create(); -```` - -> You generally don't need to use the `IBlobContainerFactory` since it is used internally, when you inject a `IBlobContainer` or `IBlobContainer`. - -## Configuring the Containers - -Containers should be configured before using them. The most fundamental configuration is to **select a BLOB storage provider** (see the "*BLOB Storage Providers*" section above). - -`AbpBlobStoringOptions` is the [options class](Options.md) to configure the containers. You can configure the options inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). - -### Configure a Single Container - -````csharp -Configure(options => -{ - options.Containers.Configure(container => - { - //TODO... - }); -}); -```` - -This example configures the `ProfilePictureContainer`. You can also configure by the container name: - -````csharp -Configure(options => -{ - options.Containers.Configure("profile-pictures", container => - { - //TODO... - }); -}); -```` - -### Configure the Default Container - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - //TODO... - }); -}); -```` - -> There is a special case about the default container; If you don't specify a configuration for a container, it **fallbacks to the default container configuration**. This is a good way to configure defaults for all containers and specialize configuration for a specific container when needed. - -### Configure All Containers - -````csharp -Configure(options => -{ - options.Containers.ConfigureAll((containerName, containerConfiguration) => - { - //TODO... - }); -}); -```` - -This is a way to configure all the containers. - -> The main difference from configuring the default container is that `ConfigureAll` overrides the configuration even if it was specialized for a specific container. - -## Multi-Tenancy - -If your application is set as multi-tenant, the BLOB Storage system **works seamlessly with the [multi-tenancy](Multi-Tenancy.md)**. All the providers implement multi-tenancy as a standard feature. They **isolate BLOBs** of different tenants from each other, so they can only access to their own BLOBs. It means you can use the **same BLOB name for different tenants**. - -If your application is multi-tenant, you may want to control **multi-tenancy behavior** of the containers individually. For example, you may want to **disable multi-tenancy** for a specific container, so the BLOBs inside it will be **available to all the tenants**. This is a way to share BLOBs among all tenants. - -**Example: Disable multi-tenancy for a specific container** - -````csharp -Configure(options => -{ - options.Containers.Configure(container => - { - container.IsMultiTenant = false; - }); -}); -```` - -> If your application is not multi-tenant, no worry, it works as expected. You don't need to configure the `IsMultiTenant` option. - -## Extending the BLOB Storing System - -Most of the times, you won't need to customize the BLOB storage system except [creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md). However, you can replace any service (injected via [dependency injection](Dependency-Injection.md)), if you need. Here, some other services not mentioned above, but you may want to know: - -* `IBlobProviderSelector` is used to get a `IBlobProvider` instance by a container name. Default implementation (`DefaultBlobProviderSelector`) selects the provider using the configuration. -* `IBlobContainerConfigurationProvider` is used to get the `BlobContainerConfiguration` for a given container name. Default implementation (`DefaultBlobContainerConfigurationProvider`) gets the configuration from the `AbpBlobStoringOptions` explained above. - -## BLOB Storing vs File Management System - -Notice that BLOB storing is not a file management system. It is a low level system that is used to save, get and delete named BLOBs. It doesn't provide a hierarchical structure like directories, you may expect from a typical file system. - -If you want to create folders and move files between folders, assign permissions to files and share files between users then you need to implement your own application on top of the BLOB Storage system. - -## See Also - -* [Creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md) diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/abp-studio-solution-runner.png b/docs/en/Blog-Posts/2024-05 ABP Unification/abp-studio-solution-runner.png new file mode 100644 index 0000000000..2b5bc20678 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/abp-studio-solution-runner.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/cover.png b/docs/en/Blog-Posts/2024-05 ABP Unification/cover.png new file mode 100644 index 0000000000..95e8363e67 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/cover.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/docs-new-navigation.png b/docs/en/Blog-Posts/2024-05 ABP Unification/docs-new-navigation.png new file mode 100644 index 0000000000..ba1f421339 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/docs-new-navigation.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/docs-project-selection.png b/docs/en/Blog-Posts/2024-05 ABP Unification/docs-project-selection.png new file mode 100644 index 0000000000..2e4dadafc6 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/docs-project-selection.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/new-get-started.png b/docs/en/Blog-Posts/2024-05 ABP Unification/new-get-started.png new file mode 100644 index 0000000000..ed1483f343 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/new-get-started.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/new-mega-menu.png b/docs/en/Blog-Posts/2024-05 ABP Unification/new-mega-menu.png new file mode 100644 index 0000000000..75c21a9693 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/new-mega-menu.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/new-pricing.png b/docs/en/Blog-Posts/2024-05 ABP Unification/new-pricing.png new file mode 100644 index 0000000000..4d299da6ee Binary files /dev/null and b/docs/en/Blog-Posts/2024-05 ABP Unification/new-pricing.png differ diff --git a/docs/en/Blog-Posts/2024-05 ABP Unification/post.md b/docs/en/Blog-Posts/2024-05 ABP Unification/post.md new file mode 100644 index 0000000000..7046c53855 --- /dev/null +++ b/docs/en/Blog-Posts/2024-05 ABP Unification/post.md @@ -0,0 +1,111 @@ +# Unifying the ABP.IO Platform + +I am very excited to announce that some big changes and improvements are coming to the ABP.IO Platform soon. In this post, I will explain the changes we are currently working on. Here, a brief list of these changes: + +* We are merging the subdomains of the ABP.IO Platform websites: Community.abp.io, commercial.abp.io, blog.abp.io, docs.abp.io websites and their contents are being merged into the main domain, abp.io. +* ABP (open source) and ABP Commercial documents are being merged into a single documentation. +* Introducing ABP Studio Community Edition. + +These changes won't effect the license conditions. The open source part will remain the same and the commercial license contents will also be the same. The aim of the changes is to make the platform more consistent, holistic, understandable and easy to start. + +Let's dive deep... + +## Merging the ABP.IO Websites + +ABP.IO website has many subdomains currently: + +* **abp.io**: Home page of the open source ABP Framework project. +* **community.abp.io**: A website that community can share contents and we organize events. +* **commercial.abp.io**: A website to promote and sell commercial ABP licenses which have pre-built modules, themes, tooling and support on top of the ABP Framework. +* **docs.abp.io**: The technical documentation of the ABP Framework and ABP Commercial. +* **blog.abp.io**: A blog website to announce the news on the platform. +* **support.abp.io**: Premium support for the ABP Commercial customers. + +All these subdomains (except the support website for now) are being merged to the abp.io domain. All their contents and UI designs are being revised and enriched. + +Some fundamental purposes of that change are; + +* Making content more coherent and holistic, +* Making the design more harmonious, +* Making the contents of the old subdomains more visible and reachable, +* Allow you to navigate through the web pages much easier, +* Reducing duplications between different websites, + +I will highlight a few important changes in the next sections. + +### The New Mega Menu + +As I said above, the abp.io UI design is also being revised. One of the big revisions is the main menu. We are replacing the current main navigation by a mega menu as shown in the following figure: + +![new-mega-menu](new-mega-menu.png) + +We believe that new mega menu will allow you to navigate through the web pages much easier. + +### The New Get Started Page + +We are constantly working to improve ABP's onboarding experience. With the new platform changes, we now offer ABP Studio as the starting point for the ABP Platform. You can still use the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to created new ABP solutions, but the new ABP Studio makes it much easier and understandable. It also provides features to easily run and monitor your applications, even in the Community edition. + +![new-get-started](new-get-started.png) + +You can easily download and install ABP Studio, login with your abp.io account and create your first ABP solution. + +### The New Pricing Page + +Since the [ABP Commercial website](https://commercial.abp.io/) has merged with the main website, you will see the *Pricing* page located on the main menu of the abp.io website. We have completely revised the design and content of this page to better reflect which features are open source and free, and what is included in the commercial licenses. + +![new-pricing](new-pricing.png) + +As mentioned above, all the free & open source features are still free & open source. In addition, we included the ABP Studio Community edition (will be explained below) to the free license. + +## Merging the ABP Platform Documentation + +Currently, ABP Framework (open source) and ABP Commercial [documents](https://docs.abp.io/) are completely separated. You can switch between them on the left side: + +![docs-project-selection](docs-project-selection.png) + +Based on our and customers' experiences, there are some problems with that approach: + +* Getting started, development tutorials, release notes, road map and some other documents are duplicated (or very similar) among ABP Framework and ABP Commercial documents. +* For ABP Commercial users, it is not clear if they also need to read the ABP Framework (open source) documentation or not. Also, when they read the framework document, some parts are different for ABP Commercial users, and it is also not clear in some cases. + +We are currently working to completely merge the ABP Framework (open source) and ABP Commercial documentation, remove duplications and revisit the contents. We will clearly indicate if a part of a document requires a commercial license. + +The left navigation panel tree is also completely revisited and simplified: + +![docs-new-navigation](docs-new-navigation.png) + +## The ABP Studio Community Edition + +[ABP Studio](https://docs.abp.io/en/commercial/latest/studio/index) is a cross-platform desktop application designed for ABP and .NET developers. It aims to provide a comfortable development environment by automating tasks, providing insights about your solution, and simplifying the processes of creation, development, execution, browsing, monitoring, tracing, and deploying your solutions. + +Here, a screenshot from the *Solution Runner* screen of ABP Studio: + +![abp-studio-solution-runner](abp-studio-solution-runner.png) + +ABP Studio has been started as a commercial product, as a part of [ABP Commercial](https://commercial.abp.io/). We are very excited to announce that the *Community Edition* will be available soon for free. It will have some missing features and limitations compared to the full edition, but will be enough to create, explore and run ABP solutions easily. + +We will be offering ABP Studio as a starting point to the ABP platform. The [Getting Started](https://docs.abp.io/en/abp/latest/Getting-Started-Overall) and other documents will use ABP Studio to create new solutions and perform ABP-related operations. + +## Other News + +We are also working on some other topics related to these changes. Some of them are; + +* Completely renewing the [startup templates](https://docs.abp.io/en/abp/latest/Startup-Templates/Index) (with ABP Studio), so they will be more flexible and will provide more options. +* Providing a tool to automatically convert ABP solutions created with open source startup templates into ABP commercial. + +## Questions + +I tried to explain all the important changes in this post. However, you may have some questions in your mind. + +### What should open source users expect? + +Since the [ABP Commercial](https://commercial.abp.io/) website content is merged with the main [abp.io](https://abp.io/) website, you will see paid features being introduced on the main website. The pricing page will also be available on the same website. This may lead you to wonder whether the ABP Platform is a fully paid product. The simple answer to this question is "No". Actually, nothing has changed on the open source side. Everything will be the same. Additionally, open source users will now have ABP Studio Community Edition for free. So open source has more for its users than before. + +### What should ABP Commercial customers expect? + +ABP Commercial license holders may wonder if any license change happens. The answer is "No". All the license types, rules, restrictions and features are the same. With the changes explained in this post, you will follow the documentation easier (since you won't need to go to another website for the framework documentation) and you will better understand what special features are available to you. + +## Last Words + +With this post, we wanted to announce the changes to be made on the ABP platform to the ABP community, so don't be surprised or curious about what happened. If you have any questions or suggestions, feel free to write a comment for this blog post or send an email to info@abp.io. + diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/POST.md b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/POST.md new file mode 100644 index 0000000000..23260950d4 --- /dev/null +++ b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/POST.md @@ -0,0 +1,269 @@ +# ABP.IO Platform 8.2 RC Has Been Released + +Today, we are happy to release the [ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) version **8.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v8.2! Thanks to all of you. + +## Get Started with the 8.2 RC + +Follow the steps below to try version 8.2.0 RC today: + +1) **Upgrade** the ABP CLI to version `8.2.0-rc.3` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 8.2.0-rc.3 +```` + +**or install** it if you haven't before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 8.2.0-rc.3 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. + +> You can also use the [Get Started](https://abp.io/get-started) page to generate a CLI command to create a new application. + +You can use any IDE that supports .NET 8.x, like [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). + +## Migration Guides + +There are a few breaking changes in this version that may affect your application. +Please see the following migration documents, if you are upgrading from v8.x or earlier: + +* [ABP Framework 8.x to 8.2 Migration Guide](https://docs.abp.io/en/abp/8.2/Migration-Guides/Abp-8_2) +* [ABP Commercial 8.x to 8.2 Migration Guide](https://docs.abp.io/en/commercial/8.2/migration-guides/v8_2) + +## What's New with ABP Framework 8.2? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +* Blazor Full-Stack Web App UI +* Introducing the `IBlockUiService` for Blazor UI +* Allowing Case-Insensitive Indexes for MongoDB +* Other News... + +### Blazor Full-Stack Web App UI + +ASP.NET Blazor in .NET 8 allows you to use a single powerful component model to handle all of your web UI needs, including server-side rendering, client-side rendering, streaming rendering, progressive enhancement, and much more! + +ABP v8.2.x supports the new Blazor Web App template, which you can directly create with the following command: + +```bash +abp new BookStore -t app -u blazor-webapp +``` + +When you create the project, you will typically see two main projects for Blazor UI, besides other projects: + +* **MyCompanyName.MyProjectName.Blazor.WebApp** (startup project of your application, and contains `App.razor` component, which is the root component of your application) +* **MyCompanyName.MyProjectName.Blazor.WebApp.Client** + +This new template overcomes the disadvantages of both Blazor WASM and Blazor Server applications and allows you to decide which approaches to use for a specific page or component. Therefore, you can imagine this new web UI as a combination of both Blazor Server and Blazor WASM. + +> This approach mainly overcomes the **large binary downloads of Blazor WASM**, and it resolves the Blazor Server's problem, which **always needs to be connected to the server via SignalR**. + +> If you are considering migrating your existing Blazor project to Blazor WebApp or want to learn more about this new template, please read the [Migrating to Blazor Web App](https://docs.abp.io/en/abp/8.2/Migration-Guides/Abp-8-2-Blazor-Web-App) guide. + +### Introducing the `IBlockUiService` for Blazor UI + +In this version, ABP Framework introduces the [`IBlockUiService`](https://docs.abp.io/en/abp/8.2/UI/Blazor/Block-Busy) for Blazor UI. This service uses UI Block API to disable/block the page or a part of the page. + +You just need to simply inject the `IBlockUiService` to your page or component and call the `Block` or `UnBlock` method to block/unblock the specified element: + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index + { + private readonly IBlockUiService _blockUiService; + + public Index(IBlockUiService _blockUiService) + { + _blockUiService = blockUiService; + } + + public async Task BlockForm() + { + /* + Parameters of Block method: + selectors: A string containing one or more selectors to match. https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector#selectors + busy : Set to true to show a progress indicator on the blocked area. + */ + await _blockUiService.Block(selectors: "#MySelectors", busy: true); + + //Unblocking the element + await _blockUiService.UnBlock(selectors: "#MySelectors"); + } + } +} + +``` + +Here is the resulting UI with all possible options (**block**, **block busy**, and **unblock**): + +![](block-ui-service-blazor.gif) + +### Allowing Case-Insensitive Indexes for MongoDB + +MongoDB allows case-insensitive string comparisons by using case-insensitive indexes. You can create a case-insensitive index by specifying a **collation**. + +To do that, you should override the `CreateModal` method, configure the `CreateCollectionOptions`, and specify the **collation** as below: + +```csharp +protected override void CreateModel(IMongoModelBuilder modelBuilder) +{ + base.CreateModel(modelBuilder); + + modelBuilder.Entity(b => + { + b.CreateCollectionOptions.Collation = new Collation(locale:"en_US", strength: CollationStrength.Secondary); + + b.ConfigureIndexes(indexes => + { + indexes.CreateOne( + new CreateIndexModel( + Builders.IndexKeys.Ascending("MyProperty"), + new CreateIndexOptions { Unique = true } + ) + ); + } + ); + }); +} +``` + +After this configuration, a unique index will be created for the `MyProperty` property and then you can perform case-insensitive string comparisons without the need to worry. See [#19073](https://github.com/abpframework/abp/pull/19073) for more information. + +### Other News + +* Angular package version has been updated to v17.3.0. See [#19915](https://github.com/abpframework/abp/pull/19915) for more info. +* OpenIddict [5.4.0 has been released on March 26](https://github.com/openiddict/openiddict-core/releases/tag/5.4.0). Therefore, we decided to upgrade the OpenIddict packages to v5.4.0. See [#19427](https://github.com/abpframework/abp/issues/19427). +* AutoMapper [13.0.1 was released on February 8](https://github.com/AutoMapper/AutoMapper/releases/tag/v13.0.1) and in this version, we upgraded AutoMapper packages to v13.0.1. See [#19564](https://github.com/abpframework/abp/pull/19564/). +* See other completed tasks in this version: [https://github.com/abpframework/abp/releases?q=8.2.0-rc](https://github.com/abpframework/abp/releases?q=8.2.0-rc&expanded=true) + +## What's New with ABP Commercial 8.2? + +We've also worked on ABP Commercial to align the features and changes made in the ABP Framework. The following sections introduce a few new features coming with ABP Commercial 8.2. + +### Session Management + +The [Session Management](https://docs.abp.io/en/commercial/8.2/modules/identity/session-management) feature allows you to prevent concurrent login and manage user sessions. You can allow concurrent login, allow only one session of the same device type, or logout from all other devices when a new session is created, by specifying in the settings page: + +![](concurrent-login-settings.png) + +Also, you can view and manage users sessions on the `Users` page of the [Identity Module](https://docs.abp.io/en/commercial/8.2/modules/identity): + +![](manage-user-sessions-1.png) +![](manage-user-sessions-2.png) + +### Suite: File/Image Property + +In this version, ABP Suite allows you to add a file/image property for an entity. You can select "File" as the property type for your properties as in the following figure: + +![](suite-file-property.png) + +Then, when you generate your entity and try to insert a record, you will see the file upload component on the create/update models: + +![](suite-file-property-create.png) + +You can upload a file with any supported extensions and under 10MB (this can be increased in the generated code, if you wish) and after that, you can download, delete and update the existing file any time you want: + +![](suite-file-upload-in-action.gif) + +> **Note:** This feature has already been implemented for MVC & Blazor UIs, but not implemented for Angular UI yet. We aim to implement it for Angular UI with v8.2.0. + +### Suite: DateOnly & TimeOnly Types + +In this version on, ABP Suite provides `DateOnly` and `TimeOnly` types as property types. You can select these types when you create an entity: + +![](suite-dateonly-timeonly-properties.png) + +Then, all related configurations (including db configurations) will be made by ABP Suite, and you will be able to see the fields in the UI: + +![](suite-dateonly-timeonly-ui.png) + +> **Note**: The `DateOnly` and `TimeOnly` types were introduced with .NET 6. Therefore, please make sure that all of your projects' target frameworks are .NET8+. With ABP v8.2, all startup templates target a single target framework, which is .NET8, so if you created your project with version 8.2+, you don't need to make any changes. + +### Periodic Log Deletion for Audit Logs + +In this version, the [Audit Logging Module](https://docs.abp.io/en/commercial/8.2/modules/audit-logging) provides a built-in periodic log deletion system. You can enable/disable the clean-up service system wide, in this way, you can turn off the clean up service for all tenants and their hosts: + +![](audit-logging-module-global-settings.png) + +> If the system wide clean up service is enabled, you can configure the global *Expired Item Deletion Period* for all tenants and hosts. + +When configuring the global settings for the audit log module from the host side in this manner, ensure that each tenant and host uses the global values. If you want to set tenant/host-specific values, you can do so under *Settings* -> *Audit Log* -> *General*. This way, you can disable the clean up service for specific tenants or host. It overrides the global settings: + +![](audit-logging-module-general-settings.png) + +> **Note**: To view the audit log settings, you need to enable the feature. For the host side, navigate to *Settings* -> *Feature Management* -> *Manage Host Features* -> *Audit Logging* -> *Enable audit log setting management*. + +## Community News + +### ABP Dotnet Conf 2024 Wrap Up + +![](abp-dotnet-conf-2024.png) + +We organized [ABP Dotnet Conference 2024](https://abp.io/conference/2024) on May 2024 and we are happy to share the success of the conference, which captivated overwhelmingly interested live viewers from all over the world. 29 great line up of speakers which includes .NET experts and Microsoft MVPs delivered captivating talks that resonated with the audiences. Each of the talks attracted a great amount of interest and a lot of questions, sparking curiosity in the attendees. + +Thanks to all speakers and attendees for joining our event. 🙏 + +> We shared our takeaways in a blog post, which you can read at [https://blog.abp.io/abp/ABP-Dotnet-Conference-2024-Wrap-Up](https://blog.abp.io/abp/ABP-Dotnet-Conference-2024-Wrap-Up). + +### DevDays Europe 2024 + +![](devdays-europe.jpg) + +Co-founder of [Volosoft](https://volosoft.com/), [Alper Ebiçoğlu](https://twitter.com/alperebicoglu) gave a speech about "How to Build a Multi-Tenant ASP.NET Core Application" at the [DevDays Europe 2024](https://devdays.lt/) on the 20th of May. + +### DevOps Pro Europe 2024 + +![](devops-pro-europe.jpg) + +We are thrilled to announce that the co-founder of [Volosoft](https://volosoft.com/) and Lead Developer of the [ABP Framework](https://abp.io/), [Halil Ibrahim Kalkan](https://x.com/hibrahimkalkan) gave a speech about "Building a Kubernetes Integrated Local Development Environment" in the [DevOps Pro Europe](https://devopspro.lt/) on the 24th of May. + +### Devnot Dotnet Conference 2024 + +We are happy to announce that core team members of the [ABP Framework](https://abp.io/), [Alper Ebiçoğlu](https://twitter.com/alperebicoglu) and [Enis Necipoğlu](https://twitter.com/EnisNecipoglu) will give speeches at the [Devnot Dotnet Conference 2024](https://dotnet.devnot.com/) on 25th of May. + +[Alper Ebiçoğlu](https://twitter.com/alperebicoglu) will talk about **"AspNet Core & Multitenancy"**: + +![](devnot-dotnet-conference-alper-ebicoglu.png) + +On the other hand, [Enis Necipoğlu](https://twitter.com/EnisNecipoglu) will talk about **"Reactive Programming with .NET MAUI"**: + +![](devnot-dotnet-conference-enis-necipoglu.png) + +### New ABP Community Articles + +There are exciting articles contributed by the ABP community as always. I will highlight some of them here: + +* [Ahmed Tarek](https://twitter.com/AhmedTarekHasa1) has created **four** new community articles: + * [🤔 When Implementations Affect Abstractions ⁉️](https://community.abp.io/posts/-when-implementations-affect-abstractions--ekx1o5xn) + * [👍 Design Best Practices In .NET C# 👀](https://community.abp.io/posts/design-best-practices-in-.net-c--eg8q8xh0) + * [👍 Chain of Responsibility Design Pattern In .NET C# 👀](https://community.abp.io/posts/chain-of-responsibility-design-pattern-in-.net-c--djmvkug1) + * [Flagged Enumerations: How To Represent Features Combinations Into One Field](https://community.abp.io/posts/flagged-enumerations-how-to-represent-features-combinations-into-one-field-9gj4l670) +* [Engincan Veske](https://github.com/EngincanV) has created **three** new community articles: + * [Performing Case-Insensitive Search in ABP Based-PostgreSQL Application: Using citext and Collation](https://community.abp.io/posts/caseinsensitive-search-in-abp-basedpostgresql-application-c9kb05dc) + * [Sentiment Analysis Within ABP-Based Application](https://community.abp.io/posts/sentiment-analysis-within-abpbased-application-lbsfkoxq) + * [Reusing and Optimizing Machine Learning Models in .NET](https://community.abp.io/posts/reusing-and-optimizing-machine-learning-models-in-.net-qj4ycnwu) +* [Unlocking Modularity in ABP.io A Closer Look at the Contributor Pattern](https://community.abp.io/posts/unlocking-modularity-in-abp.io-a-closer-look-at-the-contributor-pattern-ixf6wgbw) by [Qais Al khateeb](https://community.abp.io/members/qais.alkhateeb@devnas-jo.com) +* [Deploy Your ABP Framework MVC Project to Azure Container Apps](https://community.abp.io/posts/deploy-your-abp-framework-mvc-project-to-azure-container-apps-r93u9c6d) by [Selman Koç](https://community.abp.io/members/selmankoc) +* [How claim type works in ASP NET Core and ABP Framework](https://community.abp.io/posts/how-claim-type-works-in-asp-net-core-and-abp-framework-km5dw6g1) by [Liming Ma](https://github.com/maliming) +* [Using FluentValidation with ABP Framework](https://community.abp.io/posts/using-fluentvalidation-with-abp-framework-2cxuwl70) by [Enes Döner](https://community.abp.io/members/Enes) +* [Using Blob Storage with ABP](https://community.abp.io/posts/using-blob-storage-with-abp-framework-jygtmhn4) by [Emre Kendirli](https://community.abp.io/members/emrekenderli) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://community.abp.io/posts/submit) to the ABP Community. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://docs.abp.io/en/abp/8.2/Road-Map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v8.2 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/abp-dotnet-conf-2024.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/abp-dotnet-conf-2024.png new file mode 100644 index 0000000000..4c20ced391 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/abp-dotnet-conf-2024.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-general-settings.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-general-settings.png new file mode 100644 index 0000000000..ed774fd4fd Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-general-settings.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-global-settings.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-global-settings.png new file mode 100644 index 0000000000..5a6373be10 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/audit-logging-module-global-settings.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/block-ui-service-blazor.gif b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/block-ui-service-blazor.gif new file mode 100644 index 0000000000..ecd9819d53 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/block-ui-service-blazor.gif differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/concurrent-login-settings.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/concurrent-login-settings.png new file mode 100644 index 0000000000..2b2aa17308 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/concurrent-login-settings.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/cover-image.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/cover-image.png new file mode 100644 index 0000000000..59689ba490 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/cover-image.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devdays-europe.jpg b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devdays-europe.jpg new file mode 100644 index 0000000000..48a71dc881 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devdays-europe.jpg differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-alper-ebicoglu.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-alper-ebicoglu.png new file mode 100644 index 0000000000..f37435dc0e Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-alper-ebicoglu.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-enis-necipoglu.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-enis-necipoglu.png new file mode 100644 index 0000000000..a14a47a5a0 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devnot-dotnet-conference-enis-necipoglu.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devops-pro-europe.jpg b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devops-pro-europe.jpg new file mode 100644 index 0000000000..639d49176a Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/devops-pro-europe.jpg differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-1.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-1.png new file mode 100644 index 0000000000..82856c45b2 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-1.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-2.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-2.png new file mode 100644 index 0000000000..d2dcfddc5c Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/manage-user-sessions-2.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-properties.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-properties.png new file mode 100644 index 0000000000..5a6f8d1186 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-properties.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-ui.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-ui.png new file mode 100644 index 0000000000..5871984876 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-dateonly-timeonly-ui.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property-create.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property-create.png new file mode 100644 index 0000000000..2b6d2fdf95 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property-create.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property.png b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property.png new file mode 100644 index 0000000000..a6b64ee986 Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-property.png differ diff --git a/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-upload-in-action.gif b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-upload-in-action.gif new file mode 100644 index 0000000000..0b51b1472e Binary files /dev/null and b/docs/en/Blog-Posts/2024-05-21 v8_2_Release/suite-file-upload-in-action.gif differ diff --git a/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/POST.md b/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/POST.md new file mode 100644 index 0000000000..67db40e98a --- /dev/null +++ b/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/POST.md @@ -0,0 +1,67 @@ +# ABP.IO Platform 8.2 Final Has Been Released! + +[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 8.2 versions have been released today. + +## What's New With Version 8.2? + +All the new features were explained in detail in the [8.2 RC Announcement Post](https://blog.abp.io/abp/announcing-abp-8-2-release-candidate), so there is no need to review them again. You can check it out for more details. + +## Getting Started with 8.2 + +### Creating New Solutions + +You can create a new solution with the ABP Framework version 8.2 by either using the `abp new` command or generating the CLI command on the [get started page](https://abp.io/get-started). + +> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more. + +### How to Upgrade an Existing Solution + +#### Install/Update the ABP CLI + +First, install the ABP CLI or upgrade it to the latest version. + +If you haven't installed it yet: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +To update the existing CLI: + +```bash +dotnet tool update -g Volo.Abp.Cli +``` + +#### Upgrading Existing Solutions with the ABP Update Command + +[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: + +```bash +abp update +``` + +Run this command in the root folder of your solution. + +## Migration Guides + +There are a few breaking changes in this version that may affect your application. +Please see the following migration documents, if you are upgrading from v8.x or earlier: + +* [ABP Framework 8.x to 8.2 Migration Guide](https://docs.abp.io/en/abp/8.2/Migration-Guides/Abp-8_2) +* [ABP Commercial 8.x to 8.2 Migration Guide](https://docs.abp.io/en/commercial/8.2/migration-guides/v8_2) + +## Community News + +### New ABP Community Posts + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +* [How to use Angular Material with Form Validation on ABP](https://community.abp.io/posts/how-to-use-angular-material-with-form-validation-on-abp-jtheajj3) by [Mahmut Gündoğdu](https://x.com/mahmutgundogdu) +* [Tunnel your local host address to a public URL with ngrok](https://community.abp.io/posts/tunnel-your-local-host-address-to-a-public-url-with-ngrok-4cywnocj) by [Bart Van Hoey](https://github.com/bartvanhoey) +* [Antiforgery Token Validation When Angular and HTTP API Runs on the Same Server](https://community.abp.io/posts/antiforgery-token-validation-when-angular-and-http-api-runs-on-the-same-server-mzf5ppdq) by [dignite](https://x.com/dignite_adu) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://community.abp.io/articles/submit) to the ABP Community. + +## About the Next Version + +The next feature version will be 8.3. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. diff --git a/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/cover-image.png b/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/cover-image.png new file mode 100644 index 0000000000..59689ba490 Binary files /dev/null and b/docs/en/Blog-Posts/2024-06-27 v8_2_Release_Stable/cover-image.png differ diff --git a/docs/en/CLI-New-Command-Samples.md b/docs/en/CLI-New-Command-Samples.md deleted file mode 100644 index 635390c29e..0000000000 --- a/docs/en/CLI-New-Command-Samples.md +++ /dev/null @@ -1,258 +0,0 @@ -# ABP CLI - New Solution Sample Commands - -The `abp new` command creates an ABP solution or other artifacts based on an ABP template. [ABP CLI](CLI.md) has several parameters to create a new ABP solution. In this document we will show you some sample commands to create a new solution. All the project names are `Acme.BookStore`. Currently, the available mobile projects are `React Native` and `MAUI` mobile app. Available database providers are `Entity Framework Core` and `MongoDB`. All the commands starts with `abp new`. - -## Angular - -The following commands are for creating Angular UI projects: - -* **Entity Framework Core**, no mobile app, creates the project in a new folder: - - ````bash - abp new Acme.BookStore -u angular --mobile none --database-provider ef -csf - ```` - -* **Entity Framework Core**, default app template, **separate Auth Server**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u angular -m none --separate-auth-server --database-provider ef -csf - ``` - -* **Entity Framework Core**, **custom connection string**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u angular -csf --connection-string Server=localhost;Database=MyDatabase;Trusted_Connection=True - ``` - -* **MongoDB**, default app template, mobile project included, creates solution in `C:\MyProjects\Acme.BookStore` - - ```bash - abp new Acme.BookStore -u angular --database-provider mongodb --output-folder C:\MyProjects\Acme.BookStore - ``` - -* **MongoDB**, default app template, no mobile app, **separate Auth Server**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u angular -m none --separate-auth-server --database-provider mongodb -csf - ``` - -## MVC - -The following commands are for creating MVC UI projects: - -* **Entity Framework Core**, no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef -csf - ``` - -* **Entity Framework Core**, **tier architecture** (*Web and HTTP API are separated*), no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u mvc --mobile none --tiered --database-provider ef -csf - ``` - -* **MongoDB**, no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider mongodb -csf - ``` - -* **MongoDB**, **tier architecture**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u mvc --tiered --database-provider mongodb -csf - ``` - -* **Public Website**, Entity Framework Core, no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef -csf --with-public-website - ``` - - _Note that Public Website is only included in PRO templates._ - - -## Blazor WebAssembly - -The following commands are for creating Blazor WASM projects: - -* **Entity Framework Core**, no mobile app: - - ```bash - abp new Acme.BookStore -t app -u blazor --mobile none - ``` - -* **Entity Framework Core**, **separate Auth Server**, mobile app included: - - ```bash - abp new Acme.BookStore -u blazor --separate-auth-server - ``` - -* **MongoDB**, no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf - ``` - -## Blazor Server - -The following commands are for creating Blazor projects: - -* **Entity Framework Core**, no mobile app: - - ```bash - abp new Acme.BookStore -t app -u blazor-server --mobile none - ``` - -* **Entity Framework Core**, **separate Auth Server**, **separate API Host**, mobile app included: - - ```bash - abp new Acme.BookStore -u blazor-server --tiered - ``` - -* **MongoDB**, no mobile app, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf - ``` - -## No UI - -In the default app template, there is always a frontend project. In this option there is no frontend project. It has a `HttpApi.Host` project to serve your HTTP WebAPIs. It's appropriate if you want to create a WebAPI service. - -* **Entity Framework Core**, separate Auth Server, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u none --separate-auth-server -csf - ``` -* **MongoDB**, no mobile app: - - ```bash - abp new Acme.BookStore -u none --mobile none --database-provider mongodb - ``` - - - -## Console application - -It's a template of a basic .NET console application with ABP module architecture integrated. To create a console application use the following command: - -* This project consists of the following files: `Acme.BookStore.csproj`, `appsettings.json`, `BookStoreHostedService.cs`, `BookStoreModule.cs`, `HelloWorldService.cs` and `Program.cs`. - - ```bash - abp new Acme.BookStore -t console -csf - ``` - -## Module - -Module are reusable sub applications used by your main project. Using ABP Module is a best practice if you are building a microservice solution. As modules are not final applications, each module has all the frontend UI projects and database providers. The module template comes with an MVC UI to be able to develop without the final solution. But if you will develop your module under a final solution, you add `--no-ui` parameter to exclude MVC UI project. - -* Included frontends: `MVC`, `Angular`, `Blazor`. Included database providers: `Entity Framework Core`, `MongoDB`. Includes MVC startup project. - - ```bash - abp new Acme.IssueManagement -t module - ``` -* The same with the upper but doesn't include MVC startup project. - - ```bash - abp new Acme.IssueManagement -t module --no-ui - ``` - -* Creates the module and adds it to your solution - - ```bash - abp new Acme.IssueManagement -t module --add-to-solution-file - ``` - -## Create a solution from a specific version - -When you create a solution, it always creates with the latest version. To create a project from an older version, you can pass the `--version` parameter. - -* Create a solution from v3.3.0, with Angular UI and Entity Framework Core. - - ```bash - abp new Acme.BookStore -t app -u angular -m none --database-provider ef -csf --version 3.3.0 - ``` - -To get the ABP version list, checkout following link: https://www.nuget.org/packages/Volo.Abp.Core/ - -## Create from a custom template - -ABP CLI uses the default [app template](https://github.com/abpframework/abp/tree/dev/templates/app) to create your project. If you want to create a new solution from your customized template, you can use the parameter `--template-source`. - -* MVC UI, Entity Framework Core, no mobile app, using the template in `c:\MyProjects\templates\app` directory. - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source "c:\MyProjects\templates\app" - ``` - -* Same with the previous one except this command retrieves the template from the URL `https://myabp.com/app-template.zip`. - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source https://myabp.com/app-template.zip - ``` - -## Create a preview version - -ABP CLI always uses the latest version. In order to create a solution from a preview (RC) version add the `--preview` parameter. - -* Blazor UI, Entity Framework Core, no mobile, **preview version**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -t app -u blazor --mobile none -csf --preview - ``` - -## Choose database management system - -The default database management system (DBMS) is `Entity Framework Core` / ` SQL Server`. You can choose a DBMS by passing `--database-management-system` parameter. [Accepted values](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/DatabaseManagementSystem.cs) are `SqlServer`, `MySQL`, `SQLite`, `Oracle`, `Oracle-Devart`, `PostgreSQL`. The default value is `SqlServer`. - -* Angular UI, **PostgreSQL** database, creates the project in a new folder: - - ```bash - abp new Acme.BookStore -u angular --database-management-system PostgreSQL -csf - ``` - - - -## Use static HTTP ports - -ABP CLI always assigns random ports to the hostable projects. If you need to keep the default ports and create a solution always with the same HTTP ports, add the parameter `--no-random-port`. - -* MVC UI, Entity Framework Core, **static ports**, creates the project in a new folder: - - ```bash - abp new Acme.BookStore --no-random-port -csf - ``` - -## Use local ABP framework references - -ABP libraries are referenced from NuGet by default in the ABP solutions. Sometimes you need to reference ABP libraries locally to your solution. This is useful to debug the framework itself. Your local ABP Framework 's root directory must have the `Volo.Abp.sln` file. You can copy the content of the following directory to your file system https://github.com/abpframework/abp/tree/dev/framework - -* MVC UI, Entity Framework Core, **ABP libraries are local project references**: - -The local path must be the root directory of ABP repository. -If `C:\source\abp\framework\Volo.Abp.sln` is your framework solution path, then you must write `C:\source\abp` to the `--abp-path` paramter. - - ```bash - abp new Acme.BookStore --local-framework-ref --abp-path C:\source\abp - ``` - -**Output**: - -As seen below, ABP Framework libraries are local project references. - -```xml - - - - - - - - -``` - -## See Also - -* [ABP CLI documentation](CLI.md) diff --git a/docs/en/CLI.md b/docs/en/CLI.md deleted file mode 100644 index 58ca9f4629..0000000000 --- a/docs/en/CLI.md +++ /dev/null @@ -1,614 +0,0 @@ -# ABP CLI - -ABP CLI (Command Line Interface) is a command line tool to perform some common operations for ABP based solutions. - -## Installation - -ABP CLI is a [dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). Install it using a command line window: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -To update an existing installation: - -````bash -dotnet tool update -g Volo.Abp.Cli -```` - -## Global Options - -While each command may have a set of options, there are some global options that can be used with any command; - -* `--skip-cli-version-check`: Skips to check the latest version of the ABP CLI. If you don't specify, it will check the latest version and shows a warning message if there is a newer version of the ABP CLI. - -## Commands - -Here, is the list of all available commands before explaining their details: - -* **`help`**: Shows help on the usage of the ABP CLI. -* **`cli`**: Update or remove ABP CLI. -* **`new`**: Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md). -* **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution. -* **`clean`**: Deletes all `BIN` and `OBJ` folders in the current folder. -* **`add-package`**: Adds an ABP package to a project. -* **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution. -* **`list-modules`**: Lists names of open-source application modules. -* **`list-templates`**: Lists the names of available templates to create a solution. -* **`get-source`**: Downloads the source code of a module. -* **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints. -* **`remove-proxy`**: Removes previously generated client side proxies. -* **`switch-to-preview`**: Switches to the latest preview version of the ABP Framework. -* **`switch-to-nightly`**: Switches to the latest [nightly builds](Nightly-Builds.md) of the ABP related packages on a solution. -* **`switch-to-stable`**: Switches to the latest stable versions of the ABP related packages on a solution. -* **`switch-to-local`**: Changes NuGet package references on a solution to local project references. -* **`translate`**: Simplifies to translate localization files when you have multiple JSON [localization](Localization.md) files in a source control repository. -* **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. -* **`login-info`**: Shows the current user's login information. -* **`logout`**: Logouts from your computer if you've authenticated before. -* **`bundle`**: Generates script and style references for ABP Blazor and MAUI Blazor project. -* **`install-libs`**: Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. -* **`clear-download-cache`**: Clears the templates download cache. -* **`trust-version`**: Trusts the user's version and does not check if the version exists or not. If the template with the given version is found in the cache, it will be used, otherwise throws an exception. - -### help - -Shows basic usages of the ABP CLI. - -Usage: - -````bash -abp help [command-name] -```` - -Examples: - -````bash -abp help # Shows a general help. -abp help new # Shows help about the "new" command. -```` - -### cli - -Update or remove ABP CLI. - -Usage: - -````bash -abp cli [command-name] -```` - -Examples: - -````bash -abp cli update -abp cli update --preview -abp cli update --version 5.0.0 -abp cli remove -```` - -### new - -Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md). - -Usage: - -````bash -abp new [options] -```` - -Example: - -````bash -abp new Acme.BookStore -```` - -* `Acme.BookStore` is the solution name here. -* Common convention is to name a solution is like *YourCompany.YourProject*. However, you can use different naming like *YourProject* (single level namespacing) or *YourCompany.YourProduct.YourModule* (three levels namespacing). - -For more samples, go to [ABP CLI Create Solution Samples](CLI-New-Command-Samples.md) - -#### Options - -* `--template` or `-t`: Specifies the template name. Default template name is `app`, which generates a web application. Available templates: - * **`app`** (default): [Application template](Startup-Templates/Application.md). Additional options: - * `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: - * `mvc`: ASP.NET Core MVC. There are some additional options for this template: - * `--tiered`: Creates a tiered solution where Web and Http API layers are physically separated. If not specified, it creates a layered solution which is less complex and suitable for most scenarios. - * `angular`: Angular UI. There are some additional options for this template: - * `--separate-auth-server`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. - * `--pwa`: Specifies the project as Progressive Web Application. - * `blazor-webapp`: Blazor Web App UI. There are some additional options for this template: - * `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. - * `blazor`: Blazor UI. There are some additional options for this template: - * `--separate-auth-server`The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. - * `--pwa`: Specifies the project as Progressive Web Application. - * `blazor-server`: Blazor Server UI. There are some additional options for this template: - * `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. - * `none`: Without UI. No front-end layer will be created. There are some additional options for this template: - * `--separate-auth-server`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. - * `--mobile` or `-m`: Specifies the mobile application framework. If not specified, no mobile application will be created. Available options: - * `react-native`: React Native. - * `maui`: MAUI. This mobile option is only available for ABP Commercial. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `--theme`: Specifies the theme. Default theme is `leptonx-lite`. Available themes: - * `leptonx-lite`: [LeptonX Lite Theme](Themes/LeptonXLite/AspNetCore.md). - * `basic`: [Basic Theme](UI/AspNetCore/Basic-Theme.md). - * **`module`**: [Module template](Startup-Templates/Module.md). Additional options: - * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI). - * **`console`**: [Console template](Startup-Templates/Console.md). - * **`app-nolayers`**: [Single-layer application template](Startup-Templates/Application-Single-Layer.md). Additional options: - * `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: - * `mvc`: ASP.NET Core MVC. - * `angular`: Angular UI. - * `blazor`: Blazor UI. - * `blazor-server`: Blazor Server UI. - * `none`: Without UI. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `--theme`: Specifies the theme. Default theme is `leptonx-lite`. Available themes: - * `leptonx-lite`: [LeptonX Lite Theme](Themes/LeptonXLite/AspNetCore.md). - * `basic`: [Basic Theme](UI/AspNetCore/Basic-Theme.md). - * **`maui`**: .NET MAUI. A minimalist .NET MAUI application will be created if you specify this option. -* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. -* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version. -* `--preview`: Use latest preview version. -* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D:\local-template` or `https://.../my-template-file.zip`). -* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. -* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution). -* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's: - * `SqlServer` - * `MySQL` - * `SQLite` - * `Oracle` - * `Oracle-Devart` - * `PostgreSQL` -* `--local-framework-ref --abp-path`: Uses local projects references to the ABP framework instead of using the NuGet packages. This can be useful if you download the ABP Framework source code and have a local reference to the framework from your application. -* `--no-random-port`: Uses template's default ports. -* `--skip-installing-libs` or `-sib`: Skip installing client side packages. -* `--skip-cache` or `-sc`: Always download the latest from our server and refresh their templates folder cache. -* `--with-public-website`: **Public Website** is a front-facing website for describing your project, listing your products and doing SEO for marketing purposes. Users can login and register on your website with this website. - -See some [examples for the new command](CLI-New-Command-Samples.md) here. - -### update - -Updating all ABP related packages can be tedious since there are many packages of the framework and modules. This command automatically updates all ABP related NuGet and NPM packages in a solution or project to the latest versions. You can run it in the root folder of your solutions. - -Usage: - -````bash -abp update [options] -```` - -* If you run in a directory with a .csproj file, it updates all ABP related packages of the project to the latest versions. -* If you run in a directory with a .sln file, it updates all ABP related packages of the all projects of the solution to the latest versions. -* If you run in a directory that contains multiple solutions in sub-folders, it can update all the solutions, including Angular projects. - -Note that this command can upgrade your solution from a previous version, and also can upgrade it from a preview release to the stable release of the same version. - -#### Options - -* `--npm`: Only updates NPM packages. -* `--nuget`: Only updates NuGet packages. -* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default -* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default. -* `--check-all`: Check the new version of each package separately. Default is `false`. -* `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used. - -### clean - -Deletes all `BIN` and `OBJ` folders in the current folder. - -Usage: - -````bash -abp clean -```` - - -### add-package - -Adds an ABP package to a project by, - -* Adding related nuget package as a dependency to the project. -* Adding `[DependsOn(...)]` attribute to the module class in the project (see the [module development document](Module-Development-Basics.md)). - -> Notice that the added module may require additional configuration which is generally indicated in the documentation of the related package. - -Basic usage: - -````bash -abp add-package [options] -```` - -Example: - -```` -abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic -```` - -* This example adds the `Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic` package to the project. - -#### Options - -* `--project` or `-p`: Specifies the project (.csproj) file path. If not specified, CLI tries to find a .csproj file in the current directory. -* `--with-source-code`: Downloads the source code of the package to your solution folder and uses local project references instead of NuGet/NPM packages. -* `--add-to-solution-file`: Adds the downloaded package to your solution file, so you will also see the package when you open the solution on a IDE. (only available when `--with-source-code` is True) - -> Currently only the source code of the basic theme packages([MVC](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Basic-Theme) and [Blazor](https://docs.abp.io/en/abp/latest/UI/Blazor/Basic-Theme)) can be downloaded. -> - Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic -> - Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme -> - Volo.Abp.AspNetCore.Components.Web.BasicTheme -> - Volo.Abp.AspNetCore.Components.Server.BasicTheme - - -### add-module - -Adds a [multi-package application module](Modules/Index) to a solution by finding all packages of the module, finding related projects in the solution and adding each package to the corresponding project in the solution. - -It can also create a new module for your solution and add it to your solution. See `--new` option. - -> A business module generally consists of several packages (because of layering, different database provider options or other reasons). Using `add-module` command dramatically simplifies adding a module to a solution. However, each module may require some additional configurations which is generally indicated in the documentation of the related module. - -Usage: - -````bash -abp add-module [options] -```` - -Examples: - -```bash -abp add-module Volo.Blogging -``` - -* This example adds the `Volo.Blogging` module to the solution. - -```bash -abp add-module ProductManagement --new --add-to-solution-file -``` - -* This command creates a fresh new module customized for your solution (named `ProductManagement`) and adds it to your solution. - - -#### Options - -* `--solution` or `-s`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory. -* `--skip-db-migrations`: For EF Core database provider, it automatically adds a new code first migration (`Add-Migration`) and updates the database (`Update-Database`) if necessary. Specify this option to skip this operation. -* `-sp` or `--startup-project`: Relative path to the project folder of the startup project. Default value is the current folder. -* `--new`: Creates a fresh new module (customized for your solution) and adds it to your solution. -* `--with-source-code`: Downloads the source code of the module to your solution folder and uses local project references instead of NuGet/NPM packages. This options is always `True` if `--new` is used. -* `--add-to-solution-file`: Adds the downloaded/created module to your solution file, so you will also see the projects of the module when you open the solution on a IDE. (only available when `--with-source-code` is `True`.) - -### list-modules - -Lists names of open-source application modules. - -Usage: - -````bash -abp list-modules [options] -```` - -Example: - -```bash -abp list-modules -``` - -#### Options - -* `--include-pro-modules`: Includes commercial (pro) modules in the output. - -### list-templates - -Lists all available templates to create a solution. - -Usage: - -```bash -abp list-templates -``` - -### get-source - -Downloads the source code of a module to your computer. - -Usage: - -````bash -abp get-source [options] -```` - -Example: - -```bash -abp get-source Volo.Blogging - -abp get-source Volo.Blogging --local-framework-ref --abp-path D:\GitHub\abp -``` - -#### Options - -* `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. -* `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. -* `--preview`: If no version option is specified, this option specifies if latest [preview version](Previews.md) will be used instead of latest stable version. -* `--local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. - -### generate-proxy - -Generates Angular, C# or JavaScript service proxies for your HTTP APIs to make easy to consume your services from the client side. Your host (server) application must be up and running before running this command. - -Usage: - -````bash -abp generate-proxy -t [options] -```` - -Examples: - -````bash -abp generate-proxy -t ng -url https://localhost:44302/ -abp generate-proxy -t js -url https://localhost:44302/ -abp generate-proxy -t csharp -url https://localhost:44302/ -```` - -#### Options - -* `--type` or `-t`: The name of client type. Available clients: - * `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: - * `--without-contracts`: Avoid generating the application service interface, class, enum and dto types. - * `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. - * `ng`: Angular. There are some additional options for this client: - * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. - * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. - * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. - * `--module`: Backend module name. Default value: `app`. - * `--entry-point`: Targets the Angular project to place the generated code. - * `--url`: Specifies api definition url. Default value is API Name's url in environment file. - * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). - - * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: - * `--output` or `-o`: JavaScript file path or folder to place generated code in. -* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. -* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. -* `--service-type` or `-st`: Specifies the service type to generate. `application`, `integration` and `all`, Default value: `all` for C#, `application` for JavaScript / Angular. - -> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more. - -### remove-proxy - -Removes previously generated proxy code from the Angular, CSharp or JavaScript application. Your host (server) application must be up and running before running this command. - -This can be especially useful when you generate proxies for multiple modules before and need to remove one of them later. - -Usage: - -````bash -abp remove-proxy -t [options] -```` - -Examples: - -````bash -abp remove-proxy -t ng -abp remove-proxy -t js -m identity -o Pages/Identity/client-proxies.js -abp remove-proxy -t csharp --folder MyProxies/InnerFolder -```` - -#### Options - -* `--type` or `-t`: The name of client type. Available clients: - * `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: - * `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. - * `ng`: Angular. There are some additional options for this client: - * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. - * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. - * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. - * `--url`: Specifies api definition url. Default value is API Name's url in environment file. - * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). - * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: - * `--output` or `-o`: JavaScript file path or folder to place generated code in. -* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. -* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. - -> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more. - -### switch-to-preview - -You can use this command to switch your solution or project to latest preview version of the ABP framework. - -Usage: - -````bash -abp switch-to-preview [options] -```` - -#### Options - -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. - - -### switch-to-nightly - -You can use this command to switch your solution or project to latest [nightly](Nightly-Builds.md) preview version of the ABP framework packages. - -Usage: - -````bash -abp switch-to-nightly [options] -```` - -#### Options - -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. - -### switch-to-stable - -If you're using the ABP Framework preview packages (including nightly previews), you can switch back to latest stable version using this command. - -Usage: - -````bash -abp switch-to-stable [options] -```` -#### Options - -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. - -### switch-to-local - -Changes all NuGet package references to local project references for all the .csproj files in the specified folder (and all its subfolders with any deep). It is not limited to ABP Framework or Module packages. - -Usage: - -````bash -abp switch-to-local [options] -```` -#### Options - -* `--solution` or `-s`: Specifies the solution directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. - -* `--paths` or `-p`: Specifies the local paths that the projects are inside. You can use `|` character to separate the paths. - -Example: - -````bash -abp switch-to-local --paths "D:\Github\abp|D:\Github\my-repo" -```` - -### translate - -Simplifies to translate [localization](Localization.md) files when you have multiple JSON [localization](Localization.md) files in a source control repository. - -* This command will create a unified json file based on the reference culture. -* It searches all the localization `JSON` files in the current directory and all subdirectories (recursively). Then creates a single file (named `abp-translation.json` by default) that includes all the entries need to be translated. -* Once you translate the entries in this file, you can then apply your changes to the original localization files using the `--apply` command. - -> The main purpose of this command is to translate ABP Framework localization files (since the [abp repository](https://github.com/abpframework/abp) has tens of localization files to be translated in different directories). - -#### Creating the Translation File - -First step is to create the unified translation file: - -````bash -abp translate -c [options] -```` - -Example: - -````bash -abp translate -c de -```` - -This command created the unified translation file for the `de` (German) culture. - -##### Additional Options - -* `--reference-culture` or `-r`: Default `en`. Specifies the reference culture. -* `--output` or `-o`: Output file name. Default `abp-translation.json`. -* `--all-values` or `-all`: Include all keys to translate. By default, the unified translation file only includes the missing texts for the target culture. Specify this parameter if you may need to revise the values already translated before. - -#### Applying Changes - -Once you translate the entries in the unified translation file, you can apply your changes to the original localization files using the `--apply` parameter: - -````bash -abp translate --apply # apply all changes -abp translate -a # shortcut for --apply -```` - -Then review changes on your source control system to be sure that it has changed the proper files and send a Pull Request if you've translated ABP Framework resources. Thank you in advance for your contribution. - -##### Additional Options - -* `--file` or `-f`: Default: `abp-translation.json`. The translation file (use only if you've used the `--output` option before). - -#### Online DeepL translate - -The `translate` command also supports online translation. You need to provide your [DeepL Authentication Key](https://support.deepl.com/hc/en-us/articles/360020695820-Authentication-Key). - -It will search all the `en.json(reference-culture)` files in the directory and sub-directory and then translate and generate the corresponding `zh-Hans.json(culture)` files. - -````bash -abp translate -c zh-Hans --online --deepl-auth-key -```` - -### login - -Some features of the CLI requires to be logged in to abp.io platform. To login with your username write: - -```bash -abp login # Allows you to enter your password hidden -abp login -p # Specify the password as a parameter (password is visible) -abp login --organization # If you have multiple organizations, you need set your active organization -abp login -p -o # You can enter both your password and organization in the same command -abp login --device # Use device login flow -``` - -> When using the -p parameter, be careful as your password will be visible. It's useful for CI/CD automation pipelines. - -A new login with an already active session overwrites the previous session. - -### login-info - -Shows your login information such as **Name**, **Surname**, **Username**, **Email Address** and **Organization**. - -```bash -abp login-info -``` - -### logout - -Logs you out by removing the session token from your computer. - -```bash -abp logout -``` - -### bundle - -This command generates script and style references for ABP Blazor WebAssembly and MAUI Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order ```bundle``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain a Blazor or MAUI Blazor project file(*.csproj). - -Usage: - -````bash -abp bundle [options] -```` - -#### Options - -* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a Blazor project file. -* ```--force``` or ```-f```: Forces to build project before generating references. -* ```--project-type``` or ```-t```: Specifies the project type. Default type is `webassembly`. Available types: - * `webassembly` - * `maui-blazor` - -`bundle` command reads the `appsettings.json` file inside the Blazor and MAUI Blazor project for bundling options. For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) - -### install-libs - -This command install NPM Packages for MVC / Razor Pages and Blazor Server UI types. Its **executing directory** or passed ```--working-directory``` parameter's directory must contain a project file(*.csproj). - -`install-libs` command reads the `abp.resourcemapping.js` file to manage package. For more details see [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md). - -Usage: - -````bash -abp install-libs [options] -```` - -#### Options - -* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a project file. - -## See Also - -* [Examples for the new command](CLI-New-Command-Samples.md) -* [Video tutorial](https://abp.io/video-courses/essentials/abp-cli) diff --git a/docs/en/CSRF-Anti-Forgery.md b/docs/en/CSRF-Anti-Forgery.md deleted file mode 100644 index ee684aaa52..0000000000 --- a/docs/en/CSRF-Anti-Forgery.md +++ /dev/null @@ -1,162 +0,0 @@ -# CSRF/XSRF & Anti Forgery System - -"*Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site for which the user is currently authenticated*" ([OWASP](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet)). - -**ABP Framework completely automates CSRF preventing** and works out of the box without any configuration. Read this documentation only if you want to understand it better or need to customize. - -## The Problem - -ASP.NET Core [provides infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery) to prevent CSRF attacks by providing a system to **generate** and **validate antiforgery tokens**. However, the standard implementation has a few drawbacks; - -Antiforgery token validation is only **enabled for razor pages by default** and not enabled for **HTTP APIs**. You need to enable it yourself for the Controllers. You can use the `[ValidateAntiForgeryToken]` attribute for a specific API Controller/Action or the `[AutoValidateAntiforgeryToken]` attribute to prevent attacks globally. - -Once you enable it; - -* You need to manually add an HTTP header, named `RequestVerificationToken` to every **AJAX request** made in your application. You should care about obtaining the token, saving in the client side and adding to the HTTP header on every HTTP request. -* All your clients, including **non-browser clients**, should care about obtaining and sending the antiforgery token in every request. In fact, non-browser clients has no CSRF risk and should not care about this. - -Especially, the second point is a pain for your clients and unnecessarily consumes your server resources. - -> You can read more about the ASP.NET Core antiforgery system in its own [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery). - -## The Solution - -ABP Framework provides `[AbpValidateAntiForgeryToken]` and `[AbpAutoValidateAntiforgeryToken]` attributes, just like the attributes explained above. `[AbpAutoValidateAntiforgeryToken]` is already added to the global filters, so you should do nothing to enable it for your application. - -ABP Framework also automates the following infrastructure; - -* Server side sets a **special cookie**, named `XSRF-TOKEN` by default, that is used make the antiforgery token value available to the browser. This is **done automatically** (by the [application configuration](API/Application-Configuration.md) endpoint). Nothing to do in the client side. -* In the client side, it reads the token from the cookie and sends it in the **HTTP header** (named `RequestVerificationToken` by default). This is implemented for all the supported UI types. -* Server side validates the antiforgery token **only for same and cross site requests** made by the browser. It bypasses the validation for non-browser clients. - -That's all. The systems works smoothly. - -## Configuration / Customization - -### AbpAntiForgeryOptions - -`AbpAntiForgeryOptions` is the main [options class](Options.md) to configure the ABP Antiforgery system. It has the following properties; - -* `TokenCookie`: Can be used to configure the cookie details. This cookie is used to store the antiforgery token value in the client side, so clients can read it and sends the value as the HTTP header. Default cookie name is `XSRF-TOKEN`, expiration time is 10 years (yes, ten years! It should be a value longer than the authentication cookie max life time, for the security). -* `AuthCookieSchemaName`: The name of the authentication cookie used by your application. Default value is `Identity.Application` (which becomes `AspNetCore.Identity.Application` on runtime). The default value properly works with the ABP startup templates. **If you change the authentication cookie name, you also must change this.** -* `AutoValidate`: The single point to enable/disable the ABP automatic antiforgery validation system. Default value is `true`. -* `AutoValidateFilter`: A predicate that gets a type and returns a boolean. ABP uses this predicate to check a controller type. If it returns false for a controller type, the controller is excluded from the automatic antiforgery token validation. -* `AutoValidateIgnoredHttpMethods`: A list of HTTP Methods to ignore on automatic antiforgery validation. Default value: "GET", "HEAD", "TRACE", "OPTIONS". These HTTP Methods are safe to skip antiforgery validation since they don't change the application state. - -If you need to change these options, do it in the `ConfigureServices` method of your [module](Module-Development-Basics.md). - -**Example: Configuring the AbpAntiForgeryOptions** - -```csharp -Configure(options => -{ - options.TokenCookie.Expiration = TimeSpan.FromDays(365); - options.AutoValidateIgnoredHttpMethods.Remove("GET"); - options.AutoValidateFilter = - type => !type.Namespace.StartsWith("MyProject.MyIgnoredNamespace"); -}); -``` - -This configuration; - -* Sets the antiforgery token expiration time to ~1 year. -* Enables antiforgery token validation for GET requests too. -* Ignores the controller types in the specified namespace. - -### AntiforgeryOptions - -`AntiforgeryOptions` is the standard [options class](Options.md) of the ASP.NET Core. **You can find all the information about this class in its [own documentation](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery)**. - -`HeaderName` option is especially important for the ABP Framework point of view. Default value of this value is `RequestVerificationToken` and the clients uses this name while sending the token value in the header. So, if you change this option, you should also arrange your clients to align the change. If you don't have a good reason, leave it as default. - -### AbpValidateAntiForgeryToken Attribute - -If you disable the automatic validation or want to perform the validation for an endpoint that is not validated by default (for example, an endpoint with HTTP GET Method), you can use the `[AbpValidateAntiForgeryToken]` attribute for a **controller type or method** (action). - -**Example: Add `[AbpValidateAntiForgeryToken]` to a HTTP GET method** - -```csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.AntiForgery; - -namespace MyCompanyName.MyProjectName.Controllers -{ - [Route("api/products")] - public class ProductController : AbpController - { - [HttpGet] - [AbpValidateAntiForgeryToken] - public async Task GetAsync() - { - //TODO: ... - } - } -} -``` - -### Angular UI - -Angular supports CSRF Token out of box, but the token header name is `X-XSRF-TOKEN`. Since ABP Framework follows the ASP.NET Core conventions, it changes this value to `RequestVerificationToken` in the core package. - -You don't need to make anything unless you need to change the `AntiforgeryOptions.HeaderName` as explained before. If you change it, remember to change the header name for the Angular application too. To do that, add an import declaration for the `HttpClientXsrfModule` into your root module. - -**Example: Change the header name to *MyCustomHeaderName*** - -```typescript -@NgModule({ - // ... - imports: [ - //... - HttpClientXsrfModule.withOptions({ - cookieName: 'XSRF-TOKEN', - headerName: 'MyCustomHeaderName' - }) - ], -}) -export class AppModule {} -``` - -**Note:** XSRF-TOKEN is only valid if both frontend application and APIs run on the same domain. Therefore, when you make a request, you should use a relative path. - -For example, let's say your APIs is hosted at `https://testdomain.com/ws` -and your angular application is hosted at `https://testdomain.com/admin` - -So if your API request should look like this `https://testdomain.com/ws/api/identity/users` - -your `environment.prod.ts` has to be as follows: - -```typescript -export const environment = { - production: true, - // .... - apis: { - default: { - url: '/ws', // <- just use the context root here - // ... - }, - }, -} as Config.Environment; -``` - -Let's talk about why. - -First, take a look at [Angular's code](https://github.com/angular/angular/blob/master/packages/common/http/src/xsrf.ts#L81) - -It does not intercept any request that starts with `http://` or `https://`. There is a good reason for that. Any cross-site request does not need this token for security. This verification is only valid if the request is made to the same domain from which the web page is served. So, simply put, if you serve everything from a single domain, you just use a relative path. - -If you serve your APIs from the root, i.e. no context root (https://testdomain.com/api/identity/users), leave `url` empty as follows: - -```typescript -export const environment = { - production: true, - // .... - apis: { - default: { - url: '', // <- should be empty string, not '/' - // ... - }, - }, -} as Config.Environment; -``` diff --git a/docs/en/Caching.md b/docs/en/Caching.md deleted file mode 100644 index e39e0ead24..0000000000 --- a/docs/en/Caching.md +++ /dev/null @@ -1,284 +0,0 @@ -# Distributed Caching - -ABP Framework extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). - -> **Default implementation of the `IDistributedCache` interface is` MemoryDistributedCache` which works in-memory.** See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use Redis as the distributed cache server. - -## Installation - -> This package is already installed by default with the [application startup template](Startup-Templates/Application.md). So, most of the time, you don't need to install it manually. - -[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching) is the main package of the caching system. You can install it a project using the add-package command of the [ABP CLI](CLI.md): - -```bash -abp add-package Volo.Abp.Caching -``` - -You need to run this command on a command line terminal in a folder containing a `csproj` file (see [other options](https://abp.io/package-detail/Volo.Abp.Caching) to install). - -## Usage - -### `IDistributedCache` Interface - -ASP.NET Core defines the `IDistributedCache` interface to get/set the cache values. But it has some difficulties: - -* It works with **byte arrays** rather than .NET objects. So, you need to **serialize/deserialize** the objects you need to cache. -* It provides a **single key pool** for all cache items, so; - * You need to care about the keys to distinguish **different type of objects**. - * You need to care about the cache items of **different tenants** in a [multi-tenant](Multi-Tenancy.md) system. - -> `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications, but also available to **any type of applications**. - -See [ASP.NET Core's distributed caching document](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more information. - -### `IDistributedCache` Interface - -ABP framework defines the generic `IDistributedCache` interface in the [Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching/) package. `TCacheItem` is the type of the object stored in the cache. - -`IDistributedCache` solves the difficulties explained above; - -* It internally **serializes/deserializes** the cached objects. Uses **JSON** serialization by default, but can be overridden by replacing the `IDistributedCacheSerializer` service in the [dependency injection](Dependency-Injection.md) system. -* It automatically adds a **cache name** prefix to the cache keys based on the object type stored in the cache. Default cache name is the full name of the cache item class (`CacheItem` postfix is removed if your cache item class ends with it). You can use the **`CacheName` attribute** on the cache item class to set the cache name. -* It automatically adds the **current tenant id** to the cache key to distinguish cache items for different tenants (if your application is [multi-tenant](Multi-Tenancy.md)). Define `IgnoreMultiTenancy` attribute on the cache item class to disable this if you want to share the cached objects among all tenants in a multi-tenant application. -* Allows to define a **global cache key prefix** per application, so different applications can use their isolated key pools in a shared distributed cache server. -* It **can tolerate errors** wherever possible and bypasses the cache. This is useful when you have temporary problems on the cache server. -* It has methods like `GetManyAsync` and `SetManyAsync` which significantly improve the performance on **batch operations**. - -**Example: Store Book names and prices in the cache** - -````csharp -namespace MyProject -{ - public class BookCacheItem - { - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -You can inject and use the `IDistributedCache` service to get/set `BookCacheItem` objects: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Volo.Abp.Caching; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class BookService : ITransientDependency - { - private readonly IDistributedCache _cache; - - public BookService(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId.ToString(), //Cache key - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } - - private Task GetBookFromDatabaseAsync(Guid bookId) - { - //TODO: get from database - } - } -} -```` - -* This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. `GetOrAddAsync` is an additional method that was added by the ABP Framework to the standard ASP.NET Core distributed cache methods. -* If the book was not found in the cache, it calls the factory method (`GetBookFromDatabaseAsync` in this case) to retrieve the book item from the original source. -* `GetOrAddAsync` optionally gets a `DistributedCacheEntryOptions` which can be used to set the lifetime of the cached item. - -`IDistributedCache` supports the same methods of the ASP.NET Core's standard `IDistributedCache` interface, so you can refer [it's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). - -### `IDistributedCache` Interface - -`IDistributedCache` interface assumes that the type of your **cache key** is `string` (so, you need to manually convert your key to string if you need to use a different kind of cache key). While this is not a big deal, `IDistributedCache` can be used when your cache key type is not `string`. - -**Example: Store Book names and prices in the cache** - -````csharp -using Volo.Abp.Caching; - -namespace MyProject -{ - [CacheName("Books")] - public class BookCacheItem - { - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -* This example uses the `CacheName` attribute for the `BookCacheItem` class to set the cache name. - -You can inject and use the `IDistributedCache` service to get/set `BookCacheItem` objects: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Volo.Abp.Caching; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class BookService : ITransientDependency - { - private readonly IDistributedCache _cache; - - public BookService(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId, //Guid type used as the cache key - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } - private Task GetBookFromDatabaseAsync(Guid bookId) - { - //TODO: get from database - } - } -} -```` - -* This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. -* Since cache explicitly implemented as using `Guid` as cache key, `Guid` value passed to `_cache_GetOrAddAsync()` method. - -#### Complex Types as the Cache Key - -`IDistributedCache` internally uses `ToString()` method of the key object to convert it to a string. If you need to use a complex object as the cache key, you need to override `ToString` method of your class. - -An example class that is used as a cache key: - -````csharp -public class UserInOrganizationCacheKey -{ - public Guid UserId { get; set; } - - public Guid OrganizationId { get; set; } - - //Builds the cache key - public override string ToString() - { - return $"{UserId}_{OrganizationId}"; - } -} -```` - -Example usage: - -````csharp -public class BookService : ITransientDependency -{ - private readonly IDistributedCache _cache; - - public BookService( - IDistributedCache cache) - { - _cache = cache; - } - - ... -} -```` - -## Configuration - -### AbpDistributedCacheOptions - -`AbpDistributedCacheOptions` is the main [options class](Options.md) to configure the caching. - -**Example: Set the cache key prefix for the application** - -```csharp -Configure(options => -{ - options.KeyPrefix = "MyApp1"; -}); -``` - -> Write that code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md). - -#### Available Options - -* `HideErrors` (`bool`, default: `true`): Enables/disables hiding the errors on writing/reading values from the cache server. -* `KeyPrefix` (`string`, default: `null`): If your cache server is shared by multiple applications, you can set a prefix for the cache keys for your application. In this case, different applications can not overwrite each other's cache items. -* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): Used to set default distributed cache options (like `AbsoluteExpiration` and `SlidingExpiration`) used when you don't specify the options while saving cache items. Default value uses the `SlidingExpiration` as 20 minutes. - -## Error Handling - -When you design a cache for your objects, you typically try to get the value from cache first. If not found in the cache, you query the object from the **original source**. It may be located in a **database** or may require to perform an HTTP call to a remote server. - -In most cases, you want to **tolerate the cache errors**; If you get error from the cache server you don't want to cancel the operation. Instead, you silently hide (and log) the error and **query from the original source**. This is what the ABP Framework does by default. - -ABP's Distributed Cache [handle](Exception-Handling.md), log and hide errors by default. There is an option to change this globally (see the options below). - -In addition, all of the `IDistributedCache` (and `IDistributedCache`) methods have an optional `hideErrors` parameter, which is `null` by default. The global value is used if this parameter left as `null`, otherwise you can decide to hide or throw the exceptions for individual method calls. - -## Batch Operations - -ABP's distributed cache interfaces provide methods to perform batch methods those improves the performance when you want to batch operation multiple cache items in a single method call. - -* `SetManyAsync` and `SetMany` methods can be used to set multiple values to the cache. -* `GetManyAsync` and `GetMany` methods can be used to retrieve multiple values from the cache. -* `GetOrAddManyAsync` and `GetOrAddMany` methods can be used to retrieve multiple values and set missing values from the cache -* `RefreshManyAsync` and `RefreshMany` methods can be used to resets the sliding expiration timeout of multiple values from the cache -* `RemoveManyAsync` and `RemoveMany` methods can be used to remove multiple values from the cache - -> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](Redis-Cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` ... methods (called once for each item). - -## Caching Entities - -ABP Framework provides a [Distributed Entity Cache System](Entity-Cache.md) for caching entities. It is useful if you want to use caching for quicker access to the entity rather than repeatedly querying it from the database. - -It's designed as read-only and automatically invalidates a cached entity if the entity is updated or deleted. - -> See the [Entity Cache](Entity-Cache.md) documentation for more information. - -## Advanced Topics - -### Unit Of Work Level Cache - -Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect. - -`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**. - -### IDistributedCacheSerializer - -`IDistributedCacheSerializer` service is used to serialize and deserialize the cache items. Default implementation is the `Utf8JsonDistributedCacheSerializer` class that uses `IJsonSerializer` service to convert objects to [JSON](Json-Serialization.md) and vice verse. Then it uses UTC8 encoding to convert the JSON string to a byte array which is accepted by the distributed cache. - -You can [replace](Dependency-Injection.md) this service by your own implementation if you want to implement your own serialization logic. - -### IDistributedCacheKeyNormalizer - -`IDistributedCacheKeyNormalizer` is implemented by the `DistributedCacheKeyNormalizer` class by default. It adds cache name, application cache prefix and current tenant id to the cache key. If you need a more advanced key normalization, you can [replace](Dependency-Injection.md) this service by your own implementation. - -## See Also - -* [Entity Cache](Entity-Cache.md) -* [Redis Cache](Redis-Cache.md) diff --git a/docs/en/Cancellation-Token-Provider.md b/docs/en/Cancellation-Token-Provider.md deleted file mode 100644 index 7f7a78f8ad..0000000000 --- a/docs/en/Cancellation-Token-Provider.md +++ /dev/null @@ -1,71 +0,0 @@ -# Cancellation Token Provider - -A `CancellationToken` enables cooperative cancellation between threads, thread pool work items, or `Task` objects. To handle the possible cancellation of the operation, ABP Framework provides `ICancellationTokenProvider` to obtain the `CancellationToken` itself from the source. - -> To get more information about `CancellationToken`, see [Microsoft Documentation](https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken). - -## ICancellationTokenProvider - -`ICancellationTokenProvider` is an abstraction to provide `CancellationToken` for different scenarios. - -Generally, you should pass the `CancellationToken` as a parameter for your method to use it. With the `ICancellationTokenProvider` you don't need to pass `CancellationToken` for every method. `ICancellationTokenProvider` can be injected with the **dependency injection** and provides the token from it's source. - -**Example:** - -```csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Threading; - -namespace MyProject -{ - public class MyService : ITransientDependency - { - private readonly ICancellationTokenProvider _cancellationTokenProvider; - - public MyService(ICancellationTokenProvider cancellationTokenProvider) - { - _cancellationTokenProvider = cancellationTokenProvider; - } - - public async Task DoItAsync() - { - while (_cancellationTokenProvider.Token.IsCancellationRequested == false) - { - // ... - } - } - } -} -``` - -## Built-in providers - -- `NullCancellationTokenProvider` - - The `NullCancellationTokenProvider` is a built in provider and it supply always `CancellationToken.None`. - -- `HttpContextCancellationTokenProvider` - - The `HttpContextCancellationTokenProvider` is a built in default provider for ABP Web applications. It simply provides a `CancellationToken` that is source of the web request from the `HttpContext`. - -## Implementing the ICancellationTokenProvider - -You can easily create your CancellationTokenProvider by creating a class that implements the `ICancellationTokenProvider` interface, as shown below: - -```csharp -using System.Threading; - -namespace AbpDemo -{ - public class MyCancellationTokenProvider : ICancellationTokenProvider - { - public CancellationToken Token { get; } - - private MyCancellationTokenProvider() - { - - } - } -} -``` diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md index f412ca3170..03caff8007 100644 --- a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md +++ b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md @@ -1,477 +1,477 @@ -# Replacing Email Templates and Sending Emails - -## Introduction - -Hi, in this step by step article, we will send an email by using standard email template and then we will replace the standard email template with our new created template, thanks to [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates) and [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). Let's start by explaining what these systems do. - -* ABP framework provides a strong and flexible [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating). So, we can use the text templating system to create dynamic email contents on a template and a model. - -* In this article, we will use `StandardEmailTemplates.Message` as standard email template. Then we will create a new template and replace the standard email template with our new template by using [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). - -* The `Virtual File System` makes it possible to manage files that do not physically exist on the file system. That means we can override `StandardEmailTemplates.Message` template by changing it's path with our new template's path. - -## Creating the Solution - -> ABP Framework offers startup templates to get into the business faster. - -In this article, I will create a new startup template and perform the operations on this template. But if you already have a project you don't need to create a new startup template, you can implement the following steps to your existing project. (These steps can be applied to any project. (MVC, Angular etc.)) - -> If you have already a project you can skip this section. - -Before starting to development, we will create a solution named `TemplateReplace` (or whatever you want). We can create a new startup template by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) : - -````bash -abp new TemplateReplace -```` - -Our project boilerplate will be ready after the download is finished. Then, open the solution in the Visual Studio (or your favorite IDE). - -Run the `TemplateReplace.DbMigrator` application as below to create the database and seed initial data (which creates the admin user, admin role, permissions etc.). - -![db-migrator-1](db-migrator-1.jpg) - -* Right click to `TemplateReplace.DbMigrator` and choose the `Debug`. - -![db-migrator-2](db-migrator-2.jpg) - -* After that, click the `Start new instance` option to start the database migrations. - -![db-migrator-3](db-migrator-3.jpg) - -Then we can run the `TemplateReplace.Web` project to see our application working. - -> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ - -## Starting the Development - -First thing we need to do is, creating a email service to sending emails. ABP Framework provides `IEmailSender` service that is used to send emails. - -### Step - 1 - -Create an `Emailing` folder in the `TemplateReplace.Domain` project and add a class named `EmailService` inside of it. - -```csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace TemplateReplace.Emailing -{ - public class EmailService : ITransientDependency - { - private readonly IEmailSender _emailSender; - private readonly ITemplateRenderer _templateRenderer; - - public EmailService(IEmailSender emailSender, ITemplateRenderer templateRenderer) - { - _emailSender = emailSender; - _templateRenderer = templateRenderer; - } - - public async Task SendAsync(string targetEmail) - { - var emailBody = await _templateRenderer.RenderAsync( - StandardEmailTemplates.Message, - new - { - message = "ABP Framework provides IEmailSender service that is used to send emails." - } - ); - - await _emailSender.SendAsync( - targetEmail, - "Subject", - emailBody - ); - } - } -} -``` - -* To create an email content, we need to inject `ITemplateRenderer` and use the `RenderAsync` method to render a template. - -* We've used `StandardEmailTemplates.Message` as standart email template. This provides us a standard and simple message template to send mails. - -* The resulting email body should be like shown below: - -```html - - - - - - - ABP Framework provides IEmailSender service that is used to send emails. - - -``` - -### Step - 2 (Configuring Email Settings) - -* Now, we need to configure some email settings by following [settings documentation](https://docs.abp.io/en/abp/latest/Settings#setting-values-in-the-application-configuration). For achieve this, open the `appsettings.json` file under `TemplateReplace.Web` and configure your email settings in **settings** section like below. - -![appsettings.json](settings.jpg) - -* Here, I used Google's SMTP settings to send emails via Gmail. You can change these setting values by your need. - -> **Note:** If you want to use Google's SMTP server settings and send emails via Gmail, you should confirm [this](https://myaccount.google.com/u/0/lesssecureapps). - -### Step - 3 - -* After that we need to open `TemplateReplaceDomainModule.cs` file and change its contents as below to sending real-time emails. - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using TemplateReplace.MultiTenancy; -using Volo.Abp.AuditLogging; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.Emailing; -using Volo.Abp.FeatureManagement; -using Volo.Abp.Identity; -using Volo.Abp.IdentityServer; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; -using Volo.Abp.PermissionManagement.Identity; -using Volo.Abp.PermissionManagement.IdentityServer; -using Volo.Abp.SettingManagement; -using Volo.Abp.TenantManagement; - -namespace TemplateReplace -{ - [DependsOn( - typeof(TemplateReplaceDomainSharedModule), - typeof(AbpAuditLoggingDomainModule), - typeof(AbpBackgroundJobsDomainModule), - typeof(AbpFeatureManagementDomainModule), - typeof(AbpIdentityDomainModule), - typeof(AbpPermissionManagementDomainIdentityModule), - typeof(AbpIdentityServerDomainModule), - typeof(AbpPermissionManagementDomainIdentityServerModule), - typeof(AbpSettingManagementDomainModule), - typeof(AbpTenantManagementDomainModule), - typeof(AbpEmailingModule) - )] - public class TemplateReplaceDomainModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var settingManager = context.ServiceProvider.GetService(); - //encrypts the password on set and decrypts on get - settingManager.SetGlobalAsync(EmailSettingNames.Smtp.Password, "your_password"); - } - - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsEnabled = MultiTenancyConsts.IsEnabled; - }); - - // #if DEBUG - // context.Services.Replace(ServiceDescriptor.Singleton()); - // #endif - } - } -} - -``` - -* `NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the standard log system, rather than actually sending the emails. This class can be useful especially in development time where you generally don't want to send real emails. Therefore ABP framework defined this by default. But in our case we want to send real emails, so we must remove these lines or we must take it to the comment line. - -* `Abp.Mailing.Smtp.Password` must be an encrypted value. Therefore we used `SettingManager` in here to set the password. It internally **encrypts** the values on set and **decrypts** on get. - -* After all these steps, whenever we want to send an email, we can do it by using our `EmailService` class. We can inject this class and invoke the `SendAsync` method to sending email where its needed. - -After sending the email we should see the template like below. - -![email-message](message.jpg) - -### Step - 4 (Defining New Template) - -* So far we've sent mail by using standard email template of ABP. But we may want to replace the email template with the new one. We can achieve this by following the `Text Templating` [documentation](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates). - -* In this article, I will create a email template by using free template generator named **Bee**. You can reach the free templates from [here](https://beefree.io/templates/free/). - -* When we find a template for our purpose, we can hover the link and click the **get started** button to edit the template. (I chose a template named "gdpr".) - -* Here, you can edit your template as below. (You can delete or add sections, edit texts, and so on.) - -![bee](bee.gif) - -> **Note:** After editing our template, we need to export it to reach our created template's content. You can see the **export** button top-right of the template editing page. - -* After choosing and editing our free template, we can create a new **email template** in our project. For this, create a folder named `Templates` under `Emailing` folder in `TemplateReplace.Domain` and add `EmailTemplate.tpl` file inside of it. And copy-paste the below content or your template's content. - -```tpl - - - - - - - - - - - - - - - - - - - -``` - -* Then we need to make the template file as "Embedded Resource". We can do this as below. - -* First right click to **EmailTemplate.tpl** and choose `Properties`. - -![embedded-resource](embedded-resource.jpg) - -* Then be sure about build action is **Embedded resource**. - -![embedded-resource-2](embedded-resource-2.jpg) - -### Step - 4 (Replacing the Email Template) - -* To replace the current email template with our new email template, we need to override it. To achieve this, create a class named `EmailTemplateDefinitionProvider` under `Emailing` folder in `TemplateReplace.Domain` and fill it with the below content. - -```csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace TemplateReplace.Emailing -{ - public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider, ITransientDependency - { - public override void Define(ITemplateDefinitionContext context) - { - var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Message); - - emailLayoutTemplate - .WithVirtualFilePath( - "/Emailing/Templates/EmailTemplate.tpl", - isInlineLocalized: true - ); - } - } -} -``` - -* In here we've created a template definition provider class that gets the email layout template and change the virtual file path for the template. - -* This approach allows us to locate templates in any folder instead of the folder defined by the depended module. For more detail, check the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). - -### Step - 5 - -* Lastly, we need to configure the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). To do this open your `TemplateReplaceDomainModule.cs` in `TemplateReplace.Domain` and update the content as below. - -```csharp -using TemplateReplace.MultiTenancy; -using Volo.Abp.AuditLogging; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.Emailing; -using Volo.Abp.FeatureManagement; -using Volo.Abp.Identity; -using Volo.Abp.IdentityServer; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; -using Volo.Abp.PermissionManagement.Identity; -using Volo.Abp.PermissionManagement.IdentityServer; -using Volo.Abp.SettingManagement; -using Volo.Abp.TenantManagement; -using Volo.Abp.VirtualFileSystem; - -namespace TemplateReplace -{ - [DependsOn( - typeof(TemplateReplaceDomainSharedModule), - typeof(AbpAuditLoggingDomainModule), - typeof(AbpBackgroundJobsDomainModule), - typeof(AbpFeatureManagementDomainModule), - typeof(AbpIdentityDomainModule), - typeof(AbpPermissionManagementDomainIdentityModule), - typeof(AbpIdentityServerDomainModule), - typeof(AbpPermissionManagementDomainIdentityServerModule), - typeof(AbpSettingManagementDomainModule), - typeof(AbpTenantManagementDomainModule), - typeof(AbpEmailingModule) - )] - public class TemplateReplaceDomainModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsEnabled = MultiTenancyConsts.IsEnabled; - }); - - //Add this configuration - Configure(options => - { - options.FileSets.AddEmbedded(); - }); - } - } -} - -``` - -* And now when we send a new email, we should see our newly defined template as the message like below. - -![email-last](email-last.jpg) - -## Text Template Management - -* Generally, more than one e-mail is required in applications. We create email templates for **"password changes"** or **"welcome"** etc in our applications. In such cases, it is necessary to create different templates for each mail. ABP Commercial allows us to perform these operations on UI in a simple way. Text Template Management provides UI to easily create and manage email templates. - -![template-definitions](template-definitions.png) - -* ABP Commercial's [Text Template Management](https://commercial.abp.io/modules/Volo.TextTemplateManagement) module is really fascinating. It makes it super easy to stores and edits template contents. We can list all templates on a page, editing them, localizing them, and so on. - -![inline-content](inline-content.png) - -* ABP Commercial's text template management module, allows us to modify a template through the UI. - -* I highly recommend you to [check it out](https://commercial.abp.io/modules/Volo.TextTemplateManagement). - -## References - -* [Text Templating](https://docs.abp.io/en/abp/latest/Text-Templating) -* [Emailing](https://docs.abp.io/en/abp/latest/Emailing) +# Replacing Email Templates and Sending Emails + +## Introduction + +Hi, in this step by step article, we will send an email by using standard email template and then we will replace the standard email template with our new created template, thanks to [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates) and [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). Let's start by explaining what these systems do. + +* ABP framework provides a strong and flexible [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating). So, we can use the text templating system to create dynamic email contents on a template and a model. + +* In this article, we will use `StandardEmailTemplates.Message` as standard email template. Then we will create a new template and replace the standard email template with our new template by using [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). + +* The `Virtual File System` makes it possible to manage files that do not physically exist on the file system. That means we can override `StandardEmailTemplates.Message` template by changing it's path with our new template's path. + +## Creating the Solution + +> ABP Framework offers startup templates to get into the business faster. + +In this article, I will create a new startup template and perform the operations on this template. But if you already have a project you don't need to create a new startup template, you can implement the following steps to your existing project. (These steps can be applied to any project. (MVC, Angular etc.)) + +> If you have already a project you can skip this section. + +Before starting to development, we will create a solution named `TemplateReplace` (or whatever you want). We can create a new startup template by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) : + +````bash +abp new TemplateReplace +```` + +Our project boilerplate will be ready after the download is finished. Then, open the solution in the Visual Studio (or your favorite IDE). + +Run the `TemplateReplace.DbMigrator` application as below to create the database and seed initial data (which creates the admin user, admin role, permissions etc.). + +![db-migrator-1](db-migrator-1.jpg) + +* Right click to `TemplateReplace.DbMigrator` and choose the `Debug`. + +![db-migrator-2](db-migrator-2.jpg) + +* After that, click the `Start new instance` option to start the database migrations. + +![db-migrator-3](db-migrator-3.jpg) + +Then we can run the `TemplateReplace.Web` project to see our application working. + +> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ + +## Starting the Development + +First thing we need to do is, creating a email service to sending emails. ABP Framework provides `IEmailSender` service that is used to send emails. + +### Step - 1 + +Create an `Emailing` folder in the `TemplateReplace.Domain` project and add a class named `EmailService` inside of it. + +```csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace TemplateReplace.Emailing +{ + public class EmailService : ITransientDependency + { + private readonly IEmailSender _emailSender; + private readonly ITemplateRenderer _templateRenderer; + + public EmailService(IEmailSender emailSender, ITemplateRenderer templateRenderer) + { + _emailSender = emailSender; + _templateRenderer = templateRenderer; + } + + public async Task SendAsync(string targetEmail) + { + var emailBody = await _templateRenderer.RenderAsync( + StandardEmailTemplates.Message, + new + { + message = "ABP Framework provides IEmailSender service that is used to send emails." + } + ); + + await _emailSender.SendAsync( + targetEmail, + "Subject", + emailBody + ); + } + } +} +``` + +* To create an email content, we need to inject `ITemplateRenderer` and use the `RenderAsync` method to render a template. + +* We've used `StandardEmailTemplates.Message` as standart email template. This provides us a standard and simple message template to send mails. + +* The resulting email body should be like shown below: + +```html + + + + + + + ABP Framework provides IEmailSender service that is used to send emails. + + +``` + +### Step - 2 (Configuring Email Settings) + +* Now, we need to configure some email settings by following [settings documentation](https://docs.abp.io/en/abp/latest/Settings#setting-values-in-the-application-configuration). For achieve this, open the `appsettings.json` file under `TemplateReplace.Web` and configure your email settings in **settings** section like below. + +![appsettings.json](settings.jpg) + +* Here, I used Google's SMTP settings to send emails via Gmail. You can change these setting values by your need. + +> **Note:** If you want to use Google's SMTP server settings and send emails via Gmail, you should confirm [this](https://myaccount.google.com/u/0/lesssecureapps). + +### Step - 3 + +* After that we need to open `TemplateReplaceDomainModule.cs` file and change its contents as below to sending real-time emails. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using TemplateReplace.MultiTenancy; +using Volo.Abp.AuditLogging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.Emailing; +using Volo.Abp.FeatureManagement; +using Volo.Abp.Identity; +using Volo.Abp.IdentityServer; +using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.IdentityServer; +using Volo.Abp.SettingManagement; +using Volo.Abp.TenantManagement; + +namespace TemplateReplace +{ + [DependsOn( + typeof(TemplateReplaceDomainSharedModule), + typeof(AbpAuditLoggingDomainModule), + typeof(AbpBackgroundJobsDomainModule), + typeof(AbpFeatureManagementDomainModule), + typeof(AbpIdentityDomainModule), + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpIdentityServerDomainModule), + typeof(AbpPermissionManagementDomainIdentityServerModule), + typeof(AbpSettingManagementDomainModule), + typeof(AbpTenantManagementDomainModule), + typeof(AbpEmailingModule) + )] + public class TemplateReplaceDomainModule : AbpModule + { + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var settingManager = context.ServiceProvider.GetService(); + //encrypts the password on set and decrypts on get + settingManager.SetGlobalAsync(EmailSettingNames.Smtp.Password, "your_password"); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = MultiTenancyConsts.IsEnabled; + }); + + // #if DEBUG + // context.Services.Replace(ServiceDescriptor.Singleton()); + // #endif + } + } +} + +``` + +* `NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the standard log system, rather than actually sending the emails. This class can be useful especially in development time where you generally don't want to send real emails. Therefore ABP framework defined this by default. But in our case we want to send real emails, so we must remove these lines or we must take it to the comment line. + +* `Abp.Mailing.Smtp.Password` must be an encrypted value. Therefore we used `SettingManager` in here to set the password. It internally **encrypts** the values on set and **decrypts** on get. + +* After all these steps, whenever we want to send an email, we can do it by using our `EmailService` class. We can inject this class and invoke the `SendAsync` method to sending email where its needed. + +After sending the email we should see the template like below. + +![email-message](message.jpg) + +### Step - 4 (Defining New Template) + +* So far we've sent mail by using standard email template of ABP. But we may want to replace the email template with the new one. We can achieve this by following the `Text Templating` [documentation](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates). + +* In this article, I will create a email template by using free template generator named **Bee**. You can reach the free templates from [here](https://beefree.io/templates/free/). + +* When we find a template for our purpose, we can hover the link and click the **get started** button to edit the template. (I chose a template named "gdpr".) + +* Here, you can edit your template as below. (You can delete or add sections, edit texts, and so on.) + +![bee](bee.gif) + +> **Note:** After editing our template, we need to export it to reach our created template's content. You can see the **export** button top-right of the template editing page. + +* After choosing and editing our free template, we can create a new **email template** in our project. For this, create a folder named `Templates` under `Emailing` folder in `TemplateReplace.Domain` and add `EmailTemplate.tpl` file inside of it. And copy-paste the below content or your template's content. + +```tpl + + + + + + + + + + + + + + + + + + + +``` + +* Then we need to make the template file as "Embedded Resource". We can do this as below. + +* First right click to **EmailTemplate.tpl** and choose `Properties`. + +![embedded-resource](embedded-resource.jpg) + +* Then be sure about build action is **Embedded resource**. + +![embedded-resource-2](embedded-resource-2.jpg) + +### Step - 4 (Replacing the Email Template) + +* To replace the current email template with our new email template, we need to override it. To achieve this, create a class named `EmailTemplateDefinitionProvider` under `Emailing` folder in `TemplateReplace.Domain` and fill it with the below content. + +```csharp +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace TemplateReplace.Emailing +{ + public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider, ITransientDependency + { + public override void Define(ITemplateDefinitionContext context) + { + var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Message); + + emailLayoutTemplate + .WithVirtualFilePath( + "/Emailing/Templates/EmailTemplate.tpl", + isInlineLocalized: true + ); + } + } +} +``` + +* In here we've created a template definition provider class that gets the email layout template and change the virtual file path for the template. + +* This approach allows us to locate templates in any folder instead of the folder defined by the depended module. For more detail, check the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). + +### Step - 5 + +* Lastly, we need to configure the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). To do this open your `TemplateReplaceDomainModule.cs` in `TemplateReplace.Domain` and update the content as below. + +```csharp +using TemplateReplace.MultiTenancy; +using Volo.Abp.AuditLogging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.Emailing; +using Volo.Abp.FeatureManagement; +using Volo.Abp.Identity; +using Volo.Abp.IdentityServer; +using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.IdentityServer; +using Volo.Abp.SettingManagement; +using Volo.Abp.TenantManagement; +using Volo.Abp.VirtualFileSystem; + +namespace TemplateReplace +{ + [DependsOn( + typeof(TemplateReplaceDomainSharedModule), + typeof(AbpAuditLoggingDomainModule), + typeof(AbpBackgroundJobsDomainModule), + typeof(AbpFeatureManagementDomainModule), + typeof(AbpIdentityDomainModule), + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpIdentityServerDomainModule), + typeof(AbpPermissionManagementDomainIdentityServerModule), + typeof(AbpSettingManagementDomainModule), + typeof(AbpTenantManagementDomainModule), + typeof(AbpEmailingModule) + )] + public class TemplateReplaceDomainModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = MultiTenancyConsts.IsEnabled; + }); + + //Add this configuration + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + } + } +} + +``` + +* And now when we send a new email, we should see our newly defined template as the message like below. + +![email-last](email-last.jpg) + +## Text Template Management + +* Generally, more than one e-mail is required in applications. We create email templates for **"password changes"** or **"welcome"** etc in our applications. In such cases, it is necessary to create different templates for each mail. ABP Commercial allows us to perform these operations on UI in a simple way. Text Template Management provides UI to easily create and manage email templates. + +![template-definitions](template-definitions.png) + +* ABP Commercial's [Text Template Management](https://commercial.abp.io/modules/Volo.TextTemplateManagement) module is really fascinating. It makes it super easy to stores and edits template contents. We can list all templates on a page, editing them, localizing them, and so on. + +![inline-content](inline-content.png) + +* ABP Commercial's text template management module, allows us to modify a template through the UI. + +* I highly recommend you to [check it out](https://commercial.abp.io/modules/Volo.TextTemplateManagement). + +## References + +* [Text Templating](https://docs.abp.io/en/abp/latest/Text-Templating) +* [Emailing](https://docs.abp.io/en/abp/latest/Emailing) * [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System) \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md index 971eb88f97..6445d83e85 100644 --- a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md +++ b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md @@ -1,537 +1,537 @@ -# How to Integrate the Telerik Blazor Components to the ABP Blazor UI? - -## Introduction - -Hi, in this step by step article, we will see how we can integrate the Telerik Blazor Components to our Blazor UI. - -## Creating the Solution - -> ABP Framework offers startup templates to get into business faster. - -In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project. - -> If you already have a project with the Blazor UI, you can skip this section. - -* Before starting to development, we will create a solution named `TelerikComponents` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): - -```bash -abp new TelerikComponents --ui blazor --database-provider ef -``` - -* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `TelerikComponents.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions, etc.) - -* After the database and initial data created, -* Run the `TelerikComponents.HttpApi.Host` to see our server-side working and -* Run the `TelerikComponents.Blazor` to see our UI working. - -> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ - -## Starting the Development - -### Pre-requisite - -* First thing we need to do is downloading the [Progress Control Panel](https://www.telerik.com/download-trial-file/v2/control-panel?_ga=2.212029332.1667119438.1607582144-1944255175.1605161949) to get Telerik Blazor Components on our development machine. - -* If you will use the Telerik Blazor Components for the first time or you don't have an active license you can click [here](https://www.telerik.com/login/v2/download-b?ReturnUrl=https%3a%2f%2fwww.telerik.com%2fdownload-trial-file%2fv2-b%2fui-for-blazor%3f_ga%3d2.212029332.1667119438.1607582144-1944255175.1605161949#register) to download free trial. - -> You can find the more installation details from [here](https://docs.telerik.com/blazor-ui/getting-started/client-blazor?_ga=2.55603115.1667119438.1607582144-1944255175.1605161949&_gac=1.261851647.1607669357.CjwKCAiAq8f-BRBtEiwAGr3DgUDhBT25rs7hU0EQ8K-AfeUVxs3hSoIuIAuBOZ17CNPI4ZEArORPExoCyd4QAvD_BwE#step-0---download-the-components). - ->**Notes:** To download Telerik Blazor packages via NuGet, we need to setup Telerik NuGet package source. We can state it in the installer as below. In this way, we can download the required Telerik Blazor packages via NuGet. - -![setup-nuget-package-source](./automated-nuget-feed-setup.png) - -### Step 1 (Configurations) - -* We need to install the `Telerik.UI.for.Blazor` Nuget package to our Blazor project (`*.Blazor`). We need to choose package source to **telerik.com** for Visual Studio to see this package. - -* If you use trial version of Telerik, you can download **Telerik.UI.for.Blazor.Trial** package via NuGet. - -* After the installation finished, we need to open **index.html** (it's under *wwwroot* folder) to add css and js files in our application. - -* Add the following lines just before the closing head tag (**/head**). - -```html - ... - - - - - -``` - -* After that, we need to add the Telerik Blazor Components to our application's service collection. So just open the `TelerikComponentsBlazorModule` and update the `ConfigureServices` method with the following content. - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var environment = context.Services.GetSingletonInstance(); - var builder = context.Services.GetSingletonInstance(); - - ConfigureAuthentication(builder); - ConfigureHttpClient(context, environment); - ConfigureBlazorise(context); - ConfigureRouter(context); - ConfigureUI(builder); - ConfigureMenu(context); - ConfigureAutoMapper(context); - - //add this line to be able to use the components - builder.Services.AddTelerikBlazor(); -} -``` - -* After the service added we will continue by opening **_Imports.razor** file and add the global using statements as below. This will bring our Telerik components into scope throughout the application. - -![telerik-blazor-component-1](./telerik-blazor-component-1.jpg) - -* After all of these steps, the Telerik UI Components are ready to be used anywhere in our application. We can use the Telerik Blazor Components by wrapping our components or pages between `` and `` tags. - -### Step 2 - Checking Configurations - -* We should check, have we done the right configurations or not. For that, we can open the **Index.razor** file and we can update the component with the following content. - -```razor -@page "/" -@using Volo.Abp.MultiTenancy -@inherits TelerikComponentsComponentBase -@inject ICurrentTenant CurrentTenant -@inject AuthenticationStateProvider AuthenticationStateProvider -@using System.Timers - -@implements IDisposable - - -
-
Telerik Progress Bar Component
- -
-
- -@code { - private const int TimerInterval = 1000; - private const int TotalTime = 10 * TimerInterval; - private double ProgressValue = 0; - private int ProgressStep = 100 / (TotalTime / TimerInterval); - private Timer Timer { get; set; } = new Timer(); - - private void Dispose() - { - StopProgress(); - Timer?.Close(); - } - - protected override void OnAfterRender(bool firstRender) - { - if (Timer.Enabled == false) - { - Timer.Interval = TimerInterval; - Timer.Elapsed -= OnTimerElapsed; - Timer.Elapsed += OnTimerElapsed; - Timer.AutoReset = true; - Timer.Start(); - } - } - - private void OnTimerElapsed(Object source, ElapsedEventArgs e) - { - if (ProgressValue < 100) - { - UpdateProgress(); - } - else - { - StopProgress(); - } - } - - private void UpdateProgress() - { - ProgressValue += ProgressStep; - - InvokeAsync(StateHasChanged); - } - - private void StopProgress() - { - Timer?.Stop(); - } -} - - -``` - -* In here, we've just added the `TelerikProgressBar` component to check the integration configured properly. - -* When we run `*.HttpApi.Host` and `*.Blazor` projects, we should see that the `TelerikProgressBar` component works and has similar view as the below gif. - -![telerik-progress-bar](./telerik-progress-bar.gif) - -* If you haven't seen this component like above, you should check the above configurations and be assure every step done as stated. - -### Step 3 - Using The Telerik Blazor Components (Sample Application) - -* Let's create a sample application for use other Telerik Blazor Components (like DataGrid). - -* We will use [jsonplaceholder](https://jsonplaceholder.typicode.com/) as **mock data** to the listing, adding, updating and deleting posts. - -* Firstly, we can create a folder named `Posts` and inside this folder, we can create the classes which are highlighted in the following screenshot. - -![sample-application](./sample-application.jpg) - -* After classes created we can fill the classes with the following contents. - -**Post.cs** -```csharp -using System; - -namespace TelerikComponents.Posts -{ - [Serializable] - public class Post - { - public int Id { get; set; } - - public string Title { get; set; } - - public string Body { get; set; } - - public int UserId { get; set; } - } -} -``` - -**Comment.cs** -```csharp -using System; - -namespace TelerikComponents.Posts -{ - [Serializable] - public class Comment - { - public int PostId { get; set; } - - public int Id { get; set; } - - public string Name { get; set; } - - public string Email { get; set; } - - public string Body { get; set; } - } -} -``` - -**IPostAppService.cs** -```csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace TelerikComponents.Posts -{ - public interface IPostAppService : IApplicationService - { - Task> GetPostsAsync(); - - Task AddPostAsync(Post post); - - Task UpdatePostAsync(int postId, Post post); - - Task DeletePostAsync(int postId); - - Task GetFirstCommentByPostIdAsync(int postId); - } -} -``` -* In here, we basically created two class (which are **Post** and **Comment**). These classes are used to hold data returned as JSON. - -* After that, we need to implement `IPostAppService`. For achieve this, we can create a folder named `Posts` in **\*.Application** layer and inside this folder we can create a class named `PostAppService` with the following content. - -**PostAppService.cs** -```csharp -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace TelerikComponents.Posts -{ - public class PostAppService : ApplicationService, IPostAppService - { - private JsonSerializerOptions _options = new JsonSerializerOptions - { - IncludeFields = true, - PropertyNameCaseInsensitive = true - }; - - public async Task> GetPostsAsync() - { - var url = "https://jsonplaceholder.typicode.com/posts"; - List _posts = new List(); - - using (var client = new HttpClient()) - { - var result = await client.GetAsync(url); - - if (result.IsSuccessStatusCode) - { - var content = await result.Content.ReadAsStringAsync(); - var deserializedPosts = JsonSerializer.Deserialize>(content, _options); - - _posts = deserializedPosts; - } - } - - return _posts; - } - - public async Task AddPostAsync(Post post) - { - var url = "https://jsonplaceholder.typicode.com/posts"; - Post addedPost = null; - - using (var client = new HttpClient()) - { - var serializePost = JsonSerializer.Serialize(post); - var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); - var result = await client.PostAsync(url, content); - - if (result.IsSuccessStatusCode) - { - var response = await result.Content.ReadAsStringAsync(); - addedPost = JsonSerializer.Deserialize(response); - } - } - - return addedPost; - } - - public async Task UpdatePostAsync(int postId, Post post) - { - var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; - Post updatedPost = null; - - using (var client = new HttpClient()) - { - var serializePost = JsonSerializer.Serialize(post); - var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); - var result = await client.PutAsync(url, content); - - if (result.IsSuccessStatusCode) - { - var response = await result.Content.ReadAsStringAsync(); - updatedPost = JsonSerializer.Deserialize(response); - } - } - - return updatedPost; - } - - public async Task DeletePostAsync(int postId) - { - var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; - - using (var client = new HttpClient()) - { - await client.DeleteAsync(url); - } - } - - public async Task GetFirstCommentByPostIdAsync(int postId) - { - var url = $"https://jsonplaceholder.typicode.com/posts/{postId}/comments"; - - List _comments = new List(); - - using (var client = new HttpClient()) - { - var result = await client.GetAsync(url); - - if (result.IsSuccessStatusCode) - { - var content = await result.Content.ReadAsStringAsync(); - var deserializedPosts = JsonSerializer.Deserialize>(content, _options); - - _comments = deserializedPosts; - } - } - - return _comments[0]; - } - } -} -``` - -* In here, we've implemented `IPostAppService` methods by using [jsonplaceholder](https://jsonplaceholder.typicode.co) API. These endpoints provide us basic crud functionallity. - -* After the implemenation, we can start to create the user interface. - -#### Blazor UI - -* We can create **/Posts** page for listing, updating, deleting and creating our posts. So, create a razor page named `Posts.razor` under **Pages** folder in `*.Blazor` project. - -**Posts.razor** -```razor -@page "/Posts" -@using TelerikComponents.Posts -@using IconName = Telerik.Blazor.IconName -@inject IPostAppService PostAppService - -

Posts

- - - - - - - - Update - Edit - Delete - Display Comment - Cancel - - - - Add Post - - - - @* Modal *@ - - - Comment - - - - - -

Email: @Comment.Email

-

- Message: @Comment.Body -

-
-
-
-``` - -**Post.razor.cs** -```csharp -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Telerik.Blazor.Components; -using TelerikComponents.Posts; - -namespace TelerikComponents.Blazor.Pages -{ - public partial class Posts - { - private List GridData { get; set; } - private TelerikGrid Grid { get; set; } - private bool ModalVisible { get; set; } = false; - private Comment Comment { get; set; } - - public Posts() - { - Comment = new Comment(); - } - - protected override async Task OnInitializedAsync() - { - await LoadDataAsync(); - } - - private async Task LoadDataAsync() - { - GridData = await PostAppService.GetPostsAsync(); - } - - private async Task UpdateHandler(GridCommandEventArgs args) - { - var post = (Post) args.Item; - - await PostAppService.UpdatePostAsync(post.Id, post); - - var matchingPost = GridData.FirstOrDefault(x => x.Id == post.Id); - - if (matchingPost != null) - { - matchingPost.Body = post.Body; - matchingPost.Title = post.Title; - } - } - - private async Task DeleteHandler(GridCommandEventArgs args) - { - var post = (Post) args.Item; - - GridData.Remove(post); - } - - private async Task CreateHandler(GridCommandEventArgs args) - { - var post = (Post) args.Item; - - var addedPost = await PostAppService.AddPostAsync(post); - - GridData.Insert(0, addedPost); - } - - private async Task PostDetailAsync(GridCommandEventArgs args) - { - var post = (Post) args.Item; - - Comment = await PostAppService.GetFirstCommentByPostIdAsync(post.Id); - - ModalVisible = true; - } - } -} -``` - -* In here, we've used `TelerikGrid` component. - -* The `Telerik Grid` is a powerful component, which allows you to visualize and edit data via its table representation. It provides a variety of options about how to present and perform operations over the underlying data, such as paging, sorting, filtering and editing. - -* The Blazor UI Grid allows flexible customization of its items exposing rows, columns and edit templates for this purpose. - -### Final Result - -* After all of these steps, we can finally run our application. - * Run `*.HttpApi.Host` project for use the required endpoints, - * Run `*.Blazor` project for see the Blazor UI. - -* When we navigate to `Posts` route, we should see the following screenshot in this page. - -![final-result](./final-result.jpg) - -## Conclusion - +# How to Integrate the Telerik Blazor Components to the ABP Blazor UI? + +## Introduction + +Hi, in this step by step article, we will see how we can integrate the Telerik Blazor Components to our Blazor UI. + +## Creating the Solution + +> ABP Framework offers startup templates to get into business faster. + +In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project. + +> If you already have a project with the Blazor UI, you can skip this section. + +* Before starting to development, we will create a solution named `TelerikComponents` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): + +```bash +abp new TelerikComponents --ui blazor --database-provider ef +``` + +* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `TelerikComponents.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions, etc.) + +* After the database and initial data created, +* Run the `TelerikComponents.HttpApi.Host` to see our server-side working and +* Run the `TelerikComponents.Blazor` to see our UI working. + +> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ + +## Starting the Development + +### Pre-requisite + +* First thing we need to do is downloading the [Progress Control Panel](https://www.telerik.com/download-trial-file/v2/control-panel?_ga=2.212029332.1667119438.1607582144-1944255175.1605161949) to get Telerik Blazor Components on our development machine. + +* If you will use the Telerik Blazor Components for the first time or you don't have an active license you can click [here](https://www.telerik.com/login/v2/download-b?ReturnUrl=https%3a%2f%2fwww.telerik.com%2fdownload-trial-file%2fv2-b%2fui-for-blazor%3f_ga%3d2.212029332.1667119438.1607582144-1944255175.1605161949#register) to download free trial. + +> You can find the more installation details from [here](https://docs.telerik.com/blazor-ui/getting-started/client-blazor?_ga=2.55603115.1667119438.1607582144-1944255175.1605161949&_gac=1.261851647.1607669357.CjwKCAiAq8f-BRBtEiwAGr3DgUDhBT25rs7hU0EQ8K-AfeUVxs3hSoIuIAuBOZ17CNPI4ZEArORPExoCyd4QAvD_BwE#step-0---download-the-components). + +>**Notes:** To download Telerik Blazor packages via NuGet, we need to setup Telerik NuGet package source. We can state it in the installer as below. In this way, we can download the required Telerik Blazor packages via NuGet. + +![setup-nuget-package-source](./automated-nuget-feed-setup.png) + +### Step 1 (Configurations) + +* We need to install the `Telerik.UI.for.Blazor` Nuget package to our Blazor project (`*.Blazor`). We need to choose package source to **telerik.com** for Visual Studio to see this package. + +* If you use trial version of Telerik, you can download **Telerik.UI.for.Blazor.Trial** package via NuGet. + +* After the installation finished, we need to open **index.html** (it's under *wwwroot* folder) to add css and js files in our application. + +* Add the following lines just before the closing head tag (**/head**). + +```html + ... + + + + + +``` + +* After that, we need to add the Telerik Blazor Components to our application's service collection. So just open the `TelerikComponentsBlazorModule` and update the `ConfigureServices` method with the following content. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var environment = context.Services.GetSingletonInstance(); + var builder = context.Services.GetSingletonInstance(); + + ConfigureAuthentication(builder); + ConfigureHttpClient(context, environment); + ConfigureBlazorise(context); + ConfigureRouter(context); + ConfigureUI(builder); + ConfigureMenu(context); + ConfigureAutoMapper(context); + + //add this line to be able to use the components + builder.Services.AddTelerikBlazor(); +} +``` + +* After the service added we will continue by opening **_Imports.razor** file and add the global using statements as below. This will bring our Telerik components into scope throughout the application. + +![telerik-blazor-component-1](./telerik-blazor-component-1.jpg) + +* After all of these steps, the Telerik UI Components are ready to be used anywhere in our application. We can use the Telerik Blazor Components by wrapping our components or pages between `` and `` tags. + +### Step 2 - Checking Configurations + +* We should check, have we done the right configurations or not. For that, we can open the **Index.razor** file and we can update the component with the following content. + +```razor +@page "/" +@using Volo.Abp.MultiTenancy +@inherits TelerikComponentsComponentBase +@inject ICurrentTenant CurrentTenant +@inject AuthenticationStateProvider AuthenticationStateProvider +@using System.Timers + +@implements IDisposable + + +
+
Telerik Progress Bar Component
+ +
+
+ +@code { + private const int TimerInterval = 1000; + private const int TotalTime = 10 * TimerInterval; + private double ProgressValue = 0; + private int ProgressStep = 100 / (TotalTime / TimerInterval); + private Timer Timer { get; set; } = new Timer(); + + private void Dispose() + { + StopProgress(); + Timer?.Close(); + } + + protected override void OnAfterRender(bool firstRender) + { + if (Timer.Enabled == false) + { + Timer.Interval = TimerInterval; + Timer.Elapsed -= OnTimerElapsed; + Timer.Elapsed += OnTimerElapsed; + Timer.AutoReset = true; + Timer.Start(); + } + } + + private void OnTimerElapsed(Object source, ElapsedEventArgs e) + { + if (ProgressValue < 100) + { + UpdateProgress(); + } + else + { + StopProgress(); + } + } + + private void UpdateProgress() + { + ProgressValue += ProgressStep; + + InvokeAsync(StateHasChanged); + } + + private void StopProgress() + { + Timer?.Stop(); + } +} + + +``` + +* In here, we've just added the `TelerikProgressBar` component to check the integration configured properly. + +* When we run `*.HttpApi.Host` and `*.Blazor` projects, we should see that the `TelerikProgressBar` component works and has similar view as the below gif. + +![telerik-progress-bar](./telerik-progress-bar.gif) + +* If you haven't seen this component like above, you should check the above configurations and be assure every step done as stated. + +### Step 3 - Using The Telerik Blazor Components (Sample Application) + +* Let's create a sample application for use other Telerik Blazor Components (like DataGrid). + +* We will use [jsonplaceholder](https://jsonplaceholder.typicode.com/) as **mock data** to the listing, adding, updating and deleting posts. + +* Firstly, we can create a folder named `Posts` and inside this folder, we can create the classes which are highlighted in the following screenshot. + +![sample-application](./sample-application.jpg) + +* After classes created we can fill the classes with the following contents. + +**Post.cs** +```csharp +using System; + +namespace TelerikComponents.Posts +{ + [Serializable] + public class Post + { + public int Id { get; set; } + + public string Title { get; set; } + + public string Body { get; set; } + + public int UserId { get; set; } + } +} +``` + +**Comment.cs** +```csharp +using System; + +namespace TelerikComponents.Posts +{ + [Serializable] + public class Comment + { + public int PostId { get; set; } + + public int Id { get; set; } + + public string Name { get; set; } + + public string Email { get; set; } + + public string Body { get; set; } + } +} +``` + +**IPostAppService.cs** +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace TelerikComponents.Posts +{ + public interface IPostAppService : IApplicationService + { + Task> GetPostsAsync(); + + Task AddPostAsync(Post post); + + Task UpdatePostAsync(int postId, Post post); + + Task DeletePostAsync(int postId); + + Task GetFirstCommentByPostIdAsync(int postId); + } +} +``` +* In here, we basically created two class (which are **Post** and **Comment**). These classes are used to hold data returned as JSON. + +* After that, we need to implement `IPostAppService`. For achieve this, we can create a folder named `Posts` in **\*.Application** layer and inside this folder we can create a class named `PostAppService` with the following content. + +**PostAppService.cs** +```csharp +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace TelerikComponents.Posts +{ + public class PostAppService : ApplicationService, IPostAppService + { + private JsonSerializerOptions _options = new JsonSerializerOptions + { + IncludeFields = true, + PropertyNameCaseInsensitive = true + }; + + public async Task> GetPostsAsync() + { + var url = "https://jsonplaceholder.typicode.com/posts"; + List _posts = new List(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync(url); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + var deserializedPosts = JsonSerializer.Deserialize>(content, _options); + + _posts = deserializedPosts; + } + } + + return _posts; + } + + public async Task AddPostAsync(Post post) + { + var url = "https://jsonplaceholder.typicode.com/posts"; + Post addedPost = null; + + using (var client = new HttpClient()) + { + var serializePost = JsonSerializer.Serialize(post); + var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); + var result = await client.PostAsync(url, content); + + if (result.IsSuccessStatusCode) + { + var response = await result.Content.ReadAsStringAsync(); + addedPost = JsonSerializer.Deserialize(response); + } + } + + return addedPost; + } + + public async Task UpdatePostAsync(int postId, Post post) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; + Post updatedPost = null; + + using (var client = new HttpClient()) + { + var serializePost = JsonSerializer.Serialize(post); + var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); + var result = await client.PutAsync(url, content); + + if (result.IsSuccessStatusCode) + { + var response = await result.Content.ReadAsStringAsync(); + updatedPost = JsonSerializer.Deserialize(response); + } + } + + return updatedPost; + } + + public async Task DeletePostAsync(int postId) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; + + using (var client = new HttpClient()) + { + await client.DeleteAsync(url); + } + } + + public async Task GetFirstCommentByPostIdAsync(int postId) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}/comments"; + + List _comments = new List(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync(url); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + var deserializedPosts = JsonSerializer.Deserialize>(content, _options); + + _comments = deserializedPosts; + } + } + + return _comments[0]; + } + } +} +``` + +* In here, we've implemented `IPostAppService` methods by using [jsonplaceholder](https://jsonplaceholder.typicode.co) API. These endpoints provide us basic crud functionallity. + +* After the implemenation, we can start to create the user interface. + +#### Blazor UI + +* We can create **/Posts** page for listing, updating, deleting and creating our posts. So, create a razor page named `Posts.razor` under **Pages** folder in `*.Blazor` project. + +**Posts.razor** +```razor +@page "/Posts" +@using TelerikComponents.Posts +@using IconName = Telerik.Blazor.IconName +@inject IPostAppService PostAppService + +

Posts

+ + + + + + + + Update + Edit + Delete + Display Comment + Cancel + + + + Add Post + + + + @* Modal *@ + + + Comment + + + + + +

Email: @Comment.Email

+

+ Message: @Comment.Body +

+
+
+
+``` + +**Post.razor.cs** +```csharp +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Telerik.Blazor.Components; +using TelerikComponents.Posts; + +namespace TelerikComponents.Blazor.Pages +{ + public partial class Posts + { + private List GridData { get; set; } + private TelerikGrid Grid { get; set; } + private bool ModalVisible { get; set; } = false; + private Comment Comment { get; set; } + + public Posts() + { + Comment = new Comment(); + } + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + GridData = await PostAppService.GetPostsAsync(); + } + + private async Task UpdateHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + await PostAppService.UpdatePostAsync(post.Id, post); + + var matchingPost = GridData.FirstOrDefault(x => x.Id == post.Id); + + if (matchingPost != null) + { + matchingPost.Body = post.Body; + matchingPost.Title = post.Title; + } + } + + private async Task DeleteHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + GridData.Remove(post); + } + + private async Task CreateHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + var addedPost = await PostAppService.AddPostAsync(post); + + GridData.Insert(0, addedPost); + } + + private async Task PostDetailAsync(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + Comment = await PostAppService.GetFirstCommentByPostIdAsync(post.Id); + + ModalVisible = true; + } + } +} +``` + +* In here, we've used `TelerikGrid` component. + +* The `Telerik Grid` is a powerful component, which allows you to visualize and edit data via its table representation. It provides a variety of options about how to present and perform operations over the underlying data, such as paging, sorting, filtering and editing. + +* The Blazor UI Grid allows flexible customization of its items exposing rows, columns and edit templates for this purpose. + +### Final Result + +* After all of these steps, we can finally run our application. + * Run `*.HttpApi.Host` project for use the required endpoints, + * Run `*.Blazor` project for see the Blazor UI. + +* When we navigate to `Posts` route, we should see the following screenshot in this page. + +![final-result](./final-result.jpg) + +## Conclusion + In this article, I've tried to explain how we can integrate [Telerik Blazor Component](https://www.telerik.com/blazor-ui) to our Blazor UI. ABP Framework designed as modular, so that it can work with any UI library/framework. \ No newline at end of file diff --git a/docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md b/docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md index 5880406ffb..0ec86d40c4 100644 --- a/docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md +++ b/docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md @@ -1,456 +1,456 @@ -# Using Elsa Workflow with ABP Framework - -**Elsa Core** is an open-source workflows library that can be used in any kind of .NET Core application. Using such a workflow library can be useful to implement business rules visually or programmatically. - -![elsa-overview](./elsa-overview.gif) - -This article shows how we can use this workflow library within our ABP-based application. We will start with a couple of examples and then we will integrate the **Elsa Dashboard** (you can see it in the above gif) into our application to be able to design our workflows visually. - -## Source Code - -You can find the source of the example solution used in this article [here](https://github.com/abpframework/abp-samples/tree/master/ElsaDemo). - -## Create the Project - -In this article, I will create a new startup template with EF Core as a database provider and MVC/Razor-Pages for the UI framework. - -> If you already have a project with MVC/Razor-Pages or Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project (you can skip this section). - -* We will create a new solution named `ElsaDemo` (or whatever you want). We will create a new startup template with **EF Core** as a database provider and **MVC/Razor-Pages** for the UI framework by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): - -```bash -abp new ElsaDemo -``` - -* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE). - -* We can run the `ElsaDemo.DbMigrator` project to apply migration into our database and seed initial data. - -* After the database and initial data created, we can run the `ElsaDemo.Web` to see our UI working properly. - -> Default admin username is **admin** and password is **1q2w3E*** - -## Let's Create The First Workflow (Console Activity) - -We can start with creating our first workflow. Let's get started with creating a basic hello-world workflow by using console activity. In this example, we will **programmatically** define a workflow definition that displays the text **"Hello World from Elsa!"** to the console using Elsa's Workflow Builder API and run this workflow when the application initialized. - -### Install Packages - -We need to add two packages: `Elsa` and `Elsa.Activities.Console` into our `ElsaDemo.Web` project. We can add these two packages with the following command: - -```bash -dotnet add package Elsa -dotnet add package Elsa.Activities.Console -``` - -* After the packages installed, we can define our first workflow. To do this, create a folder named **Workflows** and in this folder create a class named `HelloWorldConsole`. - -```csharp -using Elsa.Activities.Console; -using Elsa.Builders; - -namespace ElsaDemo.Web.Workflows -{ - public class HelloWorldConsole : IWorkflow - { - public void Build(IWorkflowBuilder builder) => builder.WriteLine("Hello World from Elsa!"); - } -} -``` - -* In here we've basically implemented the `IWorkflow` interface which only has one method named **Build**. In this method, we can define our workflow's execution steps (activities). - -* As you can see in the example above, we've used an activity named **WriteLine**, which writes a line of text to the console. Elsa Core has many pre-defined activities like that. E.g **HttpEndpoint** and **WriteHttpResponse** (we will see them both in the next section). - -> "An activity is an atomic building block that represents a single executable step on the workflow." - [Elsa Core Activity Definition](https://elsa-workflows.github.io/elsa-core/docs/next/concepts/concepts-workflows#activity) - -* After defining our workflow, we need to define service registrations which required for the Elsa Core library to work properly. To do that, open your `ElsaDemoWebModule` class and update your `ElsaDemoWebModule` with the following lines. Most of the codes are abbreviated for simplicity. - -```csharp -using ElsaDemo.Web.Workflows; -using Elsa.Services; - -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var hostingEnvironment = context.Services.GetHostingEnvironment(); - var configuration = context.Services.GetConfiguration(); - - //... - - ConfigureElsa(context); -} - -private void ConfigureElsa(ServiceConfigurationContext context) -{ - context.Services.AddElsa(options => - { - options - .AddConsoleActivities() - .AddWorkflow(); - }); -} - -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - //... - - var workflowRunner = context.ServiceProvider.GetRequiredService(); - workflowRunner.BuildAndStartWorkflowAsync(); -} -``` - -* Here we basically, configured Elsa's services in our `ConfigureServices` method and after that in our `OnApplicationInitialization` method we started the `HelloWorldConsole` workflow. - -* If we run the application and examine the console outputs, we should see the message that we defined in our workflow. - -![hello-world-workflow](./hello-world-workflow.jpg) - -## Creating A Workflow By Using Http Activities - -In this example, we will create a workflow that uses **Http Activities**. It will basically listen the specified route for incoming HTTP Request and writes back a simple response. - -### Add Elsa.Activities.Http Package - -* To be able to use **HTTP Activities** we need to add `Elsa` (we've already added in the previous section) and `Elsa.Activities.Http` packages into our web application. - -```bash -dotnet add package Elsa.Activities.Http -``` - -* After the package installed, we can create our workflow. Let's started with creating a class named `HelloWorldHttp` under **Workflows** folder. - -```csharp -using System.Net; -using Elsa.Activities.Http; -using Elsa.Builders; - -namespace ElsaDemo.Web.Workflows -{ - public class HelloWorldHttp : IWorkflow - { - public void Build(IWorkflowBuilder builder) - { - builder - .HttpEndpoint("/hello-world") - .WriteHttpResponse(HttpStatusCode.OK, "

Hello World!

", "text/html"); - } - } -} -``` - -* The above workflow has two activities. The first activity `HttpEndpoint` represents an HTTP endpoint, which can be invoked using an HTTP client, including a web browser. The first activity is connected to the second activity `WriteHttpResponse`, which returns a simple response to us. - -* After defined the **HelloWorldHttp** workflow we need to define this class as workflow. So, open your `ElsaDemoWebModule` and update the `ConfigureElsa` method as below. - -```csharp -private void ConfigureElsa(ServiceConfigurationContext context) -{ - context.Services.AddElsa(options => - { - options - .AddConsoleActivities() - .AddHttpActivities() //add this line to be able to use the http activities - .AddWorkflow() - .AddWorkflow(); //workflow that we defined - }); -} -``` -* And add the **UseHttpActivities** middleware to `OnApplicationInitilization` method of your `ElsaDemoWebModule` class. - -```csharp -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - // ... - app.UseAuditing(); - app.UseAbpSerilogEnrichers(); - app.UseHttpActivities(); //add this line - app.UseConfiguredEndpoints(); - - var workflowRunner = context.ServiceProvider.GetRequiredService(); - workflowRunner.BuildAndStartWorkflowAsync(); -} -``` - -* If we run the application and navigate to the "/hello-world" route we should see the response message that we've defined (by using **WriteHttpResponse** activity) in our `HelloWorldHttp` workflow. - -![hello-world-http](./hello-world-http.jpg) - -## Integrate Elsa Dashboard To Application - -* Until now we've created two workflows programmatically. But also we can create workflows visually by using Elsa's **HTML5 Workflow Designer**. - -* Being able to design our workflows easily and taking advantage of **HTML5 Workflow Designer** we will integrate the Elsa Dashboard to our application. - -### Install Packages - -* Following three packages required for Elsa Server. - -```bash -dotnet add package Elsa.Activities.Temporal.Quartz -dotnet add package Elsa.Persistence.EntityFramework.SqlServer -dotnet add package Elsa.Server.Api -``` - -> Also, we need to install the **Elsa** and **Elsa.Activities.Http** packages but we've already installed these packages in the previous sections. - -* We need to install one more package named `Elsa.Designer.Components.Web`. This package provides us the **Elsa Dashboard** component. - -```bash -dotnet add package Elsa.Designer.Components.Web -``` - -* After the package installations completed, we need to make the necessary configurations to be able to use the **Elsa Server** and **Elsa Dashboard**. Therefore, open your `ElsaDemoWebModule` class and make the necessary changes as below. - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var configuration = context.Services.GetConfiguration(); - - //... - - ConfigureElsa(context, configuration); -} - -private void ConfigureElsa(ServiceConfigurationContext context, IConfiguration configuration) -{ - var elsaSection = configuration.GetSection("Elsa"); - - context.Services.AddElsa(elsa => - { - elsa - .UseEntityFrameworkPersistence(ef => - DbContextOptionsBuilderExtensions.UseSqlServer(ef, - configuration.GetConnectionString("Default"))) - .AddConsoleActivities() - .AddHttpActivities(elsaSection.GetSection("Server").Bind) - .AddQuartzTemporalActivities() - .AddJavaScriptActivities() - .AddWorkflowsFrom(); - }); - - context.Services.AddElsaApiEndpoints(); - context.Services.Configure(options => - { - options.UseApiBehavior = false; - }); - - context.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy - .AllowAnyHeader() - .AllowAnyMethod() - .AllowAnyOrigin() - .WithExposedHeaders("Content-Disposition")) - ); - - //Uncomment the below line if your abp version is lower than v4.4 to register controllers of Elsa . - //See https://github.com/abpframework/abp/pull/9299 (we will no longer need to specify this line of code from v4.4) - // context.Services.AddAssemblyOf(); - - //Disable antiforgery validation for elsa - Configure(options => - { - options.AutoValidateFilter = type => - type.Assembly != typeof(Elsa.Server.Api.Endpoints.WorkflowRegistry.Get).Assembly; - }); -} - -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - app.UseCors(); - - //... - - app.UseHttpActivities(); - app.UseConfiguredEndpoints(endpoints => - { - endpoints.MapFallbackToPage("/_Host"); - }); - - var workflowRunner = context.ServiceProvider.GetRequiredService(); - workflowRunner.BuildAndStartWorkflowAsync(); -} -``` - -* These services required for the dashboard. - -* We don't need to register our workflows one by one anymore. Because now we use `.AddWorkflowsFrom()`, and this registers workflows on our behalf. - -* As you may notice here, we use a section named `Elsa` and its sub-sections from the configuration system but we didn't define them yet. To define them open your `appsettings.json` and add the following Elsa section into this file. - -```json -{ - //... - - "Elsa": { - "Http": { - "BaseUrl": "https://localhost:44336" - } - } -} -``` - -#### Define Permission For Elsa Dashboard - -* We can define a [permission](https://docs.abp.io/en/abp/latest/Authorization#permission-system) to be assured of only allowed users can see the Elsa Dashboard. - -* Open your `ElsaDemoPermissions` class under the **Permissions** folder (in the `ElsaDemo.Application.Contracts` layer) and add the following permission name. - -```csharp -namespace ElsaDemo.Permissions -{ - public static class ElsaDemoPermissions - { - public const string GroupName = "ElsaDemo"; - - public const string ElsaDashboard = GroupName + ".ElsaDashboard"; - } -} -``` - -* After that, open your `ElsaDemoPermissionDefinitionProvider` class and define the permission for Elsa Dashboard. - -```csharp -using ElsaDemo.Localization; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Localization; - -namespace ElsaDemo.Permissions -{ - public class ElsaDemoPermissionDefinitionProvider : PermissionDefinitionProvider - { - public override void Define(IPermissionDefinitionContext context) - { - var myGroup = context.AddGroup(ElsaDemoPermissions.GroupName); - - myGroup.AddPermission(ElsaDemoPermissions.ElsaDashboard, L("Permission:ElsaDashboard")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } - } -} -``` - -* As you can notice, we've used a localized value (**L("Permission:ElsaDashboard")**) but haven't added this localization key and value to the localization file, so let's add this localization key and value. To do this, open your `en.json` file under **Localization/ElsaDemo** folder (under the **DomainShared** layer) and add this localization key. - -```json -{ - "culture": "en", - "texts": { - "Menu:Home": "Home", - "Welcome": "Welcome", - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", - "Permission:ElsaDashboard": "Elsa Dashboard" - } -} -``` - -#### Add Elsa Dashboard Component To Application - -* After those configurations, now we can add Elsa Dashboard to our application with an authorization check. To do this, create a razor page named **_Host.cshtml** (under **Pages** folder) and update its content as below. - -```html -@page "/elsa" -@using ElsaDemo.Permissions -@using Microsoft.AspNetCore.Authorization -@attribute [Authorize(ElsaDemoPermissions.ElsaDashboard)] -@{ - var serverUrl = $"{Request.Scheme}://{Request.Host}"; - Layout = null; -} - - - - - - Elsa Workflows - - - - - - - - - - - - - -``` - -* We've defined an attribute for authorization check here. With this authorization check, only the user who has the **Elsa Dashboard** permission allowed to see this page. - -#### Add Elsa Dashboard Page To Main Menu - -* We can open the `ElsaDemoMenuContributor` class under the **Menus** folder and define the menu item for reaching the Elsa Dashboard easily. - -```csharp -using System.Threading.Tasks; -using ElsaDemo.Localization; -using ElsaDemo.MultiTenancy; -using ElsaDemo.Permissions; -using Volo.Abp.Identity.Web.Navigation; -using Volo.Abp.SettingManagement.Web.Navigation; -using Volo.Abp.TenantManagement.Web.Navigation; -using Volo.Abp.UI.Navigation; - -namespace ElsaDemo.Web.Menus -{ - public class ElsaDemoMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if (context.Menu.Name == StandardMenus.Main) - { - await ConfigureMainMenuAsync(context); - } - } - - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - var administration = context.Menu.GetAdministration(); - var l = context.GetLocalizer(); - - context.Menu.Items.Insert( - 0, - new ApplicationMenuItem( - ElsaDemoMenus.Home, - l["Menu:Home"], - "~/", - icon: "fas fa-home", - order: 0 - ) - ); - - //add Workflow menu-item - context.Menu.Items.Insert( - 1, - new ApplicationMenuItem( - ElsaDemoMenus.Home, - "Workflow", - "~/elsa", - icon: "fas fa-code-branch", - order: 1, - requiredPermissionName: ElsaDemoPermissions.ElsaDashboard - ) - ); - - //... - } - } -} -``` - -* With that menu item configuration, only the user who has **Elsa Dashboard** permission allowed to see the defined menu item. - -## Result - -* Let's run the application and see how it looks like. - -> If the account you are logged in has the **ElsaDemoPermissions.ElsaDashboard** permission, you should see the **Workflow** menu item. If you do not see this menu item, please be assured that your logged-in account has that permission. - -* Now we can click the "Workflow" menu item, display the Elsa Dashboard and designing workflows. - +# Using Elsa Workflow with ABP Framework + +**Elsa Core** is an open-source workflows library that can be used in any kind of .NET Core application. Using such a workflow library can be useful to implement business rules visually or programmatically. + +![elsa-overview](./elsa-overview.gif) + +This article shows how we can use this workflow library within our ABP-based application. We will start with a couple of examples and then we will integrate the **Elsa Dashboard** (you can see it in the above gif) into our application to be able to design our workflows visually. + +## Source Code + +You can find the source of the example solution used in this article [here](https://github.com/abpframework/abp-samples/tree/master/ElsaDemo). + +## Create the Project + +In this article, I will create a new startup template with EF Core as a database provider and MVC/Razor-Pages for the UI framework. + +> If you already have a project with MVC/Razor-Pages or Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project (you can skip this section). + +* We will create a new solution named `ElsaDemo` (or whatever you want). We will create a new startup template with **EF Core** as a database provider and **MVC/Razor-Pages** for the UI framework by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): + +```bash +abp new ElsaDemo +``` + +* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE). + +* We can run the `ElsaDemo.DbMigrator` project to apply migration into our database and seed initial data. + +* After the database and initial data created, we can run the `ElsaDemo.Web` to see our UI working properly. + +> Default admin username is **admin** and password is **1q2w3E*** + +## Let's Create The First Workflow (Console Activity) + +We can start with creating our first workflow. Let's get started with creating a basic hello-world workflow by using console activity. In this example, we will **programmatically** define a workflow definition that displays the text **"Hello World from Elsa!"** to the console using Elsa's Workflow Builder API and run this workflow when the application initialized. + +### Install Packages + +We need to add two packages: `Elsa` and `Elsa.Activities.Console` into our `ElsaDemo.Web` project. We can add these two packages with the following command: + +```bash +dotnet add package Elsa +dotnet add package Elsa.Activities.Console +``` + +* After the packages installed, we can define our first workflow. To do this, create a folder named **Workflows** and in this folder create a class named `HelloWorldConsole`. + +```csharp +using Elsa.Activities.Console; +using Elsa.Builders; + +namespace ElsaDemo.Web.Workflows +{ + public class HelloWorldConsole : IWorkflow + { + public void Build(IWorkflowBuilder builder) => builder.WriteLine("Hello World from Elsa!"); + } +} +``` + +* In here we've basically implemented the `IWorkflow` interface which only has one method named **Build**. In this method, we can define our workflow's execution steps (activities). + +* As you can see in the example above, we've used an activity named **WriteLine**, which writes a line of text to the console. Elsa Core has many pre-defined activities like that. E.g **HttpEndpoint** and **WriteHttpResponse** (we will see them both in the next section). + +> "An activity is an atomic building block that represents a single executable step on the workflow." - [Elsa Core Activity Definition](https://elsa-workflows.github.io/elsa-core/docs/next/concepts/concepts-workflows#activity) + +* After defining our workflow, we need to define service registrations which required for the Elsa Core library to work properly. To do that, open your `ElsaDemoWebModule` class and update your `ElsaDemoWebModule` with the following lines. Most of the codes are abbreviated for simplicity. + +```csharp +using ElsaDemo.Web.Workflows; +using Elsa.Services; + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + //... + + ConfigureElsa(context); +} + +private void ConfigureElsa(ServiceConfigurationContext context) +{ + context.Services.AddElsa(options => + { + options + .AddConsoleActivities() + .AddWorkflow(); + }); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + //... + + var workflowRunner = context.ServiceProvider.GetRequiredService(); + workflowRunner.BuildAndStartWorkflowAsync(); +} +``` + +* Here we basically, configured Elsa's services in our `ConfigureServices` method and after that in our `OnApplicationInitialization` method we started the `HelloWorldConsole` workflow. + +* If we run the application and examine the console outputs, we should see the message that we defined in our workflow. + +![hello-world-workflow](./hello-world-workflow.jpg) + +## Creating A Workflow By Using Http Activities + +In this example, we will create a workflow that uses **Http Activities**. It will basically listen the specified route for incoming HTTP Request and writes back a simple response. + +### Add Elsa.Activities.Http Package + +* To be able to use **HTTP Activities** we need to add `Elsa` (we've already added in the previous section) and `Elsa.Activities.Http` packages into our web application. + +```bash +dotnet add package Elsa.Activities.Http +``` + +* After the package installed, we can create our workflow. Let's started with creating a class named `HelloWorldHttp` under **Workflows** folder. + +```csharp +using System.Net; +using Elsa.Activities.Http; +using Elsa.Builders; + +namespace ElsaDemo.Web.Workflows +{ + public class HelloWorldHttp : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .HttpEndpoint("/hello-world") + .WriteHttpResponse(HttpStatusCode.OK, "

Hello World!

", "text/html"); + } + } +} +``` + +* The above workflow has two activities. The first activity `HttpEndpoint` represents an HTTP endpoint, which can be invoked using an HTTP client, including a web browser. The first activity is connected to the second activity `WriteHttpResponse`, which returns a simple response to us. + +* After defined the **HelloWorldHttp** workflow we need to define this class as workflow. So, open your `ElsaDemoWebModule` and update the `ConfigureElsa` method as below. + +```csharp +private void ConfigureElsa(ServiceConfigurationContext context) +{ + context.Services.AddElsa(options => + { + options + .AddConsoleActivities() + .AddHttpActivities() //add this line to be able to use the http activities + .AddWorkflow() + .AddWorkflow(); //workflow that we defined + }); +} +``` +* And add the **UseHttpActivities** middleware to `OnApplicationInitilization` method of your `ElsaDemoWebModule` class. + +```csharp +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + // ... + app.UseAuditing(); + app.UseAbpSerilogEnrichers(); + app.UseHttpActivities(); //add this line + app.UseConfiguredEndpoints(); + + var workflowRunner = context.ServiceProvider.GetRequiredService(); + workflowRunner.BuildAndStartWorkflowAsync(); +} +``` + +* If we run the application and navigate to the "/hello-world" route we should see the response message that we've defined (by using **WriteHttpResponse** activity) in our `HelloWorldHttp` workflow. + +![hello-world-http](./hello-world-http.jpg) + +## Integrate Elsa Dashboard To Application + +* Until now we've created two workflows programmatically. But also we can create workflows visually by using Elsa's **HTML5 Workflow Designer**. + +* Being able to design our workflows easily and taking advantage of **HTML5 Workflow Designer** we will integrate the Elsa Dashboard to our application. + +### Install Packages + +* Following three packages required for Elsa Server. + +```bash +dotnet add package Elsa.Activities.Temporal.Quartz +dotnet add package Elsa.Persistence.EntityFramework.SqlServer +dotnet add package Elsa.Server.Api +``` + +> Also, we need to install the **Elsa** and **Elsa.Activities.Http** packages but we've already installed these packages in the previous sections. + +* We need to install one more package named `Elsa.Designer.Components.Web`. This package provides us the **Elsa Dashboard** component. + +```bash +dotnet add package Elsa.Designer.Components.Web +``` + +* After the package installations completed, we need to make the necessary configurations to be able to use the **Elsa Server** and **Elsa Dashboard**. Therefore, open your `ElsaDemoWebModule` class and make the necessary changes as below. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + + //... + + ConfigureElsa(context, configuration); +} + +private void ConfigureElsa(ServiceConfigurationContext context, IConfiguration configuration) +{ + var elsaSection = configuration.GetSection("Elsa"); + + context.Services.AddElsa(elsa => + { + elsa + .UseEntityFrameworkPersistence(ef => + DbContextOptionsBuilderExtensions.UseSqlServer(ef, + configuration.GetConnectionString("Default"))) + .AddConsoleActivities() + .AddHttpActivities(elsaSection.GetSection("Server").Bind) + .AddQuartzTemporalActivities() + .AddJavaScriptActivities() + .AddWorkflowsFrom(); + }); + + context.Services.AddElsaApiEndpoints(); + context.Services.Configure(options => + { + options.UseApiBehavior = false; + }); + + context.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy + .AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyOrigin() + .WithExposedHeaders("Content-Disposition")) + ); + + //Uncomment the below line if your abp version is lower than v4.4 to register controllers of Elsa . + //See https://github.com/abpframework/abp/pull/9299 (we will no longer need to specify this line of code from v4.4) + // context.Services.AddAssemblyOf(); + + //Disable antiforgery validation for elsa + Configure(options => + { + options.AutoValidateFilter = type => + type.Assembly != typeof(Elsa.Server.Api.Endpoints.WorkflowRegistry.Get).Assembly; + }); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + app.UseCors(); + + //... + + app.UseHttpActivities(); + app.UseConfiguredEndpoints(endpoints => + { + endpoints.MapFallbackToPage("/_Host"); + }); + + var workflowRunner = context.ServiceProvider.GetRequiredService(); + workflowRunner.BuildAndStartWorkflowAsync(); +} +``` + +* These services required for the dashboard. + +* We don't need to register our workflows one by one anymore. Because now we use `.AddWorkflowsFrom()`, and this registers workflows on our behalf. + +* As you may notice here, we use a section named `Elsa` and its sub-sections from the configuration system but we didn't define them yet. To define them open your `appsettings.json` and add the following Elsa section into this file. + +```json +{ + //... + + "Elsa": { + "Http": { + "BaseUrl": "https://localhost:44336" + } + } +} +``` + +#### Define Permission For Elsa Dashboard + +* We can define a [permission](https://docs.abp.io/en/abp/latest/Authorization#permission-system) to be assured of only allowed users can see the Elsa Dashboard. + +* Open your `ElsaDemoPermissions` class under the **Permissions** folder (in the `ElsaDemo.Application.Contracts` layer) and add the following permission name. + +```csharp +namespace ElsaDemo.Permissions +{ + public static class ElsaDemoPermissions + { + public const string GroupName = "ElsaDemo"; + + public const string ElsaDashboard = GroupName + ".ElsaDashboard"; + } +} +``` + +* After that, open your `ElsaDemoPermissionDefinitionProvider` class and define the permission for Elsa Dashboard. + +```csharp +using ElsaDemo.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace ElsaDemo.Permissions +{ + public class ElsaDemoPermissionDefinitionProvider : PermissionDefinitionProvider + { + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup(ElsaDemoPermissions.GroupName); + + myGroup.AddPermission(ElsaDemoPermissions.ElsaDashboard, L("Permission:ElsaDashboard")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } + } +} +``` + +* As you can notice, we've used a localized value (**L("Permission:ElsaDashboard")**) but haven't added this localization key and value to the localization file, so let's add this localization key and value. To do this, open your `en.json` file under **Localization/ElsaDemo** folder (under the **DomainShared** layer) and add this localization key. + +```json +{ + "culture": "en", + "texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", + "Permission:ElsaDashboard": "Elsa Dashboard" + } +} +``` + +#### Add Elsa Dashboard Component To Application + +* After those configurations, now we can add Elsa Dashboard to our application with an authorization check. To do this, create a razor page named **_Host.cshtml** (under **Pages** folder) and update its content as below. + +```html +@page "/elsa" +@using ElsaDemo.Permissions +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(ElsaDemoPermissions.ElsaDashboard)] +@{ + var serverUrl = $"{Request.Scheme}://{Request.Host}"; + Layout = null; +} + + + + + + Elsa Workflows + + + + + + + + + + + + + +``` + +* We've defined an attribute for authorization check here. With this authorization check, only the user who has the **Elsa Dashboard** permission allowed to see this page. + +#### Add Elsa Dashboard Page To Main Menu + +* We can open the `ElsaDemoMenuContributor` class under the **Menus** folder and define the menu item for reaching the Elsa Dashboard easily. + +```csharp +using System.Threading.Tasks; +using ElsaDemo.Localization; +using ElsaDemo.MultiTenancy; +using ElsaDemo.Permissions; +using Volo.Abp.Identity.Web.Navigation; +using Volo.Abp.SettingManagement.Web.Navigation; +using Volo.Abp.TenantManagement.Web.Navigation; +using Volo.Abp.UI.Navigation; + +namespace ElsaDemo.Web.Menus +{ + public class ElsaDemoMenuContributor : IMenuContributor + { + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + var administration = context.Menu.GetAdministration(); + var l = context.GetLocalizer(); + + context.Menu.Items.Insert( + 0, + new ApplicationMenuItem( + ElsaDemoMenus.Home, + l["Menu:Home"], + "~/", + icon: "fas fa-home", + order: 0 + ) + ); + + //add Workflow menu-item + context.Menu.Items.Insert( + 1, + new ApplicationMenuItem( + ElsaDemoMenus.Home, + "Workflow", + "~/elsa", + icon: "fas fa-code-branch", + order: 1, + requiredPermissionName: ElsaDemoPermissions.ElsaDashboard + ) + ); + + //... + } + } +} +``` + +* With that menu item configuration, only the user who has **Elsa Dashboard** permission allowed to see the defined menu item. + +## Result + +* Let's run the application and see how it looks like. + +> If the account you are logged in has the **ElsaDemoPermissions.ElsaDashboard** permission, you should see the **Workflow** menu item. If you do not see this menu item, please be assured that your logged-in account has that permission. + +* Now we can click the "Workflow" menu item, display the Elsa Dashboard and designing workflows. + ![elsa-demo-result](./elsa-demo-result.gif) \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-02-12-Global-Error-Handling-in-Angular/POST.md b/docs/en/Community-Articles/2024-02-12-Global-Error-Handling-in-Angular/POST.md index 2b55a0002c..a0c8319be2 100644 --- a/docs/en/Community-Articles/2024-02-12-Global-Error-Handling-in-Angular/POST.md +++ b/docs/en/Community-Articles/2024-02-12-Global-Error-Handling-in-Angular/POST.md @@ -1,266 +1,266 @@ - -# Global Error Handling in Angular - -![Error Handling](error_handling.jpg) - -Error handling is how we deal with errors that go wrong when we are running a program. There is no code that runs perfectly forever :) Things can go wrong and your application might crash. **So, in order to run your program smoothly you must handle errors.** It is just not for keeping your application in a running state. It is also useful to show messages about the error to the client. Like what went wrong, why it is not allowed to access this page etc. - -### How to handle errors - -- First of all, you have to catch them 😀. You can catch them via **try-catch block**. See an example; -#### Create an basic error -```ts -({} as any).doSomething() -``` - -![Error Image](error-image-1.png) - -#### Handle it -```ts -try { - ({} as any).doSomething(); -} catch (error) { - this.toastService.showError(error.message); -} -``` - -![toast-gif](show-toast-2.gif) - -- See, we catch the error and handle it. -- In this case, we know where the error will be thrown. Most of the time we won't know where the error will appear. Should we cover the entire application with try-catch blocks? Of course not 😀 -- **We are going to handle errors globally**. Angular provides a great way to do it. Let's do it step by step; - -### 1.Create a **service** and implement the **`ErrorHandler`** interface. - -```ts -import { ErrorHandler, Injectable, inject } from '@angular/core'; -import { ToastService } from './toast.service'; - -@Injectable({ - providedIn: 'root' -}) -export class CustomErrorHandlerService implements ErrorHandler { - toastService = inject(ToastService); - - //This method comes from interface - handleError(error: any): void { - this.toastService.showError(error.message); - } -} - -``` - -### 2.Provide the service by using the **`ErrorHandler`** class from **`@angular/core`**. - -```ts -import { ErrorHandler } from '@angular/core'; - -providers: [ - { provide: ErrorHandler, useExisting: CustomErrorHandlerService } -] - -``` - -![toast-gif](show-toast-2.gif) - -- It behaves exactly the same. Nice, now we catch the entire errors in one simple service. -- Is it that simple? I wish it is but it's not 😀. This handling mechanism only works synchronously. When we start making http requests, our **`CustomErrorHandlerService`** won't catch the errors. - -## How to handle HTTP Requests -Make an HTTP request and check if it's working. - -![http-request](http-request-4.gif) - -As you can see it doesn’t work. So how can we catch the http errors? with **`catchError()`** operator in rxjs or **`observer object`**. I will go with **`catchError()`** operator. - -```ts -getTodo(id: number) { - this.http - .get(`https://jsonplaceholder.typicode.com/todos/${id}`) - .pipe(catchError((err) => { - this.toastService.showError(err.message); - return EMPTY; - }) - ) - .subscribe(todo => this.todo = todo); -} - -``` - -![http-request](http-request-5.gif) - -- So are we going to add this **`catchError()`** operator to the entire http requests? **NO, we will use HTTP Interceptors!** -- Let's do it step by step. - -### 1.Remove catchError pipe - -```ts -getTodo(id: number) { - this.http.get('https://jsonplaceholder.typicode.com/todos/${id}').subscribe(todo => this.todo = todo); -} -``` - -### 2.Create an HTTP Interceptor - -```ts -import { Injectable, inject } from '@angular/core'; -import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { EMPTY, catchError } from 'rxjs'; -import { ToastService } from './toast.service'; - -@Injectable({ - providedIn: 'root' -}) -export class ToastInterceptor implements HttpInterceptor { - toastService = inject(ToastService); - - intercept(req: HttpRequest, next: HttpHandler) { - return next.handle(req).pipe(catchError((error) => { - this.toastService.showError(error.message); - return EMPTY; - })); - } -} -``` - -### 3.Provide the interceptor - -```ts -import { HTTP_INTERCEPTORS} from '@angular/common/http'; - -providers: [ - { provide: HTTP_INTERCEPTORS, useExisting: ToastInterceptor, multi: true } -] -``` -Now everything has set up. Let's make an HTTP request and try again. - -![http-request](http-request-5.gif) - -- So, now we are handling http errors globally. Whenever an error occurs, it will be catched from interceptor and will be showed via toast message. -- But this method has a one little disadvantage. What if we dont want to show toast message for a spesific case? [related issue about this problem](https://github.com/angular/angular/issues/18155). -- **[ABP Framework](https://abp.io/) has great solution for this problem. Let's understand the solution and apply it straightforwardly.** - - Create a singleton service called **`HttpErrorReporterService`.** This service is going to store **HttpError** in a subject, and share the httpError as an observable for subscribers. - - Create a new service called **`RestService`** which is a layer on top of **`HttpClient`**, this new service is able to get a skipHandleError parameter. If skipHandleError value is **false** then it will be reported to the **`HttpErrorReporterService`** otherwise error will be throwed. - - Create a service called **`ErrorHandler`**, This service is going to subscribe to observable in **`HttpErrorReporterService`** and handle the errors (in our case we will show toast message). -- When i first see this solution, i loved it. For more information and detail you can check the source code from the links below; - - [**Http Error Reporter Service**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts#L6) - - [**Rest Service**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/core/src/lib/services/rest.service.ts#L4) - - [**Error Handler**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts#L19) - -You can copy the source codes from ABP or use ABP directly 😀 -Lets simulate the solution in our application, this simulation is not suitable for your application it just for demonstration. - -**Rest Service** - -```ts -import { HttpClient, HttpRequest } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; -import { HttpErrorReporterService } from './http-error-reporter.service'; -import { catchError, throwError } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class RestService { - http = inject(HttpClient); - httpErrorReporterService = inject(HttpErrorReporterService); - - request(req: HttpRequest | { method: string, url: string; }, skipHandleError = false) { - const { method, url } = req; - return this.http.request(method, url).pipe(catchError((err) => { - if (!skipHandleError) { - this.httpErrorReporterService.reportError(err); - } - return throwError(() => err); - })); - } -} - -``` - -**HttpErrorReporterService** - -```ts -import { HttpErrorResponse } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class HttpErrorReporterService { - private _error$ = new Subject(); - - get error$() { - return this._error$.asObservable(); - } - - reportError(error: HttpErrorResponse) { - this._error$.next(error); - } -} - -``` - -**ErrorHandler** - -```ts -import { Injectable, inject } from '@angular/core'; -import { HttpErrorReporterService } from './http-error-reporter.service'; -import { ToastService } from './toast.service'; - -@Injectable({ - providedIn: 'root' -}) -export class ErrorHandlerService { - httpErrorReporterService = inject(HttpErrorReporterService); - toastMessageService = inject(ToastService); - - constructor(){ - this.httpErrorReporterService.error$.subscribe((error) => { - this.toastMessageService.showError(error.message); - }); - } -} - -``` - -Now lets make an http request to check is it working - -```ts -restService = inject(RestService); - -getTodo() { - this.restService.request( - { method: 'GET', url: 'https://jsonplaceholder.typicode.com/todos/1111' }, - ).subscribe(todo => { - this.todo = todo; - }); -} -``` - -![http-request](http-request-7.gif) - -Now let's pass **true to the `skipHandleError` parameter**, let's see if the **errorHandler** going to skip this error or not. - -```ts -restService = inject(RestService); - -getTodo() { - this.restService.request( - { method: 'GET', url: '' }, - skipHandleError: true, - ).subscribe(todo => { - this.todo = todo; - }); -} - -``` - -![http-request](http-request-8.gif) - -### Conclusion -- To handle synchronous errors globally, you can use [Error Handler](https://angular.io/api/core/ErrorHandler). -- To handle http errors globally, you can use [HTTP - interceptor](https://angular.io/guide/http-interceptor-use-cases). But in this method, you won't be able to skip specific cases. I recommend you use the ABP Framework's solution. - -Thanks for reading, if you have any advice please share it with me in the comments. + +# Global Error Handling in Angular + +![Error Handling](error_handling.jpg) + +Error handling is how we deal with errors that go wrong when we are running a program. There is no code that runs perfectly forever :) Things can go wrong and your application might crash. **So, in order to run your program smoothly you must handle errors.** It is just not for keeping your application in a running state. It is also useful to show messages about the error to the client. Like what went wrong, why it is not allowed to access this page etc. + +### How to handle errors + +- First of all, you have to catch them 😀. You can catch them via **try-catch block**. See an example; +#### Create an basic error +```ts +({} as any).doSomething() +``` + +![Error Image](error-image-1.png) + +#### Handle it +```ts +try { + ({} as any).doSomething(); +} catch (error) { + this.toastService.showError(error.message); +} +``` + +![toast-gif](show-toast-2.gif) + +- See, we catch the error and handle it. +- In this case, we know where the error will be thrown. Most of the time we won't know where the error will appear. Should we cover the entire application with try-catch blocks? Of course not 😀 +- **We are going to handle errors globally**. Angular provides a great way to do it. Let's do it step by step; + +### 1.Create a **service** and implement the **`ErrorHandler`** interface. + +```ts +import { ErrorHandler, Injectable, inject } from '@angular/core'; +import { ToastService } from './toast.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CustomErrorHandlerService implements ErrorHandler { + toastService = inject(ToastService); + + //This method comes from interface + handleError(error: any): void { + this.toastService.showError(error.message); + } +} + +``` + +### 2.Provide the service by using the **`ErrorHandler`** class from **`@angular/core`**. + +```ts +import { ErrorHandler } from '@angular/core'; + +providers: [ + { provide: ErrorHandler, useExisting: CustomErrorHandlerService } +] + +``` + +![toast-gif](show-toast-2.gif) + +- It behaves exactly the same. Nice, now we catch the entire errors in one simple service. +- Is it that simple? I wish it is but it's not 😀. This handling mechanism only works synchronously. When we start making http requests, our **`CustomErrorHandlerService`** won't catch the errors. + +## How to handle HTTP Requests +Make an HTTP request and check if it's working. + +![http-request](http-request-4.gif) + +As you can see it doesn’t work. So how can we catch the http errors? with **`catchError()`** operator in rxjs or **`observer object`**. I will go with **`catchError()`** operator. + +```ts +getTodo(id: number) { + this.http + .get(`https://jsonplaceholder.typicode.com/todos/${id}`) + .pipe(catchError((err) => { + this.toastService.showError(err.message); + return EMPTY; + }) + ) + .subscribe(todo => this.todo = todo); +} + +``` + +![http-request](http-request-5.gif) + +- So are we going to add this **`catchError()`** operator to the entire http requests? **NO, we will use HTTP Interceptors!** +- Let's do it step by step. + +### 1.Remove catchError pipe + +```ts +getTodo(id: number) { + this.http.get('https://jsonplaceholder.typicode.com/todos/${id}').subscribe(todo => this.todo = todo); +} +``` + +### 2.Create an HTTP Interceptor + +```ts +import { Injectable, inject } from '@angular/core'; +import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { EMPTY, catchError } from 'rxjs'; +import { ToastService } from './toast.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ToastInterceptor implements HttpInterceptor { + toastService = inject(ToastService); + + intercept(req: HttpRequest, next: HttpHandler) { + return next.handle(req).pipe(catchError((error) => { + this.toastService.showError(error.message); + return EMPTY; + })); + } +} +``` + +### 3.Provide the interceptor + +```ts +import { HTTP_INTERCEPTORS} from '@angular/common/http'; + +providers: [ + { provide: HTTP_INTERCEPTORS, useExisting: ToastInterceptor, multi: true } +] +``` +Now everything has set up. Let's make an HTTP request and try again. + +![http-request](http-request-5.gif) + +- So, now we are handling http errors globally. Whenever an error occurs, it will be catched from interceptor and will be showed via toast message. +- But this method has a one little disadvantage. What if we dont want to show toast message for a spesific case? [related issue about this problem](https://github.com/angular/angular/issues/18155). +- **[ABP Framework](https://abp.io/) has great solution for this problem. Let's understand the solution and apply it straightforwardly.** + - Create a singleton service called **`HttpErrorReporterService`.** This service is going to store **HttpError** in a subject, and share the httpError as an observable for subscribers. + - Create a new service called **`RestService`** which is a layer on top of **`HttpClient`**, this new service is able to get a skipHandleError parameter. If skipHandleError value is **false** then it will be reported to the **`HttpErrorReporterService`** otherwise error will be throwed. + - Create a service called **`ErrorHandler`**, This service is going to subscribe to observable in **`HttpErrorReporterService`** and handle the errors (in our case we will show toast message). +- When i first see this solution, i loved it. For more information and detail you can check the source code from the links below; + - [**Http Error Reporter Service**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts#L6) + - [**Rest Service**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/core/src/lib/services/rest.service.ts#L4) + - [**Error Handler**](https://github.com/abpframework/abp/blob/360a3395aa0e44fb77574ea7eac745e8ba94b82e/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts#L19) + +You can copy the source codes from ABP or use ABP directly 😀 +Lets simulate the solution in our application, this simulation is not suitable for your application it just for demonstration. + +**Rest Service** + +```ts +import { HttpClient, HttpRequest } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { HttpErrorReporterService } from './http-error-reporter.service'; +import { catchError, throwError } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class RestService { + http = inject(HttpClient); + httpErrorReporterService = inject(HttpErrorReporterService); + + request(req: HttpRequest | { method: string, url: string; }, skipHandleError = false) { + const { method, url } = req; + return this.http.request(method, url).pipe(catchError((err) => { + if (!skipHandleError) { + this.httpErrorReporterService.reportError(err); + } + return throwError(() => err); + })); + } +} + +``` + +**HttpErrorReporterService** + +```ts +import { HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class HttpErrorReporterService { + private _error$ = new Subject(); + + get error$() { + return this._error$.asObservable(); + } + + reportError(error: HttpErrorResponse) { + this._error$.next(error); + } +} + +``` + +**ErrorHandler** + +```ts +import { Injectable, inject } from '@angular/core'; +import { HttpErrorReporterService } from './http-error-reporter.service'; +import { ToastService } from './toast.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ErrorHandlerService { + httpErrorReporterService = inject(HttpErrorReporterService); + toastMessageService = inject(ToastService); + + constructor(){ + this.httpErrorReporterService.error$.subscribe((error) => { + this.toastMessageService.showError(error.message); + }); + } +} + +``` + +Now lets make an http request to check is it working + +```ts +restService = inject(RestService); + +getTodo() { + this.restService.request( + { method: 'GET', url: 'https://jsonplaceholder.typicode.com/todos/1111' }, + ).subscribe(todo => { + this.todo = todo; + }); +} +``` + +![http-request](http-request-7.gif) + +Now let's pass **true to the `skipHandleError` parameter**, let's see if the **errorHandler** going to skip this error or not. + +```ts +restService = inject(RestService); + +getTodo() { + this.restService.request( + { method: 'GET', url: '' }, + skipHandleError: true, + ).subscribe(todo => { + this.todo = todo; + }); +} + +``` + +![http-request](http-request-8.gif) + +### Conclusion +- To handle synchronous errors globally, you can use [Error Handler](https://angular.io/api/core/ErrorHandler). +- To handle http errors globally, you can use [HTTP - interceptor](https://angular.io/guide/http-interceptor-use-cases). But in this method, you won't be able to skip specific cases. I recommend you use the ABP Framework's solution. + +Thanks for reading, if you have any advice please share it with me in the comments. diff --git a/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/images/blob-storage.png b/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/images/blob-storage.png new file mode 100644 index 0000000000..05aa32eaad Binary files /dev/null and b/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/images/blob-storage.png differ diff --git a/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/post.md b/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/post.md new file mode 100644 index 0000000000..e76f7f2728 --- /dev/null +++ b/docs/en/Community-Articles/2024-04-19-using-blob-storage-with-abp/post.md @@ -0,0 +1,114 @@ +# Using Blob Storage with ABP +ABP Framework provides a comprehensive solution to meet the needs of modern application development, while addressing the important requirement of BLOB Storing. ABP Framework [provides an easy integration for Blob Storing](https://docs.abp.io/en/abp/latest/Blob-Storing) and offers many storage services that you can easily integrate. In addition to efficiently storing large files, these services offer significant advantages such as scalability, security and backup. + +## What is Blob Storage ? + +Blob Storage is a service for storing unstructured data. It is becoming increasingly important to efficiently store and manage large data types (e.g. images, videos, documents). Blob Storage was developed to meet these needs and offers a secure solution with the advantages of scalability, durability and low cost. + + ![Blob Stroge](./images/blob-storage.png) + + +## How to use Blob Storage ? + +I would like to explain this to you with an example.For example, storing large files such as user profile pictures in the database negatively affects the performance and database.You can store this data using Blob storage. One of the advantages of storing user profile pictures in blob storage is that it improves database performance. Blob storage is a more efficient option than storing large size files in the database and allows database queries to run faster. Furthermore, blob storage provides scalability, so that the number of profile pictures can grow with the number of users, but without storage issues. This approach also maintains database normalization and makes the database design cleaner. + + How do we store user profile pictures with Blob Storage using ABP Framework? + +- #### Step 1: Configure the Blob Container + +Define a Blob Container named `profile-pictures` using the `[BlobContainerName("profile-pictures")]` attribute. + +````csharp +[BlobContainerName("profile-pictures")] +public class ProfilePictureContainer +{ + +} +```` +- #### Step 2: Create the ProfileAppService (Saving & Reading BLOBs) + +Create the `ProfileAppService` class and derive it from the `ApplicationService` class. This class will perform the necessary operations to store and retrieve profile pictures. + +````csharp +using Volo.Abp.Application.Services; + +public class ProfileAppService : ApplicationService +{ + // Code snippets will come here +} +```` + +- #### Step 3: Inject the `IBlobContainer` Service + +Inject the `IBlobContainer` service, in the constructor of the `ProfileAppService` class. The `IBlobContainer` is the main interface to store and read BLOB and is used to interact with the container. + +````csharp +private readonly IBlobContainer _blobContainer; + +public ProfileAppService(IBlobContainer blobContainer) +{ + _blobContainer = blobContainer; +} +```` + +- #### Step 4: Save Profile Picture + +The SaveProfilePictureAsync method is used to store the user's profile picture. A unique name is generated based on the user's credentials and the profile picture byte array with this name is saved in the Blob Container. + +````csharp +public async Task SaveProfilePictureAsync(byte[] bytes) +{ + var blobName = CurrentUser.GetId().ToString(); + await _blobContainer.SaveAsync(blobName, bytes); +} +```` + +- #### Step 5: Getting Profile Picture + +The GetProfilePictureAsync method is used to get the user's profile picture. A profile picture byte array is retrieved from the Blob Container with a specified name based on the user's credential. + +````csharp +public async Task GetProfilePictureAsync() +{ + var blobName = CurrentUser.GetId().ToString(); + return await _blobContainer.GetAllBytesOrNullAsync(blobName); +} +```` + + +Finally, add controls in the user interface that will allow users to upload and view their profile pictures. These controls will perform the operations by calling the corresponding methods in the ProfileAppService class. + +These steps cover the basic steps to store user profile pictures with Blob Storage using the ABP Framework. [Check out the documentation for more information.](https://docs.abp.io/en/abp/latest/Blob-Storing) + + +## What are the Advantages/Disadvantages of Keeping the BLOBs in a Database? + +#### Advantages: + +- Data Integrity and Relational Model: To ensure data integrity and preserve the relational model, it is important to store blob data in the database. This approach preserves the relationships between data and maintains the structural integrity of the database. + +- A Single Storage Location: Storing blob data in the database allows you to collect all data in a single storage location. This simplifies the management of data and increases data integrity. + +- Advanced Security Controls: Database systems often offer advanced security controls. Storing blob data in a database allows you to take advantage of these security features and ensures that data is accessed by authorized users. + +#### Disadvantages: + +- Performance Issues: Storing blob data in a database can negatively impact database performance. Oversized blob data can slow down query processing and reduce database performance. + +- Storage Space Issue: Storing blob data in the database can increase the size of the database and require more storage space. This can increase storage costs and complicate infrastructure requirements. + +- Backup and Recovery Challenges: Storing blob data in a database can make backup and recovery difficult. The large size of blob data can make backup and recovery time-consuming and data recovery difficult. + + +## Other Blob Storage Providers + +ABP Framework provides developers with a variety of options and flexibility by offering integration infrastructure for multiple cloud providers. This makes it easy for users to choose between different cloud platforms and select the most suitable solution for their business needs. + + +- Azure Blob Storage: A cloud storage service offered on the Microsoft Azure platform. It is used to store and access large amounts of data. It supports various data types such as files, images, videos and provides high scalability. ABP Framework provides integration with [Azure Blob Storage](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure). + +- Aliyun Object Storage Service (OSS): OSS, Alibaba Cloud's cloud storage service, is an ideal solution for use cases such as big data storage, backup and media storage. It offers flexible storage options and provides a high level of security. ABP Framework interfaces with [Aliyun Blob Storage](https://docs.abp.io/en/abp/latest/Blob-Storing-Aliyun), making it easier for developers to manage data storage and access. + +- MinIO: MinIO is known as an open source object storage system and offers an Amazon S3 compatible cloud storage solution. It is a high-performance, scalable and fast storage service. ABP Framework integrates with [MinIO Blob Storage](https://docs.abp.io/en/abp/latest/Blob-Storing-Minio) to provide developers with cloud-based file and object storage. + +- Amazon Simple Storage Service (S3): Amazon S3 is a cloud storage service offered on the Amazon Web Services (AWS) platform. It can be used to store virtually unlimited amounts of data. It provides high durability, scalability and low cost.ABP Framework integrates with [Amazon S3 Blob Storage](https://docs.abp.io/en/abp/latest/Blob-Storing-Aws) to provide developers with cloud-based file and object storage. diff --git a/docs/en/Community-Articles/2024-05-01-How-ABP-get-current-user/POST.md b/docs/en/Community-Articles/2024-05-01-How-ABP-get-current-user/POST.md new file mode 100644 index 0000000000..5e32e184e1 --- /dev/null +++ b/docs/en/Community-Articles/2024-05-01-How-ABP-get-current-user/POST.md @@ -0,0 +1,201 @@ +# How claim type works in ASP NET Core and ABP Framework + +## The Claim Type + +A web application may use one or more authentication schemes to obtain the current user's information, such as `Cookies`, `JwtBearer`, `OpenID Connect`, `Google` etc. + +After authentication, we get a set of claims that can be issued using a trusted identity provider. A claim is a type/name-value pair representing the subject. The type property provides the semantic content of the claim, that is, it states what the claim is about. + +The [`ICurrentUser`](https://docs.abp.io/en/abp/latest/CurrentUser) service of the ABP Framework provides a convenient way to access the current user's information from the claims. + +The claim type is the key to getting the correct value of the current user, and we have a static `AbpClaimTypes` class that defines the names of the standard claims in the ABP Framework: + +```cs +public static class AbpClaimTypes +{ + public static string UserId { get; set; } = ClaimTypes.NameIdentifier; + public static string UserName { get; set; } = ClaimTypes.Name; + public static string Role { get; set; } = ClaimTypes.Role; + public static string Email { get; set; } = ClaimTypes.Email; + //... +} +``` + +As you can see, the default claim type of `AbpClaimTypes` comes from the [`System.Security.Claims.ClaimTypes`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) class, which is the recommended practice in NET. + +## Claim type in different authentication schemes + +We usually see two types of claim types in our daily development. One of them is the [`System.Security.Claims.ClaimTypes`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) and the other one is the `OpenId Connect` [standard claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). + +### ASP NET Core Identity + +There is a [`ClaimsIdentityOptions`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions) property in the `IdentityOptions`, which can be used to configure the claim type: + +| Property | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------| +| EmailClaimType | Gets or sets the ClaimType used for the user email claim. Defaults to Email. | +| RoleClaimType | Gets or sets the ClaimType used for a Role claim. Defaults to Role. | +| SecurityStampClaimType | Gets or sets the ClaimType used for the security stamp claim. Defaults to "AspNet.Identity.SecurityStamp". | +| UserIdClaimType | Gets or sets the ClaimType used for the user identifier claim. Defaults to NameIdentifier. | +| UserNameClaimType | Gets or sets the ClaimType used for the user name claim. Defaults to Name. | + +* The Identity creates a `ClaimsIdentity` object with the claim type that you have configured in the `ClaimsIdentityOptions` class. +* The ABP Framework configures it based on `AbpClaimTypes,` so usually you don't need to worry about it. + +### JwtBearer/OpenID Connect Client + +The `JwtBearer/OpenID Connect` gets claims from `id_token` or fetches user information from the `AuthServer`, and then maps/adds it to the current `ClaimsIdentity`. + +To map the [standard claim](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) type to the [`System.Security.Claims.ClaimTypes`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) via [azure-activedirectory-identitymodel-extensions-for-dotnet](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) library by default, which is maintained by the Microsoft team: + +```cs +Dictionary ClaimTypeMapping = new Dictionary +{ + { "actort", ClaimTypes.Actor }, + { "birthdate", ClaimTypes.DateOfBirth }, + { "email", ClaimTypes.Email }, + { "family_name", ClaimTypes.Surname }, + { "gender", ClaimTypes.Gender }, + { "given_name", ClaimTypes.GivenName }, + { "nameid", ClaimTypes.NameIdentifier }, + { "sub", ClaimTypes.NameIdentifier }, + { "website", ClaimTypes.Webpage }, + { "unique_name", ClaimTypes.Name }, + { "oid", "http://schemas.microsoft.com/identity/claims/objectidentifier" }, + { "scp", "http://schemas.microsoft.com/identity/claims/scope" }, + { "tid", "http://schemas.microsoft.com/identity/claims/tenantid" }, + { "acr", "http://schemas.microsoft.com/claims/authnclassreference" }, + { "adfs1email", "http://schemas.xmlsoap.org/claims/EmailAddress" }, + { "adfs1upn", "http://schemas.xmlsoap.org/claims/UPN" }, + { "amr", "http://schemas.microsoft.com/claims/authnmethodsreferences" }, + { "authmethod", ClaimTypes.AuthenticationMethod }, + { "certapppolicy", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/applicationpolicy" }, + { "certauthoritykeyidentifier", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/authoritykeyidentifier" }, + { "certbasicconstraints", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/basicconstraints" }, + { "certeku", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/eku" }, + { "certissuer", "http://schemas.microsoft.com/2012/12/certificatecontext/field/issuer" }, + { "certissuername", "http://schemas.microsoft.com/2012/12/certificatecontext/field/issuername" }, + { "certkeyusage", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/keyusage" }, + { "certnotafter", "http://schemas.microsoft.com/2012/12/certificatecontext/field/notafter" }, + { "certnotbefore", "http://schemas.microsoft.com/2012/12/certificatecontext/field/notbefore" }, + { "certpolicy", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatepolicy" }, + { "certpublickey", ClaimTypes.Rsa }, + { "certrawdata", "http://schemas.microsoft.com/2012/12/certificatecontext/field/rawdata" }, + { "certserialnumber", ClaimTypes.SerialNumber }, + { "certsignaturealgorithm", "http://schemas.microsoft.com/2012/12/certificatecontext/field/signaturealgorithm" }, + { "certsubject", "http://schemas.microsoft.com/2012/12/certificatecontext/field/subject" }, + { "certsubjectaltname", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/san" }, + { "certsubjectkeyidentifier", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/subjectkeyidentifier" }, + { "certsubjectname", "http://schemas.microsoft.com/2012/12/certificatecontext/field/subjectname" }, + { "certtemplateinformation", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatetemplateinformation" }, + { "certtemplatename", "http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatetemplatename" }, + { "certthumbprint", ClaimTypes.Thumbprint }, + { "certx509version", "http://schemas.microsoft.com/2012/12/certificatecontext/field/x509version" }, + { "clientapplication", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-application" }, + { "clientip", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-ip" }, + { "clientuseragent", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-user-agent" }, + { "commonname", "http://schemas.xmlsoap.org/claims/CommonName" }, + { "denyonlyprimarygroupsid", ClaimTypes.DenyOnlyPrimaryGroupSid }, + { "denyonlyprimarysid", ClaimTypes.DenyOnlyPrimarySid }, + { "denyonlysid", ClaimTypes.DenyOnlySid }, + { "devicedispname", "http://schemas.microsoft.com/2012/01/devicecontext/claims/displayname" }, + { "deviceid", "http://schemas.microsoft.com/2012/01/devicecontext/claims/identifier" }, + { "deviceismanaged", "http://schemas.microsoft.com/2012/01/devicecontext/claims/ismanaged" }, + { "deviceostype", "http://schemas.microsoft.com/2012/01/devicecontext/claims/ostype" }, + { "deviceosver", "http://schemas.microsoft.com/2012/01/devicecontext/claims/osversion" }, + { "deviceowner", "http://schemas.microsoft.com/2012/01/devicecontext/claims/userowner" }, + { "deviceregid", "http://schemas.microsoft.com/2012/01/devicecontext/claims/registrationid" }, + { "endpointpath", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-endpoint-absolute-path" }, + { "forwardedclientip", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip" }, + { "group", "http://schemas.xmlsoap.org/claims/Group" }, + { "groupsid", ClaimTypes.GroupSid }, + { "idp", "http://schemas.microsoft.com/identity/claims/identityprovider" }, + { "insidecorporatenetwork", "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork" }, + { "isregistereduser", "http://schemas.microsoft.com/2012/01/devicecontext/claims/isregistereduser" }, + { "ppid", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier" }, + { "primarygroupsid", ClaimTypes.PrimaryGroupSid }, + { "primarysid", ClaimTypes.PrimarySid }, + { "proxy", "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-proxy" }, + { "pwdchgurl", "http://schemas.microsoft.com/ws/2012/01/passwordchangeurl" }, + { "pwdexpdays", "http://schemas.microsoft.com/ws/2012/01/passwordexpirationdays" }, + { "pwdexptime", "http://schemas.microsoft.com/ws/2012/01/passwordexpirationtime" }, + { "relyingpartytrustid", "http://schemas.microsoft.com/2012/01/requestcontext/claims/relyingpartytrustid" }, + { "role", ClaimTypes.Role }, + { "roles", ClaimTypes.Role }, + { "upn", ClaimTypes.Upn }, + { "winaccountname", ClaimTypes.WindowsAccountName }, +}; +``` + +#### Disable JwtBearer/OpenID Connect Client Claim Type Mapping + +To turn off the claim type mapping, you can set the `MapInboundClaims` property of `JwtBearerOptions` or `OpenIdConnectOptions` to `false`. Then, you can get the original claim types from the token(`access_token` or `id_token`): + +JWT Example: + +```json +{ + "iss": "https://localhost:44305/", + "exp": 1714466127, + "iat": 1714466127, + "aud": "MyProjectName", + "scope": "MyProjectName offline_access", + "sub": "ed7f5cfd-7311-0402-245c-3a123ff787f9", + "unique_name": "admin", + "preferred_username": "admin", + "given_name": "admin", + "role": "admin", + "email": "admin@abp.io", + "email_verified": "False", + "phone_number_verified": "False", +} +``` + +### OAuth2(Google, Facebook, Twitter, Microsoft) Extenal Login Client + +The `OAuth2 handler` fetchs a JSON containing user information from the `OAuth2` server. The third-party provider issues the claim type based on their standard server and then maps/adds it to the current `ClaimsIdentity`. The ASP NET Core provides some built-in claim-type mappings for different providers as can be seen below examples: + +**Example**: The `ClaimActions` property of the `GoogleOptions` maps the Google's claim types to [`System.Security.Claims.ClaimTypes`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes): + +```cs +ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); // v2 +ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); // v3 +ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); +ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name"); +ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name"); +ClaimActions.MapJsonKey("urn:google:profile", "link"); +ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); +``` + +**Example**: The `ClaimActions` property of the `FacebookOptions` maps the Facebook's claim types to [`System.Security.Claims.ClaimTypes`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes): + +```cs +ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); +ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min"); +ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max"); +ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); +ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); +ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); +ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name"); +ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name"); +ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name"); +ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender"); +ClaimActions.MapJsonKey("urn:facebook:link", "link"); +ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name"); +ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale"); +ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone"); +``` + +### OpenIddict AuthServer + +The `OpenIddict` uses the [standard claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) as the claim type of the `id_token` or `access_token` and `UserInfo` endpoint response, etc. + +* For JWT token, it also uses the [azure-activedirectory-identitymodel-extensions-for-dotnet](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) to get the claims from the `id_token` or `access_token`. +* For reference token, it gets the claims from the `database`. + +## Summary + +Once you find the claims you received do not meet your expectations, follow the instructions above to troubleshoot the problem. + +This article can help you understand the claim type in the ABP Framework and ASP NET Core. + diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/POST.md b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/POST.md new file mode 100644 index 0000000000..4234062de6 --- /dev/null +++ b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/POST.md @@ -0,0 +1,86 @@ +# Deploy Your ABP Framework MVC Project to Azure Container Apps + +![](azure-container-abp.png) + +In this article, we will show the seamless deployment of an ABP Framework MVC project to Azure Container Apps. enabling you to deploy and run containerized applications without the hassle of managing the infrastructure underneath. It provides an uncomplicated and cost-effective method for deploying and scaling your applications. + +### Getting Started with ABP Framework MVC and Azure Container Apps + +To get started, you will need an ABP Framework MVC project that you want to deploy. If you don't have one, you can [create a new project using the ABP CLI](https://docs.abp.io/en/abp/latest/Startup-Templates/Application). You will also need [an Azure subscription](https://azure.microsoft.com) and [an Azure SQL database](https://azure.microsoft.com/en-gb/products/azure-sql). + +Before creating Azure container apps resources and deploying the ABP Framework MVC project, I show you how you can effortlessly create Docker images and push them to Docker Hub, leveraging the pre-configured Docker file and scripts that come with the ABP MVC framework. + +### Creating a Docker Image for ABP Framework MVC + +To create a Docker image for your ABP Framework MVC project, navigate to `etc/build/build-images-locally.ps1` and fix the script to match your Docker Hub username and image name. Then, run the script to build the Docker image locally. + +![Build Docker Image](build-docker-image.png) + +Next, check the Docker Hub repository to confirm that the image has been pushed successfully. + +![Docker Hub Repository](docker-hub-repository.png) + +### Deploying to Azure Container Apps + +Now that you have Docker images for your ABP Framework MVC project, you can proceed to deploy it to Azure Container Apps. To do this, navigate to the Azure portal and create a new Azure Container Apps resource. Ypu will not need just an Azure Container Apps resource, but also an Azure Container Apps Job resource to migrate the database schema and seed data for your ABP Framework MVC project. + +![Create Azure Container Apps](create-azure-container-apps.png) + +#### Step 1: Deploy the Docker Image + +Firstly, create a new Azure Container Apps resource without environment variables. You will need web url so that you can set it as an environment variable in the next step. Then, check the deployment status to confirm that the deployment was successful. + +![Deploy Docker Image](deploy-docker-image.png) + +#### Step 2: Migrate Database Schema and Seed Data + +Secondly, create a new Azure Container Apps Job resource to migrate the database schema and seed data. You can do this by creating a new job with the following environment variables: + +```text +ConnectionStrings__Default - Server=tcp:demoabpapp.database.windows.net,1433;Initial Catalog=mvcapppro;Persist Security Info=False;User ID=demoapppro;Password={your_password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; + +OpenIddict__Applications__mvcapppro_Web__RootUrl - https://mvcwebapp.victoriousgrass-8b06438d.northeurope.azurecontainerapps.io +``` + +To get ConnectionStrings of Sql Database and url of the web app, you can navigate to the Azure portal and check the properties of the Azure SQL database and Azure Container Apps resource. + + +![Azure SQL Database Connection Strings](azure-sql-database-connection-strings.png) + +![Create Azure Container Apps Job](create-azure-container-apps-job.png) + +Finally, check the job status to confirm that the database migration and seeding were successful. You can connect to the Azure SQL database to verify that the schema and seed data have been applied. + +![Check Job Status](check-job-status.png) + +#### Step 3: Edit the Azure Container Apps Resource + +After completing these steps, you have to edit the Azure Container Apps resource to add the required environment variables for your ABP Framework MVC project. You can do this by adding the following environment variables: + +```text +App__SelfUrl - https://mvcwebapp.victoriousgrass-8b06438d.northeurope.azurecontainerapps.io + +ASPNETCORE_URLS - http://+:80 + +AuthServer__Authority - https://mvcwebapp.victoriousgrass-8b06438d.northeurope.azurecontainerapps.io + +ConnectionStrings__Default - Server=tcp:demoabpapp.database.windows.net,1433;Initial Catalog=mvcapppro;Persist Security Info=False;User ID=demoapppro;Password={your_password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; +``` + +![Add Environment Variables](add-environment-variables.png) + +#### Step 4: Create a New Deployment + +Once you have added the environment variables, save and create a new deployment to apply the changes. You can now access your ABP Framework MVC project running on Azure Container Apps by navigating to the URL provided in the environment variables. + +![Access ABP Framework MVC Project](access-abp-framework-mvc-project.png) + +You can see the Azure resources created for the ABP Framework MVC project deployment that includes the Azure Container Apps resource, Azure Container Apps Job resource, and Azure SQL database. + +![Azure Resources](azure-resources.png) + +### Conclusion + +Azure Container Apps provides a simple and cost-effective way to deploy and scale your ABP Framework MVC projects without managing the underlying infrastructure. By following the steps outlined in this article, you can seamlessly deploy your ABP Framework MVC projects to Azure Container Apps and enjoy the benefits it offers. + +I hope you found this article helpful. If you have any questions or feedback, please feel free to leave a comment below. diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/access-abp-framework-mvc-project.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/access-abp-framework-mvc-project.png new file mode 100644 index 0000000000..ff0aa2f811 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/access-abp-framework-mvc-project.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/add-environment-variables.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/add-environment-variables.png new file mode 100644 index 0000000000..56f9df7fa4 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/add-environment-variables.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-container-abp.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-container-abp.png new file mode 100644 index 0000000000..ec96dd0b2e Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-container-abp.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-resources.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-resources.png new file mode 100644 index 0000000000..3bab322580 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-resources.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-sql-database-connection-strings.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-sql-database-connection-strings.png new file mode 100644 index 0000000000..5ec51f6edb Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/azure-sql-database-connection-strings.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/build-docker-image.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/build-docker-image.png new file mode 100644 index 0000000000..496dd637cd Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/build-docker-image.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/check-job-status.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/check-job-status.png new file mode 100644 index 0000000000..e87ee2081f Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/check-job-status.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps-job.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps-job.png new file mode 100644 index 0000000000..10229d5707 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps-job.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps.png new file mode 100644 index 0000000000..3e8e899960 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/create-azure-container-apps.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/deploy-docker-image.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/deploy-docker-image.png new file mode 100644 index 0000000000..df431b6c51 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/deploy-docker-image.png differ diff --git a/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/docker-hub-repository.png b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/docker-hub-repository.png new file mode 100644 index 0000000000..b25e01a629 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-Azure-Container-Apps-Deployment-with-ABP/docker-hub-repository.png differ diff --git a/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/adding-incorrect-data.png b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/adding-incorrect-data.png new file mode 100644 index 0000000000..6c8a704093 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/adding-incorrect-data.png differ diff --git a/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/error.png b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/error.png new file mode 100644 index 0000000000..7c611cdc49 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/error.png differ diff --git a/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/validation.png b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/validation.png new file mode 100644 index 0000000000..6131739d68 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/images/validation.png differ diff --git a/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/post.md b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/post.md new file mode 100644 index 0000000000..494f6a9a0a --- /dev/null +++ b/docs/en/Community-Articles/2024-05-07-using-fluent-validation-with-abp/post.md @@ -0,0 +1,115 @@ +# Using FluentValidation with ABP Framework + +## What is Validation? Why Do We Need to Validate? +Validation is checking whether data is valid or not. We can liken validations to the cell membrane of our application. Just as the cell does not want to let in harmful substances, we do not want to add erroneous data to the database, which is critical to our application. Validations allow data that follows the rules to reach the database. Data that does not comply with the rules will not access the database at all, and the operation will fail. + +![Validation](./images/validation.png) + +Validations make your application run more efficiently as they are not directly tied to the database. They should be implemented across various layers of the application, including the UI, backend, and database, to prevent malicious users from bypassing these checks. Fluent validation is frequently employed for creating backend validations. + +## What is Fluent Validation? +Fluent validation is a library for checking whether data is valid or not. Fluent validation can be applied to your code in a fluent and understandable way. + +## Why We Should Use Fluent Validation? +Fluent Validation allows you to define your validation rules in a clear and flexible way. This means you can comfortably handle complex validation scenarios in your code. This makes your development process much more manageable. The readability that Fluent Validation offers really makes things easier. Having a clear understanding of what your validation rules do is a huge advantage when working on your code. In short, your code is cleaner and clearer. Fluent Validation is also very functional in terms of testability. By defining your validation rules in separate classes, you can easily test and maintain these rules. Fluent Validation is a widely used validation library on the .NET platform. As such, it has become a common standard among .NET developers. This provides advantages in terms of community support and compatibility. So, using Fluent Validation simplifies your development process by making your code more understandable, manageable and testable. + +In this section, I will show you how to use FluentValidation library within an ABP-based application. Therefore, I assume that you have an ABP-based application that has already been created. If you haven't created an ABP-based application yet, please follow the [Getting Started documentation](https://docs.abp.io/en/abp/latest/Getting-Started-Create-Solution?UI=MVC&DB=EF&Tiered=No). + +Using Fluent validation with Abp is quite simple. Open a command line window in the folder of the project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.FluentValidation +``` + +If you have not created your abp project, review the [steps to create](https://docs.abp.io/en/abp/latest/Tutorials/Todo/Overall) it now. + +Create the `Product` entity as below: + +````csharp + public class Product: FullAuditedAggregateRoot + { + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public int Stock { get; set; } + public string? LicenseKey { get; set; } + } +```` + +You must have ProductCreateDto : + +````csharp + public class ProductCreateDto + { + public string Name { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } + public string? LicenseKey { get; set; } + } +```` +Create the `ProductCreateDtoValidator` class in the **Products** folder in the `Application.Contracts` project: + +````csharp + public class ProductCreateDtoValidator :AbstractValidator +{ + public ProductCreateDtoValidator() + { + RuleFor(p=>p.Name). + NotEmpty(). + WithMessage("Product name cannot be empty"); + + RuleFor(p=>p.Name). + MinimumLength(3). + MaximumLength(100). + WithMessage("Product name must be between 3 and 100 characters"); + + RuleFor(p => p.Stock). + GreaterThanOrEqualTo(0). + WithMessage("Product stock should not be negative"); + + RuleFor(p => p.Price). + GreaterThanOrEqualTo(0). + WithMessage("Product Price should not be negative"); + } +} +```` +The validator class you create should inherit from `AbstractValidator`. You should give `AbstractValidator` the class you want to validate generically. You have to create a constructor method. You must write the code in this constructor method. Fluent validation provides you with ready-made methods that you can use to write validations very easily. + +The **RuleFor** method allows you to write a validation rule. You must specify in the parameter for which property you want to write a validation rule. + +With the **NotEmpty** method you can specify that a property cannot be null. + +With **MinimumLength** and **MaximumLength** you can specify the minimum and maximum number of characters the property can take. + +With the **GreaterThanOrEqualTo** method you can specify that the value of the property must be greater than or equal to the value entered as a parameter. + +**WithMessage** method you can specify the message to be sent when the validation fails. + +You can add a method to write your own customized validation code. For example, let's write the code that requires the license key field to contain the word “key”. + +````csharp + private bool ContainsKey(string arg) + { + return arg.IndexOf("key", StringComparison.OrdinalIgnoreCase) >= 0; + } +```` + +Add the code to the constructor to use this method: + +````csharp +RuleFor(p => p.LicenseKey). + Must(ContainsKey). + WithMessage("Product license key must contain the word 'key'"); + +```` +Try to add data that does not meet the validation rules + +![Adding Incorrect Data](./images/adding-incorrect-data.png) + +If one of the validation rules does not meet the rules, then the following error will be received for the custom rule that we defined: + +![Error](./images/error.png) + +For more information on fluent validation with abp framework, see the [documentation](https://docs.abp.io/en/abp/latest/FluentValidation) + +For more information on fluent validation, see the [documentation](https://docs.fluentvalidation.net/en/latest/)  + diff --git a/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/automapper.png b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/automapper.png new file mode 100644 index 0000000000..103e8f2261 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/automapper.png differ diff --git a/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger1.png b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger1.png new file mode 100644 index 0000000000..d4e2cc10f8 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger1.png differ diff --git a/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger2.png b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger2.png new file mode 100644 index 0000000000..67a4b7ff3f Binary files /dev/null and b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/images/swagger2.png differ diff --git a/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/post.md b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/post.md new file mode 100644 index 0000000000..283dd8e00d --- /dev/null +++ b/docs/en/Community-Articles/2024-05-09-what-is-object-to-object-mapping/post.md @@ -0,0 +1,134 @@ +# What is Object to Object Mapping? + +Mapping the properties of one object to the properties of another object is called **Object to Object Mapping**. Most of the time, you don't want to show the data you store in your database to end users as it is. Instead, you only return users the information they need for that operation and reduce the output size. + +For example, in database tables that contain relationships, we analyze the relationships and present meaningful data to users. Suppose we have a product and a category object, we keep a property called `categoryId` in the `Product` entity. However, it would be illogical to show the `categoryId ` property to users. Therefore, we can create a DTO (data transfer object) and show the **category name** to the end users, instead of the `categoryId` directly. + +DTOs are used to transfer data of objects from one object to another one. We often need to map our entities to DTOs and DTOs to entities. For example, consider the code below: + +````csharp + public virtual async Task CreateAsync(CustomerCreateDto input) + { + + var customer = await _customerManager.CreateAsync( + input.BirthDay, input.MembershipDate, input.FirstName, input.LastName + ); + CustomerDto customerDto = new CustomerDto + { + Id = customer.Id, + FirstName = input.FirstName, + LastName = input.LastName, + // ...other + }; + + return customerDto; + } + +```` +As can be seen here, it's repetitive and tedious to manually map an object to another similar object. Also, it violates the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), makes your code more complicated and reduces readability. Instead of manually mapping objects, you can use the [AutoMapper](https://automapper.org/) library to automatically map two objects' properties: + + +````csharp + public virtual async Task CreateAsync(CustomerCreateDto input) + { + + var customer = await _customerManager.CreateAsync( + input.BirthDay, input.MembershipDate, input.FirstName, input.LastName + ); + + return ObjectMapper.Map(customer); + } +```` +The **ObjectMapper.Map** method allows you to convert your `Customer` entity to `CustomerDto`. `IObjectMapper` interface is a service, that comes from the **AutoMapper** library, so let's learn more about **AutoMapper** in the next section. + +# What is AutoMapper? + +Automapper is a .NET library that automates object-to-object mapping. ABP provides abstractions for object-to-object mapping and has an integration package to use [AutoMapper](http://automapper.org/) as the object mapper. + +Automapper is a library that transforms similar objects into each other. We can imagine Automapper as a machine that transforms an apple with a hat into an apple without a hat: + +![AutoMapper](./images/automapper.png) + +In this chapter, I will show you how to use the AutoMapper library in an ABP-based application. For this reason, I assume that you already have an ABP-based application created. If you have not yet created an ABP-based application, please follow the [Getting Started documentation](https://docs.abp.io/en/abp/latest/Getting-Started-Create-Solution?UI=MVC&DB=EF&Tiered=No). + +Create a domain entity similar to this one: + +````csharp + public class Customer : FullAuditedAggregateRoot + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + public DateTime BirthDay { get; set; } + public DateTime MembershipDate { get; set; } + } +```` +`Customer` entity contains some properties (such as `FirstName`, `LastName`, ... and other audited properties coming from the base class - `DeleterId`, `IsDeleted`, `CreationTime` etc. -). Typically, you would not want to show/return all of these properties to end users, at that point, you can create a DTO class and only define the properties that you want to return to the end users. + +Let's create the `CustomerGetDto` class in the `*.Application.Contracts` project as follows: + +````csharp + public class CustomerGetDto + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + public DateTime BirthDay { get; set; } + } +```` + +After creating our entity and output DTO classes, now in the application service implementation, we can return the `CustomerGetDto` class, as a result of listing the customers. For that reason, we can write a code as follows: + +````csharp +public virtual async Task> GetListAsync(GetCustomersInput input) + { + var totalCount = await _customerRepository.GetCountAsync(input.FilterText, input.FirstName, input.LastName, input.BirthDayMin, input.BirthDayMax, input.MembershipDateMin, input.MembershipDateMax); + var items = await _customerRepository.GetListAsync(input.FilterText, input.FirstName, input.LastName, input.BirthDayMin, input.BirthDayMax, input.MembershipDateMin, input.MembershipDateMax, input.Sorting, input.MaxResultCount, input.SkipCount); + + return new PagedResultDto + { + TotalCount = totalCount, + Items = ObjectMapper.Map, List>(items) + }; + } +```` +In this code, we first get the total number of our customers and all customers according to the specified filters, then we map `List` to `List` using the `ObjectMapper.Map` method from the **ApplicationService** base class. This way we have full control over which properties are returned to the end users. + +After mapping the `Customer` entity to the `CustomerGetDto` class. Before running our application, we should define the mappings in the `*AutoMapperProfile` class in the `*.Application` project as follows: + +````csharp +public class YourApplicationAutoMapperProfile : Profile +{ + public YourApplicationAutoMapperProfile() + { + CreateMap(); + } +} +```` +Finally, we can run our application and navigate to the **/swagger** endpoint to try our endpoint. When we send a request, we should get the result as follows: + +![Swagger](./images/swagger1.png) + +## Advanced: Mapping Configurations + +In some scenarios, you may want to make some customizations when mapping two objects. For example, let's assume that you want to create the `CustomerGetDto` class as follows: + +````csharp + public class CustomerGetDto + { + public string? FullName { get; set; } + public int Age { get; set; } + } +```` +AutoMapper can't map these properties automatically, because they do not exist in the source object, which is the `Customer` entity. Therefore, you need to specify the exception and update your `YourApplicationAutoMapperProfile` class as follows: + +````csharp + +CreateMap().ForMember(c=>c.FullName,opt=> opt.MapFrom(src => src.FirstName + " " + src.LastName)) + .ForMember(c=>c.Age, opt=> opt.MapFrom(src=> DateTime.UtcNow.Year -src.BirthDay.Year)); + +```` +This configuration concatenates and matches **FirstName** and **LastName** properties into the **FullName** property and subtracts **BirthDate** from today's year and assigns it to the customer's **Age**. +After these configurations, if you make a request to the relevant endpoint, the output will look like: + +![Swagger](./images/swagger2.png) + +For more information on object-to-object mapping with [ABP Framework](https://abp.io/), see the [documentation](https://docs.abp.io/en/abp/latest/Object-To-Object-Mapping). diff --git a/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/POST.md b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/POST.md new file mode 100644 index 0000000000..e6417b70ce --- /dev/null +++ b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/POST.md @@ -0,0 +1,340 @@ +# Sentiment Analysis Within ABP-Based Application + +In this article, first I will briefly explain what sentiment analysis is and then show you how you can apply sentiment analysis in an ABP-Based application (or any kind of .NET application). + +We will use ML.NET Framework, which is an open-source machine learning framework created by the dotnet team and also we will create a layered ABP Solution by using the application template and finally we will use CMS Kit's Comment Feature and extend its behavior by adding spam detection while creating or updating a comment, at that point we will make sentiment analysis. + +## Sentiment Analysis + +[Sentiment Analysis (or opinion mining)](https://en.wikipedia.org/wiki/Sentiment_analysis) refers to determining the emotion from the given input. The primary goal of sentiment analysis is to identify, extract, and categorize (positive, negative, or neutral) the sentiments expressed in textual data. + +To understand it better, let's check the following figure and examine the comments: + +![](sentiment-analysis.png) + +* If you look at these comments, you will notice that comments have ratings and it's easy to understand the emotion or thoughts of the users who commented about the related product. +* But even if there was not any rating specified for the given comments we still can get the emotion of the users. Because, as you can see, the comments specified some obvious words that express emotions, for example, in the first comment, the user says **he/she liked the product**, **it's easy to use** and **its battery is good**, and therefore this is obviously a positive comment. +* On the other hand, if we look at the second comment, we will notice some negative statements such as **useless phone**, **cannot maintain any data connection** and the user suggests **do not buy this phone**. Actually, this is what sentiment analysis is all about, abstracting the emotion from a given input, it's comment in that case but it can be any kind of input or input-group. + +## Demo: Spam Detection (Applying Sentiment Analysis) + +> You can get the source code of the demo from [https://github.com/EngincanV/SentimentAnalysisDemo](https://github.com/EngincanV/SentimentAnalysisDemo). + +In this demo application, we will create an [ABP-based application](https://docs.abp.io/en/abp/8.1/Startup-Templates/Application) and integrate the [ABP's CMS Kit Module's Comment Feature](https://docs.abp.io/en/abp/latest/Modules/Cms-Kit/Comments), which provides a comment system to add a comment to any kind of resource, such as blog posts or products. + +By default, CMS Kit's Comment Feature does not provide spam detection and therefore in this sample application, we will add [spam detection](https://github.com/EngincanV/SentimentAnalysisDemo/blob/master/src/SentimentAnalysisDemo.Application/ML/SpamDetector.cs) while creating or updating a comment. Thus, whenever a comment is being added or updated, the spam detection service will validate the comment and reject it if it contains spam content otherwise it will make the other validations and save the comment: + +![](sentiment-analysis-steps.png) + +To get started, we will first create an application, and add the CMS Kit Module to the solution and then we will enable the Comment Feature of the CMS Kit Module, and finally, we will add the Comment Component to the homepage and add spam detection by extending the behavior. Let's start with creating the application! + +### Creating an ABP-Based Application + +You can use the following command to create a layered ABP solution (with MongoDB as the database option): + +```bash +abp new SentimentAnalysisDemo -t app -d mongodb --version 8.1.1 +``` + +### Installing the CMS Kit Module + +After creating the project, we can add the CMS Kit module to our project. [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides the `add-module` command to install a specific module to a solution. + +You can use the following command to install the CMS Kit module into your application (run this command in the solution directory): + +```bash +abp add-module Volo.CmsKit --skip-db-migrations +``` + +After this command is executed, all related CMS Kit packages will be added to the correct layers and then you can enable any CMS Kit feature you want. + +### Enabling the Comment Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can either enable all of them or enable them one by one. In our demo application, we only need the **Comment Feature**, therefore we can only enable it. + +To enable the Comment Feature, you can open the `SentimentAnalysisDemoGlobalFeatureConfigurator` class (under the `*.Domain.Shared` project) and update it as follows: + +```csharp +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Threading; + +namespace SentimentAnalysisDemo; + +public static class SentimentAnalysisDemoGlobalFeatureConfigurator +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => + { + cmsKit.Comments.Enable(); + }); + }); + } +} +``` + +After enabling the feature, now we can make the final configurations and directly use it in our application. + +### Configurations for Comment Feature + +Open the `SentimentAnalysisDemoDomainModule` class and add the following code-block into the `ConfigureServices` method: + +```csharp + Configure(options => + { + options.EntityTypes.Add(new CommentEntityTypeDefinition("Comment")); + options.IsRecaptchaEnabled = true; + }); +``` + +Here, we simply defining what should be the entity-type name of our comment and also enable the reCaptcha for the comment system. After this configuration, now we can open the `Index.cshtml` file in the `*.Web` project and invoke the `CommentingViewComponent` as below: + +```html +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using SentimentAnalysisDemo.Localization +@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting +@model SentimentAnalysisDemo.Web.Pages.IndexModel + +
+
Comments:
+ + @await Component.InvokeAsync(typeof(CommentingViewComponent), new + { + entityType = "Comment", + entityId = "SentimentAnalysisDemo", + isReadOnly = false + }) +
+ +``` + +After adding the related component, now you can run the web project and see the comment component if you want. + +### Applying Sentiment Analysis (Creating the Spam Detection Service) + +By default, CMS Kit's Comment Feature does not provide a spam detection system. In this demo application, we will override the `CommentPublicAppService`'s `CreateAsync` and `UpdateAsync` methods and then will add the spam detection control whenever a new comment has been submitted or an existing one is being updated. + +To override the `CommentPublicAppService` and extend its use-case implementations, create a `MyCommentAppService` class and update its content as below: + +```csharp +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using SentimentAnalysisDemo.ML; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.CmsKit.Comments; +using Volo.CmsKit.Public.Comments; +using Volo.CmsKit.Users; + +namespace SentimentAnalysisDemo.Volo.CmsKit.Public.Comments; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(ICommentPublicAppService), typeof(CommentPublicAppService), typeof(MyCommentAppService))] +public class MyCommentAppService : CommentPublicAppService +{ + protected ISpamDetector SpamDetector { get; } + + public MyCommentAppService( + ICommentRepository commentRepository, + ICmsUserLookupService cmsUserLookupService, + IDistributedEventBus distributedEventBus, + CommentManager commentManager, + IOptionsSnapshot cmsCommentOptions, + ISpamDetector spamDetector + ) + : base(commentRepository, cmsUserLookupService, distributedEventBus, commentManager, cmsCommentOptions) + { + SpamDetector = spamDetector; + } + + public override async Task CreateAsync(string entityType, string entityId, CreateCommentInput input) + { + //Check message: spam or ham. + await SpamDetector.CheckAsync(input.Text); + + return await base.CreateAsync(entityType, entityId, input); + } + + public override async Task UpdateAsync(Guid id, UpdateCommentInput input) + { + //Check message: spam or ham. + await SpamDetector.CheckAsync(input.Text); + + return await base.UpdateAsync(id, input); + } +} +``` + +Here, we simply just inject the `ISpamDetector` service, which we will create in a minute, and use its `CheckAsync` method to make a spam check before the comment is created or updated. + +Now, we can create the `ISpamDetector` service in the `*.Application.Contracts` project as follows: + +```csharp +using System.Threading.Tasks; + +namespace SentimentAnalysisDemo.ML; + +public interface ISpamDetector +{ + Task CheckAsync(string text); +} +``` + +Then, we can create the `SpamDetector` and implement the `ISpamDetector` interface (in the `*.Application` project): + +```csharp +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.ML; +using SentimentAnalysisDemo.ML.Model; +using Volo.Abp; +using Volo.Abp.DependencyInjection; + +namespace SentimentAnalysisDemo.ML; + +public class SpamDetector : ISpamDetector, ITransientDependency +{ + public async Task CheckAsync(string text) + { + //check if the text contains a spam content or not... + + } +} +``` + +The `CheckAsync` method is where we need to make the sentiment analysis and detect if the comment contains spam content or not. If it's spam, then we should throw a [UserFriendlyException](https://docs.abp.io/en/abp/latest/Exception-Handling#user-friendly-exception) and notify the user that the comment should be updated and should not contain any spam content. + +#### Spam Detection + +Before, making the spam check, we should have a dataset to train a machine-learning model and add `Microsoft.ML` package into our project. For that purpose, I searched in [Kaggle](https://www.kaggle.com/) for spam datasets, found the **Spam-Mail-Detection-Dataset** from Kaggle, and downloaded the csv file to use in my application. Therefore, [you should also download the dataset from the link and put it under the **/ML/Data/spam_data.csv** directory of the `*.Web` project](https://github.com/EngincanV/SentimentAnalysisDemo/blob/master/src/SentimentAnalysisDemo.Web/ML/Data/spam_data.csv). + +Here is what our dataset looks like (**0 -> not spam / 1 -> spam**): + +| Category | Message | +|----------|---------| +| 0 | Is that seriously how you spell his name? | +| 1 | Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's | +| . | . | +| . | . | +| . | . | + +> **Note:** This dataset is not ready-to use in a real-world solution. It's for mail spam detection but for the simplicity of the sample, it's not important and can be used for development purposes. + +After, downloading the dataset and putting it in the directory of **/ML/Data**, now we can add the `Microsoft.ML` package into our `*.Application` project: + +```bash +dotnet add package Microsoft.ML +``` + +Finally, we can implement the `CheckAsync` method and use sentiment analysis to make spam checks as follows: + +```csharp + + public async Task CheckAsync(string text) + { + var dataPath = Path.Combine(Environment.CurrentDirectory, "ML", "Data", "spam_data.csv"); + + var mlContext = new MLContext(); + + //Step 1: Load Data 👇 + IDataView dataView = mlContext.Data.LoadFromTextFile(dataPath, hasHeader: true, separatorChar: ','); + + //Step 2: Split data to train-test data 👇 + DataOperationsCatalog.TrainTestData trainTestSplit = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2); + IDataView trainingData = trainTestSplit.TrainSet; //80% of the data. + IDataView testData = trainTestSplit.TestSet; //20% of the data. + + //Step 3: Common data process configuration with pipeline data transformations + choose and set the training algorithm 👇 + var estimator = mlContext.Transforms.Text.FeaturizeText(outputColumnName: "Features", inputColumnName: nameof(SentimentAnalyzeInput.Message)) + .Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression(labelColumnName: "Label", featureColumnName: "Features")); + + //Step 4: Train the model 👇 + ITransformer model = estimator.Fit(trainingData); + + //Step 5: Predict 👇 + var sentimentAnalyzeInput = new SentimentAnalyzeInput + { + Message = text + }; + + var predictionEngine = mlContext.Model.CreatePredictionEngine(model); + var result = predictionEngine.Predict(sentimentAnalyzeInput); + if (IsSpam(result)) + { + throw new UserFriendlyException("Spam detected! Please update the message!"); + } + } + + private static bool IsSpam(SentimentAnalyzeResult result) + { + //1 -> spam / 0 -> ham (for 'Prediction' column) + return result is { Prediction: true, Probability: >= 0.5f }; + } + +``` + +Here, we have done the following things: + +1. **First, we loaded the data**: For that reason, we created a `MLContext` object, which is a main class for all ML.NET operations. Then, we used its `LoadFromTextFile` method and specified the dataset path in our application. Also, we mapped the dataset columns to the `SentimentAnalyzeInput` class, which we will create later on. +2. **For the second step, we split the data as training and testing data**: To be able to train a machine learning model and then evaluate its accuracy, we should not use all the data for training purposes, instead, we should split the data as training and testing data and after training the model, compare the training data accuracy with the testing data. +3. **For the third step, we should make data transformation, convert the text-based data into numeric vectors and then choose a training algorithm**: After splitting the data for training and testing purposes, now we can apply some data transformations for the *Message* column in our dataset. Because, as you would see, messages are text-based inputs and machine-learning algorithms work best with the numeric vectors. So, we are making data transformations and representing the data as numeric values. Then, we can apply `BinaryClassification` with the **SdcaLogicticRegression** algorithm to our training data. +4. **Train the model**: Since we make the data transformations and chose the correct algorithm for our model, now we can train the model. +5. **Predict the sample data**: Finally, we can pass a comment to this method and make spam check and either approve our reject the comment according to the predicted result. (To make predictions, we need to create a **PredictionEngine** and get the final results in the output class that we specified, `SentimentAnalyzeResult` in our example) + +Let's create the `SentimentAnalyzeInput` and `SentimentAnalyzeResult` classes as follows. + +**SentimentAnalyzeInput.cs:** + +```csharp +using Microsoft.ML.Data; + +namespace SentimentAnalysisDemo.ML.Model; + +public class SentimentAnalyzeInput +{ + [LoadColumn(0), ColumnName("Label")] + public bool Category { get; set; } + + [LoadColumn(1), ColumnName("Message")] + public string Message { get; set; } +} +``` + +**SentimentAnalyzeResult.cs:** + +```csharp +using Microsoft.ML.Data; + +namespace SentimentAnalysisDemo.ML.Model; + +public class SentimentAnalyzeResult +{ + [ColumnName("PredictedLabel")] + public bool Prediction { get; set; } + + public float Probability { get; set; } + + public float Score { get; set; } +} +``` + +Then, finally, we can run the application to see the final results: + +![](demo.gif) + +## Advanced: Reusing And Optimizing Machine Learning Models + +Once the model is trained and evaluated, we can save the trained model and use it directly for further use. In this way, you don’t have to retrain the model every time when you want to make predictions. It’s essential to save the trained model for future use and a must for the production-ready code. I created a separate article dedicated to that topic, and if you are interested, you can read it from [here](https://engincanv.github.io/machine-learning/sentiment-analysis/best-practises/2024/05/16/reusing-and-optimizing-machine-learning-models-in-dotnet.html). + +## Conclusion + +In this article, I briefly explain what sentiment analysis is, created a sample ABP-based application, integrated the CMS Kit Module and finally, applied sentiment analysis to make spam checks whenever a new comment has been submitted or updated. You can get the source code of the demo from [https://github.com/EngincanV/SentimentAnalysisDemo](https://github.com/EngincanV/SentimentAnalysisDemo) + +Thanks for reading :) diff --git a/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/cover-image.png b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/cover-image.png new file mode 100644 index 0000000000..7091f1a6b5 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/cover-image.png differ diff --git a/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/demo.gif b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/demo.gif new file mode 100644 index 0000000000..5a373ba110 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/demo.gif differ diff --git a/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis-steps.png b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis-steps.png new file mode 100644 index 0000000000..4222b4c97c Binary files /dev/null and b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis-steps.png differ diff --git a/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis.png b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis.png new file mode 100644 index 0000000000..9597476dca Binary files /dev/null and b/docs/en/Community-Articles/2024-05-10-Sentiment-Analysis-within-ABP-Based-Application/sentiment-analysis.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/POST.md b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/POST.md new file mode 100644 index 0000000000..f94e92a548 --- /dev/null +++ b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/POST.md @@ -0,0 +1,228 @@ +# Deploy Your ABP Framework Angular Project to Azure Kubernetes Service (AKS) + +![ABP Framework Angular Project](abp-angular-aks-helm.png) + +In my previous article on [Deploy Your ABP Framework MVC Project to Azure Container Apps](https://community.abp.io/posts/deploy-your-abp-framework-mvc-project-to-azure-container-apps-r93u9c6d), I talked about how ABP Mvc project can be easily deployed to Azure Container Apps. Now I will show how we can deploy to kubernetes environment, which looks a bit more complex but is more preferred for production, using a Helm chart. + +### Getting Started with ABP Framework Angular and Azure Kubernetes Service + +To get started, you will need an ABP Framework Angular project that you want to deploy. If you don't have one, you can [create a new project using the ABP CLI](https://docs.abp.io/en/abp/latest/Startup-Templates/Application). You will also need [an Azure subscription](https://azure.microsoft.com) and [an Azure Kubernetes Service](https://azure.microsoft.com/en-gb/services/kubernetes-service/). + +### Configuring Your ABP Framework Angular Project + +We have a sample ABP Framework Angular project that we will use for this deployment. Before creating the Docker image and Helm chart, you just need to configure `aspnet-core/src/***.HttpApi.Host/***.HttpApiHostModule.cs` file to allow CORS requests from your frontend application. You can do this by updating the following code to the `ConfigureServices` method: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); + + if (!configuration.GetValue("App:DisablePII")) + { + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; + } + + if (!configuration.GetValue("AuthServer:RequireHttpsMetadata")) + { + Configure(options => + { + options.DisableTransportSecurityRequirement = true; + }); + } + context.Services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForw +``` + +![Configure CORS](configure-cors.png) + +If your ABP Framework Angular project and Azure kubernetes cluster ready, we can start to build the docker images and pushing them to any container registry. In this article, I will use DockerHub as the container registry. + +I will also show you how I automated the steps that I originally did manually to make it simpler in the beginning and then automated them in Azure Devops. + +### Creating a Docker Image for ABP Framework Angular + +To create a Docker image for your ABP Framework Angular project, navigate to `etc/build/build-images-locally.ps1` and fix the script to match your Docker Hub username and image name. Then, run the script to build the Docker image locally. + +![Build Docker Image](build-docker-image.png) + +At the end of this process, check your Docker Hub repository to confirm that the image has been pushed successfully. My Docker Hub repository looks like this. Also you can use these my public images to test the deployment. + +![Docker Hub Repository](docker-hub-repository.png) + +### Creating Helm Chart for ABP Framework Angular + +To deploy your ABP Framework Angular project to Azure Kubernetes Service, you need to create a Helm chart. Helm is a package manager for Kubernetes that allows you to define, install, and upgrade even the most complex Kubernetes applications. Helm uses a packaging format called charts, which are a collection of files that describe a related set of Kubernetes resources. + +These helm charts are prepared to create a deployment, configmap, service and ingress for your ABP Framework Angular project. It is prepared not only for migration, frontend and backend, but also to create the sqlserver and redis that the application needs as a kubernetes service. If you already have redis and sqlserver, you don't need to stand them up in kubernetes, of course. + +![Helm Chart](helm-chart.png) + +You can find the helm chart in the https://github.com/skoc10/k8s_works/tree/main/demo/helm/k8s/angular repository. You can configure the `values-azure.yaml` file according to your needs. + +### Deploying to Azure Kubernetes Service + +`Note:` You need to have nginx-ingress-controller and cert-manager installed for letsencrypt certificate in your kubernetes cluster. + +Now that you have Docker images for your ABP Framework Angular project and a Helm chart, you can proceed to deploy it to Azure Kubernetes Service. To do this, you need to create a new Azure Kubernetes Service resource and configure the `values-azure.yaml` file according to your needs. If you want, you can deploy a single Helm with `demo/helm/k8s/deploy-staging.ps1` script you can deploy each chart separately. + +![Create Azure Kubernetes Service](create-azure-kubernetes-service.png) + +After deploying the Helm chart, you can check the deployment status to confirm that the deployment was successful. You can also check the logs of the pods to see if there are any errors. + +![Deploy Helm Chart](deploy-helm-chart.png) + +Finally, you can navigate to the web url to see your ABP Framework Angular project running in Azure Kubernetes Service. + +![ABP Framework Angular Project](abp-angular-aks.png) + +### CI/CD with Azure DevOps + +I have automated the steps that I originally did manually to make it simpler in the beginning and then automated them in Azure Devops. You can find the `azure-pipelines.yml` file in the https://github.com/skoc10/k8s_works/blob/main/demo/azure/azure-pipelines.yml repository. You can configure the `azure-pipelines.yml` file according to your needs. + +```yaml +trigger: + tags: + include: + - "*.*.*" + +variables: + dockerRegistryServiceConnection: 'demo-reg' + buildContextBasePath: '$(Build.SourcesDirectory)' + tag: $(Build.BuildNumber) + DOCKER_BUILDKIT: 1 + +pool: + name: ubuntu + +stages: +- stage: Build + displayName: Build + jobs: + - job: CheckChanges + displayName: Check if Angular or ASP.NET Core has changed + pool: + name: ubuntu + steps: + - checkout: self + +# Migration + - task: Docker@2 + displayName: 'Build Migration Docker image' + inputs: + command: build + repository: demo-angular-apppro/migration + dockerfile: $(buildContextBasePath)/aspnet-core/src/Demo.AzureAppsAngular.DbMigrator/Dockerfile.azure + buildContext: $(buildContextBasePath)/aspnet-core + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: Docker@2 + displayName: 'Push Migration Docker image' + inputs: + command: push + repository: demo-angular-apppro/migration + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: HelmDeploy@0 + displayName: 'Delete Migrator' + inputs: + connectionType: Kubernetes Service Connection + kubernetesServiceConnection: 'aks-demoms' + namespace: 'angular' + command: delete + arguments: dbmigrator + continueOnError: true + + - task: HelmDeploy@0 + displayName: 'Deploy Migration to AKS' + inputs: + connectionType: Kubernetes Service Connection + kubernetesServiceConnection: 'aks-demoms' + namespace: 'angular' + command: 'upgrade' + chartType: 'FilePath' + chartPath: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/dbmigrator' + releaseName: 'dbmigrator' + overrideValues: 'image.tag=$(tag)' + valueFile: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/dbmigrator/values.yaml' + waitForExecution: false + +# Backend + - task: Docker@2 + displayName: 'Build Backend Docker image' + inputs: + command: build + repository: demo-angular-apppro/backend + dockerfile: $(buildContextBasePath)/aspnet-core/src/Demo.AzureAppsAngular.HttpApi.Host/Dockerfile.azure + buildContext: $(buildContextBasePath)/aspnet-core + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: Docker@2 + displayName: 'Push Backend Docker image' + inputs: + command: push + repository: demo-angular-apppro/backend + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: HelmDeploy@0 + displayName: 'Deploy Backend to AKS' + inputs: + connectionType: Kubernetes Service Connection + kubernetesServiceConnection: 'aks-demoms' + namespace: 'angular' + command: 'upgrade' + chartType: 'FilePath' + chartPath: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/backend' + releaseName: 'backend' + overrideValues: 'image.tag=$(tag)' + valueFile: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/backend/values.yaml' + waitForExecution: false + +# Frontend + - task: Docker@2 + displayName: 'Build Frontend Docker image' + inputs: + command: build + repository: demo-angular-apppro/frontend + dockerfile: $(buildContextBasePath)/angular/Dockerfile.azure + buildContext: $(buildContextBasePath)/angular + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: Docker@2 + displayName: 'Push Frontend Docker image' + inputs: + command: push + repository: demo-angular-apppro/frontend + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - task: HelmDeploy@0 + displayName: 'Deploy Frontend to AKS' + inputs: + connectionType: Kubernetes Service Connection + kubernetesServiceConnection: 'aks-demoms' + namespace: 'angular' + command: 'upgrade' + chartType: 'FilePath' + chartPath: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/angular' + releaseName: 'frontend' + overrideValues: 'image.tag=$(tag)' + valueFile: '$(buildContextBasePath)/aspnet-core/etc/k8s/angular/charts/angular/values.yaml' + waitForExecution: false +``` + +### Conclusion + +In this article, I showed you how you can deploy your ABP Framework Angular project to Azure Kubernetes Service using Helm chart. I also showed you how you can automate the deployment process using Azure DevOps. I hope you found this article helpful. If you have any questions or feedback, please feel free to leave a comment below. \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks-helm.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks-helm.png new file mode 100644 index 0000000000..0e861b2a68 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks-helm.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks.png new file mode 100644 index 0000000000..1790b100e2 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/abp-angular-aks.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/build-docker-image.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/build-docker-image.png new file mode 100644 index 0000000000..12baa30af4 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/build-docker-image.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/configure-cors.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/configure-cors.png new file mode 100644 index 0000000000..def6e9b97e Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/configure-cors.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/create-azure-kubernetes-service.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/create-azure-kubernetes-service.png new file mode 100644 index 0000000000..88c5c3b97e Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/create-azure-kubernetes-service.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/deploy-helm-chart.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/deploy-helm-chart.png new file mode 100644 index 0000000000..b58be08115 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/deploy-helm-chart.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/docker-hub-repository.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/docker-hub-repository.png new file mode 100644 index 0000000000..1a58455658 Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/docker-hub-repository.png differ diff --git a/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/helm-chart.png b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/helm-chart.png new file mode 100644 index 0000000000..398a724baf Binary files /dev/null and b/docs/en/Community-Articles/2024-05-28-AKS-Helm-deployment-ABP-Angular/helm-chart.png differ diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md new file mode 100644 index 0000000000..7d78d476c5 --- /dev/null +++ b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md @@ -0,0 +1,302 @@ +# How to use .NET Aspire with ABP framework + +[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) is an opinionated, cloud-ready stack designed for building observable, production-ready, and distributed applications. On the other hand, the [ABP framework](https://docs.abp.io/en/abp/latest) offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. This guide explores how to combine .NET Aspire with ABP, enabling developers to create observable, and feature-rich applications. + +## When to Use .NET Aspire? + +Using .NET Aspire with the ABP framework can be beneficial in various scenarios where you need to combine the strengths of both technologies. Here are some situations when using .NET Aspire with ABP can be advantageous: + +- **Enterprise Web Applications:** ABP is well-suited for building enterprise web applications with its opinionated architecture and best practices. When combined with .NET Aspire, you can leverage ABP's features for rapid development of user interfaces, backend services, and business logic while benefiting from .NET Aspire's cloud-native capabilities and observability features. +- **Observability and Monitoring:** .NET Aspire's emphasis on observability, including logging, monitoring, and tracing, can enhance ABP applications by providing deeper insights into system behavior, performance metrics, and diagnostics, which is key for maintaining and optimizing enterprise-grade applications. + +## Creating a new ABP Solution + +To demonstrate the usage of .NET Aspire with the ABP framework, I've created an ABP solution. If you want to create the same solution from scratch, follow the steps below: + +Install the ABP CLI if you haven't installed it before: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +Create a new solution with the ABP framework's Application Startup Template with Tiered MVC UI and EF Core database: + +```bash +abp new AspirationalAbp -u mvc --database-provider ef -dbms PostgreSQL --csf --tiered +``` + +> The startup template selection matters for this article. I chose these options so that the demo solution can cover complex scenarios. + +**Disclaimer-I:** This article is based on version `8.0.1` of .NET Aspire and version `8.2.0` of ABP Framework. + +**Disclaimer-II:** ABP and .NET Aspire may not be fully compatible in some respects. This article aims to explain how these two technologies can be used together in the simplest way possible, even if they are not fully compatible. +## Add .NET Aspire + +After creating the solution, run the following commands in the `src` folder of your solution to add .NET Aspire: + +```bash +// Adding AppHost +dotnet new aspire-apphost -n AspirationalAbp.AppHost +dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj + +// Adding ServiceDefaults +dotnet new aspire-servicedefaults -n AspirationalAbp.ServiceDefaults +dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj +``` + +These commands add two new projects to the solution: +- **AspirationalAbp.AppHost**: An orchestrator project designed to connect and configure the different projects and services of your app. +- **AspirationalAbp.ServiceDefaults**: A .NET Aspire shared project to manage configurations that are reused across the projects in your solution related to [resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience), [service discovery](https://learn.microsoft.com/en-us/dotnet/aspire/service-discovery/overview), and [telemetry](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/telemetry). + +We have added .NET Aspire to our ABP based solution, but we have not registered our projects in the .NET Aspire orchestration. Now, let's enroll our projects, which implement the db migrator, web user interface, API, and auth, in .NET Aspire orchestration. + +## Registering projects to .NET Aspire orchestration + +First of all, we need to add the reference of related projects to the `AspirationalAbp.AppHost` project. For this, add the following `ItemGroups` to the `AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj` file: + +```csharp + + + + + + + + + + + +``` + +With the first `ItemGroup`, we added the references of `AuthServer`, `HttpApi.Host`, `Web`, and `DbMigrator` projects to the app host project. So, we can orchestrate them within the app model. + +With the second `ItemGroup`, to model the **PostgreSQL** server resource and **Redis** resource in the app host, installed the `Aspire.Hosting.PostgreSQL` and `Aspire.Hosting.Redis` + +Now let's update the `Program` class of the `AspirationalAbp.AppHost` project as follows: + +```csharp +using Microsoft.Extensions.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder + .AddPostgres("postgres") + .AddDatabase("AspirationalAbp"); + +var redis = builder.AddRedis("redis"); + +// DbMigrator +if (builder.Environment.IsDevelopment()) +{ + builder + .AddProject("dbMigrator") + .WithReference(postgres, "Default") + .WithReference(redis, "Redis") + .WithReplicas(1); +} + +// AuthServer +var authServerLaunchProfile = "AspirationalAbp.AuthServer"; +builder + .AddProject("authserver", launchProfileName: authServerLaunchProfile) + .WithExternalHttpEndpoints() + .WithReference(postgres, "Default") + .WithReference(redis); + +// HttpApi.Host +var httpApiHostLaunchProfile = "AspirationalAbp.HttpApi.Host"; +builder + .AddProject("httpapihost", launchProfileName: httpApiHostLaunchProfile) + .WithExternalHttpEndpoints() + .WithReference(postgres, "Default") + .WithReference(redis); + +// Web +builder + .AddProject("web", "AspirationalAbp.Web") + .WithReference(redis); + +builder.Build().Run(); +``` + +With the code above, the following operations were performed below: + +1. Creates an `IDistributedApplicationBuilder` instance by calling `DistributedApplication.CreateBuilder(args)`. +2. Adds PostgreSQL and the `AspirationalAbp` database. +3. Adds Redis. +4. Adds the `DbMigrator` project with references to PostgreSQL and Redis, ensuring one replica in development. +5. Adds the `AuthServer` project with external HTTP endpoints, referencing PostgreSQL and Redis. +6. Adds the `HttpApi.Host` project with external HTTP endpoints, referencing PostgreSQL and Redis. +7. Adds the `Web` project, referencing Redis. +8. Builds and runs the application. + +Now let's make the projects we added to the app host compatible with .NET Aspire. + +## Configuring Projects for Aspire + +To make the `AspirationalAbp.DbMigrator`, `AspirationalAbp.AuthServer`, `AspirationalAbp.HttpApi.Host`, and `AspirationalAbp.Web` projects compatible with .NET Aspire, we need to add and configure several packages. For that, we need to add the `Aspire.StackExchange.Redis` package to all these projects and the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. Additionally, we will add the `AspirationalAbp.ServiceDefaults` reference to host projects except `AspirationalAbp.DbMigrator`. Also, we need to convert [Serilog](https://serilog.net/) events into [OpenTelemetry](https://opentelemetry.io/) `LogRecord`s, for that we will add a `Serilog.Sinks.OpenTelemetry` reference to host projects. Let's begin with configuring `AspirationalAbp.DbMigrator`. + +### AspirationalAbp.DbMigrator + +First, let's add the `Aspire.StackExchange.Redis`, and `Serilog.Sinks.OpenTelemetry` packages to the `AspirationalAbp.DbMigrator` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.DbMigrator` project: + +```bash +dotnet add package Aspire.StackExchange.Redis --version 8.0.1 +dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 +``` + +Then let's override the `PreConfigureServices` method in `AspirationalAbpDbMigratorModule` as below: + +```csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; +} +``` + +To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in the logger configuration in `Program.cs`: + +```csharp +/// .WriteTo.Async(c => c.Console()) +.WriteTo.Async(c => c.OpenTelemetry()) +/// .CreateLogger(); +``` + +Now let's continue with `AspirationalAbp.EntityFrameworkCore`. + +### AspirationalAbp.EntityFrameworkCore + +Now let's add the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. For this, you can run the following command in the `AspirationalAbp.EntityFrameworkCore` project: + +```bash +dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.1 +``` + +Now let's continue with `AspirationalAbp.AuthServer`. + +### AspirationalAbp.AuthServer + +First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.AuthServer` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.AuthServer` project: + +```bash +dotnet add package Aspire.StackExchange.Redis --version 8.0.1 +dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj +dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 +``` + +Then add the following code block after defining the builder variable in `Program.cs`: + +```csharp +builder.AddServiceDefaults(); +builder.AddRedisClient("redis"); +builder.AddNpgsqlDbContext("Default", + options => + { + options.DisableRetry = true; + }); +``` + +Then add the following code to the `PreConfigureServices` method in the `AspirationalAbpAuthServerModule` class: + +```csharp +configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; +``` + +To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in logger configuration in `Program.cs`: + +```csharp +/// .WriteTo.Async(c => c.Console()) +.WriteTo.Async(c => c.OpenTelemetry()) +/// .CreateLogger(); +``` + +So far we have made `AspirationalAbp.DbMigrator`, `AspirationalAbp.EntityFrameworkCore`, and `AspirationalAbp.AuthServer` compatible with .NET Aspire. Now let's continue with `AspirationalAbp.HttpApi.Host`. + +### AspirationalAbp.HttpApi.Host + +First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.HttpApi.Host` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.HttpApi.Host` project: + +```bash +dotnet add package Aspire.StackExchange.Redis --version 8.0.1 +dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj +dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 +``` + +Then add the following code block after defining the builder variable in `Program.cs`: + +```csharp +builder.AddServiceDefaults(); +builder.AddRedisClient("redis"); +builder.AddNpgsqlDbContext("Default", + options => + { + options.DisableRetry = true; + }); +``` + +Then let's override the `PreConfigureServices` method in `AspirationalAbpHttpApiHostModule` as below: + +```csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; +} +``` + +To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in the logger configuration in `Program.cs`: + +```csharp +/// .WriteTo.Async(c => c.Console()) +.WriteTo.Async(c => c.OpenTelemetry()) +/// .CreateLogger(); +``` + +Finally, let's make `AspirationalAbp.Web` compatible with .NET Aspire. + +### AspirationalAbp.Web + +First, let's add the `Serilog.Sinks.OpenTelemetry`, `Aspire.StackExchange.Redis` and `AspirationalAbp.ServiceDefaults` packages to the `AspirationalAbp.Web` project. For this, let's run the following .NET CLI command inside the `AspirationalAbp.Web` project: + +```bash +dotnet add package Aspire.StackExchange.Redis --version 8.0.1 +dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj +dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313 +``` + +Then add the following code block after defining the builder variable in `Program.cs`: + +```csharp +builder.AddServiceDefaults(); +builder.AddRedisClient("redis"); +``` + +Then add the following code to the `PreConfigureServices` method in the `AspirationalAbpWebModule` class: + +```bash +var configuration = context.Services.GetConfiguration(); +configuration["Redis:Configuration"] = configuration["ConnectionStrings:Redis"]; +``` + +To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelemetry` package and now let's enable the sink. For this, let's write the following code block just before calling the `CreateLogger` method in logger configuration in `Program.cs`: + +```csharp +/// .WriteTo.Async(c => c.Console()) +.WriteTo.Async(c => c.OpenTelemetry()) +/// .CreateLogger(); +``` + +After making all our changes, we can run the `AspirationalAbp.AppHost` project. + +![aspire-dashboard](aspire-dashboard.png) + +![aspire-structured-logs](aspire-structured-logs.png) + +![aspire-traces](aspire-traces.png) + +![aspire-metrics](aspire-metrics.png) + +## Conclusion + +Combining .NET Aspire with the ABP framework creates a powerful setup for building robust, observable, and feature-rich applications. By integrating Aspire's observability and cloud capabilities with ABP's approach of focusing on your business without repeating yourself, you can develop feature-rich, scalable applications with enhanced monitoring and seamless cloud integration. This guide provides a clear path to set up and configure these technologies, ensuring your applications are well-structured, maintainable, and ready for modern cloud environments. diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-dashboard.png b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-dashboard.png new file mode 100644 index 0000000000..b1141c4644 Binary files /dev/null and b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-dashboard.png differ diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-metrics.png b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-metrics.png new file mode 100644 index 0000000000..ec2c9abda7 Binary files /dev/null and b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-metrics.png differ diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-structured-logs.png b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-structured-logs.png new file mode 100644 index 0000000000..af0b685000 Binary files /dev/null and b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-structured-logs.png differ diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-traces.png b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-traces.png new file mode 100644 index 0000000000..20a5d65d3c Binary files /dev/null and b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-traces.png differ diff --git a/docs/en/Community-Articles/2024-07-01-Use_User-Defined_Function_Mapping_For_Global_Filter/POST.md b/docs/en/Community-Articles/2024-07-01-Use_User-Defined_Function_Mapping_For_Global_Filter/POST.md new file mode 100644 index 0000000000..37171d1219 --- /dev/null +++ b/docs/en/Community-Articles/2024-07-01-Use_User-Defined_Function_Mapping_For_Global_Filter/POST.md @@ -0,0 +1,130 @@ +# Use User-Defined Function Mapping for Global Filter + +## Introduction + +ABP provides data filters that can filter queries automatically based on some rules. This feature is useful for implementing multi-tenancy, soft delete, and other global filters. It uses [EF Core's Global Query Filters system](https://learn.microsoft.com/en-us/ef/core/querying/filters) for the EF Core Integration. + +EF Core Global Query Filters generate filter conditions and apply them to SQL queries. ABP controls whether this filter condition takes effect through a variable. However, this variable may cause performance losses in some scenarios. + +## The Filter Condition Variable + +Think of a scenario with a global filter `IIsActive`, which filters out inactive entities: + +```csharp +public class Book : IIsActive +{ + public string Name { get; set; } + + public bool IsActive { get; set; } +} +``` + +The SQL generated by the [EF Core Global Query Filters](https://learn.microsoft.com/en-us/ef/core/querying/filters) is as follows: + +```SQL +SELECT * FROM [AppBooks] AS [a] +WHERE (@__ef_filter__p_0 = CAST(1 AS bit) OR [a].[IsActive] = CAST(1 AS bit)) +``` + +> The `__ef_filter__p_0` variable controls whether the filter condition takes effect. + +The generated SQL is not optimal, and some databases do not optimize it well. + +## Using User-defined function mapping for global filters + +In the [upcoming preview version of ABP, v8.3.0-rc.1](https://github.com/abpframework/abp/pull/20065), we start the [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping) to implement global filters more efficiently. This feature is enabled by default, so you don't need to make any changes if you create a new solution and start from scratch. Otherwise, you can enable it easily by following the instructions below. + +To use this new feature for your custom global filters, you need to change your `DbContext` as follows: + +````csharp +protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; + +protected override bool ShouldFilterEntity(IMutableEntityType entityType) +{ + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return base.ShouldFilterEntity(entityType); +} + +protected override Expression> CreateFilterExpression(ModelBuilder modelBuilder) +{ + var expression = base.CreateFilterExpression(modelBuilder); + + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + Expression> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + + if (UseDbFunction()) + { + isActiveFilter = e => IsActiveFilter(((IIsActive)e).IsActive, true); + + var abpEfCoreCurrentDbContext = this.GetService(); + modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!) + .HasTranslation(args => + { + // (bool isActive, bool boolParam) + var isActive = args[0]; + var boolParam = args[1]; + + if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) + { + // isActive == true + return new SqlBinaryExpression( + ExpressionType.Equal, + isActive, + new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping), + boolParam.Type, + boolParam.TypeMapping); + } + + // empty where sql + return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); + }); + } + + expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); + } + + return expression; +} + +public static bool IsActiveFilter(bool isActive, bool boolParam) +{ + throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage); +} + +public override string GetCompiledQueryCacheKey() +{ + return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}"; +} +```` + +After these changes, the SQL generated by the EF Core Global Query Filters will be as follows: + +Enabling the `IIsActive` filter: + +```SQL +SELECT * FROM [AppBooks] AS [a] WHERE +[a].[IsActive] = CAST(1 AS bit) +``` + +Disabling the `IIsActive` filter: + +```SQL +SELECT * FROM [AppBooks] AS [a] +``` + +## Conclusion + +We have implemented global filters using [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping), which can generate more efficient SQL and thus improve performance. + +Upgrade to the latest ABP version and enjoy the performance improvement! + +## References + +- [ABP Framework Data Filtering](https://docs.abp.io/en/abp/latest/Data-Filtering) +- [EF Core's Global Query Filters system](https://learn.microsoft.com/en-us/ef/core/querying/filters) +- [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping) diff --git a/docs/en/Concurrency-Check.md b/docs/en/Concurrency-Check.md deleted file mode 100644 index 25e60713c7..0000000000 --- a/docs/en/Concurrency-Check.md +++ /dev/null @@ -1,149 +0,0 @@ -## Concurrency Check - -### Introduction - -Concurrency Check (also known as **Concurrency Control**) refers to specific mechanisms used to ensure data consistency in the presence of concurrent changes (multiple processes, users access or change the same data in a database at the same time). - -There are two commonly used concurrency control mechanisms/approaches: -* **Optimistic Concurrency Control**: Optimistic Concurrency Control allows multiple users to attempt to **update** the same record without informing the users that others are also attempting to **update** it. - - * If a user successfully updates the record, the other users need to get the latest changes for the current record to be able to make changes. - * ABP's concurrency check system uses the **Optimistic Concurrency Control**. - -* **Pessimistic Concurrency Control**: Pessimistic Concurrency Control prevents simultaneous updates to records and uses a locking mechanism. For more information please see [here](https://www.martinfowler.com/eaaCatalog/pessimisticOfflineLock.html). - -### Usage - -#### `IHasConcurrencyStamp` Interface - -To enable **concurrency control** to your entity class, you should implement the `IHasConcurrencyStamp` interface, directly or indirectly. - -```csharp -public interface IHasConcurrencyStamp -{ - public string ConcurrencyStamp { get; set; } -} -``` - -* It is the base interface for **concurrency control** and only has a simple property named `ConcurrencyStamp`. -* While a new record is **creating**, if the entity implements the `IHasConcurrencyStamp` interface, ABP Framework automatically sets a unique value to the **ConcurrencyStamp** property. -* While a record is **updating**, ABP Framework compares the **ConcurrencyStamp** property of the entity with the provided **ConcurrencyStamp** value by the user and if the values match, it automatically updates the **ConcurrencyStamp** property with the new unique value. If there is a mismatch, `AbpDbConcurrencyException` is thrown. - -> If there is a unit of work, you need to call the [SaveChangesAsync](./Unit-Of-Work.md#savechangesasync) method to get the generated `ConcurrencyStamp` when creating or updating. - -**Example: Applying Concurrency Control for the Book Entity** - -Implement the `IHasConcurrencyStamp` interface for your entity: - -```csharp -public class Book : Entity, IHasConcurrencyStamp -{ - public string ConcurrencyStamp { get; set; } - - //... -} -``` - -Also, implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: - -```csharp -public class BookDto : EntityDto, IHasConcurrencyStamp -{ - //... - - public string ConcurrencyStamp { get; set; } -} - -public class UpdateBookDto : IHasConcurrencyStamp -{ - //... - - public string ConcurrencyStamp { get; set; } -} -``` - -Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below: - -```csharp -public class BookAppService : ApplicationService, IBookAppService -{ - //... - - public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) - { - var book = await BookRepository.GetAsync(id); - - book.ConcurrencyStamp = input.ConcurrencyStamp; - - //set other input values to the entity ... - //use autoSave: true to get the latest ConcurrencyStamp - await BookRepository.UpdateAsync(book, autoSave: true); - } -} -``` - -* After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. - -#### Base Classes - -[Aggregate Root](./Entities.md#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface. So, if you are deriving from one of these base classes, you don't need to manually implement the `IHasConcurrencyStamp` interface: - -- `AggregateRoot`, `AggregateRoot` -- `CreationAuditedAggregateRoot`, `CreationAuditedAggregateRoot` -- `AuditedAggregateRoot`, `AuditedAggregateRoot` -- `FullAuditedAggregateRoot`, `FullAuditedAggregateRoot` - -**Example: Applying Concurrency Control for the Book Entity** - -You can inherit your entity from one of [the base classes](#base-classes): - -```csharp -public class Book : FullAuditedAggregateRoot -{ - //... -} -``` - -Then, you can implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: - -```csharp -public class BookDto : EntityDto, IHasConcurrencyStamp -{ - //... - - public string ConcurrencyStamp { get; set; } -} - -public class UpdateBookDto : IHasConcurrencyStamp -{ - //... - - public string ConcurrencyStamp { get; set; } -} -``` - -Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below: - -```csharp -public class BookAppService : ApplicationService, IBookAppService -{ - //... - - public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) - { - var book = await BookRepository.GetAsync(id); - - book.ConcurrencyStamp = input.ConcurrencyStamp; - - //set other input values to the entity ... - //use autoSave: true to get the latest ConcurrencyStamp - await BookRepository.UpdateAsync(book, autoSave: true); - } -} -``` - -After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. You can either handle the exception manually or let the ABP Framework handle it for you. - -ABP Framework shows a user-friendly error message as in the image below, if you don't handle the exception manually. - -![Optimistic Concurrency](./images/optimistic-concurrency.png) diff --git a/docs/en/Configuration.md b/docs/en/Configuration.md deleted file mode 100644 index 30d881c06e..0000000000 --- a/docs/en/Configuration.md +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration - -ASP.NET Core has an flexible and extensible key-value based configuration system. In fact, the configuration system is a part of Microsoft.Extensions libraries and it is independent from ASP.NET Core. That means it can be used in any type of application. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) to learn the configuration infrastructure. ABP framework is 100% compatible with the configuration system. \ No newline at end of file diff --git a/docs/en/Connection-Strings.md b/docs/en/Connection-Strings.md deleted file mode 100644 index 76e535a960..0000000000 --- a/docs/en/Connection-Strings.md +++ /dev/null @@ -1,138 +0,0 @@ -# Connection Strings - -> Connection string system is especially needed when you want to create or use a modular system. If you have a monolithic application with a single database, you can go with the [ABP startup solution template](Startup-Templates/Application.md), which is properly configured for you. - -ABP Framework is designed to be [modular](Module-Development-Basics.md) and [multi-tenancy](Multi-Tenancy.md) aware. Connection string management is also designed to support these scenarios; - -* Allows to set separate connection strings for every module, so every module can have its own physical database. Modules even might be configured to use different database providers. -* Allows to set separate connection string and use a separate database per tenant (in a SaaS application). - -It also supports hybrid scenarios; - -* Allows to group modules into databases (e.g., all modules into a single shared database or two modules to database A, three modules to database B, one module to database C and rest of the modules to database D) -* Allows to group tenants into databases, just like the modules. -* Allows to separate databases per tenant per module (which might be hard to maintain for you because of too many databases, but the ABP framework supports it). - -All the [pre-built application modules](Modules/Index.md) are designed to be compatible these scenarios. - -## Configure the Connection Strings - -See the following configuration: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MyMainDb;Trusted_Connection=True;", - "AbpIdentityServer": "Server=localhost;Database=MyIdsDb;Trusted_Connection=True;", - "AbpPermissionManagement": "Server=localhost;Database=MyPermissionDb;Trusted_Connection=True;" -} -```` - -> ABP uses the `IConfiguration` service to get the application configuration. While the simplest way to write configuration into the `appsettings.json` file, it is not limited to this file. You can use environment variables, user secrets, Azure Key Vault... etc. See the [configuration](Configuration.md) document for more. - -This configuration defines three different connection strings: - -* `MyMainDb` (the `Default` connection string) is the main connection string of the application. If you don't specify a connection string for a module, it fallbacks to the `Default` connection string. The [application startup template](Startup-Templates/Application.md) is configured to use a single connection string, so all the modules uses a single, shared database. -* `MyIdsDb` (the `AbpIdentityServer` connection string) is used by the [IdentityServer](Modules/IdentityServer.md) module. -* `MyPermissionDb` (the `AbpPermissionManagement` connection string) is used by the [Permission Management](Modules/Permission-Management.md) module. - -[Pre-built application modules](Modules/Index.md) define constants for the connection string names. For example, the [IdentityServer module](Modules/IdentityServer.md) defines a ` ConnectionStringName ` constant in the ` AbpIdentityServerDbProperties ` class (located in the ` Volo.Abp.IdentityServer ` namespace). Other modules similarly define constants, so you can investigate the connection string name. - -### AbpDbConnectionOptions - -`AbpDbConnectionOptions` is the options class that is used to set the connection strings and configure database structures. - -#### Setting the connection strings - -ABP uses the `AbpDbConnectionOptions` to get the connection strings. If you configure the connection strings as explained above, `AbpDbConnectionOptions` is automatically filled. However, you can set or override the connection strings using [the options pattern](Options.md). You can configure the `AbpDbConnectionOptions` in the `ConfigureServices` method of your [module](Module-Development-Basics.md) as shown below: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.ConnectionStrings.Default = "..."; - options.ConnectionStrings["AbpPermissionManagement"] = "..."; - }); -} -```` - -#### Configuring the database structures - -`Databases` property of the `AbpDbConnectionOptions` class is used to group multiple connection strings (of multiple modules) to a single connection string. - -See the following connection strings: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MyMainDb;Trusted_Connection=True;", - "AbpIdentity": "Server=localhost;Database=MySecondaryDb;Trusted_Connection=True;", - "AbpIdentityServer": "Server=localhost;Database=MySecondaryDb;Trusted_Connection=True;", - "AbpPermissionManagement": "Server=localhost;Database=MySecondaryDb;Trusted_Connection=True;" -} -```` - -In this example, we've defined four connection strings, but the last three of them are the same; `AbpIdentity`, `AbpIdentityServer` and `AbpPermissionManagement` uses the same database, named `MySecondaryDb`. The main application and the other modules use the `Default` connection string, hence the `MyMainDb` database. - -What we want to do here is to group three modules (`AbpIdentity`, `AbpIdentityServer` and `AbpPermissionManagement`) in a single database, but we needed to specify each one manually. Because the fallback connection string is the `Default` one, if we don't specify it for a module. - -To eliminate the repetitive connection string definition, we can configure the `AbpDbConnectionOptions.Databases` property to group these connection strings, as shown in the following code (we place that in the `ConfigureServices` method of our [module class](Module-Development-Basics.md)): - -````csharp -Configure(options => -{ - options.Databases.Configure("MySecondaryDb", db => - { - db.MappedConnections.Add("AbpIdentity"); - db.MappedConnections.Add("AbpIdentityServer"); - db.MappedConnections.Add("AbpPermissionManagement"); - }); -}); -```` - -Then we can change the `appsettings.json` file as shown in the following code block: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MyMainDb;Trusted_Connection=True;", - "MySecondaryDb": "Server=localhost;Database=MySecondaryDb;Trusted_Connection=True;" -} -```` - -`MySecondaryDb` becomes the new connection string for the mapped connections. - -> ABP first looks for the module-specific connection string, then looks if a database mapping is available, finally fallbacks to the `Default` connection string. - -## Set the Connection String Name - -A module typically has a unique connection string name associated to its `DbContext` class using the `ConnectionStringName` attribute. Example: - -````csharp -[ConnectionStringName("AbpIdentityServer")] -public class IdentityServerDbContext - : AbpDbContext, IIdentityServerDbContext -{ -} -```` - -For [Entity Framework Core](Entity-Framework-Core.md) and [MongoDB](MongoDB.md), write this to your `DbContext` class (and the interface if it has). In this way, ABP uses the specified connection string for the related `DbContext` instances. - -## Database Migrations for the Entity Framework Core - -Relational databases require to create the database and the database schema (tables, views... etc.) before using it. - -The startup template (with EF Core ORM) comes with a single database and a `.EntityFrameworkCore` project that contains related classes and the migration files for that database. This project mainly defines a `YourProjectNameDbContext` class that calls the `Configure...()` methods of the used modules, like `builder.ConfigurePermissionManagement()`. - -Once you want to separate a module's database, you typically will need to create a second migration path. See the [EF Core Migrations](Entity-Framework-Core-Migrations.md) document to learn how to create and use a different database for a desired module. - -## Multi-Tenancy - -See [the multi-tenancy document](Multi-Tenancy.md) to learn how to use separate databases for tenants. - -## Replace the Connection String Resolver - -ABP defines the `IConnectionStringResolver` and uses it whenever it needs a connection string. It has two pre-built implementations: - -* `DefaultConnectionStringResolver` uses the `AbpDbConnectionOptions` to select the connection string based on the rules defined in the "Configure the Connection Strings" section above. -* `MultiTenantConnectionStringResolver` used for multi-tenant applications and tries to get the configured connection string for the current tenant if available. It uses the `ITenantStore` to find the connection strings. It inherits from the `DefaultConnectionStringResolver` and fallbacks to the base logic if no connection string specified for the current tenant. - -If you need a custom logic to determine the connection string, implement the `IConnectionStringResolver` interface (optionally derive from the existing implementations) and replace the existing implementation using the [dependency injection](Dependency-Injection.md) system. \ No newline at end of file diff --git a/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md b/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md deleted file mode 100644 index 719caf844f..0000000000 --- a/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md +++ /dev/null @@ -1,58 +0,0 @@ -# How to contribute to abp.io as a frontend developer - -## How to setup development environment - -### Pre-requirements - -- Dotnet core SDK https://dotnet.microsoft.com/en-us/download -- Nodejs LTS https://nodejs.org/en/ -- Docker https://docs.docker.com/engine/install -- Angular CLI. https://angular.io/guide/what-is-angular#angular-cli -- Abp CLI https://docs.abp.io/en/abp/latest/cli -- A code editor - -Note: This article prepare Windows OS. You may change the path type of your OS. an Example - -Windows: `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json` - -Unix: `templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/appsettings.json` - -### Sample docker commands - -You need to install SQL Server and Redis. You can install these programs without docker, but my example uses docker containers. Your computer should have Docker Engine. Then open the terminal en execute the commands one by one. -For the Sql Server - -```cmd -docker run -v sqlvolume:/var/opt/mssql -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=yourpassword" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-CU3-ubuntu-18.04 -``` - -For the Redis - -```cmd -docker run -p 6379:6379 -d redis -``` - -Then we are ready to download and execute the code. - -## Folder Structure - -The app has a backend written in .net core (c#) and an angular app. It would help if you ran both of them. - -### Running Backend App - -The path of the Backend app is “templates\app\aspnet-core.” If you want to work with dockerized SQL Server, you should change connection strings for running with docker. The path of the connection string is -`templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json`. - -Before running the backend, you should run the Db migrator project. The DbMigrator created initial tables and values. The path of DbMigrator is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator`. Open a terminal in the path and execute the command `dotnet run` in terminal - -One last step before the running the backend is installing client-side libraries. Go to `templates\app\aspnet-core`. Open a terminal in the path and execute the command `abp install-libs` in terminal - -Next step you should go to path of backend host project. The path is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.HttpApi.HostWithIds`. Open a terminal in the path and execute the command `dotnet run` in terminal - -Your backend should be running successfully - -### Running Frontend App - -There is a demo app. The path of the demo app is `npm\ng-packs\apps\dev-app`. The demo app is connected to the packages with local references. Open the terminal in `npm\ng-packs\apps\dev-app` and execute `yarn` or `npm i` in terminal. After the package installed run `npm start` or `yarn start`. - -The repo uses Nx and packages connected with `local references`. The packages path is `npm\ng-packs\packages` diff --git a/docs/en/Contribution/Index.md b/docs/en/Contribution/Index.md deleted file mode 100644 index 8ebac70c20..0000000000 --- a/docs/en/Contribution/Index.md +++ /dev/null @@ -1,80 +0,0 @@ -# Contribution Guide - -ABP is an [open source](https://github.com/abpframework) and community driven project. This guide is aims to help anyone wants to contribute to the project. - -## ABP Community Website - -If you want to write **articles** or **how to guides** related to the ABP Framework and ASP.NET Core, please submit your article to the [community.abp.io](https://community.abp.io/) website. - -## Code Contribution - -You can always send pull requests to the GitHub repository. - -- [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the [ABP repository](https://github.com/abpframework/abp/) from GitHub. -- Build the repository using the `/build/build-all.ps1 -f` for one time. -- Make the necessary changes, including unit/integration tests. -- Send a pull request. - -> When you open a solution in Visual Studio, you may need to execute `dotnet restore` in the root folder of the solution for one time, after it is fully opened in the Visual Studio. This is needed since VS can't properly resolves local references to projects out of the solution. - -### GitHub Issues - -Before making any change, please discuss it on the [Github issues](https://github.com/abpframework/abp/issues). In this way, no other developer will work on the same issue and your PR will have a better chance to be accepted. - -#### Bug Fixes & Enhancements - -You may want to fix a known bug or work on a planned enhancement. See [the issue list](https://github.com/abpframework/abp/issues) on Github. - -#### Feature Requests - -If you have a feature idea for the framework or modules, [create an issue](https://github.com/abpframework/abp/issues/new) on Github or attend to an existing discussion. Then you can implement it if it's embraced by the community. - -## Document Translation - -You may want to translate the complete [documentation](https://docs.abp.io) (including this one) to your mother language. If so, follow these steps: - -* Clone the [ABP repository](https://github.com/abpframework/abp/) from Github. -* To add a new language, create a new folder inside the [docs](https://github.com/abpframework/abp/tree/master/docs) folder. Folder names can be "en", "es", "fr", "tr" and so on based on the language (see [all culture codes](https://msdn.microsoft.com/en-us/library/hh441729.aspx)). -* Get the ["en" folder](https://github.com/abpframework/abp/tree/master/docs/en) as a reference for the file names and folder structure. Keep the same naming if you are translating the same documentation. -* Send a pull request (PR) once you translate any document. Please translate documents & send PRs one by one. Don't wait to finish translations for all documents. - -There are some fundamental documents need to be translated before publishing a language on the [ABP documentation web site](https://docs.abp.io): - -* Index (Home) -* Getting Started -* Web Application Development Tutorial - -A new language is published after these minimum translations have been completed. - -## Resource Localization - -ABP framework has a flexible [localization system](../Localization.md). You can create localized user interfaces for your own application. - -In addition to that, the framework and the [pre-build modules](https://docs.abp.io/en/abp/latest/Modules/Index) have localized texts. As an example, see [the localization texts for the Volo.Abp.UI package](https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json). - -### Using the "abp translate" command - -This is the recommended approach, since it automatically finds all missing texts for a specific culture and lets you to translate in one place. - -* Clone the [ABP repository](https://github.com/abpframework/abp/) from Github. -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Run `abp translate -c ` command for your language in the root folder of the abp repository. For example, use `abp translate -c fr` for French. Check [this document](https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/supported-culture-codes) to find the culture code for your language. -* This command creates a file in the same folder, named `abp-translation.json`. Open this file in your favorite editor and fill the missing text values. -* Once you done the translation, use `abp translate -a` command to apply changes to the related files. -* Send a pull request on GitHub. - -### Manual Translation - -If you want to make a change on a specific resource file, you can find the file yourself, make the necessary change (or create a new file for your language) and send a pull request on GitHub. - -## Bug Report - -If you find any bug, please [create an issue on the Github repository](https://github.com/abpframework/abp/issues/new). - -## Setup Frontend Development Environment - -[How to contribute to abp.io as a frontend developer](How-to-Contribute-abp.io-as-a-frontend-developer.md) - -## See Also - -* [ABP Community Talks 2022.4: How can you contribute to the open source ABP Framework?](https://www.youtube.com/watch?v=Wz4Z-O-YoPg&list=PLsNclT2aHJcOsPustEkzG6DywiO8eh0lB) \ No newline at end of file diff --git a/docs/en/CorrelationId.md b/docs/en/CorrelationId.md deleted file mode 100644 index 99bcd448f4..0000000000 --- a/docs/en/CorrelationId.md +++ /dev/null @@ -1,3 +0,0 @@ -# Correlation ID - -TODO \ No newline at end of file diff --git a/docs/en/CurrentUser.md b/docs/en/CurrentUser.md deleted file mode 100644 index ac2aaa20bf..0000000000 --- a/docs/en/CurrentUser.md +++ /dev/null @@ -1,167 +0,0 @@ -# Current User - -It is very common to retrieve the information about the logged in user in a web application. The current user is the active user related to the current request in a web application. - -## ICurrentUser - -`ICurrentUser` is the main service to get info about the current active user. - -Example: [Injecting](Dependency-Injection.md) the `ICurrentUser` into a service: - -````csharp -using System; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Users; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly ICurrentUser _currentUser; - - public MyService(ICurrentUser currentUser) - { - _currentUser = currentUser; - } - - public void Foo() - { - Guid? userId = _currentUser.Id; - } - } -} -```` - -Common base classes have already injected this service as a base property. For example, you can directly use the `CurrentUser` property in an [application service](Application-Services.md): - -````csharp -using System; -using Volo.Abp.Application.Services; - -namespace AbpDemo -{ - public class MyAppService : ApplicationService - { - public void Foo() - { - Guid? userId = CurrentUser.Id; - } - } -} -```` - -### Properties - -Here are the fundamental properties of the `ICurrentUser` interface: - -* **IsAuthenticated** (bool): Returns `true` if the current user has logged in (authenticated). If the user has not logged in then `Id` and `UserName` returns `null`. -* **Id** (Guid?): Id of the current user. Returns `null`, if the current user has not logged in. -* **UserName** (string): User name of the current user. Returns `null`, if the current user has not logged in. -* **TenantId** (Guid?): Tenant Id of the current user, which can be useful for a [multi-tenant](Multi-Tenancy.md) application. Returns `null`, if the current user is not assigned to a tenant. -* **Email** (string): Email address of the current user.Returns `null`, if the current user has not logged in or not set an email address. -* **EmailVerified** (bool): Returns `true`, if the email address of the current user has been verified. -* **PhoneNumber** (string): Phone number of the current user. Returns `null`, if the current user has not logged in or not set a phone number. -* **PhoneNumberVerified** (bool): Returns `true`, if the phone number of the current user has been verified. -* **Roles** (string[]): Roles of the current user. Returns a string array of the role names of the current user. - -### Methods - -`ICurrentUser` is implemented on the `ICurrentPrincipalAccessor` (see the section below) and works with the claims. So, all of the above properties are actually retrieved from the claims of the current authenticated user. - -`ICurrentUser` has some methods to directly work with the claims, if you have custom claims or get other non-common claim types. - -* **FindClaim**: Gets a claim with the given name. Returns `null` if not found. -* **FindClaims**: Gets all the claims with the given name (it is allowed to have multiple claim values with the same name). -* **GetAllClaims**: Gets all the claims. -* **IsInRole**: A shortcut method to check if the current user is in the specified role. - -Beside these standard methods, there are some extension methods: - -* **FindClaimValue**: Gets the value of the claim with the given name, or `null` if not found. It has a generic overload that also casts the value to a specific type. -* **GetId**: Returns `Id` of the current user. If the current user has not logged in, it throws an exception (instead of returning `null`) . Use this only if you are sure that the user has already authenticated in your code context. - -### Authentication & Authorization - -`ICurrentUser` works independently of how the user is authenticated or authorized. It seamlessly works with any authentication system that works with the current principal (see the section below). - -## ICurrentPrincipalAccessor - -`ICurrentPrincipalAccessor` is the service that should be used (by the ABP Framework and your application code) whenever the current principal of the current user is needed. - -For a web application, it gets the `User` property of the current `HttpContext`. For a non-web application, it returns the `Thread.CurrentPrincipal`. - -> You generally don't need to use this low level `ICurrentPrincipalAccessor` service and just directly work with the `ICurrentUser` explained above. - -### Basic Usage - -You can inject `ICurrentPrincipalAccessor` and use the `Principal` property to the the current principal: - -````csharp -public class MyService : ITransientDependency -{ - private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; - - public MyService(ICurrentPrincipalAccessor currentPrincipalAccessor) - { - _currentPrincipalAccessor = currentPrincipalAccessor; - } - - public void Foo() - { - var allClaims = _currentPrincipalAccessor.Principal.Claims.ToList(); - //... - } -} -```` - -### Changing the Current Principal - -Current principal is not something you want to set or change, except at some advanced scenarios. If you need it, use the `Change` method of the `ICurrentPrincipalAccessor`. It takes a `ClaimsPrincipal` object and makes it "current" for a scope. - -Example: - -````csharp -public class MyAppService : ApplicationService -{ - private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; - - public MyAppService(ICurrentPrincipalAccessor currentPrincipalAccessor) - { - _currentPrincipalAccessor = currentPrincipalAccessor; - } - - public void Foo() - { - var newPrincipal = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] - { - new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString()), - new Claim(AbpClaimTypes.UserName, "john"), - new Claim("MyCustomCliam", "42") - } - ) - ); - - using (_currentPrincipalAccessor.Change(newPrincipal)) - { - var userName = CurrentUser.UserName; //returns "john" - //... - } - } -} -```` - -Use the `Change` method always in a `using` statement, so it will be restored to the original value after the `using` scope ends. - -This can be a way to simulate a user login for a scope of the application code, however try to use it carefully. - -## AbpClaimTypes - -`AbpClaimTypes` is a static class that defines the names of the standard claims and used by the ABP Framework. - -* Default values for the `UserName`, `UserId`, `Role` and `Email` properties are set from the [System.Security.Claims.ClaimTypes](https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) class, but you can change them. -* Other properties, like `EmailVerified`, `PhoneNumber`, `TenantId`... are defined by the ABP Framework by following the standard names wherever possible. - -It is suggested to use properties of this class instead of magic strings for claim names. - diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md deleted file mode 100644 index c2cacb4e4c..0000000000 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ /dev/null @@ -1,137 +0,0 @@ -# Customizing the Application Modules: Extending Entities - -In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. - -## Extra Properties - -[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects. - -Example: - -````csharp -//SET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -user.SetProperty("Title", "My custom title value!"); -await _identityUserRepository.UpdateAsync(user); - -//GET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -return user.GetProperty("Title"); -```` - -This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). - -Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. - -See the [entities document](Entities.md) for more about the extra properties system. - -> It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above. - -## Entity Extensions (EF Core) - -As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to; - -* Create **indexes** and **foreign keys** for an extra property. -* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example). -* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more). - -To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table. - -Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `ObjectExtensionManager`: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreProperty( - "SocialSecurityNumber", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(32); - } - ); -```` - -* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table). -* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties). - -> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEfCoreEntityExtensionMappings`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. - -Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database. - -You can then use the same extra properties system defined in the previous section to manipulate the property over the entity. - -## Creating a New Entity Maps to the Same Database Table/Collection - -Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). - -## Creating a New Entity with Its Own Database Table/Collection - -Mapping your entity to an **existing table** of a depended module has a few disadvantages; - -* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. -* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. - -If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. - -In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; - -* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. -* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. - -Once you handle the event, you can update your own entity in your own database. - -### Subscribing to Local Events - -[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. - -Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler>` interface. - -````csharp -public class MyLocalIdentityUserChangeEventHandler : - ILocalEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityChangedEventData eventData) - { - var userId = eventData.Entity.Id; - var userName = eventData.Entity.UserName; - - //... - } -} -```` - -* `EntityChangedEventData` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). -* This code will be executed in the **current unit of work**, the whole process becomes transactional. - -> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). - -### Subscribing to Distributed Events - -[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. - -Assume that you want to get informed when `Tenant` entity (of the [Tenant Management](Modules/Tenant-Management.md) module) has created. In this case, you can subscribe to the `EntityCreatedEto` event as shown in the following example: - -````csharp -public class MyDistributedEventHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - var tenantId = eventData.Entity.Id; - var tenantName = eventData.Entity.Name; - //...your custom logic - } - - //... -} -```` - -This handler is executed only when a new tenant has been created. All the pre-built ABP [application modules](Modules/Index.md) define corresponding `ETO` types for their entities. So, you can easily get informed when they changes. - -> Notice that ABP doesn't publish distributed events for an entity by default. Because it has a cost and should be enabled by intention. See the [distributed event bus document](Distributed-Event-Bus.md) to learn more. - -## See Also - -* [Migration System for the EF Core](Entity-Framework-Core-Migrations.md) -* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) diff --git a/docs/en/Customizing-Application-Modules-Guide.md b/docs/en/Customizing-Application-Modules-Guide.md deleted file mode 100644 index 4fe3c71dbe..0000000000 --- a/docs/en/Customizing-Application-Modules-Guide.md +++ /dev/null @@ -1,108 +0,0 @@ -# Customizing the Existing Modules - -ABP Framework has been designed to support to build fully [modular applications](Module-Development-Basics.md) and systems. It also provides some [pre-built application modules](Modules/Index.md) those are **ready to use** in any kind of application. - -For example, you can **re-use** the [Identity Management Module](Modules/Identity.md) to add user, role and permission management to your application. The [application startup template](Startup-Templates/Application.md) already comes with Identity and some other modules **pre-installed**. - -## Re-Using an Application Module - -You have two options to re-use an application module. - -### As Package References - -You can add **NuGet** & **NPM** package references of the related module to your application and configure the module (based on its documentation) to integrate to your application. - -As mentioned before, the [application startup template](Startup-Templates/Application.md) already comes with some **fundamental modules pre-installed**. It uses the modules as NuGet & NPM package references. - -This approach has the following benefits: - -* Your solution will be **clean** and only contains your **own application code**. -* You can **easily upgrade** a module when a new version is available. `abp update` [CLI](CLI.md) command makes it even easier. In this way, you can continue to get **new features and bug fixes**. - -However, there is a drawback: - -* You may not able to **customize** the module because the module source is not in your solution. - -This document explains **how to customize or extend** a depended module without need to change its source code. While it is limited compared to a full source code change opportunity, there are still some good ways to make some customizations. - -If you don't think to make huge changes on the pre-built modules, re-using them as package reference is the recommended way. - -### Including the Source Code - -If you want to make **huge changes** or add **major features** on a pre-built module, but the available extension points are not enough, you can consider to directly work the source code of the depended module. - -In this case, you typically **add the source code** of the module to your solution and replace **every** package reference in the solution with its corresponding local project references. **[ABP CLI](CLI.md)**'s `add-module` command automates this process for you with the `--with-source-code` parameter. This command can also replace a module by its source code if the module already installed as NuGet packages. - - -#### Separating the Module Solution - -You may prefer to not include the module source code **directly into your solution**. Every module consists of 10+ project files and adding **multiple modules** may impact on the **size** of your solution **load & development time.** Also, you may have different development teams working on different modules, so you don't want to make the module code available to the application development team. - -In any case, you can create a **separate solution** for the desired module and depend on the module as project references out of the solution. We do it like that for the [abp repository](https://github.com/abpframework/abp/). - -> One problem we see is Visual Studio doesn't play nice with this kind of approach (it doesn't support well to have references to local projects out of the solution directory). If you get error while building the application (depends on an external module), run `dotnet restore` in the command line after opening the application's solution in the Visual Studio. - -#### Publishing the Customized Module as Packages - -One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company, for example. - -## Module Customization / Extending Approaches - -This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways. - -### Module Entity Extension System - -> Module entity extension system is the **main and high level extension system** that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. - -See the [Module Entity Extensions document](Module-Entity-Extensions.md) to learn how to use it. - -### Extending Entities - -If you only need to get/set extra data on an existing entity, follow the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document. - -### Overriding Services/Components - -In addition to the extensibility systems, you can partially or completely override any service or user interface page/component. - -* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) -* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) - -### Additional UI Extensibility Points - -There are some low level systems that you can control entity actions, table columns and page toolbar of a page defined by a module. - -#### Entity Actions - -Entity action extension system allows you to add a new action to the action menu for an entity on the user interface; - -* [Entity Action Extensions for ASP.NET Core UI](UI/AspNetCore/Entity-Action-Extensions.md) -* [Entity Action Extensions for Blazor UI](UI/Blazor/Entity-Action-Extensions.md) -* [Entity Action Extensions for Angular](UI/Angular/Entity-Action-Extensions.md) - -#### Data Table Column Extensions - -Data table column extension system allows you to add a new column in the data table on the user interface; - -* [Data Table Column Extensions for ASP.NET Core UI](UI/AspNetCore/Data-Table-Column-Extensions.md) -* [Data Table Column Extensions for Blazor UI](UI/Blazor/Data-Table-Column-Extensions.md) -* [Data Table Column Extensions for Angular](UI/Angular/Data-Table-Column-Extensions.md) - -#### Page Toolbar - -Page toolbar system allows you to add components to the toolbar of a page; - -* [Page Toolbar Extensions for ASP.NET Core UI](UI/AspNetCore/Page-Toolbar-Extensions.md) -* [Page Toolbar Extensions for Blazor UI](UI/Blazor/Page-Toolbar-Extensions.md) -* [Page Toolbar Extensions for Angular](UI/Angular/Page-Toolbar-Extensions.md) - -#### Others - -* [Dynamic Form Extensions for Angular](UI/Angular/Dynamic-Form-Extensions.md) - -## See Also - -Also, see the following documents: - -* See [the localization document](Localization.md) to learn how to extend existing localization resources. -* See [the settings document](Settings.md) to learn how to change setting definitions of a depended module. -* See [the authorization document](Authorization.md) to learn how to change permission definitions of a depended module. diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md deleted file mode 100644 index a1a6c8b778..0000000000 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ /dev/null @@ -1,385 +0,0 @@ -# Customizing the Application Modules: Overriding Services - -You may need to **change behavior (business logic)** of a depended module for your application. In this case, you can use the power of the [dependency injection system](Dependency-Injection.md) to replace a service, controller or even a page model of the depended module by your own implementation. - -**Replacing a service** is possible for any type of class registered to the dependency injection, including services of the ABP Framework. - -You have different options can be used based on your requirement those will be explained in the next sections. - -> Notice that some service methods may not be virtual, so you may not be able to override. We make all virtual by design. If you find any method that is not overridable, please [create an issue](https://github.com/abpframework/abp/issues/new) or do it yourself and send a **pull request** on GitHub. - -## Replacing an Interface - -If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityUserAppService`, you can re-implement the same interface and replace the current implementation by your class. Example: - -````csharp -public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -`MyIdentityUserAppService` replaces the `IIdentityUserAppService` by naming convention (since both ends with `IdentityUserAppService`). If your class name doesn't match, you need to manually expose the service interface: - -````csharp -[ExposeServices(typeof(IIdentityUserAppService))] -public class TestAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. - -Example: - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IIdentityUserAppService))] -public class TestAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -In this way, there will be a single implementation of the `IIdentityUserAppService` interface, while it doesn't change the result for this case. Replacing a service is also possible by code: - -````csharp -context.Services.Replace( - ServiceDescriptor.Transient() -); -```` - -You can write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). - -## Overriding a Service Class - -In most cases, you will want to change one or a few methods of the current implementation for a service. Re-implementing the complete interface would not be efficient in this case. As a better approach, inherit from the original class and override the desired method. - -### Example: Overriding an Application Service - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))] -public class MyIdentityUserAppService : IdentityUserAppService -{ - //... - public MyIdentityUserAppService( - IdentityUserManager userManager, - IIdentityUserRepository userRepository, - IGuidGenerator guidGenerator - ) : base( - userManager, - userRepository, - guidGenerator) - { - } - - public async override Task CreateAsync(IdentityUserCreateDto input) - { - if (input.PhoneNumber.IsNullOrWhiteSpace()) - { - throw new AbpValidationException( - "Phone number is required for new users!", - new List - { - new ValidationResult( - "Phone number can not be empty!", - new []{"PhoneNumber"} - ) - } - ); } - - return await base.CreateAsync(input); - } -} -```` - -This class **overrides** the `CreateAsync` method of the `IdentityUserAppService` [application service](Application-Services.md) to check the phone number. Then calls the base method to continue to the **underlying business logic**. In this way, you can perform additional business logic **before** and **after** the base logic. - -You could completely **re-write** the entire business logic for a user creation without calling the base method. - -### Example: Overriding a Domain Service - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IdentityUserManager))] -public class MyIdentityUserManager : IdentityUserManager -{ - public MyIdentityUserManager( - IdentityUserStore store, - IIdentityRoleRepository roleRepository, - IIdentityUserRepository userRepository, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger logger, - ICancellationTokenProvider cancellationTokenProvider) : - base(store, - roleRepository, - userRepository, - optionsAccessor, - passwordHasher, - userValidators, - passwordValidators, - keyNormalizer, - errors, - services, - logger, - cancellationTokenProvider) - { - } - - public async override Task CreateAsync(IdentityUser user) - { - if (user.PhoneNumber.IsNullOrWhiteSpace()) - { - throw new AbpValidationException( - "Phone number is required for new users!", - new List - { - new ValidationResult( - "Phone number can not be empty!", - new []{"PhoneNumber"} - ) - } - ); - } - - return await base.CreateAsync(user); - } -} -```` - -This example class inherits from the `IdentityUserManager` [domain service](Domain-Services.md) and overrides the `CreateAsync` method to perform the same phone number check implemented above. The result is same, but this time we've implemented it inside the domain service assuming that this is a **core domain logic** for our system. - -> `[ExposeServices(typeof(IdentityUserManager))]` attribute is **required** here since `IdentityUserManager` does not define an interface (like `IIdentityUserManager`) and dependency injection system doesn't expose services for inherited classes (like it does for the implemented interfaces) by convention. - -Check the [localization system](Localization.md) to learn how to localize the error messages. - -### Example: Overriding a Repository - -````csharp -public class MyEfCoreIdentityUserRepository : EfCoreIdentityUserRepository -{ - public MyEfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - /* You can override any base method here */ -} -```` - -In this example, we are overriding the `EfCoreIdentityUserRepository` class that is defined by the [Identity module](Modules/Identity.md). This is the [Entity Framework Core](Entity-Framework-Core.md) implementation of the user repository. - -Thanks to the naming convention (`MyEfCoreIdentityUserRepository` ends with `EfCoreIdentityUserRepository`), no additional setup is required. You can override any base method to customize it for your needs. - -However, if you inject `IRepository` or `IRepository`, it will still use the default repository implementation. To replace the default repository implementation, write the following code in the `ConfigureServices` method of your module class: - -````csharp -context.Services.AddDefaultRepository( - typeof(Volo.Abp.Identity.IdentityUser), - typeof(MyEfCoreIdentityUserRepository), - replaceExisting: true -); -```` - -In this way, your implementation will be used if you inject `IRepository`, `IRepository` or `IIdentityUserRepository`. - -If you want to add extra methods to your repository and use it in your own code, you can define an interface and expose it from your repository implementation. You can also extend the pre-built repository interface. Example: - -````csharp -public interface IMyIdentityUserRepository : IIdentityUserRepository -{ - public Task DeleteByEmailAddress(string email); -} -```` - -The `IMyIdentityUserRepository` interface extends the Identity module's `IIdentityUserRepository` interface. Then you can implement it as shown in the following example: - -````csharp -[ExposeServices(typeof(IMyIdentityUserRepository), IncludeDefaults = true)] -public class MyEfCoreIdentityUserRepository - : EfCoreIdentityUserRepository, IMyIdentityUserRepository -{ - public MyEfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteByEmailAddress(string email) - { - var dbContext = await GetDbContextAsync(); - var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == email); - if (user != null) - { - dbContext.Users.Remove(user); - } - } -} -```` - -The `MyEfCoreIdentityUserRepository` class implements the `IMyIdentityUserRepository` interface. `ExposeServices` attribute is needed since ABP can not expose `IMyIdentityUserRepository` by naming conventions (`MyEfCoreIdentityUserRepository` doesn't end with `MyIdentityUserRepository`). Now, you can inject the `IMyIdentityUserRepository` interface into your services and call its `DeleteByEmailAddress` method. - -### Example: Overriding a Controller - -````csharp -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Volo.Abp.Account; -using Volo.Abp.DependencyInjection; - -namespace MyProject.Controllers -{ - [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(AccountController))] - public class MyAccountController : AccountController - { - public MyAccountController(IAccountAppService accountAppService) - : base(accountAppService) - { - - } - - public async override Task SendPasswordResetCodeAsync( - SendPasswordResetCodeDto input) - { - Logger.LogInformation("Your custom logic..."); - - await base.SendPasswordResetCodeAsync(input); - } - } -} -```` - -This example replaces the `AccountController` (An API Controller defined in the [Account Module](Modules/Account.md)) and overrides the `SendPasswordResetCodeAsync` method. - -**`[ExposeServices(typeof(AccountController))]` is essential** here since it registers this controller for the `AccountController` in the dependency injection system. `[Dependency(ReplaceServices = true)]` is also recommended to clear the old registration (even the ASP.NET Core DI system selects the last registered one). - -In addition, the `MyAccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. - -If `IncludeSelf = true` is specified, i.e. `[ExposeServices(typeof(AccountController), IncludeSelf = true)]`, then `AccountController` will be removed instead. This is useful for **extending** a controller. - -If you don't want to remove either controller, you can configure `AbpAspNetCoreMvcOptions`: - -```csharp -Configure(options => -{ - options.IgnoredControllersOnModelExclusion - .AddIfNotContains(typeof(MyAccountController)); -}); -``` - -### Overriding Other Classes - -Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. - -## Extending Data Transfer Objects - -**Extending [entities](Entities.md)** is possible as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md). In this way, you can add **custom properties** to entities and perform **additional business logic** by overriding the related services as described above. - -It is also possible to extend Data Transfer Objects (**DTOs**) used by the application services. In this way, you can get extra properties from the UI (or client) and return extra properties from the service. - -### Example - -Assuming that you've already added a `SocialSecurityNumber` as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md) and want to include this information while getting the list of users from the `GetListAsync` method of the `IdentityUserAppService`. - -You can use the [object extension system](Object-Extensions.md) to add the property to the `IdentityUserDto`. Write this code inside the `YourProjectNameDtoExtensions` class comes with the application startup template: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber" - ); -```` - -This code defines a `SocialSecurityNumber` to the `IdentityUserDto` class as a `string` type. That's all. Now, if you call the `/api/identity/users` HTTP API (which uses the `IdentityUserAppService` internally) from a REST API client, you will see the `SocialSecurityNumber` value in the `extraProperties` section. - -````json -{ - "totalCount": 1, - "items": [{ - "tenantId": null, - "userName": "admin", - "name": "admin", - "surname": null, - "email": "admin@abp.io", - "emailConfirmed": false, - "phoneNumber": null, - "phoneNumberConfirmed": false, - "twoFactorEnabled": false, - "lockoutEnabled": true, - "lockoutEnd": null, - "concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c", - "isDeleted": false, - "deleterId": null, - "deletionTime": null, - "lastModificationTime": "2020-04-09T21:25:47.0740706", - "lastModifierId": null, - "creationTime": "2020-04-09T21:25:46.8308744", - "creatorId": null, - "id": "8edecb8f-1894-a9b1-833b-39f4725db2a3", - "extraProperties": { - "SocialSecurityNumber": "123456789" - } - }] -} -```` - -Manually added the `123456789` value to the database for now. - -All pre-built modules support extra properties in their DTOs, so you can configure easily. - -### Definition Check - -When you [define](Customizing-Application-Modules-Extending-Entities.md) an extra property for an entity, it doesn't automatically appear in all the related DTOs, because of the security. The extra property may contain a sensitive data and you may not want to expose it to the clients by default. - -So, you need to explicitly define the same property for the corresponding DTO if you want to make it available for the DTO (as just done above). If you want to allow to set it on user creation, you also need to define it for the `IdentityUserCreateDto`. - -If the property is not so secure, this can be tedious. Object extension system allows you to ignore this definition check for a desired property. See the example below: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.MapEfCore(b => b.HasMaxLength(32)); - options.CheckPairDefinitionOnMapping = false; - } - ); -```` - -This is another approach to define a property for an entity (`ObjectExtensionManager` has more, see [its document](Object-Extensions.md)). This time, we set `CheckPairDefinitionOnMapping` to false to skip definition check while mapping entities to DTOs and vice verse. - -If you don't like this approach but want to add a single property to multiple objects (DTOs) easier, `AddOrUpdateProperty` can get an array of types to add the extra property: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - new[] - { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), - typeof(IdentityUserUpdateDto) - }, - "SocialSecurityNumber" - ); -```` - -### About the User Interface - -This system allows you to add extra properties to entities and DTOs and execute custom business code, however it does nothing related to the User Interface. - -See [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) guide for the UI part. - -## How to Find the Services? - -[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. diff --git a/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md deleted file mode 100644 index b6ad7b05c2..0000000000 --- a/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md +++ /dev/null @@ -1,7 +0,0 @@ -# Overriding the User Interface - -You may want to override a page, a component, a JavaScript, CSS or an image file of your depended module. Overriding the UI completely depends on the UI framework you're using. Select the UI framework to continue: - -* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) -* [Angular](UI/Angular/Customization-User-Interface.md) -* [Blazor](UI/Blazor/Customization-Overriding-Components.md) \ No newline at end of file diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md deleted file mode 100644 index 727d4961c2..0000000000 --- a/docs/en/Dapper.md +++ /dev/null @@ -1,69 +0,0 @@ -# Dapper Integration - -[Dapper](https://github.com/DapperLib/Dapper) is a simple and lightweight object mapper for .NET. A key feature of Dapper is its [high performance](https://github.com/DapperLib/Dapper#performance) compared to other ORMs. - -While you can use Dapper as is in your ABP applications, there is also an integration package that simplifies creating repository classes using Dapper. - -> ABP's Dapper integration package is based on Entity Framework Core (EF Core). That means it assumes you will use Dapper mixed with EF Core where EF Core is the primary database provider and you use Dapper when you need to fine-tune your quires and get the maximum performance. See [this article](https://community.abp.io/posts/using-dapper-with-the-abp-framework-shp74p2l) if you want to know why it is like that. - -## Installation - -You can use the [ABP CLI](CLI.md) to install the [Volo.Abp.Dapper](https://www.nuget.org/packages/Volo.Abp.Dapper) package to your project. Execute the following command in the folder of the `.csproj` file that you want to install the package on: - -````bash -abp add-package Volo.Abp.Dapper -```` - -> If you haven't done it yet, you first need to install the ABP CLI. For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Dapper). -> -> If you have a layered solution, it is suggested to install that package to your database layer of the solution. - -## Implement a Dapper Repository - -The best way to interact with Dapper is to create a [repository](Repositories.md) class that abstracts your Dapper database operations. The following example creates a new repository class that works with the `People` table: - -```C# -public class PersonDapperRepository : - DapperRepository, ITransientDependency -{ - public PersonDapperRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public virtual async Task> GetAllPersonNamesAsync() - { - var dbConnection = await GetDbConnectionAsync(); - return (await dbConnection.QueryAsync( - "select Name from People", - transaction: await GetDbTransactionAsync()) - ).ToList(); - } - - public virtual async Task UpdatePersonNamesAsync(string name) - { - var dbConnection = await GetDbConnectionAsync(); - return await dbConnection.ExecuteAsync( - "update People set Name = @NewName", - new { NewName = name }, - await GetDbTransactionAsync() - ); - } -} -``` - -Let's examine this class: - -- It inherits from the `DapperRepository` class, which provides useful methods and properties for database operations. It also implements the `IUnitOfWorkEnabled` interface, so ABP makes the database connection (and transaction if requested) available in the method body by implementing dynamic proxies (a.k.a. interception). -- It gets an `IDbContextProvider` object where `MyAppDbContext` is type of your Entity Framework Core `DbContext` class. It should be configured as explained in the [EF Core document](Entity-Framework-Core.md). If you've created by ABP's startup template, then it should already be configured. -- The `GetAllPersonNamesAsync` and `UpdatePersonNamesAsync` method's been made `virtual`. That's needed to make the interception process working. -- We've used the `GetDbConnectionAsync` and `GetDbTransactionAsync` methods to obtain the current database connection and transaction (that is managed by ABP's [Unit of Work](Unit-Of-Work.md) system). - -Then you can [inject](Dependency-Injection.md) `PersonDapperRepository` to any service to perform these database operations. If you want to implement a layered solution, we suggest to introduce an `IPersonDapperRepository` interface in your domain layer, implement it in your database later, then inject the interface to use the repository service. - -> If you want to learn more details and examples of using Dapper with the ABP Framework, [check this community article](https://community.abp.io/posts/using-dapper-with-the-abp-framework-shp74p2l). - -## See Also - -* [Community Article: Using Dapper with the ABP Framework](https://community.abp.io/posts/using-dapper-with-the-abp-framework-shp74p2l) -* [Entity Framework Core integration document](Entity-Framework-Core.md) diff --git a/docs/en/Dapr/Index.md b/docs/en/Dapr/Index.md deleted file mode 100644 index 464b56ea30..0000000000 --- a/docs/en/Dapr/Index.md +++ /dev/null @@ -1,443 +0,0 @@ -# ABP Dapr Integration - -> This document assumes that you are already familiar with [Dapr](https://dapr.io/) and you want to use it in your ABP based applications. - -[Dapr](https://dapr.io/) (Distributed Application Runtime) provides APIs that simplify microservice connectivity. It is an open source project that is mainly backed by Microsoft. It is also a CNCF (Cloud Native Computing Foundation) project and trusted by the community. - -ABP and Dapr have some intersecting features like service-to-service communication, distributed message bus and distributed locking. However their purposes are totally different. ABP's goal is to provide an end-to-end developer experience by offering an opinionated architecture and providing the necessary infrastructure libraries, reusable modules and tools to implement that architecture properly. Dapr's purpose, on the other hand, is to provide a runtime to decouple common microservice communication patterns from your application logic. - -ABP and Dapr can perfectly work together in the same application. ABP offers some packages to provide better integration where Dapr features intersect with ABP. You can use other Dapr features with no ABP integration packages based on [its own documentation](https://docs.dapr.io/). - -## ABP Dapr Integration Packages - -ABP provides the following NuGet packages for the Dapr integration: - -* [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr): The main Dapr integration package. All other packages depend on this package. -* [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr): Integration package for ABP's [dynamic](../API/Dynamic-CSharp-API-Clients.md) and [static](../API/Static-CSharp-API-Clients.md) C# API Client Proxies systems with Dapr's [service invocation](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/) building block. -* [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr): Implements ABP's distributed event bus with Dapr's [publish & subscribe](https://docs.dapr.io/developing-applications/building-blocks/pubsub/) building block. With this package, you can send events, but can not receive. -* [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus): Provides the endpoints to receive events from Dapr's [publish & subscribe](https://docs.dapr.io/developing-applications/building-blocks/pubsub/) building block. Use this package to send and receive events. -* [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr): Uses Dapr's [distributed lock](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/) building block for [distributed locking](../Distributed-Locking.md) service of the ABP Framework. - -In the following sections, we will see how to use these packages to use Dapr in your ABP based solutions. - -## Basics - -### Installation - -> This section explains how to add [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr), the core Dapr integration package to your project. If you are using one of the other Dapr integration packages, you can skip this section since this package will be indirectly added. - -Use the ABP CLI to add the [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.Dapr` package. -* Run the `abp add-package Volo.Abp.Dapr` command. - -If you want to do it manually, install the [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. - -### AbpDaprOptions - -`AbpDaprOptions` is the main [options class](../Options.md) that you can configure the global Dapr settings with. **All settings are optional and you mostly don't need to configure them.** If you need, you can configure it in the `ConfigureServices` method of your [module class](../Module-Development-Basics.md): - -````csharp -Configure(options => -{ - // ... -}); -```` - -Available properties of the `AbpDaprOptions` class: - -* `HttpEndpoint` (optional): HTTP endpoint that is used while creating a `DaprClient` object. If you don't specify, the default value is used. -* `GrpcEndpoint` (optional): The gRPC endpoint that is used while creating a `DaprClient` object. If you don't specify, the default value is used. -* `DaprApiToken` (optional): The [Dapr API token](https://docs.dapr.io/operations/security/api-token/) that is used while sending requests from the application to Dapr. It is filled from the `DAPR_API_TOKEN` environment variable by default (which is set by Dapr once it is configured). See the *Security* section in this document for details. -* `AppApiToken` (optional): The [App API token](https://docs.dapr.io/operations/security/app-api-token/) that is used to validate requests coming from Dapr. It is filled from the `APP_API_TOKEN` environment variable by default (which is set by Dapr once it is configured). See the *Security* section in this document for details. - -Alternatively, you can configure the options in the `Dapr` section of your `appsettings.json` file. Example: - -````csharp -"Dapr": { - "HttpEndpoint": "http://localhost:3500/" -} -```` - -### IAbpDaprClientFactory - -`IAbpDaprClientFactory` can be used to create `DaprClient` or `HttpClient` objects to perform operations on Dapr. It uses `AbpDaprOptions`, so you can configure the settings in a central place. - -**Example usages:** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAbpDaprClientFactory _daprClientFactory; - - public MyService(IAbpDaprClientFactory daprClientFactory) - { - _daprClientFactory = daprClientFactory; - } - - public async Task DoItAsync() - { - // Create a DaprClient object with default options - DaprClient daprClient = await _daprClientFactory.CreateAsync(); - - /* Create a DaprClient object with configuring - * the DaprClientBuilder object */ - DaprClient daprClient2 = await _daprClientFactory - .CreateAsync(builder => - { - builder.UseDaprApiToken("..."); - }); - - // Create an HttpClient object - HttpClient httpClient = await _daprClientFactory.CreateHttpClientAsync("target-app-id"); - } -} -```` - -`CreateHttpClientAsync` method also gets optional `daprEndpoint` and `daprApiToken` parameters. - -> You can use Dapr API to create client objects in your application. Using `IAbpDaprClientFactory` is recommended, but not required. - -## C# API Client Proxies Integration - -ABP can [dynamically](../API/Dynamic-CSharp-API-Clients.md) or [statically](../API/Static-CSharp-API-Clients.md) generate proxy classes to invoke your HTTP APIs from a Dotnet client application. It makes perfect sense to consume HTTP APIs in a distributed system. The [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) package configures the client-side proxies system, so it uses Dapr's service invocation building block for the communication between your applications. - -### Installation - -Use the ABP CLI to add the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package to your project (to the client side): - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.Http.Client.Dapr` package to. -* Run the `abp add-package Volo.Abp.Http.Client.Dapr` command. - -If you want to do it manually, install the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpHttpClientDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. - -### Configuration - -Once you install the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package, all you need to do is to configure ABP's remote services option either in `appsettings.json` or using the `AbpRemoteServiceOptions` [options class](../Options.md). - -**Example:** - -````csharp -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://dapr-httpapi/" - } - } -} -```` - -`dapr-httpapi` in this example is the application id of the server application in your Dapr configuration. - -The remote service name (`Default` in this example) should match the remote service name specified in the `AddHttpClientProxies` call for dynamic client proxies or the `AddStaticHttpClientProxies` call for static client proxies. Using `Default` is fine if your client communicates to a single server. However, if your client uses multiple servers, you typically have multiple keys in the `RemoteServices` configuration. Once you configure the remote service endpoints as Dapr application ids, it will automatically work and make the HTTP calls through Dapr when you use ABP's client proxy system. - -> See the [dynamic](../API/Dynamic-CSharp-API-Clients.md) and [static](../API/Static-CSharp-API-Clients.md) client proxy documents for details about the ABP's client proxy system. - -## Distributed Event Bus Integration - -[ABP's distributed event bus](../Distributed-Event-Bus.md) system provides a convenient abstraction to allow applications to communicate asynchronously via events. ABP has integration packages with various distributed messaging systems, like RabbitMQ, Kafka, and Azure. Dapr also has a [publish & subscribe building block](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/) for the same purpose: distributed messaging / events. - -ABP's [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) and [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) packages make it possible to use the Dapr infrastructure for ABP's distributed event bus. - -The [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package can be used by any type of application (e.g., a Console or ASP.NET Core application) to publish events through Dapr. To be able to receive messages (by subscribing to events), you need to have the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package installed, and your application should be an ASP.NET Core application. - -### Installation - -If your application is an ASP.NET Core application and you want to send and receive events, you need to install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package as described below: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.AspNetCore.Mvc.Dapr.EventBus` package to. -* Run the `abp add-package Volo.Abp.AspNetCore.Mvc.Dapr.EventBus` command. - -If you want to do it manually, install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) NuGet package to your project and add `[DependsOn(typeof(AbpAspNetCoreMvcDaprEventBusModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. - -> **If you install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package, you don't need to install the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package, because the first one already has a reference to the latter one.** - -If your application is not an ASP.NET Core application, you can't receive events from Dapr, at least with ABP's integration packages (see [Dapr's document](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/) if you want to receive events in a different type of application). However, you can still publish messages using the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package. In this case, follow the steps below to install that package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Dapr` package to. -* Run the `abp add-package Volo.Abp.EventBus.Dapr` command. - -If you want to do it manually, install the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. - -### Configuration - -You can configure the `AbpDaprEventBusOptions` [options class](../Options.md) for Dapr configuration: - -````csharp -Configure(options => -{ - options.PubSubName = "pubsub"; -}); -```` - -Available properties of the `AbpDaprEventBusOptions` class: - -* `PubSubName` (optional): The `pubsubName` parameter while publishing messages through the `DaprClient.PublishEventAsync` method. Default value: `pubsub`. - -### The ABP Subscription Endpoints - -ABP provides the following endpoints to receive events from Dapr: - -* `dapr/subscribe`: Dapr uses this endpoint to get a list of subscriptions from the application. ABP automatically returns all the subscriptions for your distributed event handler classes and custom controller actions with the `Topic` attribute. -* `api/abp/dapr/event`: The unified endpoint to receive all the events from Dapr. ABP dispatches the events to your event handlers based on the topic name. - -> **Since ABP will call `MapSubscribeHandler` internally, you should not manually call it anymore.** You can use the `app.UseCloudEvents()` middleware in your ASP.NET Core pipeline if you want to support the [CloudEvents](https://cloudevents.io/) standard. - -### Usage - -#### The ABP Way - -You can follow [ABP's distributed event bus documentation](../Distributed-Event-Bus.md) to learn how to publish and subscribe to events in the ABP way. No change required in your application code to use Dapr pub-sub. ABP will automatically subscribe to Dapr for your event handler classes (that implement the `IDistributedEventHandler` interface). - -ABP provides `api/abp/dapr/event` - -**Example: Publish an event using the `IDistributedEventBus` service** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IDistributedEventBus _distributedEventBus; - - public MyService(IDistributedEventBus distributedEventBus) - { - _distributedEventBus = distributedEventBus; - } - - public async Task DoItAsync() - { - await _distributedEventBus.PublishAsync(new StockCountChangedEto - { - ProductCode = "AT837234", - NewStockCount = 42 - }); - } -} -```` - -**Example: Subscribe to an event by implementing the `IDistributedEventHandler` interface** - -````csharp -public class MyHandler : - IDistributedEventHandler, - ITransientDependency -{ - public async Task HandleEventAsync(StockCountChangedEto eventData) - { - var productCode = eventData.ProductCode; - // ... - } -} -```` - -See [ABP's distributed event bus documentation](../Distributed-Event-Bus.md) to learn the details. - -#### Using the Dapr API - -In addition to ABP's standard distributed event bus system, you can also use Dapr's API to publish events. - -> If you directly use the Dapr API to publish events, you may not benefit from ABP's standard distributed event bus features, like the outbox/inbox pattern implementation. - -**Example: Publish an event using `DaprClient`** - -````csharp -public class MyService : ITransientDependency -{ - private readonly DaprClient _daprClient; - - public MyService(DaprClient daprClient) - { - _daprClient = daprClient; - } - - public async Task DoItAsync() - { - await _daprClient.PublishEventAsync( - "pubsub", // pubsub name - "StockChanged", // topic name - new StockCountChangedEto // event data - { - ProductCode = "AT837234", - NewStockCount = 42 - } - ); - } -} -```` - -**Example: Subscribe to an event by creating an ASP.NET Core controller** - -````csharp -public class MyController : AbpController -{ - [HttpPost("/stock-changed")] - [Topic("pubsub", "StockChanged")] - public async Task TestRouteAsync([FromBody] StockCountChangedEto model) - { - HttpContext.ValidateDaprAppApiToken(); - - // Do something with the event - return Ok(); - } -} -```` - -`HttpContext.ValidateDaprAppApiToken()` extension method is provided by ABP to check if the request is coming from Dapr. This is optional. You should configure Dapr to send the App API token to your application if you want to enable the validation. If not configured, `ValidateDaprAppApiToken()` does nothing. See [Dapr's App API Token document](https://docs.dapr.io/operations/security/app-api-token/) for more information. Also see the *AbpDaprOptions* and *Security* sections in this document. - -See the [Dapr documentation](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/publish-subscribe) to learn the details of sending & receiving events with the Dapr API. - -## Distributed Lock - -> Dapr's distributed lock feature is currently in the Alpha stage and may not be stable yet. It is not suggested to replace ABP's distributed lock with Dapr in that point. - -ABP provides a [Distributed Locking](../Distributed-Locking.md) abstraction to control access to a shared resource by multiple applications. Dapr also has a [distributed lock building block](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/). The [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) package makes ABP use Dapr's distributed locking system. - -### Installation - -Use the ABP CLI to add the [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) NuGet package to your project (to the client side): - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.DistributedLocking.Dapr` package to. -* Run the `abp add-package Volo.Abp.DistributedLocking.Dapr` command. - -If you want to do it manually, install the [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpDistributedLockingDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. - -### Configuration - -You can use the `AbpDistributedLockDaprOptions` options class in the `ConfigureServices` method of [your module](../Module-Development-Basics.md) to configure the Dapr distributed lock: - -````csharp -Configure(options => -{ - options.StoreName = "mystore"; -}); -```` - -The following options are available: - -* **`StoreName`** (required): The store name used by Dapr. Lock key names are scoped in the same store. That means different applications can acquire the same lock name in different stores. Use the same store name for the same resources you want to control the access of. -* `Owner` (optional): The `owner` value used by the `DaprClient.Lock` method. If you don't specify, ABP uses a random value, which is fine in general. -* `DefaultExpirationTimeout` (optional): Default value of the time after which the lock gets expired. Default value: 2 minutes. - -### Usage - -You can inject and use the `IAbpDistributedLock` service, just like explained in the [Distributed Locking document](../Distributed-Locking.md). - -**Example:** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAbpDistributedLock _distributedLock; - - public MyService(IAbpDistributedLock distributedLock) - { - _distributedLock = distributedLock; - } - - public async Task MyMethodAsync() - { - await using (var handle = - await _distributedLock.TryAcquireAsync("MyLockName")) - { - if (handle != null) - { - // your code that access the shared resource - } - } - } -} -```` - -There are two points we should mention about the `TryAcquireAsync` method, as different from ABP's standard usage: - -* The `timeout` parameter is currently not used (even if you specify it), because Dapr doesn't support waiting to obtain the lock. -* Dapr uses the expiration timeout system (that means the lock is automatically released after that timeout even if you don't release the lock by disposing the handler). However, ABP's `TryAcquireAsync` method has no such a parameter. Currently, you can set `AbpDistributedLockDaprOptions.DefaultExpirationTimeout` as a global value in your application. - -As mentioned first, Dapr's distributed lock feature is currently in the Alpha stage and its API is a candidate to change. You should use it as is if you want, but be ready for the changes in the future. For now, we are recommending to use the [DistributedLock](https://github.com/madelson/DistributedLock) library as explained in ABP's [Distributed Locking document](../Distributed-Locking.md). - -## Security - -If you are using Dapr, most or all the incoming and outgoing requests in your application pass through Dapr. Dapr uses two kinds of API tokens to secure the communication between your application and Dapr. - -### Dapr API Token - -> This token is automatically set by default and generally you don't care about it. - -The [Enable API token authentication in Dapr](https://docs.dapr.io/operations/security/api-token/) document describes what the Dapr API token is and how it is configured. Please read that document if you want to enable it for your application. - -If you enable the Dapr API token, you should send that token in every request to Dapr from your application. `AbpDaprOptions` defines a `DaprApiToken` property as a central point to configure the Dapr API token in your application. - -The default value of the `DaprApiToken` property is set from the `DAPR_API_TOKEN` environment variable and that environment variable is set by Dapr when it runs. So, most of the time, you don't need to configure `AbpDaprOptions.DaprApiToken` in your application. However, if you need to configure (or override) it, you can do in the `ConfigureServices` method of your module class as shown in the following code block: - -````csharp -Configure(options => -{ - options.DaprApiToken = "..."; -}); -```` - -Or you can set it in your `appsettings.json` file: - -````json -"Dapr": { - "DaprApiToken": "..." -} -```` - -Once you set it, it is used when you use `IAbpDaprClientFactory`. If you need that value in your application, you can inject `IDaprApiTokenProvider` and use its `GetDaprApiToken()` method. - -### App API Token - -> Enabling App API token validation is strongly recommended. Otherwise, for example, any client can directly call your event subscription endpoint, and your application acts like an event has occurred (if there is no other security policy in your event subscription endpoint). - -The [Authenticate requests from Dapr using token authentication](https://docs.dapr.io/operations/security/app-api-token/) document describes what the App API token is and how it is configured. Please read that document if you want to enable it for your application. - -If you enable the App API token, you can validate it to ensure that the request is coming from Dapr. ABP provides useful shortcuts to validate it. - -**Example: Validate the App API token in an event handling HTTP API** - -````csharp -public class MyController : AbpController -{ - [HttpPost("/stock-changed")] - [Topic("pubsub", "StockChanged")] - public async Task TestRouteAsync([FromBody] StockCountChangedEto model) - { - // Validate the App API token! - HttpContext.ValidateDaprAppApiToken(); - - // Do something with the event - return Ok(); - } -} -```` - -`HttpContext.ValidateDaprAppApiToken()` is an extension method provided by the ABP Framework. It throws an `AbpAuthorizationException` if the token was missing or wrong in the HTTP header (the header name is `dapr-api-token`). You can also inject `IDaprAppApiTokenValidator` and use its methods to validate the token in any service (not only in a controller class). - -You can configure `AbpDaprOptions.AppApiToken` if you want to set (or override) the App API token value. The default value is set by the `APP_API_TOKEN` environment variable. You can change it in the `ConfigureServices` method of your module class as shown in the following code block: - -````csharp -Configure(options => -{ - options.AppApiToken = "..."; -}); -```` - -Or you can set it in your `appsettings.json` file: - -````json -"Dapr": { - "AppApiToken": "..." -} -```` - -If you need that value in your application, you can inject `IDaprApiTokenProvider` and use its `GetAppApiToken()` method. - -## See Also - -* [Dapr for .NET Developers](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/) -* [The Official Dapr Documentation](https://docs.dapr.io/) diff --git a/docs/en/Data-Access.md b/docs/en/Data-Access.md deleted file mode 100644 index 7ba85956b9..0000000000 --- a/docs/en/Data-Access.md +++ /dev/null @@ -1,13 +0,0 @@ -# Data Access - -ABP framework was designed as database agnostic. It can work any type of data source by the help of the [repository](Repositories.md) and [unit of work](Unit-Of-Work.md) abstractions. Currently, the following providers are implemented as official: - -* [Entity Framework Core](Entity-Framework-Core.md) (works with [various DBMS and providers](https://docs.microsoft.com/en-us/ef/core/providers/).) -* [MongoDB](MongoDB.md) -* [Dapper](Dapper.md) - -## See Also - -* [Connection Strings](Connection-Strings.md) -* [Data Seeding](Data-Seeding.md) -* [Data Filtering](Data-Filtering.md) \ No newline at end of file diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md deleted file mode 100644 index 830979e710..0000000000 --- a/docs/en/Data-Filtering.md +++ /dev/null @@ -1,318 +0,0 @@ -# Data Filtering - -[Volo.Abp.Data](https://www.nuget.org/packages/Volo.Abp.Data) package defines services to automatically filter data on querying from a database. - -## Pre-Defined Filters - -ABP defines some filters out of the box. - -### ISoftDelete - -Used to mark an [entity](Entities.md) as deleted instead of actually deleting it. Implement the `ISoftDelete` interface to make your entity "soft delete". - -Example: - -````csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities; - -namespace Acme.BookStore -{ - public class Book : AggregateRoot, ISoftDelete - { - public string Name { get; set; } - - public bool IsDeleted { get; set; } //Defined by ISoftDelete - } -} -```` - -`ISoftDelete` defines the `IsDeleted` property. When you delete a book using [repositories](Repositories.md), ABP automatically sets `IsDeleted` to true and protects it from actual deletion (you can also manually set the `IsDeleted` property to true if you need). In addition, it **automatically filters deleted entities** when you query the database. - -> `ISoftDelete` filter is enabled by default and you can not get deleted entities from database unless you explicitly disable it. See the `IDataFilter` service below. - -> Soft-delete entities can be hard-deleted when you use `HardDeleteAsync` method on the repositories. - -### IMultiTenant - -[Multi-tenancy](Multi-Tenancy.md) is an efficient way of creating SaaS applications. Once you create a multi-tenant application, you typically want to isolate data between tenants. Implement `IMultiTenant` interface to make your entity "multi-tenant aware". - -Example: - -````csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities; -using Volo.Abp.MultiTenancy; - -namespace Acme.BookStore -{ - public class Book : AggregateRoot, ISoftDelete, IMultiTenant - { - public string Name { get; set; } - - public bool IsDeleted { get; set; } //Defined by ISoftDelete - - public Guid? TenantId { get; set; } //Defined by IMultiTenant - } -} -```` - -`IMultiTenant` interface defines the `TenantId` property which is then used to automatically filter the entities for the current tenant. See the [Multi-tenancy](Multi-Tenancy.md) document for more. - -## IDataFilter Service: Enable/Disable Data Filters - -You can control the filters using `IDataFilter` service. - -Example: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class MyBookService : ITransientDependency - { - private readonly IDataFilter _dataFilter; - private readonly IRepository _bookRepository; - - public MyBookService( - IDataFilter dataFilter, - IRepository bookRepository) - { - _dataFilter = dataFilter; - _bookRepository = bookRepository; - } - - public async Task> GetAllBooksIncludingDeletedAsync() - { - //Temporary disable the ISoftDelete filter - using (_dataFilter.Disable()) - { - return await _bookRepository.GetListAsync(); - } - } - } -} -```` - -* [Inject](Dependency-Injection.md) the `IDataFilter` service to your class. -* Use the `Disable` method within a `using` statement to create a code block where the `ISoftDelete` filter is disabled inside it. - -In addition to the `Disable()` method; - -* `IDataFilter.Enable()` method can be used to enable a filter. `Enable` and `Disable` methods can be used in a **nested** way to define inner scopes. - -* `IDataFilter.IsEnabled()` can be used to check whether a filter is currently enabled or not. - -> Always use the `Disable` and `Enable` methods it inside a `using` block to guarantee that the filter is reset to its previous state. - -### The Generic IDataFilter Service - -`IDataFilter` service has a generic version, `IDataFilter` that injects a more restricted and explicit data filter based on the filter type. - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class MyBookService : ITransientDependency - { - private readonly IDataFilter _softDeleteFilter; - private readonly IRepository _bookRepository; - - public MyBookService( - IDataFilter softDeleteFilter, - IRepository bookRepository) - { - _softDeleteFilter = softDeleteFilter; - _bookRepository = bookRepository; - } - - public async Task> GetAllBooksIncludingDeletedAsync() - { - //Temporary disable the ISoftDelete filter - using (_softDeleteFilter.Disable()) - { - return await _bookRepository.GetListAsync(); - } - } - } -} -```` - -* This usage determines the filter type while injecting the `IDataFilter` service. -* In this case you can use the `Disable()` and `Enable()` methods without specifying the filter type. - -## AbpDataFilterOptions - -`AbpDataFilterOptions` can be used to [set options](Options.md) for the data filter system. - -The example code below disables the `ISoftDelete` filter by default which will cause to include deleted entities when you query the database unless you explicitly enable the filter: - -````csharp -Configure(options => -{ - options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false); -}); -```` - -> Carefully change defaults for global filters, especially if you are using a pre-built module which might be developed assuming the soft delete filter is turned on by default. But you can do it for your own defined filters safely. - -## Defining Custom Filters - -Defining and implementing a new filter highly depends on the database provider. ABP implements all pre-defined filters for all database providers. - -When you need it, start by defining an interface (like `ISoftDelete` and `IMultiTenant`) for your filter and implement it for your entities. - -Example: - -````csharp -public interface IIsActive -{ - bool IsActive { get; } -} -```` - -Such an `IIsActive` interface can be used to filter active/passive data and can be easily implemented by any [entity](Entities.md): - -````csharp -public class Book : AggregateRoot, IIsActive -{ - public string Name { get; set; } - - public bool IsActive { get; set; } //Defined by IIsActive -} -```` - -### EntityFramework Core - -ABP uses [EF Core's Global Query Filters](https://docs.microsoft.com/en-us/ef/core/querying/filters) system for the [EF Core Integration](Entity-Framework-Core.md). So, it is well integrated to EF Core and works as expected even if you directly work with `DbContext`. - -Best way to implement a custom filter is to override `ShouldFilterEntity` and `CreateFilterExpression` method for your `DbContext`. Example: - -````csharp -protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; - -protected override bool ShouldFilterEntity(IMutableEntityType entityType) -{ - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) - { - return true; - } - - return base.ShouldFilterEntity(entityType); -} - -protected override Expression> CreateFilterExpression() -{ - var expression = base.CreateFilterExpression(); - - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) - { - Expression> isActiveFilter = - e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); - expression = expression == null - ? isActiveFilter - : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); - } - - return expression; -} -```` - -* Added a `IsActiveFilterEnabled` property to check if `IIsActive` is enabled or not. It internally uses the `IDataFilter` service introduced before. -* Overrided the `ShouldFilterEntity` and `CreateFilterExpression` methods, checked if given entity implements the `IIsActive` interface and combines the expressions if necessary. - -In addition you can also use `HasAbpQueryFilter` to set a filter for an entity. It will combine your filter with ABP EF Core builtin global query filters. - -````csharp -protected override void OnModelCreating(ModelBuilder modelBuilder) -{ - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); - }); -} -```` - -### MongoDB - -ABP abstracts the `IMongoDbRepositoryFilterer` interface to implement data filtering for the [MongoDB Integration](MongoDB.md), it works only if you use the repositories properly. Otherwise, you should manually filter the data. - -Currently, the best way to implement a data filter for the MongoDB integration is to create a derived class of `MongoDbRepositoryFilterer` and override `FilterQueryable`. Example: - -````csharp -[ExposeServices(typeof(IMongoDbRepositoryFilterer))] -public class BookMongoDbRepositoryFilterer : MongoDbRepositoryFilterer , ITransientDependency -{ - public BookMongoDbRepositoryFilterer( - IDataFilter dataFilter, - ICurrentTenant currentTenant) : - base(dataFilter, currentTenant) - { - } - - public override TQueryable FilterQueryable(TQueryable query) - { - if (DataFilter.IsEnabled()) - { - return (TQueryable)query.Where(x => x.IsActive); - } - - return base.FilterQueryable(query); - } -} -```` - -This example implements it only for the `Book` entity. If you want to implement for all entities (those implement the `IIsActive` interface), create your own custom MongoDB repository filterer base class and override the `AddGlobalFilters` as shown below: - -````csharp -public abstract class MyMongoRepository : MongoDbRepository - where TMongoDbContext : IAbpMongoDbContext - where TEntity : class, IEntity -{ - protected MyMongoRepository(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - protected override void AddGlobalFilters(List> filters) - { - base.AddGlobalFilters(filters); - - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)) - && DataFilter.IsEnabled()) - { - filters.Add(Builders.Filter.Eq(e => ((IIsActive)e).IsActive, true)); - } - } -} - - -public class MyMongoDbModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //....... - context.Services - .Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>))); - } -} -```` \ No newline at end of file diff --git a/docs/en/Data-Seeding.md b/docs/en/Data-Seeding.md deleted file mode 100644 index 011a5bf37a..0000000000 --- a/docs/en/Data-Seeding.md +++ /dev/null @@ -1,176 +0,0 @@ -# Data Seeding - -## Introduction - -Some applications (or modules) using a database may need to have some **initial data** to be able to properly start and run. For example, an **admin user** & roles must be available at the beginning. Otherwise you can not **login** to the application to create new users and roles. - -Data seeding is also useful for [testing](Testing.md) purpose, so your automatic tests can assume some initial data available in the database. - -### Why a Data Seed System? - -While EF Core Data Seeding system provides a way, it is very limited and doesn't cover production scenarios. Also, it is only for EF Core. - -ABP Framework provides a data seed system that is; - -* **Modular**: Any [module](Module-Development-Basics.md) can silently contribute to the data seeding process without knowing and effecting each other. In this way, a module seeds its own initial data. -* **Database Independent**: It is not only for EF Core, it also works for other database providers (like [MongoDB](MongoDB.md)). -* **Production Ready**: It solves the problems on production environments. See the "*On Production*" section below. -* **Dependency Injection**: It takes the full advantage of dependency injection, so you can use any internal or external service while seeding the initial data. Actually, you can do much more than data seeding. - -## IDataSeedContributor - -`IDataSeedContributor` is the interface that should be implemented in order to seed data to the database. - -**Example: Seed one initial book to the database if there is no book** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - private readonly ICurrentTenant _currentTenant; - - public BookStoreDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator, - ICurrentTenant currentTenant) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - _currentTenant = currentTenant; - } - - public async Task SeedAsync(DataSeedContext context) - { - using (_currentTenant.Change(context?.TenantId)) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - var book = new Book( - id: _guidGenerator.Create(), - name: "The Hitchhiker's Guide to the Galaxy", - type: BookType.ScienceFiction, - publishDate: new DateTime(1979, 10, 12), - price: 42 - ); - - await _bookRepository.InsertAsync(book); - } - } - } -} -```` - -* `IDataSeedContributor` defines the `SeedAsync` method to execute the **data seed logic**. -* It is typical to **check database** if the seeding data is already present. -* You can **inject** service and perform any logic needed to seed the data. - -> Data seed contributors are automatically discovered by the ABP Framework and executed as a part of the data seed process. - -### DataSeedContext - -`DataSeedContext` contains `TenantId` if your application is [multi-tenant](Multi-Tenancy.md), so you can use this value while inserting data or performing custom logic based on the tenant. - -`DataSeedContext` also contains name-value style configuration parameters for passing to the seeder contributors from the `IDataSeeder`. - -## Modularity - -An application can have multiple data seed contributor (`IDataSeedContributor`) class. So, any reusable module can also implement this interface to seed its own initial data. - -For example, the [Identity Module](Modules/Identity.md) has a data seed contributor that creates an admin role and admin user and assign all the permissions. - -## IDataSeeder - -> You typically never need to directly use the `IDataSeeder` service since it is already done if you've started with the [application startup template](Startup-Templates/Application.md). But its suggested to read it to understand the design behind the data seed system. - -`IDataSeeder` is the main service that is used to seed initial data. It is pretty easy to use; - -````csharp -public class MyService : ITransientDependency -{ - private readonly IDataSeeder _dataSeeder; - - public MyService(IDataSeeder dataSeeder) - { - _dataSeeder = dataSeeder; - } - - public async Task FooAsync() - { - await _dataSeeder.SeedAsync(); - } -} -```` - -You can [inject](Dependency-Injection.md) the `IDataSeeder` and use it to seed the initial data when you need. It internally calls all the `IDataSeedContributor` implementations to complete the data seeding. - -It is possible to send named configuration parameters to the `SeedAsync` method as shown below: - -````csharp -await _dataSeeder.SeedAsync( - new DataSeedContext() - .WithProperty("MyProperty1", "MyValue1") - .WithProperty("MyProperty2", 42) -); -```` - -Then the data seed contributors can access to these properties via the `DataSeedContext` explained before. - -If a module needs to a parameter, it should be declared on the [module documentation](Modules/Index.md). For example, the [Identity Module](Modules/Identity.md) can use `AdminEmail` and `AdminPassword` parameters if you provide (otherwise uses the default values). - -### Separate Unit Of Works - -The default seed will be in a unit of work and may use transactions. If there are multiple `IDataSeedContributor` or too much data written, it may cause a database timeout error. - -We provide an extension method of `SeedInSeparateUowAsync` for the `IDataSeeder` service to create a separate unit of work for each `IDataSeedContributor`. - -````csharp -public static Task SeedInSeparateUowAsync(this IDataSeeder seeder, Guid? tenantId = null, AbpUnitOfWorkOptions options = null, bool requiresNew = false) -```` - -### Where & How to Seed Data? - -It is important to understand where & how to execute the `IDataSeeder.SeedAsync()`? - -#### On Production - -The [application startup template](Startup-Templates/Application.md) comes with a *YourProjectName***.DbMigrator** project (Acme.BookStore.DbMigrator on the picture below), which is a **console application** that is responsible to **migrate** the database schema (for relational databases) and **seed** the initial data: - -![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) - -This console application is properly configured for you. It even supports **multi-tenant** scenarios where each tenant has its own database (migrates & seeds all necessary databases). - -It is expected to run this DbMigrator application whenever you **deploy a new version** of your solution to the server. It will migrate your **database schema** (create new tables/fields... etc.) and **seed new initial data** needed to properly run the new version of your solution. Then you can deploy/start your actual application. - -Even if you are using MongoDB or another NoSQL database (that doesn't need to schema migrations), it is recommended to use the DbMigrator application to seed your data or perform your data migration. - -Having such a separate console application has several advantages; - -* You can **run it before** updating your application, so your application will run on the ready database. -* Your application **starts faster** compared to if it seeds the initial data itself. -* Your application can properly run on a **clustered environment** (where multiple instances of your application run concurrently). If you seed data on application startup you would have conflicts in this case. - -#### On Development - -We suggest the same way on development. Run the DbMigrator console application whenever you [create a database migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/) (using EF Core `Add-Migration` command, for example) or change the data seed code (will be explained later). - -> You can continue to use the standard `Update-Database` command for EF Core, but it will not seed if you've created a new seed data. - -#### On Testing - -You probably want to seed the data also for automated [testing](Testing.md), so want to use the `IDataSeeder.SeedAsync()`. In the [application startup template](Startup-Templates/Application.md), it is done in the [OnApplicationInitialization](Module-Development-Basics.md) method of the *YourProjectName*TestBaseModule class of the TestBase project. - -In addition to the standard seed data (that is also used on production), you may want to seed additional data unique to the automated tests. If so, you can create a new data seed contributor in the test project to have more data to work on. diff --git a/docs/en/Data-Transfer-Objects.md b/docs/en/Data-Transfer-Objects.md deleted file mode 100644 index 45533cca72..0000000000 --- a/docs/en/Data-Transfer-Objects.md +++ /dev/null @@ -1,284 +0,0 @@ -# Data Transfer Objects - -## Introduction - -**Data Transfer Objects** (DTO) are used to transfer data between the **Application Layer** and the **Presentation Layer** or other type of clients. - -Typically, an [application service](Application-Services.md) is called from the presentation layer (optionally) with a **DTO** as the parameter. It uses domain objects to **perform some specific business logic** and (optionally) returns a DTO back to the presentation layer. Thus, the presentation layer is completely **isolated** from domain layer. - -### The Need for DTOs - -> **You can skip this section** if you feel that you know and confirm the benefits of using DTOs. - -At first, creating a DTO class for each application service method can be seen as tedious and time-consuming work. However, they can save your application if you correctly use them. Why & how? - -#### Abstraction of the Domain Layer - -DTOs provide an efficient way of **abstracting domain objects** from the presentation layer. In effect, your **layers** are correctly separated. If you want to change the presentation layer completely, you can continue with the existing application and domain layers. Alternatively, you can re-write your domain layer, completely change the database schema, entities and O/RM framework, all without changing the presentation layer. This, of course, is as long as the contracts (method signatures and DTOs) of your application services remain unchanged. - -#### Data Hiding - -Say you have a `User` entity with the properties Id, Name, EmailAddress and Password. If a `GetAllUsers()` method of a `UserAppService` returns a `List`, anyone can access the passwords of all your users, even if you do not show it on the screen. It's not just about security, it's about data hiding. Application services should return only what it needs by the presentation layer (or client). Not more, not less. - -#### Serialization & Lazy Load Problems - -When you return data (an object) to the presentation layer, it's most likely serialized. For example, in a REST API that returns JSON, your object will be serialized to JSON and sent to the client. Returning an Entity to the presentation layer can be problematic in that regard, especially if you are using a relational database and an ORM provider like Entity Framework Core. How? - -In a real-world application, your entities may have references to each other. The `User` entity can have a reference to it's `Role`s. If you want to serialize `User`, its `Role`s are also serialized. The `Role` class may have a `List` and the `Permission` class can has a reference to a `PermissionGroup` class and so on... Imagine all of these objects being serialized at once. You could easily and accidentally serialize your whole database! Also, if your objects have circular references, they may **not** be serialized at all. - -What's the solution? Marking properties as `NonSerialized`? No, you can not know when it should be serialized and when it shouldn't be. It may be needed in one application service method, and not needed in another. Returning safe, serializable, and specially designed DTOs is a good choice in this situation. - -Almost all O/RM frameworks support lazy-loading. It's a feature that loads entities from the database when they're needed. Say a `User` class has a reference to a `Role` class. When you get a `User` from the database, the `Role` property (or collection) is not filled. When you first read the `Role` property, it's loaded from the database. So, if you return such an Entity to the presentation layer, it will cause it to retrieve additional entities from the database by executing additional queries. If a serialization tool reads the entity, it reads all properties recursively and again your whole database can be retrieved (if there are relations between entities). - -More problems can arise if you use Entities in the presentation layer. **It's best not to reference the domain/business layer assembly in the presentation layer.** - -If you are convinced about using DTOs, we can continue to what ABP Framework provides and suggests about DTOs. - -> ABP doesn't force you to use DTOs, however using DTOs is **strongly suggested as a best practice**. - -## Standard Interfaces & Base Classes - -A DTO is a simple class that has no dependency and you can design it in any way. However, ABP introduces some **interfaces** to determine the **conventions** for naming **standard properties** and **base classes** to **don't repeat yourself** while declaring **common properties**. - -**None of them are required**, but using them **simplifies and standardizes** your application code. - -### Entity Related DTOs - -You typically create DTOs corresponding to your entities, which results similar classes to your entities. ABP Framework provides some base classes to simplify while creating such DTOs. - -#### EntityDto - -`IEntityDto` is a simple interface that only defines an `Id` property. You can implement it or inherit from the `EntityDto` for your DTOs that matches to an [entity](Entities.md). - -**Example:** - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace AbpDemo -{ - public class ProductDto : EntityDto - { - public string Name { get; set; } - //... - } -} -```` - -#### Audited DTOs - -If your entity inherits from audited entity classes (or implements auditing interfaces), you can use the following base classes to create your DTOs: - -* `CreationAuditedEntityDto` -* `CreationAuditedEntityWithUserDto` -* `AuditedEntityDto` -* `AuditedEntityWithUserDto` -* `FullAuditedEntityDto` -* `FullAuditedEntityWithUserDto` - -#### Extensible DTOs - -If you want to use the [object extension system](Object-Extensions.md) for your DTOs, you can use or inherit from the following DTO classes: - -* `ExtensibleObject` implements the `IHasExtraProperties` (other classes inherits this class). -* `ExtensibleEntityDto` -* `ExtensibleCreationAuditedEntityDto` -* `ExtensibleCreationAuditedEntityWithUserDto` -* `ExtensibleAuditedEntityDto` -* `ExtensibleAuditedEntityWithUserDto` -* `ExtensibleFullAuditedEntityDto` -* `ExtensibleFullAuditedEntityWithUserDto` - -### List Results - -It is common to return a list of DTOs to the client. `IListResult` interface and `ListResultDto` class is used to make it standard. - -The definition of the `IListResult` interface: - -````csharp -public interface IListResult -{ - IReadOnlyList Items { get; set; } -} -```` - -**Example: Return a list of products** - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - - public ProductAppService(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task> GetListAsync() - { - //Get entities from the repository - List products = await _productRepository.GetListAsync(); - - //Map entities to DTOs - List productDtos = - ObjectMapper.Map, List>(products); - - //Return the result - return new ListResultDto(productDtos); - } - } -} -```` - -You could simply return the `productDtos` object (and change the method return type) and it has nothing wrong. Returning a `ListResultDto` makes your `List` wrapped into another object as an `Items` property. This has one advantage: You can later add more properties to your return value without breaking your remote clients (when they get the value as a JSON result). So, it is especially suggested when you are developing reusable application modules. - -### Paged & Sorted List Results - -It is more common to request a paged list from server and return a paged list to the client. ABP defines a few interface and classes to standardize it: - -#### Input (Request) Types - -The following interfaces and classes is to standardize the input sent by the clients. - -* `ILimitedResultRequest`: Defines a `MaxResultCount` (`int`) property to request a limited result from the server. -* `IPagedResultRequest`: Inherits from the `ILimitedResultRequest` (so it inherently has the `MaxResultCount` property) and defines a `SkipCount` (`int`) to declare the skip count while requesting a paged result from the server. -* `ISortedResultRequest`: Defines a `Sorting` (`string`) property to request a sorted result from the server. Sorting value can be "*Name*", "*Name DESC*", "*Name ASC, Age DESC*"... etc. -* `IPagedAndSortedResultRequest` inherits from both of the `IPagedResultRequest` and `ISortedResultRequest`, so has `MaxResultCount`, `SkipCount` and `Sorting` properties. - -Instead of implementing the interfaces manually, it is suggested to inherit one of the following base DTO classes: - -* `LimitedResultRequestDto` implements `ILimitedResultRequest`. -* `PagedResultRequestDto` implements `IPagedResultRequest` (and inherits from the `LimitedResultRequestDto`). -* `PagedAndSortedResultRequestDto` implements `IPagedAndSortedResultRequest` (and inherit from the `PagedResultRequestDto`). - -##### Max Result Count - -`LimitedResultRequestDto` (and inherently the others) limits and validates the `MaxResultCount` by the following rules; - -* If the client doesn't set `MaxResultCount`, it is assumed as **10** (the default page size). This value can be changed by setting the `LimitedResultRequestDto.DefaultMaxResultCount` static property. -* If the client sends `MaxResultCount` greater than **1,000**, it produces a **validation error**. It is important to protect the server from abuse of the service. If you want, you can change this value by setting the `LimitedResultRequestDto.MaxMaxResultCount` static property. - -Static properties suggested to be set on application startup since they are static (global). - -#### Output (Response) Types - -The following interfaces and classes is to standardize the output sent to the clients. - -* `IHasTotalCount` defines a `TotalCount` (`long`) property to return the total count of the records in case of paging. -* `IPagedResult` inherits from the `IListResult` and `IHasTotalCount`, so it has the `Items` and `TotalCount` properties. - -Instead of implementing the interfaces manually, it is suggested to inherit one of the following base DTO classes: - -* `PagedResultDto` inherits from the `ListResultDto` and also implements the `IPagedResult`. - -**Example: Request a paged & sorted result from server and return a paged list** - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - - public ProductAppService(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task> GetListAsync( - PagedAndSortedResultRequestDto input) - { - //Create the query - var query = _productRepository - .OrderBy(input.Sorting); - - //Get total count from the repository - var totalCount = await query.CountAsync(); - - //Get entities from the repository - List products = await query - .Skip(input.SkipCount) - .Take(input.MaxResultCount).ToListAsync(); - - //Map entities to DTOs - List productDtos = - ObjectMapper.Map, List>(products); - - //Return the result - return new PagedResultDto(totalCount, productDtos); - } - } -} -```` - -ABP Framework also defines a `PageBy` extension method (that is compatible with the `IPagedResultRequest`) that can be used instead of `Skip` + `Take` calls: - -````csharp -var query = _productRepository - .OrderBy(input.Sorting) - .PageBy(input); -```` - -> Notice that we added `Volo.Abp.EntityFrameworkCore` package to the project to be able to use the `ToListAsync` and `CountAsync` methods since they are not included in the standard LINQ, but defined by the Entity Framework Core. - -See also the [repository documentation](Repositories.md) to if you haven't understood the example code. - -## Related Topics - -### Validation - -Inputs of [application service](Application-Services.md) methods, controller actions, page model inputs... are automatically validated. You can use the standard data annotation attributes or a custom validation method to perform the validation. - -See the [validation document](Validation.md) for more. - -### Object to Object Mapping - -When you create a DTO that is related to an entity, you generally need to map these objects. ABP provides an object to object mapping system to simplify the mapping process. See the following documents: - -* [Object to Object Mapping document](Object-To-Object-Mapping.md) covers all the features. -* [Application Services document](Application-Services.md) provides a full example. - -## Best Practices - -You are free to design your DTO classes. However, there are some best practices & suggestions that you may want to follow. - -### Common Principles - -* DTOs should be **well serializable** since they are generally serialized and deserialized (to JSON or other format). It is suggested to have an empty (parameterless) public constructor if you have another constructor with parameter(s). -* DTOs **should not contain any business logic**, except some formal [validation](Validation.md) code. -* Do not inherit DTOs from entities and **do not reference to entities**. The [application startup template](Startup-Templates/Application.md) already prevents it by separating the projects. -* If you use an auto [object to object mapping](Object-To-Object-Mapping.md) library, like AutoMapper, enable the **mapping configuration validation** to prevent potential bugs. - -### Input DTO Principles - -* Define only the **properties needed** for the use case. Do not include properties not used for the use case, which confuses developers if you do so. - -* **Don't reuse** input DTOs among different application service methods. Because, different use cases will need to and use different properties of the DTO which results some properties are not used in some cases and that makes harder to understand and use the services and causes potential bugs in the future. - -### Output DTO Principles - -* You can **reuse output DTOs** if you **fill all the properties** on all the cases. - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/data-transfer-objects) \ No newline at end of file diff --git a/docs/en/Dependency-Injection.md b/docs/en/Dependency-Injection.md deleted file mode 100644 index 6a1ea66d41..0000000000 --- a/docs/en/Dependency-Injection.md +++ /dev/null @@ -1,570 +0,0 @@ -# Dependency Injection - -ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) library (Microsoft.Extensions.DependencyInjection nuget package). So, its documentation is valid in ABP too. - -> While ABP has no core dependency to any 3rd-party DI provider. However, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with [Autofac](https://autofac.org/) installed. See [Autofac integration](Autofac-Integration.md) document for more information. - -## Modularity - -Since ABP is a modular framework, every module defines its own services and registers via dependency injection in its own separate [module class](Module-Development-Basics.md). Example: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //register dependencies here - } -} -```` - -## Conventional Registration - -ABP introduces conventional service registration. You need not do anything to register a service by convention. It's automatically done. If you want to disable it, you can set `SkipAutoServiceRegistration` to `true` in the constructor of your module class. Example: - -````C# -public class BlogModule : AbpModule -{ - public BlogModule() - { - SkipAutoServiceRegistration = true; - } -} -```` - -Once you skip the auto registration, you should manually register your services. In that case, ``AddAssemblyOf`` extension method can help you to register all your services by convention. Example: - -````c# -public class BlogModule : AbpModule -{ - public BlogModule() - { - SkipAutoServiceRegistration = true; - } - - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAssemblyOf(); - } -} -```` - -The sections below explain the conventions and configurations. - -### Inherently Registered Types - -Some specific types are registered to dependency injection by default. Examples: - -* Module classes are registered as singleton. -* MVC controllers (inherit ``Controller`` or ``AbpController``) are registered as transient. -* MVC page models (inherit ``PageModel`` or ``AbpPageModel``) are registered as transient. -* MVC view components (inherit ``ViewComponent`` or ``AbpViewComponent``) are registered as transient. -* Application services (inherit ``ApplicationService`` class or its subclasses) are registered as transient. -* Repositories (implement ``BasicRepositoryBase`` class or its subclasses) are registered as transient. -* Domain services (implement ``IDomainService`` interface or inherit ``DomainService`` class) are registered as transient. - -Example: - -````C# -public class BlogPostAppService : ApplicationService -{ -} -```` - -``BlogPostAppService`` is automatically registered with transient lifetime since it's derived from a known base class. - -### Dependency Interfaces - -If you implement these interfaces, your class is registered to dependency injection automatically: - -* ``ITransientDependency`` to register with transient lifetime. -* ``ISingletonDependency`` to register with singleton lifetime. -* ``IScopedDependency`` to register with scoped lifetime. - -Example: - -````C# -public class TaxCalculator : ITransientDependency -{ -} -```` - -``TaxCalculator`` is automatically registered with a transient lifetime since it implements ``ITransientDependency``. - -### Dependency Attribute - -Another way of configuring a service for dependency injection is to use ``DependencyAttribute``. It has the following properties: - -* ``Lifetime``: Lifetime of the registration: ``Singleton``, ``Transient`` or ``Scoped``. -* ``TryRegister``: Set ``true`` to register the service only if it's not registered before. Uses TryAdd... extension methods of IServiceCollection. -* ``ReplaceServices``: Set ``true`` to replace services if they are already registered before. Uses Replace extension method of IServiceCollection. - -Example: - -````C# -[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -public class TaxCalculator -{ -} -```` - -``Dependency`` attribute has a higher priority than other dependency interfaces if it defines the ``Lifetime`` property. - -### ExposeServices Attribute - -``ExposeServicesAttribute`` is used to control which services are provided by the related class. Example: - -````C# -[ExposeServices(typeof(ITaxCalculator))] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ -} -```` - -``TaxCalculator`` class only exposes ``ITaxCalculator`` interface. That means you can only inject ``ITaxCalculator``, but can not inject ``TaxCalculator`` or ``ICalculator`` in your application. - -### Exposed Services by Convention - -If you do not specify which services to expose, ABP expose services by convention. So taking the ``TaxCalculator`` defined above: - -* The class itself is exposed by default. That means you can inject it by ``TaxCalculator`` class. -* Default interfaces are exposed by default. Default interfaces are determined by naming convention. In this example, ``ICalculator`` and ``ITaxCalculator`` are default interfaces of ``TaxCalculator``, but ``ICanCalculate`` is not. A generic interface (e.g. `ICalculator`) is also considered as a default interface if the naming convention is satisfied. -* The resolved instances will be the same if multiple services are exposed for **Singleton** and **Scoped** services. This behavior requires exposing the class itself. - -### Combining All Together - -Combining attributes and interfaces is possible as long as it's meaningful. - -````C# -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(ITaxCalculator))] -public class TaxCalculator : ITaxCalculator, ITransientDependency -{ - -} -```` - -### ExposeKeyedService Attribute - -`ExposeKeyedServiceAttribute` is used to control which keyed services are provided by the related class. Example: - -````C# -[ExposeKeyedService("taxCalculator")] -[ExposeKeyedService("calculator")] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ -} -```` - -In the example above, the `TaxCalculator` class exposes the `ITaxCalculator` interface with the key `taxCalculator` and the `ICalculator` interface with the key `calculator`. That means you can get keyed services from the `IServiceProvider` as shown below: - -````C# -var taxCalculator = ServiceProvider.GetRequiredKeyedService("taxCalculator"); -var calculator = ServiceProvider.GetRequiredKeyedService("calculator"); -```` - -Also, you can use the [`FromKeyedServicesAttribute`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute?view=dotnet-plat-ext-8.0) to resolve a certain keyed service in the constructor: - -```csharp -public class MyClass -{ - //... - - public MyClass([FromKeyedServices("taxCalculator")] ITaxCalculator taxCalculator) - { - TaxCalculator = taxCalculator; - } -} -``` - -> Notice that the `ExposeKeyedServiceAttribute` only exposes the keyed services. So, you can not inject the `ITaxCalculator` or `ICalculator` interfaces in your application without using the `FromKeyedServicesAttribute` as shown in the example above. If you want to expose both keyed and non-keyed services, you can use the `ExposeServicesAttribute` and `ExposeKeyedServiceAttribute` attributes together as shown below: - -````C# -[ExposeKeyedService("taxCalculator")] -[ExposeKeyedService("calculator")] -[ExposeServices(typeof(ITaxCalculator), typeof(ICalculator))] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ -} -```` - -### Manually Registering - -In some cases, you may need to register a service to the `IServiceCollection` manually, especially if you need to use custom factory methods or singleton instances. In that case, you can directly add services just as [Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) describes. Example: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Register an instance as singleton - context.Services.AddSingleton(new TaxCalculator(taxRatio: 0.18)); - - //Register a factory method that resolves from IServiceProvider - context.Services.AddScoped( - sp => sp.GetRequiredService() - ); - } -} -```` - -### Replace a Service - -If you need to replace an existing service (defined by the ABP framework or another module dependency), you have two options; - -1. Use the `Dependency` attribute of the ABP framework as explained above. -2. Use the `IServiceCollection.Replace` method of the Microsoft Dependency Injection library. Example: - -````csharp -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Replacing the IConnectionStringResolver service - context.Services.Replace( - ServiceDescriptor.Transient< - IConnectionStringResolver, - MyConnectionStringResolver - >()); - } -} -```` - -## Injecting Dependencies - -There are three common ways of using a service that has already been registered. - -### Constructor Injection - -This is the most common way of injecting a service into a class. For example: - -````C# -public class TaxAppService : ApplicationService -{ - private readonly ITaxCalculator _taxCalculator; - - public TaxAppService(ITaxCalculator taxCalculator) - { - _taxCalculator = taxCalculator; - } - - public async Task DoSomethingAsync() - { - //...use _taxCalculator... - } -} -```` - -``TaxAppService`` gets ``ITaxCalculator`` in its constructor. The dependency injection system automatically provides the requested service at runtime. - -Constructor injection is preffered way of injecting dependencies to a class. In that way, the class can not be constructed unless all constructor-injected dependencies are provided. Thus, the class explicitly declares it's required services. - -### Property Injection - -Property injection is not supported by Microsoft Dependency Injection library. However, ABP can integrate with 3rd-party DI providers ([Autofac](https://autofac.org/), for example) to make property injection possible. Example: - -````C# -public class MyService : ITransientDependency -{ - public ILogger Logger { get; set; } - - public MyService() - { - Logger = NullLogger.Instance; - } - - public async Task DoSomethingAsync() - { - //...use Logger to write logs... - } -} -```` - -For a property-injection dependency, you declare a public property with public setter. This allows the DI framework to set it after creating your class. - -Property injected dependencies are generally considered as **optional** dependencies. That means the service can properly work without them. ``Logger`` is such a dependency, ``MyService`` can continue to work without logging. - -To make the dependency properly optional, we generally set a default/fallback value to the dependency. In this sample, NullLogger is used as fallback. Thus, ``MyService`` can work but does not write logs if DI framework or you don't set Logger property after creating ``MyService``. - -One restriction of property injection is that you cannot use the dependency in your constructor, since it's set after the object construction. - -Property injection is also useful when you want to design a base class that has some common services injected by default. If you're going to use constructor injection, all derived classes should also inject depended services into their own constructors which makes development harder. However, be very careful using property injection for non-optional services as it makes it harder to clearly see the requirements of a class. - -#### DisablePropertyInjection Attribute - -You can use `[DisablePropertyInjection]` attribute on classes or their properties to disable property injection for the whole class or some specific properties. - -````C# -// Disabling for all properties of the MyService class -[DisablePropertyInjection] -public class MyService : ITransientDependency -{ - public ILogger Logger { get; set; } - - public ITaxCalculator TaxCalculator { get; set; } -} - -// Disabling only for the TaxCalculator property -public class MyService : ITransientDependency -{ - public ILogger Logger { get; set; } - - [DisablePropertyInjection] - public ITaxCalculator TaxCalculator { get; set; } -} -```` - -#### IInjectPropertiesService - -You can use the `IInjectPropertiesService` service to inject properties of an object. Generally, it is a service outside of DI, such as manually created services. - -````C# -var injectPropertiesService = serviceProvider.GetRequiredService(); -var instance = new TestService(); - -// Set any properties on instance that can be resolved by IServiceProvider. -injectPropertiesService.InjectProperties(instance); - -// Set any null-valued properties on instance that can be resolved by the IServiceProvider. -injectPropertiesService.InjectUnsetProperties(instance); -```` - -### Resolve Service from IServiceProvider - -You may want to resolve a service directly from ``IServiceProvider``. In that case, you can inject `IServiceProvider` into your class and use the ``GetService`` or the `GetRequiredService` method as shown below: - -````C# -public class MyService : ITransientDependency -{ - private readonly ITaxCalculator _taxCalculator; - - public MyService(IServiceProvider serviceProvider) - { - _taxCalculator = serviceProvider.GetRequiredService(); - } -} -```` - -### Dealing with multiple implementations - -You can register multiple implementations of the same service interface. Assume that you have an `IExternalLogger` interface with two implementations: - -````csharp -public interface IExternalLogger -{ - Task LogAsync(string logText); -} - -public class ElasticsearchExternalLogger : IExternalLogger -{ - public async Task LogAsync(string logText) - { - //TODO... - } -} - -public class AzureExternalLogger : IExternalLogger -{ - public Task LogAsync(string logText) - { - throw new System.NotImplementedException(); - } -} -```` - -In this example, we haven't registered any of the implementation classes to the dependency injection system yet. So, if we try to inject the `IExternalLogger` interface, we get an error indicating that no implementation found. - -If we register both of the `ElasticsearchExternalLogger` and `AzureExternalLogger` services for the `IExternalLogger` interface, and then try to inject the `IExternalLogger` interface, then the last registered implementation will be used. - -An example service injecting the `IExternalLogger` interface: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IExternalLogger _externalLogger; - - public MyService(IExternalLogger externalLogger) - { - _externalLogger = externalLogger; - } - - public async Task DemoAsync() - { - await _externalLogger.LogAsync("Example log message..."); - } -} -```` - -Here, as said before, we get the last registered implementation. However, how to determine the last registered implementation? - -If we implement one of the dependency interfaces (e.g. `ITransientDependency`), then the registration order will be uncertain (it may depend on the namespaces of the classes). The *last registered implementation* can be different than you expect. So, it is not suggested to use the dependency interfaces to register multiple implementations. - -You can register your services in the `ConfigureServices` method of your module: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.AddTransient(); - context.Services.AddTransient(); -} -```` - -In this case, you get an `AzureExternalLogger` instance when you inject the `IExternalLogger` interface, because the last registered implementation is the `AzureExternalLogger` class. - -When you have multiple implementation of an interface, you may want to work with all these implementations. Assume that you want to write log to all the external loggers. We can change the `MyService` implementation as the following: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IEnumerable _externalLoggers; - - public MyService(IEnumerable externalLoggers) - { - _externalLoggers = externalLoggers; - } - - public async Task DemoAsync() - { - foreach (var externalLogger in _externalLoggers) - { - await externalLogger.LogAsync("Example log message..."); - } - } -} -```` - -In this example, we are injecting `IEnumerable` instead of `IExternalLogger`, so we have a collection of the `IExternalLogger` implementations. Then we are using a `foreach` loop to write the same log text to all the `IExternalLogger` implementations. - -If you are using `IServiceProvider` to resolve dependencies, then use its `GetServices` method to obtain a collection of the service implementations: - -````csharp -IEnumerable services = _serviceProvider.GetServices(); -```` - -### Releasing/Disposing Services - -If you used a constructor or property injection, you don't need to be concerned about releasing the service's resources. However, if you have resolved a service from ``IServiceProvider``, in some cases, you might need to take care about releasing the service resources. - -ASP.NET Core releases all services at the end of a current HTTP request, even if you directly resolved from ``IServiceProvider`` (assuming you injected `IServiceProvider`). But, there are several cases where you may want to release/dispose manually resolved services: - -* Your code is executed outside of ASP.NET Core request and the executer hasn't handled the service scope. -* You only have a reference to the root service provider. -* You may want to immediately release & dispose services (for example, you may creating too many services with big memory usages and don't want to overuse the memory). - -In any case, you can create a service scope block to safely and immediately release services: - -````C# -using (var scope = _serviceProvider.CreateScope()) -{ - var service1 = scope.ServiceProvider.GetService(); - var service2 = scope.ServiceProvider.GetService(); -} -```` - -Both services are released when the created scope is disposed (at the end of the `using` block). - -### Cached Service Providers - -ABP provides two special services to optimize resolving services from `IServiceProvider`. `ICachedServiceProvider` and `ITransientCachedServiceProvider` both inherits from the `IServiceProvider` interface and internally caches the resolved services, so you get the same service instance even if you resolve a service multiple times. - -The main difference is the `ICachedServiceProvider` is itself registered as scoped, while the `ITransientCachedServiceProvider` is registered as transient to the dependency injection system. - -The following example injects the `ICachedServiceProvider` service and resolves a service in the `DoSomethingAsync` method: - -````csharp -public class MyService : ITransientDependency -{ - private readonly ICachedServiceProvider _serviceProvider; - - public MyService(ICachedServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task DoSomethingAsync() - { - var taxCalculator = _serviceProvider.GetRequiredService(); - // TODO: Use the taxCalculator - } -} - -```` - -With such a usage, you don't need to deal with creating service scopes and disposing the resolved services (as explained in the *Releasing/Disposing Services* section above). Because all the services resolved from the `ICachedServiceProvider` will be released once the service scope of the `MyService` instance is disposed. Also, you don't need to care about memory leaks (because of creating too many `ITaxCalculator` instances if we call `DoSomethingAsync` too many times), because only one `ITaxCalculator` instance is created, and it is reused. - -Since `ICachedServiceProvider` and `ITransientCachedServiceProvider` extends the standard `IServiceProvider` interface, you can use all the extension method of the `IServiceProvider` interface on them. In addition, they provides some other methods to provide a default value or a factory method for the services that are not found (that means not registered to the dependency injection system). Notice that the default value (or the value returned from your factory method) is also cached and reused. - -Use `ICachedServiceProvider` (instead of `ITransientCachedServiceProvider`) unless you need to create the service cache per usage. `ITransientCachedServiceProvider` guarantees that the created service instances are not shared with any other service, even they are in the same service scope. The services resolved from `ICachedServiceProvider` are shared with other services in the same service scope (in the same HTTP Request, for example), so it can be thought as more optimized. - -> ABP Framework also provides the `IAbpLazyServiceProvider` service. It does exists for backward compatibility and works exactly same with the `ITransientCachedServiceProvider` service. So, use the `ITransientCachedServiceProvider` since the `IAbpLazyServiceProvider` might be removed in future ABP versions. - -## Advanced Features - -### IServiceCollection.OnRegistered Event - -You may want to perform an action for every service registered to the dependency injection. In the `PreConfigureServices` method of your module, register a callback using the `OnRegistered` method as shown below: - -````csharp -public class AppModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - context.Services.OnRegistered(ctx => - { - var type = ctx.ImplementationType; - //... - }); - } -} -```` - -`ImplementationType` provides the service type. This callback is generally used to add interceptor to a service. Example: - -````csharp -public class AppModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - context.Services.OnRegistered(ctx => - { - if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true)) - { - ctx.Interceptors.TryAdd(); - } - }); - } -} -```` - -This example simply checks if the service class has `MyLogAttribute` attribute and adds `MyLogInterceptor` to the interceptor list if so. - -> Notice that `OnRegistered` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](Dynamic-Proxying-Interceptors.md) of dynamic proxying / interceptors. - -### IServiceCollection.OnActivated Event - -The `OnActivated` event is raised once a service is fully constructed. Here you can perform application-level tasks that depend on the service being fully constructed - these should be rare. - -````csharp -var serviceDescriptor = ServiceDescriptor.Transient(); -services.Add(serviceDescriptor); -if (setIsReadOnly) -{ - services.OnActivated(serviceDescriptor, x => - { - x.Instance.As().IsReadOnly = true; - }); -} -```` - -> Notice that `OnActivated` event can be registered multiple times for the same `ServiceDescriptor`. - -## 3rd-Party Providers - -While ABP has no core dependency to any 3rd-party DI provider, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. - -Startup templates come with Autofac installed. See [Autofac integration](Autofac-Integration.md) document for more information. - -## See Also - -* [ASP.NET Core Dependency Injection Best Practices, Tips & Tricks](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) -* [Video tutorial](https://abp.io/video-courses/essentials/dependency-injection) diff --git a/docs/en/Deploy-azure-app-service.md b/docs/en/Deploy-azure-app-service.md deleted file mode 100644 index 9e0309f3b9..0000000000 --- a/docs/en/Deploy-azure-app-service.md +++ /dev/null @@ -1,452 +0,0 @@ -# Deploying ABP Project to Azure App Service - -In this document, you will learn how to create and deploy your first ABP web app to [Azure App Service](https://docs.microsoft.com/en-us/azure/app-service/overview). The App Service supports various versions of .NET apps, and provides a highly scalable, self-patching web hosting service. ABP web apps are cross-platform and can be hosted on Linux, Windows or MacOS. - -****Prerequisites**** - -- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/dotnet). -- A GitHub account [Create an account for free](http://github.com/). - - - -## Creating a new ABP application - -Create a repository on [GitHub.com](https://github.com/) (keep all settings as default). - -Open the command prompt and clone the repository into a folder on your computer - -```bash -git clone https://github.com/your-username/your-repository-name.git -``` - -Check your dotnet version. It should be at least 3.1.x - -```bash -dotnet --version -``` - -Install or update the [ABP CLI](https://docs.abp.io/en/abp/latest/cli) with the following command: - -```bash -dotnet tool install -g Volo.Abp.Cli || dotnet tool update -g Volo.Abp.Cli -``` - -Open the command prompt in the *GitHub repository folder* and create a new ABP Blazor solution with the command below: - -```bash -abp new YourAppName -u blazor -``` - - - -## Running the application - -Open the command prompt in the *[YourAppName].DbMigrator* project and enter the command below to apply the database migrations: - -```bash -dotnet run -``` - -Open the command prompt in the *[YourAppName].HttpApi.Host* project to run the API project: - -```bash -dotnet run -``` - -Navigate to the *applicationUrl* specified in *the launchSettings.json* file of the *[YourAppName].HttpApi.Host project*. You should get the *Swagger window* - -Open the command prompt in the *[YourAppName].Blazor* folder and enter the command below to run the Blazor project: - -```bash -dotnet run -``` - -Navigate to the *applicationUrl* specified in the *launchSettings.json* file of the *[YourAppName].Blazor* project and you should see the landing page. - -Stop both the *API* and the *Blazor* project by pressing **CTRL+C** - - - -## Committing to GitHub - -Before the GitHub commit, you have to delete the line "**/wwwroot/libs/*" at *.gitignore* file. - -![azdevops-23](images/azdevops-23.png) - -Open the command prompt in the root folder of your project and *add, commit and push* all your changes to your GitHub repository: - -```bash -git add . -git commit -m initialcommit -git push -``` - - - -## Configuring Azure database connection string - -Create a SQL database on Azure and change the connection string in all the *appsettings.json* files. - -* Login into [Azure Portal](https://portal.azure.com/) - -* Click **Create a resource** - -* Search for *SQL Database* - -* Click the **Create** button in the *SQL Database window* - -* Create a new resource group. Name it *rg[YourAppName]* - -* Enter *[YourAppName]Db* as database name - -* Create a new Server and name it *[yourappname]server* - -* Enter a serveradmin login and passwords. Click the **OK** button - -* Select your *Location* - -* Check *Allow Azure services to access server* - -* Click **Configure database**. Go to the *Basic* version and click the **Apply** button - -* Click the **Review + create** button. Click **Create** - -* Click **Go to resource** and click **SQL server** when the SQL Database is created - -* Click **Networking** under Security left side menu - -* Select **Selected networks** and click **Add your client IP$ address** at the Firewall rules - -* Select **Allow Azure and resources to access this seerver** and save - -* Go to your **SQL database**, click **Connection strings** and copy the connection string - -* Copy/paste the *appsettings.json* files of the *[YourAppName].HttpApi.Host* and the *[YourAppName].DbMigrator* project - -* Do not forget to replace {your_password} with the correct server password you entered in Azure SQL Database - - - -## Running DB Migrations - -Open the command prompt in the *[YourAppName].DbMigrator* project again and enter the command below to apply the database migrations: - -```bash -dotnet run -``` - -Open the command prompt in the *[YourAppName].HttpApi.Host* project and enter the command below to check your API is working: - -```bash -dotnet run -``` - -Stop the *[YourAppName].HttpApi.Host* by pressing CTRL+C. - - - -## Committing to GitHub - -Open the command prompt in the root folder of your project and add, commit and push all your changes to your GitHub repository - -```bash -git add . -git commit -m initialcommit -git push -``` - - - -## Setting up the Build pipeline in AzureDevops and publish the Build Artifacts - -* Sign in Azure DevOps - -* Click **New organization** and follow the steps to create a new organisation. Name it [YourAppName]org - -* Enter [YourAppName]Proj as project name in the ***Create a project to get started*** window - -* Select **Public visibility** and click the **Create project** button - -* Click the **Pipelines** button to continue - -* Click the **Create Pipeline** button - - Select GitHub in the Select your repository window - -![azdevops-1](images/azdevops-1.png) - -* Enter the Connection name. *[YourAppName]GitHubConnection* and click **Authorize using OAuth** - -* Select your **GitHub** [YourAppName]repo and click Continue - -* Search for **ASP.NET** in the ***Select a template*** window - -![azdevops-2](images/azdevops-2.png) - -* Select the ASP.NET Core template and click the **Apply** button - -* Add the below commands block as a first step in the pipeline - - ``` - - task: UseDotNet@2 - inputs: - packageType: 'sdk' - version: '6.0.106' - ``` - -![azdevops-18](images/azdevops-18.png) - -* Select **Settings** on the second task(Nugetcommand@2) in the pipeline - -* Select **Feeds in my Nuget.config** and type **Nuget.config** in the text box - -![azdevops-3](images/azdevops-3.png) - -* Add the below commands block to the end of the pipeline - - ``` - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - ArtifactName: '$(Parameters.ArtifactName)' - condition: succeededOrFailed() - ``` - - ![azdevops-4](images/azdevops-4.png) - -``` -# ASP.NET -# Build and test ASP.NET projects. -# Add steps that publish symbols, save build artifacts, deploy, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4 - -trigger: -- main - -pool: - vmImage: 'windows-latest' - -variables: - solution: '**/*.sln' - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' - -steps: -- task: UseDotNet@2 - inputs: - packageType: 'sdk' - version: '6.0.106' - -- task: NuGetToolInstaller@1 - -- task: NuGetCommand@2 - inputs: - command: 'restore' - restoreSolution: '$(solution)' - feedsToUse: 'config' - nugetConfigPath: 'NuGet.config' - -- task: VSBuild@1 - inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - -- task: VSTest@2 - inputs: - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' - inputs: - PathtoPublish: '$(build.artifactstagingdirectory)' - ArtifactName: '$(Parameters.ArtifactName)' - publishLocation: 'Container' - condition: succeededOrFailed() -``` - -* Click **Save & queue** in the top menu. Click **Save & queue** again and click **Save and run** to run the Build pipeline - -* When the Build pipeline has finished. Click **1 published; 1 consumed** - - - -## Creating a Web App in the Azure Portal to deploy [YourAppName].HttpApi.Host project - -* Search for Web App in the *Search the Marketplace* field - -* Click the **Create** button in the Web App window - -* Select rg[YourAppName] in the *Resource Group* dropdown - -* Enter [YourAppName]API in the *Name input* field - -* Select code, .NET Core 3.1 (LTS) and windows as *Operating System* - -* Enter [YourAppName]API in the *Name input* field - -* Select .NET Core 3.1 (LTS) in the *Runtime stack* dropdown - -* Select Windows as *Operating System* - -* Select the same *Region* as in the SQL server you created in Part 3 - -![azdevops-5](images/azdevops-5.png) - -* Click **Create new** in the Windows Plan. Name it [YourAppName]ApiWinPlan - -* Click **Change size** in Sku and size. Go to the Dev/Test Free F1 version and click the **Apply** button - -![azdevops-6](images/azdevops-6.png) - -* Click the **Review + create** button. Click the **Create** button - -* Click **Go to resource** when the Web App has been created - - - -## Creating a release pipeline in the AzureDevops and deploy [YourAppName].HttpApi.Host project - -* Sign in into [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) - -* Click [YourAppName]Proj and click **Releases** in the *Pipelines* menu - -* Click the **New pipeline** button in the *No release pipelines found* window - -* Select *Azure App Service deployment* and click the **Apply** button - -![azdevops-7](images/azdevops-7.png) - -* Enter *[YourAppName]staging* in the *Stage name* field in the *Stage* window. And close the window - -* Click **+ Add an artifact** in the *Pipeline* tab - -* Select the **Build** icon as *Source type* in the *Add an artifact* window - -* Select Build pipeline in the *Source (build pipeline)* dropdown and click the **Add** button - -![azdevops-8](images/azdevops-8.png) - -* Click the **Continuous deployment trigger (thunderbolt icon)** - -* Set the toggle to **Enabled** in the the *Continuous deployment trigger* window - -* Click **+ Add** in *No filters added*. Select **Include** in the *Type* dropdown. Select your branch in the *Build branch* dropdown and close the window - -![azdevops-9](images/azdevops-9.png) - -* Click **the little red circle with the exclamation mark** in the *Tasks* tab menu - -* Select your subscription in the *Azure subscription* dropdown. - -![azdevops-10](images/azdevops-10.png) - -* Click **Authorize** and enter your credentials in the next screens - -* After Authorization, select the **[YourAppName]API** in the *App service name* dropdown - -* Click the **Deploy Azure App Service** task - -* Select **[YourAppName].HttpApi.Host.zip** in the *Package or folder* input field - -![azdevops-11](images/azdevops-11.png) - -* Click the **Save** icon in the top menu and click **OK** - -* Click **Create release** in the top menu. Click **Create** to create a release - -* Click the *Pipeline* tab and wait until the Deployment succeeds - -![azdevops-12](images/azdevops-12.png) - -* Open a browser and navigate to the URL of your Web App - -``` -https://[YourAppName]api.azurewebsites.net -``` - -![azdevops-13](images/azdevops-13.png) - - - -## Creating a Web App in Azure Portal to deploy [YourAppName].Blazor project - -* Login into [Azure Portal](https://portal.azure.com/) - -* Click **Create a resource** - -* Search for *Web App* in the *Search the Marketplace* field - -* Click the **Create** button in the *Web App* window - -* Select *rg[YourAppName]* in the *Resource Group* dropdown - -* Enter *[YourAppName]Blazor* in the *Name* input field - -* Select *.NET Core 3.1 (LTS)* in the *Runtime stack* dropdown - -* Select *Windows* as *Operating System* - -* Select the same region as the SQL server you created in Part 3 - -* Select the [YourAppName]ApiWinPlan in the *Windows Plan* dropdown - -![azdevops-14](images/azdevops-14.png) - -* Click the **Review + create** button. Click **Create** button - -* Click **Go to resource** when the Web App has been created - -* Copy the URL of the Blazor Web App for later use - -``` -https://[YourAppName]blazor.azurewebsites.net -``` - - -## Changing the Web App configuration for the Azure App Service - -Copy the URL of the Api Host and Blazor Web App. Change appsettings.json files in the Web App as follows images. - -![azdevops-19](images/azdevops-19.png) - -![azdevops-20](images/azdevops-20.png) - -![azdevops-21](images/azdevops-21.png) - - - -## Adding an extra Stage in the Release pipeline in the AzureDevops to deploy [YourAppName].Blazor project - -* Go to the *Release* pipeline in [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) and click **Edit** - -* Click the **+ Add** link and add a **New Stage** - -![azdevops-15](images/azdevops-15.png) - -* Select *Azure App Service deployment* and click the **Apply** button - -* Enter *BlazorDeployment* in the *Stage name* input field and close the *Stage* window - -* Click the **little red circle with the exclamation mark** in the BlazorDeployment stage - -* Select your subscription in the *Azure subscription* dropdown - -* Select your Blazor Web App in the *App service name* dropdown - -* Click the **Deploy Azure App Service task** - -* Select *[YourAppName].Blazor.zip* in the *Package or folder* input field - -![azdevops-16](images/azdevops-16.png) - -* Click **Save** in the top menu and click the **OK** button after - -* Click **Create release** in the top menu and click the **Create** button - -![azdevops-17](images/azdevops-17.png) - -![azdevops-22](images/azdevops-22.png) diff --git a/docs/en/Deployment/Clustered-Environment.md b/docs/en/Deployment/Clustered-Environment.md deleted file mode 100644 index e3294b3469..0000000000 --- a/docs/en/Deployment/Clustered-Environment.md +++ /dev/null @@ -1,109 +0,0 @@ -# Deploying to a Clustered Environment - -This document introduces the topics that you should consider when you are deploying your application to a clustered environment where **multiple instances of your application run concurrently**, and explains how you can deal with these topics in your ABP based application. - -> This document is valid regardless you have a monolith application or a microservice solution. The Application term is used for a process. An application can be a monolith web application, a service in a microservice solution, a console application, or another kind of an executable process. -> -> For example, if you are deploying your application to Kubernetes and configure your application or service to run in multiple pods, then your application or service runs in a clustered environment. - -## Understanding the Clustered Environment - -> You can skip this section if you are already familiar with clustered deployment and load balancers. - -### Single Instance Deployment - -Consider an application deployed as a **single instance**, as illustrated in the following figure: - -![deployment-single-instance](../images/deployment-single-instance.png) - -Browsers and other client applications can directly make HTTP requests to your application. You can put a web server (e.g. IIS or NGINX) between the clients and your application, but you still have a single application instance running in a single server or container. Single-instance configuration is **limited to scale** since it runs in a single server and you are limited with the server's capacity. - -### Clustered Deployment - -**Clustered deployment** is the way of running **multiple instances** of your application **concurrently** in a single or multiple servers. In this way, different instances can serve different requests and you can scale by adding new servers to the system. The following figure shows a typical implementation of clustering using a **load balancer**: - -![deployment-clustered](../images/deployment-clustered.png) - -### Load Balancers - -[Load balancers](https://en.wikipedia.org/wiki/Load_balancing_(computing)) have a lot of features, but they fundamentally **forward an incoming HTTP request** to an instance of your application and return your response back to the client application. - -Load balancers can use different algorithms for selecting the application instance while determining the application instance that is used to deliver the incoming request. **Round Robin** is one of the simplest and most used algorithms. Requests are delivered to the application instances in rotation. First instance gets the first request, second instance gets the second, and so on. It returns to the first instance after all the instances are used, and the algorithm goes like that for the next requests. - -### Potential Problems - -Once multiple instances of your application run in parallel, you should carefully consider the following topics: - -* Any **state (data) stored in memory** of your application will become a problem when you have multiple instances. A state stored in memory of an application instance may not be available in the next request since the next request will be handled by a different application instance. While there are some solutions (like sticky sessions) to overcome this problem user-basis, it is a **best practice to design your application as stateless** if you want to run it in a cluster, container or/and cloud. -* **In-memory caching** is a kind of in-memory state and should not be used in a clustered application. You should use **distributed caching** instead. -* You shouldn't store data in the **local file system**. It should be available to all instances of your application. Different application instance may run in different containers or servers and they may not be able to have access to the same file system. You can use a **cloud or external storage provider** as a solution. -* If you have **background workers** or **job queue managers**, you should be careful since multiple instances may try to execute the same job or perform the same work concurrently. As a result, you may have the same work done multiple times or you may get a lot of errors while trying to access and change the same resources. - -You may have more problems with clustered deployment, but these are the most common ones. ABP has been designed to be compatible with the clustered deployment scenario. The following sections explain what you should do when you are deploying your ABP based application to a clustered environment. - -## Switching to a Distributed Cache - -ASP.NET Core provides different kind of caching features. [In-memory cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory) stores your objects in the memory of the local server and is only available to the application that stored the object. Non-sticky sessions in a clustered environment should use the [distributed caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) except some specific scenarios (for example, you can cache a local CSS file into memory. It is read-only data and it is the same in all application instances. You can cache it in memory for performance reasons without any problem). - -[ABP's Distributed Cache](../Caching.md) extends [ASP.NET Core's distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) infrastructure. It works in-memory by default. You should configure an actual distributed cache provider when you want to deploy your application to a clustered environment. - -> You should configure the cache provider for clustered deployment, even if your application doesn't directly use `IDistributedCache`. Because the ABP Framework and the pre-built [application modules](../Modules/Index.md) are using distributed cache. - -ASP.NET Core provides multiple integrations to use as your distributed cache provider, like [Redis](https://redis.io/) and [NCache](https://www.alachisoft.com/ncache/). You can follow [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to learn how to use them in your applications. - -If you decided to use Redis as your distributed cache provider, **follow [ABP's Redis Cache Integration document](../Redis-Cache.md)** for the steps you need to follow to install it into your application and setup your Redis configuration. - -> Based on your preferences while creating a new ABP solution, Redis cache might be pre-installed in your solution. For example, if you have selected the *Tiered* option with the MVC UI, Redis cache comes as pre-installed. Because, in this case, you have two applications in your solution and they should use the same cache source to be consistent. - -## Using a Proper BLOB Storage Provider - -If you have used ABP's [BLOB Storing](../Blob-Storing.md) feature with the [File System provider](../Blob-Storing-File-System.md), you should use another provider in your clustered environment since the File System provider uses the application's local file system. - -The [Database BLOB provider](../Blob-Storing-Database) is the easiest way since it uses your application's main database (or another database if you configure) to store BLOBs. However, you should remember that BLOBs are large objects and may quickly increase your database's size. - -> [ABP Commercial](https://commercial.abp.io/) startup solution templates come with the database BLOB provider as pre-installed, and stores BLOBs in the application's database. - -Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOB storage providers. - -## Configuring Background Jobs - -ABP's [background job system](../Background-Jobs.md) is used to queue tasks to be executed in the background. Background job queue is persistent and a queued task is guaranteed to be executed (it is re-tried if it fails). - -ABP's default background job manager is compatible with clustered environments. It uses a [distributed lock](../Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time. See the *Configuring a Distributed Lock Provider* section below to learn how to configure a distributed lock provider for your application, so the default background job manager properly works in a clustered environment. - -If you don't want to use a distributed lock provider, you may go with the following options: - -* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs). -* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance. - -> If you are using an external background job integration (e.g. [Hangfire](../Background-Workers-Hangfire.md) or [Quartz](../Background-Workers-Quartz.md)) instead of the default background job manager, then please refer to your provider's documentation to learn how it should be configured for a clustered environment. - -## Configuring a Distributed Lock Provider - -ABP provides a distributed locking abstraction with an implementation made with the [DistributedLock](https://github.com/madelson/DistributedLock) library. A distributed lock is used to control concurrent access to a shared resource by multiple applications to prevent corruption of the resource because of concurrent writes. The ABP Framework and some pre-built [application modules](../Modules/Index.md) are using distributed locking for several reasons. - -However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, please follow the [distributed lock](../Distributed-Locking.md) document to configure a provider for your application, if it is not already configured. - -## Configuring SignalR - -ABP provides [SignalR](../SignalR-Integration.md) integration packages to simplify integration and usage. SignalR can be used whenever you need to add real-time web functionality (real-time messaging, real-time notification etc.) into your application. - -SignalR requires that all HTTP requests for a specific connection be handled (needs to keep track of all its connections) by the same server process. So, when SignalR is running on a clustered environment (with multiple servers) **"sticky sessions"** must be used. - -If you are considering [scaling out](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-6.0#scale-out) your servers and don't want to have inconsistency with the active socket connections, you can use [Azure SignalR Service](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-6.0#azure-signalr-service) or [Redis backplane](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-6.0#redis-backplane). - -> To learn more about how to host and scale SignalR in a clustered environment, please check the [ASP.NET Core SignalR hosting and scaling](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-6.0). - -## Implementing Background Workers - -ASP.NET Core provides [hosted services](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) and ABP provides [background workers](../Background-Workers.md) to perform tasks in background threads in your application. - -If your application has tasks running in the background, you should consider how they will behave in a clustered environment, especially if your background tasks are using the same resources. You should design your background tasks so that they continue to work properly in the clustered environment. - -Assume that your background worker in your SaaS application checks user subscriptions and sends emails if their subscription renewal date approaches. If the background task runs in multiple application instances, it is probable to send the same email many times to some users, which will disturb them. - -We suggest you to use one of the following approaches to overcome the problem: - -* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](../Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed. -* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers. -* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance. diff --git a/docs/en/Deployment/Configuring-Production.md b/docs/en/Deployment/Configuring-Production.md deleted file mode 100644 index db928c8590..0000000000 --- a/docs/en/Deployment/Configuring-Production.md +++ /dev/null @@ -1,104 +0,0 @@ -# Configuring Your Application for Production Environments - -ABP Framework has a lot of options to configure and fine-tune its features. They are all explained in their own documents. Default values for these options are pretty well for most of the deployment environments. However, you may need to care about some options based on how you've structured your deployment environment. In this document, we will highlight these kind of options. So, it is highly recommended to read this document in order to not have unexpected behaviors in your system in production. - -## Distributed Cache Prefix - -ABP's [distributed cache infrastructure](../Caching.md) provides an option to set a key prefix for all of your data that is saved into your distributed cache provider. The default value of this option is not set (it is `null`). If you are using a distributed cache server that is shared by different applications, then you can set a prefix value to isolate an application's cache data from others. - -````csharp -Configure(options => -{ - options.KeyPrefix = "MyCrmApp"; -}); -```` - -That's all. ABP, then will add this prefix to all of your cache keys in your application as along as you use ABP's `IDistributedCache` or `IDistributedCache` services. See the [Caching documentation](../Caching.md) if you are new to distributed caching. - -> **Warning**: If you use ASP.NET Core's standard `IDistributedCache` service, it's your responsibility to add the key prefix (you can get the value by injecting `IOptions`). ABP can not do it. - -> **Warning**: Even if you have never used distributed caching in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your caching server is shared among multiple systems. - -> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed cache server. In such systems, all applications (or services) should normally use the same cache prefix, because you want all the applications to use the same cache data to have consistency between them. - -> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for the distributed cache. So, please check your application code if it is already configured. - -## Distributed Lock Prefix - -ABP's [distributed locking infrastructure](../Distributed-Locking.md) provides an option to set a prefix for all the keys you are using in the distributed lock server. The default value of this option is not set (it is `null`). If you are using a distributed lock server that is shared by different applications, then you can set a prefix value to isolate an application's lock from others. - -````csharp -Configure(options => -{ - options.KeyPrefix = "MyCrmApp"; -}); -```` - -That's all. ABP, then will add this prefix to all of your keys in your application. See the [Distributed Locking documentation](../Distributed-Locking.md) if you are new to distributed locking. - -> **Warning**: Even if you have never used distributed locking in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your distributed lock server is shared among multiple systems. - -> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed locking server. In such systems, all applications (or services) should normally use the same lock prefix, because you want to globally lock your resources in your system. - -> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for distributed locking. So, please check your application code if it is already configured. - -## Email Sender - -ABP's [Email Sending](../Emailing.md) system abstracts sending emails from your application and module code and allows you to configure the email provider and settings in a single place. - -Email service is configured to write email contents to the standard [application log](../Logging.md) in development environment. You should configure the email settings to be able to send emails to users in your production environment. - -Please see the [Email Sending](../Emailing.md) document to learn how to configure its settings to really send emails. - -> **Warning**: If you don't configure the email settings, you will get errors while trying to send emails. For example, the [Account module](../Modules/Account.md)'s *Password Reset* feature sends email to the users to reset their passwords if they forget it. - -## SMS Sender - -ABP's [SMS Sending abstraction](https://docs.abp.io/en/abp/latest/SMS-Sending) provides a unified interface to send SMS to users. However, its implementation is left to you. Because, typically a paid SMS service is used to send SMS, and ABP doesn't depend on a specific SMS provider. - -So, if you are using the `ISmsSender` service, you must implement it yourself, as shown in the following code block: - -````csharp -public class MySmsSender : ISmsSender, ITransientDependency -{ - public async Task SendAsync(SmsMessage smsMessage) - { - // TODO: Send it using your provider... - } -} -```` - -> [ABP Commercial](https://commercial.abp.io/) provides a [Twilio SMS Module](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) as a pre-built integration with the popular [Twilio](https://www.twilio.com/) platform. - -## BLOB Provider - -If you use ABP's [BLOB Storing](https://docs.abp.io/en/abp/latest/Blob-Storing) infrastructure, you should care about the BLOB provider in your production environment. For example, if you use the [File System](../Blob-Storing-File-System.md) provider and your application is running in a Docker container, you should configure a volume mapping for the BLOB storage path. Otherwise, your data will be lost when the container is restarted. Also, the File System is not a good provider for production if you have a [clustered deployment](Clustered-Environment.md) or a microservice system. - -Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOB storage providers. - -> **Warning**: Even if you don't directly use the BLOB Storage system, a module you are depending on may use it. For example, ABP Commercial's [File Management](https://docs.abp.io/en/commercial/latest/modules/file-management) module stores file contents, and the [Account](https://docs.abp.io/en/commercial/latest/modules/account) module stores user profile pictures in the BLOB Storage system. So, be careful with the BLOB Storing configuration in production. Note that ABP Commercial uses the [Database Provider](../Blob-Storing-Database.md) as a pre-configured BLOB storage provider, which works in production without any problem, but you may still want to use another provider. - -## String Encryption - -ABP's [`IStringEncryptionService` Service](../String-Encryption.md) simply encrypts and decrypts given strings based on a password phrase. You should configure the `AbpStringEncryptionOptions` options for the production with a strong password and keep it as a secret. You can also configure the other properties of those options class. See the following example: - -````csharp -Configure(options => -{ - options.DefaultPassPhrase = "gs5nTT042HAL4it1"; -}); -```` - -Note that ABP CLI automatically sets the password to a random value on a new project creation. However, it is stored in the `appsettings.json` file and is generally added to your source control. It is suggested to use [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) or [Environment Variables](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) to set that value. - -## Logging - -ABP uses .NET's standard [Logging services](../Logging.md). So, it is compatible with any logging provider that works with .NET. ABP's startup solution templates come with [Serilog](https://serilog.net/) pre-installed and configured for you. It writes logs to file system and console with the initial configuration. File system is useful for development environment, but it is suggested you to use a different provider for your production environment, like Elasticsearch, database or any other provider that can properly work. - -## The Swagger UI - -ABP's startup solution templates come with [Swagger UI](https://swagger.io/) pre-installed. Swagger is a pretty standard and useful tool to discover and test your HTTP APIs on a built-in UI that is embedded into your application or service. It is typically used in development environment, but you may want to enable it on staging or production environments too. - -While you will always secure your HTTP APIs with other techniques (like the [Authorization](../Authorization.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment. - -> You may also want to see the [ABP Swagger integration](../API/Swagger-Integration.md) document. diff --git a/docs/en/Deployment/Distributed-Microservice.md b/docs/en/Deployment/Distributed-Microservice.md deleted file mode 100644 index 9af4a1c839..0000000000 --- a/docs/en/Deployment/Distributed-Microservice.md +++ /dev/null @@ -1,45 +0,0 @@ -# Deploying Distributed / Microservice Solutions - -The ABP Framework is designed to consider distributed and microservice systems, where you have multiple applications and/or services communicating internally. All of its features are compatible with distributed scenarios. This document highlights some points you should care about when you deploy your distributed or microservice solution. - -## Application Name & Instance Id - -ABP provides the `IApplicationInfoAccessor` service that provides the following properties: - -* `ApplicationName`: A human-readable name for an application. It is a unique value for an application. -* `InstanceId`: A random (GUID) value generated by the ABP Framework each time you start the application. - -These values are used by the ABP Framework in several places to distinguish the application and the application instance (process) in the system. For example, the [audit logging](../Audit-Logging.md) system saves the `ApplicationName` in each audit log record written by the related application, so you can understand which application has created the audit log entry. So, if your system consists of multiple applications saving audit logs to a single point, you should be sure that each application has a different `ApplicationName`. - -The `ApplicationName` property's value is set automatically from the **entry assembly's name** (generally, the project name in a .NET solution) by default, which is proper for most cases, since each application typically has a unique entry assembly name. - -There are two ways to set the application name to a different value. In this first approach, you can set the `ApplicationName` property in your application's [configuration](../Configuration.md). The easiest way is to add an `ApplicationName` field to your `appsettings.json` file: - -````json -{ - "ApplicationName": "Services.Ordering" -} -```` - -Alternatively, you can set `AbpApplicationCreationOptions.ApplicationName` while creating the ABP application. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `ApplicationName` option as shown below: - -````csharp -await builder.AddApplicationAsync(options => -{ - options.ApplicationName = "Services.Ordering"; -}); -```` - -## Using a Distributed Event Bus - -ABP's [Distributed Event Bus](../Distributed-Event-Bus.md) system provides a standard interface to communicate with other applications and services. While the name is "distributed", the default implementation is in-process. That means, your applications / services can not communicate with each other unless you explicitly configure a distributed event bus provider. - -If you are building a distributed system, then the applications should communicate through an external distributed messaging server. Please follow the [Distributed Event Bus](../Distributed-Event-Bus.md) document to learn how to install and configure your distributed event bus provider. - -> **Warning**: Even if you don't use the distributed event bus directly in your application code, the ABP Framework and some of the modules you are using may use it. So, if you are building a distributed system, always configure a distributed event bus provider. - -> **Info**: [Clustered deployment](Clustered-Environment.md) of a single application is not considered as a distributed system. So, if you only have a single application with multiple instances serving behind a load balancer, a real distributed messaging server may not be needed. - -## See Also - -* [Deploying to a Clustered Environment](Clustered-Environment.md) diff --git a/docs/en/Deployment/Index.md b/docs/en/Deployment/Index.md deleted file mode 100644 index cf7137464f..0000000000 --- a/docs/en/Deployment/Index.md +++ /dev/null @@ -1,14 +0,0 @@ -# Deployment - -Deploying an ABP application is not different than deploying any .NET or ASP.NET Core application. You can deploy it to a cloud provider (e.g. Azure, AWS, Google Could) or on-premise server, IIS or any other web server. ABP's documentation doesn't contain much information on deployment. You can refer to your provider's documentation. - -However, there are some topics that you should care about when you are deploying your applications. Most of them are general software deployment considerations, but you should understand how to handle them within your ABP based applications. We've prepared guides for this purpose and we suggest you to read these guides carefully before designing your deployment configuration. - -## Guides - -* [Configuring SSL certificate(HTTPS)](SSL.md): Explains how to configure SSL certificate(HTTPS) for your application. -* [Configuring OpenIddict](Configuring-OpenIddict.md): Notes for some essential configurations for OpenIddict. -* [Configuring for Production](Configuring-Production.md): Notes for some essential configurations for production environments. -* [Optimization for Production](Optimizing-Production.md): Tips and suggestions for optimizing your application on production environments. -* [Deploying to a Clustered Environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently. -* [Deploying Distributed / Microservice Solutions](Distributed-Microservice.md): Deployment notes for solutions consisting of multiple applications and/or services. diff --git a/docs/en/Deployment/Optimizing-Production.md b/docs/en/Deployment/Optimizing-Production.md deleted file mode 100644 index 7e896f9b1c..0000000000 --- a/docs/en/Deployment/Optimizing-Production.md +++ /dev/null @@ -1,22 +0,0 @@ -# Optimizing Your Application for Production Environments - -ABP Framework and the startup solution templates are configured well to get the maximum performance on production environments. However, there are still some points you need to pay attention to in order to optimize your system in production. In this document, we will mention some of these topics. - -## Caching Static Contents - -The following items are contents that can be cached in the client side (typically in the Browser) or in a CDN server: - -* **Static images** can always be cached. Here, you should be careful that if you change an image, use a different file name, or use a versioning query-string parameter, so the browser (or CDN) understands it's been changed. -* **CSS and JavaScript files**. ABP's [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) system always uses a query-string versioning parameter and a hash value in the files names of the CSS & JavaScript files for the [MVC (Razor Pages)](../UI/AspNetCore/Overall.md) UI. So, you can safely cache these files in the client side or in a CDN server. -* **Application bundle files** of an [Angular UI](../UI/Angular/Quick-Start.md) application. -* **[Application Localization Endpoint](../API/Application-Localization.md)** can be cached per culture (it already has a `cultureName` query string parameter) if you don't use dynamic localization on the server-side. ABP Commercial's [Language Management](https://commercial.abp.io/modules/Volo.LanguageManagement) module provides dynamic localization. If you're using it, you can't cache that endpoint forever. However, you can still cache it for a while. Applying dynamic localization text changes to the application can delay for a few minutes, even for a few hours in a real life scenario. - -There may be more ways based on your solution structure and deployment environment, but these are the essential points you should consider to client-side cache in a production environment. - -## Bundling & Minification for MVC (Razor Pages) UI - -ABP's [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) system automatically bundles, minifies and versions your CSS and JavaScript files in production environment. Normally, you don't need to do anything, if you haven't disabled it yourself in your application code. It is important to follow the [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) document and truly use the system to get the maximum optimization. - -## Background Jobs - -ABP's [Background Jobs](../Background-Jobs.md) system provides an abstraction with a basic implementation to enqueue jobs and execute them in a background thread. ABP's Default Background Job Manager may not be enough if you are adding too many jobs to the queue and want them to be executed in parallel by multiple servers with a high performance. If you need these, you should consider to configure a dedicated background job software, like [Hangfire](https://www.hangfire.io/). ABP has a pre-built [Hangfire integration](../Background-Jobs-Hangfire.md), so you can switch to Hangfire without changing your application code. diff --git a/docs/en/Deployment/SSL.md b/docs/en/Deployment/SSL.md deleted file mode 100644 index c48c916d93..0000000000 --- a/docs/en/Deployment/SSL.md +++ /dev/null @@ -1,34 +0,0 @@ -# Configuring Configuring SSL certificate(HTTPS) - -A website needs an SSL certificate in order to keep user data secure, verify ownership of the website, prevent attackers from creating a fake version of the site, and gain user trust. - -This document introduces how to get and use SSL certificate(HTTPS) for your application. - -## Get a SSL Certificate from a Certificate Authority - -You can get a SSL certificate from a certificate authority (CA) such as [Let's Encrypt](https://letsencrypt.org/) or [Cloudflare](https://www.cloudflare.com/learning/ssl/what-is-an-ssl-certificate/) and so on. - -Once you have a certificate, you need to configure your web server to use it. The following references show how to configure your web server to use a certificate. - -* [Host ASP.NET Core on Linux with Apache: HTTPS configuration](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache) -* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx) -* [How to Set Up SSL on IIS 7 or later](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis) - -## Create a Self-Signed Certificate - -You can create a self-signed certificate for testing purposes or internal use. - -There is an article about [how to create a self-signed certificate](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/self-signed-certificates-guide), If you are using IIS, you can use the following this document to [obtain a Certificate](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis#obtain-a-certificate) - -## Common Problems - -### The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot - -This error may occur when using IIS. You need to trust your certificate by `Manage computer certificates`. - -## References - -* [ABP commercial IIS Deployment](https://docs.abp.io/en/commercial/latest/startup-templates/application/deployment-iis) -* [HTTPS in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl) -* [Let's Encrypt](https://letsencrypt.org/getting-started) -* [Cloudflare's Free SSL / TLS](https://www.cloudflare.com/application-services/products/ssl/) \ No newline at end of file diff --git a/docs/en/Distributed-Event-Bus-Azure-Integration.md b/docs/en/Distributed-Event-Bus-Azure-Integration.md deleted file mode 100644 index acca2e0172..0000000000 --- a/docs/en/Distributed-Event-Bus-Azure-Integration.md +++ /dev/null @@ -1,133 +0,0 @@ -# Distributed Event Bus Azure Integration - -> This document explains **how to configure the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system - -## Installation - -Use the ABP CLI to add [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Azure` package. -* Run `abp add-package Volo.Abp.EventBus.Azure` command. - -If you want to do it manually, install the [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusAzureModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. - -### `appsettings.json` file configuration - -This is the simplest way to configure the Azure Service Bus settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). - -**Example: The minimal configuration to connect to Azure Service Bus Namespace with default configurations** - -````json -{ - "Azure": { - "ServiceBus": { - "Connections": { - "Default": { - "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={%{{{Policy Name}}}%};SharedAccessKey={};EntityPath=marketing-consent" - } - } - }, - "EventBus": { - "ConnectionName": "Default", - "SubscriberName": "MySubscriberName", - "TopicName": "MyTopicName" - } - } -} -```` - -* `MySubscriberName` is the name of this subscription, which is used as the **Subscriber** on the Azure Service Bus. -* `MyTopicName` is the **topic name**. - -See [the Azure Service Bus document](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions) to understand these options better. - -#### Connections - -If you need to connect to another Azure Service Bus Namespace the Default, you need to configure the connection properties. - -**Example: Declare two connections and use one of them for the event bus** - -````json -{ - "Azure": { - "ServiceBus": { - "Connections": { - "Default": { - "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={%{{{SharedAccessKey}}}%}" - }, - "SecondConnection": { - "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={%{{{Policy Name}}}%};SharedAccessKey={%{{{SharedAccessKey}}}%}" - } - } - }, - "EventBus": { - "ConnectionName": "SecondConnection", - "SubscriberName": "MySubscriberName", - "TopicName": "MyTopicName" - } - } -} -```` - -This allows you to use multiple Azure Service Bus namespaces in your application, but select one of them for the event bus. - -You can use any of the [ServiceBusAdministrationClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.administration.servicebusadministrationclientoptions?view=azure-dotnet), [ServiceBusClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusclientoptions?view=azure-dotnet), [ServiceBusProcessorOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusprocessoroptions?view=azure-dotnet) properties for the connection. - -**Example: Specify the Admin, Client and Processor options** - -````json -{ - "Azure": { - "ServiceBus": { - "Connections": { - "Default": { - "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={%{{{Policy Name}}}%};SharedAccessKey={};EntityPath=marketing-consent", - "Admin": { - "Retry": { - "MaxRetries": 3 - } - }, - "Client": { - "RetryOptions": { - "MaxRetries": 1 - } - }, - "Processor": { - "AutoCompleteMessages": true, - "ReceiveMode": "ReceiveAndDelete" - } - } - } - }, - "EventBus": { - "ConnectionName": "Default", - "SubscriberName": "MySubscriberName", - "TopicName": "MyTopicName" - } - } -} -```` - -### The Options Classes - -`AbpAzureServiceBusOptions` and `AbpAzureEventBusOptions` classes can be used to configure the connection strings and event bus options for Azure Service Bus. - -You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). - -**Example: Configure the connection** - -````csharp -Configure(options => -{ - options.Connections.Default.ConnectionString = "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={%{{{Policy Name}}}%};SharedAccessKey={}"; - options.Connections.Default.Admin.Retry.MaxRetries = 3; - options.Connections.Default.Client.RetryOptions.MaxRetries = 1; -}); -```` - -Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. diff --git a/docs/en/Distributed-Event-Bus-Kafka-Integration.md b/docs/en/Distributed-Event-Bus-Kafka-Integration.md deleted file mode 100644 index 82e658f48d..0000000000 --- a/docs/en/Distributed-Event-Bus-Kafka-Integration.md +++ /dev/null @@ -1,167 +0,0 @@ -# Distributed Event Bus Kafka Integration - -> This document explains **how to configure the [Kafka](https://kafka.apache.org/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system - -## Installation - -Use the ABP CLI to add [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Kafka` package. -* Run `abp add-package Volo.Abp.EventBus.Kafka` command. - -If you want to do it manually, install the [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusKafkaModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. - -### `appsettings.json` file configuration - -This is the simplest way to configure the Kafka settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). - -**Example: The minimal configuration to connect to a local kafka server with default configurations** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "localhost:9092" - } - }, - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName" - } - } -} -```` - -* `MyGroupId` is the name of this application, which is used as the **GroupId** on the Kakfa. -* `MyTopicName` is the **topic name**. - -See [the Kafka document](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html) to understand these options better. - -#### Connections - -If you need to connect to another server than the localhost, you need to configure the connection properties. - -**Example: Specify the host name (as an IP address)** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092" - } - }, - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName" - } - } -} -```` - -Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus. - -**Example: Declare two connections and use one of them for the event bus** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092" - }, - "SecondConnection": { - "BootstrapServers": "321.321.321.321:9092" - } - }, - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName", - "ConnectionName": "SecondConnection" - } - } -} -```` - -This allows you to use multiple Kafka cluster in your application, but select one of them for the event bus. - -You can use any of the [ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html) properties as the connection properties. - -**Example: Specify the socket timeout** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092", - "SocketTimeoutMs": 60000 - } - } - } -} -```` - -### The Options Classes - -`AbpKafkaOptions` and `AbpKafkaEventBusOptions` classes can be used to configure the connection strings and event bus options for the Kafka. - -You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). - -**Example: Configure the connection** - -````csharp -Configure(options => -{ - options.Connections.Default.BootstrapServers = "123.123.123.123:9092"; - options.Connections.Default.SaslUsername = "user"; - options.Connections.Default.SaslPassword = "pwd"; -}); -```` - -**Example: Configure the consumer config** - -````csharp -Configure(options => -{ - options.ConfigureConsumer = config => - { - config.GroupId = "MyGroupId"; - config.EnableAutoCommit = false; - }; -}); -```` - -**Example: Configure the producer config** - -````csharp -Configure(options => -{ - options.ConfigureProducer = config => - { - config.MessageTimeoutMs = 6000; - config.Acks = Acks.All; - }; -}); -```` - -**Example: Configure the topic specification** - -````csharp -Configure(options => -{ - options.ConfigureTopic = specification => - { - specification.ReplicationFactor = 3; - specification.NumPartitions = 3; - }; -}); -```` - -Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. diff --git a/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md b/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md deleted file mode 100644 index ef174f92b0..0000000000 --- a/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md +++ /dev/null @@ -1,165 +0,0 @@ -# Distributed Event Bus RabbitMQ Integration - -> This document explains **how to configure the [RabbitMQ](https://www.rabbitmq.com/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system - -## Installation - -Use the ABP CLI to add [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.RabbitMQ` package. -* Run `abp add-package Volo.Abp.EventBus.RabbitMQ` command. - -If you want to do it manually, install the [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusRabbitMqModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. - -### `appsettings.json` file configuration - -This is the simplest way to configure the RabbitMQ settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). - -**Example: The minimal configuration to connect to a local RabbitMQ server with default configurations** - -````json -{ - "RabbitMQ": { - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -```` - -* `ClientName` is the name of this application, which is used as the **queue name** on the RabbitMQ. -* `ExchangeName` is the **exchange name**. - -See [the RabbitMQ document](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues) to understand these options better. - -#### Connections - -If you need to connect to another server than the localhost, you need to configure the connection properties. - -**Example: Specify the host name (as an IP address)** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -```` - -Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus. - -**Example: Declare two connections and use one of them for the event bus** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - }, - "SecondConnection": { - "HostName": "321.321.321.321" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName", - "ConnectionName": "SecondConnection" - } - } -} -```` - -This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus. - -You can use any of the [ConnectionFactory](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) properties as the connection properties. - -**Example: Specify the connection port** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123", - "Port": "5672" - } - } - } -} -```` - -If you need to connect to the RabbitMQ cluster, you can use the `;` character to separate the host names. - -**Example: Connect to the RabbitMQ cluster** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123;234.234.234.234" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -``` - -### The Options Classes - -`AbpRabbitMqOptions` and `AbpRabbitMqEventBusOptions` classes can be used to configure the connection strings and event bus options for the RabbitMQ. - -You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). - -**Example: Configure the connection** - -````csharp -Configure(options => -{ - options.Connections.Default.UserName = "user"; - options.Connections.Default.Password = "pass"; - options.Connections.Default.HostName = "123.123.123.123"; - options.Connections.Default.Port = 5672; -}); -```` - -**Example: Configure the client, exchange names and prefetchCount** - -````csharp -Configure(options => -{ - options.ClientName = "TestApp1"; - options.ExchangeName = "TestMessages"; - options.PrefetchCount = 1; -}); -```` - -**Example: Configure the queue and exchange optional arguments** - -```csharp -Configure(options => -{ - options.ExchangeArguments["x-delayed-type"] = "direct"; - options.QueueArguments["x-message-ttl"] = 60000; -}); -``` - -Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. diff --git a/docs/en/Distributed-Event-Bus-Rebus-Integration.md b/docs/en/Distributed-Event-Bus-Rebus-Integration.md deleted file mode 100644 index 39fb35ede9..0000000000 --- a/docs/en/Distributed-Event-Bus-Rebus-Integration.md +++ /dev/null @@ -1,65 +0,0 @@ -# Distributed Event Bus Rebus Integration - -> This document explains **how to configure the [Rebus](http://mookid.dk/category/rebus/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system - -## Installation - -Use the ABP CLI to add [Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet package to your project: - -* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. -* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Rebus` package. -* Run `abp add-package Volo.Abp.EventBus.Rebus` command. - -If you want to do it manually, install the [Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusRebusModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. - -## Configuration - -You can configure using the standard [configuration system](Configuration.md), like using the [options](Options.md) classes. - -### The Options Classes - -`AbpRebusEventBusOptions` class can be used to configure the event bus options for the Rebus. - -You can configure this options inside the `PreConfigureServices` of your [module](Module-Development-Basics.md). - -**Example: Minimize configuration** - -```csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; -}); -``` - -Rebus has many options, you can use the `Configurer` property of `AbpRebusEventBusOptions` class to configure. - -Default events are **stored in memory**. See the [rebus document](https://github.com/rebus-org/Rebus/wiki/Transport) for more details. - -**Example: Configure the store** - -````csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; - options.Configurer = rebusConfigurer => - { - rebusConfigurer.Transport(t => t.UseMsmq("eventbus")); - rebusConfigurer.Subscriptions(s => s.UseJsonFile(@"subscriptions.json")); - }; -}); -```` - -You can use the `Publish` properpty of `AbpRebusEventBusOptions` class to change the publishing method - -**Example: Configure the event publishing** - -````csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; - options.Publish = async (bus, type, data) => - { - await bus.Publish(data); - }; -}); -```` diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md deleted file mode 100644 index fa825911f7..0000000000 --- a/docs/en/Distributed-Event-Bus.md +++ /dev/null @@ -1,710 +0,0 @@ -# Distributed Event Bus - -Distributed Event bus system allows to **publish** and **subscribe** to events that can be **transferred across application/service boundaries**. You can use the distributed event bus to asynchronously send and receive messages between **microservices** or **applications**. - -## Providers - -Distributed event bus system provides an **abstraction** that can be implemented by any vendor/provider. There are four providers implemented out of the box: - -* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider. -* `AzureDistributedEventBus` implements the distributed event bus with the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/). See the [Azure Service Bus integration document](Distributed-Event-Bus-Azure-Integration.md) to learn how to configure it. -* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it. -* `KafkaDistributedEventBus` implements the distributed event bus with the [Kafka](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it. -* `RebusDistributedEventBus` implements the distributed event bus with the [Rebus](http://mookid.dk/category/rebus/). See the [Rebus integration document](Distributed-Event-Bus-Rebus-Integration.md) to learn how to configure it. - -Using a local event bus as default has a few important advantages. The most important one is that: It allows you to write your code compatible to distributed architecture. You can write a monolithic application now that can be split into microservices later. It is a good practice to communicate between bounded contexts (or between application modules) via distributed events instead of local events. - -For example, [pre-built application modules](Modules/Index.md) is designed to work as a service in a distributed system while they can also work as a module in a monolithic application without depending an external message broker. - -## Publishing Events - -There are two ways of publishing distributed events explained in the following sections. - -### Using IDistributedEventBus to Publish Events - -`IDistributedEventBus` can be [injected](Dependency-Injection.md) and used to publish a distributed event. - -**Example: Publish a distributed event when the stock count of a product changes** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IDistributedEventBus _distributedEventBus; - - public MyService(IDistributedEventBus distributedEventBus) - { - _distributedEventBus = distributedEventBus; - } - - public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) - { - await _distributedEventBus.PublishAsync( - new StockCountChangedEto - { - ProductId = productId, - NewCount = newCount - } - ); - } - } -} -```` - -`PublishAsync` method gets the event object, which is responsible to hold the data related to the event. It is a simple plain class: - -````csharp -using System; - -namespace AbpDemo -{ - [EventName("MyApp.Product.StockChange")] - public class StockCountChangedEto - { - public Guid ProductId { get; set; } - - public int NewCount { get; set; } - } -} -```` - -Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case). - -> `Eto` is a suffix for **E**vent **T**ransfer **O**bjects we use by convention. While it is not required, we find it useful to identify such event classes (just like [DTOs](Data-Transfer-Objects.md) on the application layer). - -#### Event Name - -`EventName` attribute is optional, but suggested. If you don't declare it for an event type (ETO class), the event name will be the full name of the event class, `AbpDemo.StockCountChangedEto` in this case. - -#### About Serialization for the Event Objects - -Event transfer objects (ETOs) **must be serializable** since they will be serialized/deserialized to JSON or other format when it is transferred to out of the process. - -Avoid circular references, polymorphism, private setters and provide default (empty) constructors if you have any other constructor as a good practice (while some serializers may tolerate it), just like the DTOs. - -### Publishing Events Inside Entity / Aggregate Root Classes - -[Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish distributed events inside entity / aggregate root classes. - -**Example: Publish a distributed event inside an aggregate root method** - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - public int StockCount { get; private set; } - - private Product() { } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - - public void ChangeStockCount(int newCount) - { - StockCount = newCount; - - //ADD an EVENT TO BE PUBLISHED - AddDistributedEvent( - new StockCountChangedEto - { - ProductId = Id, - NewCount = newCount - } - ); - } - } -} -```` - -`AggregateRoot` class defines the `AddDistributedEvent` to add a new distributed event, that is published when the aggregate root object is saved (created, updated or deleted) into the database. - -> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event. - -#### IGeneratesDomainEvents Interface - -Actually, adding distributed events are not unique to the `AggregateRoot` class. You can implement `IGeneratesDomainEvents` for any entity class. But, `AggregateRoot` implements it by default and makes it easy for you. - -> It is not suggested to implement this interface for entities those are not aggregate roots, since it may not work for some database providers for such entities. It works for EF Core, but not works for MongoDB for example. - -#### How It Was Implemented? - -Calling the `AddDistributedEvent` doesn't immediately publish the event. The event is published when you save changes to the database; - -* For EF Core, it is published on `DbContext.SaveChanges`. -* For MongoDB, it is published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). - -## Subscribing to Events - -A service can implement the `IDistributedEventHandler` to handle the event. - -**Example: Handle the `StockCountChangedEto` defined above** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyHandler - : IDistributedEventHandler, - ITransientDependency - { - public async Task HandleEventAsync(StockCountChangedEto eventData) - { - var productId = eventData.ProductId; - } - } -} -```` - -That's all. - -* `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEto` event occurs. -* If you are using a distributed message broker, like RabbitMQ, ABP automatically **subscribes to the event on the message broker**, gets the message, executes the handler. -* It sends **confirmation (ACK)** to the message broker if the event handler was successfully executed (did not throw any exception). - -You can inject any service and perform any required logic here. A single event handler class can **subscribe to multiple events** but implementing the `IDistributedEventHandler` interface for each event type. - -If you perform **database operations** and use the [repositories](Repositories.md) inside the event handler, you may need to create a [unit of work](Unit-Of-Work.md), because some repository methods need to work inside an **active unit of work**. Make the handle method `virtual` and add a `[UnitOfWork]` attribute for the method, or manually use the `IUnitOfWorkManager` to create a unit of work scope. - -> The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options. - -## Monitoring Distributed Events - -The ABP Framework allows you to stay informed when your application **receives** or **sends** a distributed event. This capability enables you to track the event flow within your application and take appropriate actions based on the received or sent distributed events. - -### Received Events - -The `DistributedEventReceived` local event is published when your application receives an event from the distributed event bus. `DistributedEventReceived` class has the following fields: - -- **`Source`:** It represents the source of the distributed event. Source can be `Direct`, `Inbox`, `Outbox`. -- **`EventName`:** It represents the [name](#event-name) of the event received. -- **`EventData`:** It represents the actual data associated with the event received. Since it is of type `object`, it can hold any type of data. - -**Example: Get informed when your application receives an event from the distributed event bus** - -```csharp -public class DistributedEventReceivedHandler : ILocalEventHandler, ITransientDependency -{ - public async Task HandleEventAsync(DistributedEventReceived eventData) - { - // TODO: IMPLEMENT YOUR LOGIC... - } -} -``` - -### Sent Events - -The `DistributedEventSent` local event is published when your application sends an event to the distributed event bus. `DistributedEventSent` class has the following fields: - -- **`Source`:** It represents the source of the distributed event. Source can be `Direct`, `Inbox`, `Outbox`. -- **`EventName`:** It represents the [name](#event-name) of the event sent. -- **`EventData`:** It represents the actual data associated with the event sent. Since it is of type `object`, it can hold any type of data. - -**Example: Get informed when your application sends an event to the distributed event bus** - -```csharp -public class DistributedEventSentHandler : ILocalEventHandler, ITransientDependency -{ - public async Task HandleEventAsync(DistributedEventSent eventData) - { - // TODO: IMPLEMENT YOUR LOGIC... - } -} -``` - -You can seamlessly integrate event-tracking capabilities into your application by subscribing to the `DistributedEventReceived` and `DistributedEventSent` local events as above examples. This empowers you to effectively monitor the messaging flow, diagnose any potential issues, and gain valuable insights into the behavior of your distributed messaging system. - -## Pre-Defined Events - -ABP Framework **automatically publishes** distributed events for **create, update and delete** operations for an [entity](Entities.md) once you configure it. - -### Event Types - -There are three pre-defined event types: - -* `EntityCreatedEto` is published when an entity of type `T` was created. -* `EntityUpdatedEto` is published when an entity of type `T` was updated. -* `EntityDeletedEto` is published when an entity of type `T` was deleted. - -These types are generics. `T` is actually the type of the **E**vent **T**ransfer **O**bject (ETO) rather than the type of the entity. Because, an entity object can not be transferred as a part of the event data. So, it is typical to define a ETO class for an entity class, like `ProductEto` for `Product` entity. - -### Subscribing to the Events - -Subscribing to the auto events is same as subscribing a regular distributed event. - -**Example: Get notified once a product updated** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyHandler : - IDistributedEventHandler>, - ITransientDependency - { - public async Task HandleEventAsync(EntityUpdatedEto eventData) - { - var productId = eventData.Entity.Id; - //TODO - } - } -} -```` - -* `MyHandler` implements the `IDistributedEventHandler>`. -* It is required to register your handler class to the [dependency injection](Dependency-Injection.md) system. Implementing `ITransientDependency` like in this example is an easy way. - -### Configuration - -You can configure the `AbpDistributedEntityEventOptions` in the `ConfigureServices` of your [module](Module-Development-Basics.md) to add a selector. - -**Example: Configuration samples** - -````csharp -Configure(options => -{ - //Enable for all entities - options.AutoEventSelectors.AddAll(); - - //Enable for a single entity - options.AutoEventSelectors.Add(); - - //Enable for all entities in a namespace (and child namespaces) - options.AutoEventSelectors.AddNamespace("MyProject.Products"); - - //Custom predicate expression that should return true to select a type - options.AutoEventSelectors.Add( - type => type.Namespace.StartsWith("MyProject.") - ); -}); -```` - -* The last one provides flexibility to decide if the events should be published for the given entity type. Returns `true` to accept a `Type`. - -You can add more than one selector. If one of the selectors match for an entity type, then it is selected. - -### Event Transfer Object - -Once you enable **auto events** for an entity, ABP Framework starts to publish events on the changes on this entity. If you don't specify a corresponding **E**vent **T**ransfer **O**bject (ETO) for the entity, ABP Framework uses a standard type, named `EntityEto`, which has only two properties: - -* `EntityType` (`string`): Full name (including namespace) of the entity class. -* `KeysAsString` (`string`): Primary key(s) of the changed entity. If it has a single key, this property will be the primary key value. For a composite key, it will contain all keys separated by `,` (comma). - -So, you can implement the `IDistributedEventHandler>` to subscribe the update events. However, it is not a good approach to subscribe to such a generic event, because you handle the update events for all entities in a single handler (since they all use the same ETO object). You can define the corresponding ETO type for the entity type. - -**Example: Declare to use `ProductEto` for the `Product` entity** - -````csharp -public class ProductEto -{ - public Guid Id { get; set; } - public string Name { get; set; } - public float Price { get; set; } -} -```` - -Then you can use the `AbpDistributedEntityEventOptions.EtoMappings` option to map your `Product` entity to the `ProductEto`: - -````csharp -Configure(options => -{ - options.AutoEventSelectors.Add(); - options.EtoMappings.Add(); -}); -```` - -This example; - -* Adds a selector to allow to publish the create, update and delete events for the `Product` entity. -* Configure to use the `ProductEto` as the event transfer object to publish for the `Product` related events. - -> Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it. - -## Entity Synchronizer - -In a distributed (or microservice) system, it is typical to subscribe to change events for an [entity](Entities.md) type of another service, so you can get notifications when the subscribed entity changes. In that case, you can use ABP's Pre-Defined Events as explained in the previous section. - -If your purpose is to store your local copies of a remote entity, you typically subscribe to create, update and delete events of the remote entity and update your local database in your event handler. ABP provides a pre-built `EntitySynchronizer` base class to make that operation easier for you. - -Assume that there is a `Product` entity (probably an aggregate root entity) in a Catalog microservice, and you want to keep copies of the products in your Ordering microservice, with a local `OrderProduct` entity. In practice, properties of the `OrderProduct` class will be a subset of the `Product` properties, because not all the product data is needed in the Ordering microservice (however, you can make a full copy if you need). Also, the `OrderProduct` entity may have additional properties that are populated and used in the Ordering microservice. - -The first step to establish the synchronization is to define an ETO (Event Transfer Object) class in the Catalog microservice that is used to transfer the event data. Assuming the `Product` entity has a `Guid` key, your ETO can be as shown below: - -````csharp -[EventName("product")] -public class ProductEto : EntityEto -{ - // Your Product properties here... -} -```` - -`ProductEto` can be put in a shared project (DLL) that is referenced by the Catalog and the Ordering microservices. Alternatively, you can put a copy of the `ProductEto` class in the Ordering microservice if you don't want to introduce a common project dependency between the services. In this case, the `EventName` attribute becomes critical to map the `ProductEto` classes across two services (you should use the same event name). - -Once you define an ETO class, you should configure the ABP Framework to publish auto (create, update and delete) events for the `Product` entity, as explained in the previous section: - -````csharp -Configure(options => -{ - options.AutoEventSelectors.Add(); - options.EtoMappings.Add(); -}); -```` - -Finally, you should create a class in the Ordering microservice, that is derived from the `EntitySynchronizer` class: - -````csharp -public class ProductSynchronizer : EntitySynchronizer -{ - public ProductSynchronizer( - IObjectMapper objectMapper, - IRepository repository - ) : base(objectMapper, repository) - { - } -} -```` - -The main point of this class is it subscribes to the create, update and delete events of the source entity and updates the local entity in the database. It uses the [Object Mapper](Object-To-Object-Mapping.md) system to create or update the `OrderProduct` objects from the `ProductEto` objects. So, you should also configure the object mapper to make it properly work. Otherwise, you should manually perform the object mapping by overriding the `MapToEntityAsync(TSourceEntityEto)` and `MapToEntityAsync(TSourceEntityEto,TEntity)` methods in your `ProductSynchronizer` class. - -If your entity has a composite primary key (see the [Entities document](Entities.md)), then you should inherit from the `EntitySynchronizer` class (just don't use the `Guid` generic argument in the previous example) and implement `FindLocalEntityAsync` to find the entity in your local database using the `Repository`. - -`EntitySynchronizer` is compatible with the *Entity Versioning* system (see the [Entities document](Entities.md)). So, it works as expected even if the events are received as disordered. If the entity's version in your local database is newer than the entity in the received event, then the event is ignored. You should implement the `IHasEntityVersion` interface for the entity and ETO classes (for this example, you should implement for the `Product`, `ProductEto` and `OrderProduct` classes). - -If you want to ignore some type of change events, you can set `IgnoreEntityCreatedEvent`, `IgnoreEntityUpdatedEvent` and `IgnoreEntityDeletedEvent` in the constructor of your class. Example: - -````csharp -public class ProductSynchronizer - : EntitySynchronizer -{ - public ProductSynchronizer( - IObjectMapper objectMapper, - IRepository repository - ) : base(objectMapper, repository) - { - IgnoreEntityDeletedEvent = true; - } -} -```` - -> Notice that the `EntitySynchronizer` can only create/update the entities after you use it. If you have an existing system with existing data, you should manually copy the data for one time, because the `EntitySynchronizer` starts to work. - -## Transaction and Exception Handling - -Distributed event bus works in-process (since default implementation is `LocalDistributedEventBus`) unless you configure an actual provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [RabbitMQ](Distributed-Event-Bus-RabbitMQ-Integration.md)). In-process event bus always executes event handlers in the same [unit of work](Unit-Of-Work.md) scope that you publishes the events in. That means, if an event handler throws an exception, then the related unit of work (the database transaction) is rolled back. In this way, your application logic and event handling logic becomes transactional (atomic) and consistent. If you want to ignore errors in an event handler, you must use a `try-catch` block in your handler and shouldn't re-throw the exception. - -When you switch to an actual distributed event bus provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [RabbitMQ](Distributed-Event-Bus-RabbitMQ-Integration.md)), then the event handlers will be executed in different processes/applications as their purpose is to create distributed systems. In this case, the only way to implement transactional event publishing is to use the outbox/inbox patterns as explained in the *Outbox / Inbox for Transactional Events* section. - -If you don't configure outbox/inbox pattern or use the `LocalDistributedEventBus`, then events are published at the end of the unit of work by default, just before the unit of work is completed (that means throwing exception in an event handler still rollbacks the unit of work), even if you publish them in the middle of unit of work. If you want to immediately publish the event, you can set `onUnitOfWorkComplete` to `false` while using `IDistributedEventBus.PublishAsync` method. - -> Keeping the default behavior is recommended unless you don't have a unique requirement. `onUnitOfWorkComplete` option is not available when you publish events inside entity / aggregate root classes (see the *Publishing Events Inside Entity / Aggregate Root Classes* section). - -## Outbox / Inbox for Transactional Events - -The **[transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html)** is used to publishing distributed events within the **same transaction** that manipulates the application's database. When you enable outbox, distributed events are saved into the database inside the same transaction with your data changes, then sent to the actual message broker by a separate [background worker](Background-Workers.md) with a re-try system. In this way, it ensures the consistency between your database state and the published events. - -The **transactional inbox pattern**, on the other hand, saves incoming events into database first. Then (in a [background worker](Background-Workers.md)) executes the event handler in a transactional manner and removes the event from the inbox queue in the same transaction. It ensures that the event is only executed one time by keeping the processed messages for a while and discarding the duplicate events received from the message broker. - -Enabling the event outbox and inbox systems require a few manual steps for your application. Please apply the instructions in the following sections to make them running. - -> Outbox and Inbox can be separately enabled and configured, so you may only use one of them if you want. - -### Pre-requirements - -* The outbox/inbox system uses the distributed lock system to handle concurrency when you run multiple instances of your application/service. So, you should **configure the distributed lock system** with one of the providers as [explained in this document](Distributed-Locking.md). -* The outbox/inbox system supports [Entity Framework Core](Entity-Framework-Core.md) (EF Core) and [MongoDB](MongoDB.md) **database providers** out of the box. So, your applications should use one of these database providers. For other database providers, see the *Implementing a Custom Database Provider* section. - -> If you are using MongoDB, be sure that you enabled multi-document database transactions that was introduced in MongoDB version 4.0. See the *Transactions* section of the [MongoDB](MongoDB.md) document. - -### Enabling event outbox - -Enabling event outbox depends on your database provider. - -#### Enabling event outbox for Entity Framework Core - -Open your `DbContext` class, implement the `IHasEventOutbox` interface. You should end up by adding a `DbSet` property into your `DbContext` class: - -```csharp -public DbSet OutgoingEvents { get; set; } -``` - -Add the following lines inside the `OnModelCreating` method of your `DbContext` class: - -```csharp -builder.ConfigureEventOutbox(); -``` - -Use the standard `Add-Migration` and `Update-Database` commands to apply changes into your database. If you want to use the command-line terminal, run the following commands in the root directory of the database integration project: - -```bash -dotnet ef migrations add "Added_Event_Outbox" -dotnet ef database update -``` - -Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): - -````csharp -Configure(options => -{ - options.Outboxes.Configure(config => - { - config.UseDbContext(); - }); -}); -```` - -#### Enabling event outbox for MongoDB - -Open your `DbContext` class, implement the `IHasEventOutbox` interface. You should end up by adding a `IMongoCollection` property into your `DbContext` class: - -```csharp -public IMongoCollection OutgoingEvents => Collection(); -``` - -Add the following lines inside the `CreateModel` method of your `DbContext` class: - -```csharp -modelBuilder.ConfigureEventOutbox(); -``` - -Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): - -````csharp -Configure(options => -{ - options.Outboxes.Configure(config => - { - config.UseMongoDbContext(); - }); -}); -```` - -#### Distributed Locking for Outbox - -> **IMPORTANT**: Outbox sending service uses distributed locks to ensure only a single instance of your application consumes the outbox queue concurrently. Distributed locking key should be unique per database. The `config` object (in the `options.Outboxes.Configure(...)` method) has a `DatabaseName` property, which is used in the distributed lock key to ensure the uniqueness. `DatabaseName` is automatically set by the `UseDbContext` method, getting the database name from the `ConnectionStringName` attribute of the `YourDbContext` class. So, if you have multiple databases in your system, ensure that you use the same connection string name for the same database, but different connection string names for different databases. If you can't ensure that, you can manually set `config.DatabaseName` (after the `UseDbContext` line) to ensure that uniqueness. - -### Enabling event inbox - -Enabling event inbox depends on your database provider. - -#### Enabling event inbox for Entity Framework Core - -Open your `DbContext` class, implement the `IHasEventInbox` interface. You should end up by adding a `DbSet` property into your `DbContext` class: - -```csharp -public DbSet IncomingEvents { get; set; } -``` - -Add the following lines inside the `OnModelCreating` method of your `DbContext` class: - -```csharp -builder.ConfigureEventInbox(); -``` - -Use the standard `Add-Migration` and `Update-Database` commands to apply changes into your database. If you want to use the command-line terminal, run the following commands in the root directory of the database integration project: - -```bash -dotnet ef migrations add "Added_Event_Inbox" -dotnet ef database update -``` - -Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): - -````csharp -Configure(options => -{ - options.Inboxes.Configure(config => - { - config.UseDbContext(); - }); -}); -```` - -#### Enabling event inbox for MongoDB - -Open your `DbContext` class, implement the `IHasEventInbox` interface. You should end up by adding a `IMongoCollection` property into your `DbContext` class: - -```csharp -public IMongoCollection IncomingEvents => Collection(); -``` - -Add the following lines inside the `CreateModel` method of your `DbContext` class: - -```csharp -modelBuilder.ConfigureEventInbox(); -``` - -Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): - -````csharp -Configure(options => -{ - options.Inboxes.Configure(config => - { - config.UseMongoDbContext(); - }); -}); -```` - -#### Distributed Locking for Inbox - -> **IMPORTANT**: Inbox processing service uses distributed locks to ensure only a single instance of your application consumes the inbox queue concurrently. Distributed locking key should be unique per database. The `config` object (in the `options.Inboxes.Configure(...)` method) has a `DatabaseName` property, which is used in the distributed lock key to ensure the uniqueness. `DatabaseName` is automatically set by the `UseDbContext` method, getting the database name from the `ConnectionStringName` attribute of the `YourDbContext` class. So, if you have multiple databases in your system, ensure that you use the same connection string name for the same database, but different connection string names for different databases. If you can't ensure that, you can manually set `config.DatabaseName` (after the `UseDbContext` line) to ensure that uniqueness. - -### Additional Configuration - -> The default configuration will be enough for most cases. However, there are some options you may want to set for outbox and inbox. - -#### Outbox configuration - -Remember how outboxes are configured: - -````csharp -Configure(options => -{ - options.Outboxes.Configure(config => - { - // TODO: Set options - }); -}); -```` - -Here, the following properties are available on the `config` object: - -* `IsSendingEnabled` (default: `true`): You can set to `false` to disable sending outbox events to the actual event bus. If you disable this, events can still be added to outbox, but not sent. This can be helpful if you have multiple applications (or application instances) writing to outbox, but use one of them to send the events. -* `Selector`: A predicate to filter the event (ETO) types to be used for this configuration. Should return `true` to select the event. It selects all the events by default. This is especially useful if you want to ignore some ETO types from the outbox, or want to define named outbox configurations and group events within these configurations. See the *Named Configurations* section. -* `ImplementationType`: Type of the class that implements the database operations for the outbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. -* `DatabaseName`: Unique database name for the database that is used for this outbox configuration. See the **IMPORTANT** paragraph at the end of the *Enabling event outbox/inbox* sections. - -#### Inbox configuration - -Remember how inboxes are configured: - -````csharp -Configure(options => -{ - options.Inboxes.Configure(config => - { - // TODO: Set options - }); -}); -```` - -Here, the following properties are available on the `config` object: - -* `IsProcessingEnabled` (default: `true`): You can set to `false` to disable processing (handling) events in the inbox. If you disable this, events can still be received, but not executed. This can be helpful if you have multiple applications (or application instances), but use one of them to execute the event handlers. -* `EventSelector`: A predicate to filter the event (ETO) types to be used for this configuration. This is especially useful if you want to ignore some ETO types from the inbox, or want to define named inbox configurations and group events within these configurations. See the *Named Configurations* section. -* `HandlerSelector`: A predicate to filter the event handled types (classes implementing the `IDistributedEventHandler` interface) to be used for this configuration. This is especially useful if you want to ignore some event handler types from inbox processing, or want to define named inbox configurations and group event handlers within these configurations. See the *Named Configurations* section. -* `ImplementationType`: Type of the class that implements the database operations for the inbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. -* `DatabaseName`: Unique database name for the database that is used for this outbox configuration. See the **IMPORTANT** paragraph at the end of the *Enabling event inbox* section. - -#### AbpEventBusBoxesOptions - -`AbpEventBusBoxesOptions` can be used to fine-tune how inbox and outbox systems work. For most of the systems, using the defaults would be more than enough, but you can configure it to optimize your system when it is needed. - -Just like all the [options classes](Options.md), `AbpEventBusBoxesOptions` can be configured in the `ConfigureServices` method of your [module class](Module-Development-Basics.md) as shown in the following code block: - -````csharp -Configure(options => -{ - // TODO: configure the options -}); -```` - -`AbpEventBusBoxesOptions` has the following properties to be configured: - -* `BatchPublishOutboxEvents`: Can be used to enable or disable batch publishing events to the message broker. Batch publishing works if it is supported by the distributed event bus provider. If not supported, events are sent one by one as the fallback logic. Keep it as enabled since it has a great performance gain wherever possible. Default value is `true` (enabled). -* `PeriodTimeSpan`: The period of the inbox and outbox message processors to check if there is a new event in the database. Default value is 2 seconds (`TimeSpan.FromSeconds(2)`). -* `CleanOldEventTimeIntervalSpan`: The event inbox system periodically checks and deletes the old processed events from the inbox in the database. You can set this value to determine the check period. Default value is 6 hours (`TimeSpan.FromHours(6)`). -* `WaitTimeToDeleteProcessedInboxEvents`: Inbox events are not deleted from the database for a while even if they are successfully processed. This is for a system to prevent multiple process of the same event (if the event broker sends it twice). This configuration value determines the time to keep the processed events. Default value is 2 hours (`TimeSpan.FromHours(2)`). -* `InboxWaitingEventMaxCount`: The maximum number of events to query at once from the inbox in the database. Default value is 1000. -* `OutboxWaitingEventMaxCount`: The maximum number of events to query at once from the outbox in the database. Default value is 1000. -* `DistributedLockWaitDuration`: ABP uses [distributed locking](Distributed-Locking.md) to prevent concurrent access to the inbox and outbox messages in the database, when running multiple instance of the same application. If an instance of the application can not obtain the lock, it tries after a duration. This is the configuration of that duration. Default value is 15 seconds (`TimeSpan.FromSeconds(15)`). - -### Skipping Outbox - -`IDistributedEventBus.PublishAsync` method provides an optional parameter, `useOutbox`, which is set to `true` by default. If you bypass outbox and immediately publish an event, you can set it to `false` for a specific event publishing operation. - -### Advanced Topics - -#### Named Configurations - -> All the concepts explained in this section is also valid for inbox configurations. We will show examples only for outbox to keep the document shorter. - -See the following outbox configuration code: - -````csharp -Configure(options => -{ - options.Outboxes.Configure(config => - { - //TODO - }); -}); -```` - -This is equivalent of the following code: - -````csharp -Configure(options => -{ - options.Outboxes.Configure("Default", config => - { - //TODO - }); -}); -```` - -`Default` is this code indicates the configuration name. If you don't specify it (like in the previous code block), `Default` is used as the configuration name. - -That means you can define more than one configuration for outbox (also for inbox) with different names. ABP runs all the configured outboxes. - -Multiple outboxes can be needed if your application have more than one database and you want to run different outbox queues for different databases. In this case, you can use the `Selector` option to decide the events should be handled by an outbox. See the *Additional Configurations* section above. - -#### Implementing a Custom Outbox/Inbox Database Provider - -If your application or service is using a database provider other than [EF Core](Entity-Framework-Core.md) and [MongoDB](MongoDB.md), you should manually integrate outbox/inbox system with your database provider. - -> Outbox and Inbox table/data must be stored in the same database with your application's data (since we want to create a single database transaction that includes application's database operations and outbox/inbox table operations). Otherwise, you should care about distributed (multi-database) transaction support which is not provided by most of the vendors and may require additional configuration. - -ABP provides `IEventOutbox` and `IEventInbox` abstractions as extension point for the outbox/inbox system. You can create classes by implementing these interfaces and register them to [dependency injection](Dependency-Injection.md). - -Once you implement your custom event boxes, you can configure `AbpDistributedEventBusOptions` to use your event box classes: - -````csharp -Configure(options => -{ - options.Outboxes.Configure(config => - { - config.ImplementationType = typeof(MyOutbox); //Your Outbox class - }); - - options.Inboxes.Configure(config => - { - config.ImplementationType = typeof(MyInbox); //Your Inbox class - }); -}); -```` - -## See Also - -* [Local Event Bus](Local-Event-Bus.md) diff --git a/docs/en/Distributed-Locking.md b/docs/en/Distributed-Locking.md deleted file mode 100644 index 6474949dec..0000000000 --- a/docs/en/Distributed-Locking.md +++ /dev/null @@ -1,131 +0,0 @@ -# Distributed Locking -Distributed locking is a technique to manage many applications that try to access the same resource. The main purpose is to allow only one of many applications to access the same resource at the same time. Otherwise, accessing the same object from various applications may corrupt the value of the resources. - -> ABP's current distributed locking implementation is based on the [DistributedLock](https://github.com/madelson/DistributedLock) library. - -## Installation - -You can open a command-line terminal and type the following command to install the [Volo.Abp.DistributedLocking](https://www.nuget.org/packages/Volo.Abp.DistributedLocking) package into your project: - -````bash -abp add-package Volo.Abp.DistributedLocking -```` - -This package provides the necessary API to use the distributed locking system, however, you should configure a provider before using it. - -### Configuring a Provider - -The [DistributedLock](https://github.com/madelson/DistributedLock) library provides [various of implementations](https://github.com/madelson/DistributedLock#implementations) for the locking, like [Redis](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md) and [ZooKeeper](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.ZooKeeper.md). - -For example, if you want to use the [Redis provider](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md), you should add [DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet package to your project, then add the following code into the `ConfigureServices` method of your ABP [module](Module-Development-Basics.md) class: - -````csharp -using Medallion.Threading; -using Medallion.Threading.Redis; - -namespace AbpDemo -{ - [DependsOn( - typeof(AbpDistributedLockingModule) - //If you have the other dependencies, you should do here - )] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - context.Services.AddSingleton(sp => - { - var connection = ConnectionMultiplexer - .Connect(configuration["Redis:Configuration"]); - return new - RedisDistributedSynchronizationProvider(connection.GetDatabase()); - }); - } - } -} -```` - -This code gets the Redis connection string from the [configuration](Configuration.md), so you can add the following lines to your `appsettings.json` file: - -````json -"Redis": { - "Configuration": "127.0.0.1" -} -```` - -## Usage - -There are two ways to use the distributed locking API: ABP's `IAbpDistributedLock` abstraction and [DistributedLock](https://github.com/madelson/DistributedLock) library's API. - -### Using the IAbpDistributedLock Service - -`IAbpDistributedLock` is a simple service provided by the ABP framework for simple usage of distributed locking. - -**Example: Using the `IAbpDistributedLock.TryAcquireAsync` method** - -````csharp -using Volo.Abp.DistributedLocking; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IAbpDistributedLock _distributedLock; - public MyService(IAbpDistributedLock distributedLock) - { - _distributedLock = distributedLock; - } - - public async Task MyMethodAsync() - { - await using (var handle = - await _distributedLock.TryAcquireAsync("MyLockName")) - { - if (handle != null) - { - // your code that access the shared resource - } - } - } - } -} -```` - -`TryAcquireAsync` may not acquire the lock. It returns `null` if the lock could not be acquired. In this case, you shouldn't access the resource. If the handle is not `null`, it means that you've obtained the lock and can safely access the resource. - -`TryAcquireAsync` method gets the following parameters: - -* `name` (`string`, required): Unique name of your lock. Different named locks are used to access different resources. -* `timeout` (`TimeSpan`): A timeout value to wait to obtain the lock. Default value is `TimeSpan.Zero`, which means it doesn't wait if the lock is already owned by another application. -* `cancellationToken`: A cancellation token that can be triggered later to cancel the operation. - -### Configuration - -#### AbpDistributedLockOptions - -`AbpDistributedLockOptions` is the main options class to configure the distributed locking. - -**Example: Set the distributed lock key prefix for the application** - -```csharp -Configure(options => -{ - options.KeyPrefix = "MyApp1"; -}); -``` - -> Write that code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md). - -##### Available Options - -* KeyPrefix (string, default: null): Specify the lock name prefix. - -### Using DistributedLock Library's API - -ABP's `IAbpDistributedLock` service is very limited and mainly designed to be internally used by the ABP Framework. For your own applications, you can use the DistributedLock library's own API. See its [own documentation](https://github.com/madelson/DistributedLock) for details. - -## The Volo.Abp.DistributedLocking.Abstractions Package - -If you are building a reusable library or an application module, then you may not want to bring an additional dependency to your module for simple applications that run as a single instance. In this case, your library can depend on the [Volo.Abp.DistributedLocking.Abstractions](https://nuget.org/packages/Volo.Abp.DistributedLocking.Abstractions) package which defines the `IAbpDistributedLock` service and implements it as in-process (not distributed actually). In this way, your library can run properly (without a distributed lock provider dependency) in an application that runs as a single instance. If the application is deployed to a [clustered environment](Deployment/Clustered-Environment.md), then the application developer should install a real distributed provider as explained in the *Installation* section. diff --git a/docs/en/Domain-Driven-Design-Implementation-Guide.md b/docs/en/Domain-Driven-Design-Implementation-Guide.md deleted file mode 100644 index dc28845396..0000000000 --- a/docs/en/Domain-Driven-Design-Implementation-Guide.md +++ /dev/null @@ -1,5 +0,0 @@ -# Implementing Domain Driven Design - -This document has been packaged as a **free e-book** and can be downloaded from the following URL: - -https://abp.io/books/implementing-domain-driven-design diff --git a/docs/en/Domain-Driven-Design.md b/docs/en/Domain-Driven-Design.md deleted file mode 100644 index 03a6232ba3..0000000000 --- a/docs/en/Domain-Driven-Design.md +++ /dev/null @@ -1,37 +0,0 @@ -# Domain Driven Design - -## What is DDD? - -ABP framework provides an **infrastructure** to make **Domain Driven Design** based development easier to implement. DDD is [defined in the Wikipedia](https://en.wikipedia.org/wiki/Domain-driven_design) as below: - -> **Domain-driven design** (**DDD**) is an approach to software development for complex needs by connecting the implementation to an evolving model. The premise of domain-driven design is the following: -> -> - Placing the project's primary focus on the core domain and domain logic; -> - Basing complex designs on a model of the domain; -> - Initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems. - -## Layers & Building Blocks - -ABP follows DDD principles and patterns to achieve a layered application model which consists of four fundamental layers: - -- **Presentation Layer**: Provides an interface to the user. Uses the *Application Layer* to achieve user interactions. -- **Application Layer**: Mediates between the Presentation and Domain Layers. Orchestrates business objects to perform specific application tasks. Implements use cases as the application logic. -- **Domain Layer**: Includes business objects and the core (domain) business rules. This is the heart of the application. -- **Infrastructure Layer**: Provides generic technical capabilities that support higher layers mostly using 3rd-party libraries. - -DDD mostly interest in the **Domain** and the **Application** layers, rather than the Infrastructure and the Presentation layers. The following documents explains the **infrastructure** provided by the ABP Framework to implement **Building Blocks** of the DDD: - -* **Domain Layer** - * [Entities & Aggregate Roots](Entities.md) - * [Repositories](Repositories.md) - * [Domain Services](Domain-Services.md) - * [Value Objects](Value-Objects.md) - * [Specifications](Specifications.md) -* **Application Layer** - * [Application Services](Application-Services.md) - * [Data Transfer Objects (DTOs)](Data-Transfer-Objects.md) - * [Unit of Work](Unit-Of-Work.md) - -## Free E-Book: Implementing DDD - -See the [Implementing Domain Driven Design book](https://abp.io/books/implementing-domain-driven-design) as a **complete reference**. This book explains the Domain Driven Design and introduces explicit **rules and examples** to give a deep understanding of the **implementation details**. \ No newline at end of file diff --git a/docs/en/Domain-Services.md b/docs/en/Domain-Services.md deleted file mode 100644 index 877ccfe27e..0000000000 --- a/docs/en/Domain-Services.md +++ /dev/null @@ -1,139 +0,0 @@ -# Domain Services - -## Introduction - -In a [Domain Driven Design](Domain-Driven-Design.md) (DDD) solution, the core business logic is generally implemented in aggregates ([entities](Entities.md)) and the Domain Services. Creating a Domain Service is especially needed when; - -* You implement a core domain logic that depends on some services (like repositories or other external services). -* The logic you need to implement is related to more than one aggregate/entity, so it doesn't properly fit in any of the aggregates. - -## ABP Domain Service Infrastructure - -Domain Services are simple, stateless classes. While you don't have to derive from any service or interface, ABP Framework provides some useful base classes and conventions. - -### DomainService & IDomainService - -Either derive a Domain Service from the `DomainService` base class or directly implement the `IDomainService` interface. - -**Example: Create a Domain Service deriving from the `DomainService` base class.** - -````csharp -using Volo.Abp.Domain.Services; - -namespace MyProject.Issues -{ - public class IssueManager : DomainService - { - - } -} -```` - -When you do that; - -* ABP Framework automatically registers the class to the Dependency Injection system with a Transient lifetime. -* You can directly use some common services as base properties, without needing to manually inject (e.g. [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)). - -> It is suggested to name a Domain Service with a `Manager` or `Service` suffix. We typically use the `Manager` suffix as used in the sample above. - -**Example: Implement the domain logic of assigning an Issue to a User** - -````csharp -public class IssueManager : DomainService -{ - private readonly IRepository _issueRepository; - - public IssueManager(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task AssignAsync(Issue issue, AppUser user) - { - var currentIssueCount = await _issueRepository - .CountAsync(i => i.AssignedUserId == user.Id); - - //Implementing a core business validation - if (currentIssueCount >= 3) - { - throw new IssueAssignmentException(user.UserName); - } - - issue.AssignedUserId = user.Id; - } -} -```` - -Issue is an [aggregate root](Entities.md) defined as shown below: - -````csharp -public class Issue : AggregateRoot -{ - public Guid? AssignedUserId { get; internal set; } - - //... -} -```` - -* Making the setter `internal` ensures that it can not directly set in the upper layers and forces to always use the `IssueManager` to assign an `Issue` to a `User`. - -### Using a Domain Service - -A Domain Service is typically used in an [application service](Application-Services.md). - -**Example: Use the `IssueManager` to assign an Issue to a User** - -````csharp -using System; -using System.Threading.Tasks; -using MyProject.Users; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace MyProject.Issues -{ - public class IssueAppService : ApplicationService, IIssueAppService - { - private readonly IssueManager _issueManager; - private readonly IRepository _userRepository; - private readonly IRepository _issueRepository; - - public IssueAppService( - IssueManager issueManager, - IRepository userRepository, - IRepository issueRepository) - { - _issueManager = issueManager; - _userRepository = userRepository; - _issueRepository = issueRepository; - } - - public async Task AssignAsync(Guid id, Guid userId) - { - var issue = await _issueRepository.GetAsync(id); - var user = await _userRepository.GetAsync(userId); - - await _issueManager.AssignAsync(issue, user); - await _issueRepository.UpdateAsync(issue); - } - } -} -```` - -Since the `IssueAppService` is in the Application Layer, it can't directly assign an issue to a user. So, it uses the `IssueManager`. - -## Application Services vs Domain Services - -While both of [Application Services](Application-Services.md) and Domain Services implement the business rules, there are fundamental logical and formal differences; - -* Application Services implement the **use cases** of the application (user interactions in a typical web application), while Domain Services implement the **core, use case independent domain logic**. -* Application Services get/return [Data Transfer Objects](Data-Transfer-Objects.md), Domain Service methods typically get and return the **domain objects** ([entities](Entities.md), [value objects](Value-Objects.md)). -* Domain services are typically used by the Application Services or other Domain Services, while Application Services are used by the Presentation Layer or Client Applications. - -## Lifetime - -Lifetime of Domain Services are [transient](https://docs.abp.io/en/abp/latest/Dependency-Injection) and they are automatically registered to the dependency injection system. - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/domain-services) \ No newline at end of file diff --git a/docs/en/Dynamic-Claims.md b/docs/en/Dynamic-Claims.md deleted file mode 100644 index 5985be9510..0000000000 --- a/docs/en/Dynamic-Claims.md +++ /dev/null @@ -1,92 +0,0 @@ -# Dynamic Claims - -When a client authenticates and obtains an access token or an authentication cookie, the claims in that token or cookie are not changed unless it re-authenticates. For most of the claims, that may not be a problem since claims are not frequently changing values. However, some claims may be changed and these changes should be reflected to the current session immediately. For example, we can revoke a role from a user and that should be immediately effective, otherwise user will continue to use that role's permissions until re-login to the application. - -ABP's dynamic claims feature is used to automatically and dynamically override the configured claim values in the client's authentication token/cookie by the latest values of these claims. - -## How to Use - -This feature is disabled by default. You should enable it for your application and use the Dynamic Claims middleware. - -> **Beginning from the v8.0, all the [startup templates](Startup-Templates/Index.md) are pre-configured and the dynamic claims feature is enabled by default. So, if you have created a solution with v8.0 and above, you don't need to make any configuration. Follow the instructions only if you've upgraded from a version lower than 8.0.** - -### Enabling the Dynamic Claims - -You can enable it by the following code: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.IsDynamicClaimsEnabled = true; - }); -} -```` - -This is typically done on the authentication server. In a monolith application, you will typically have a single application, so you can configure it. If you are using the tiered solution structure (where the UI part is hosted in a separate application) you will need to also set the `RemoteRefreshUrl` to the Authentication Server's URL in the UI application. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.IsDynamicClaimsEnabled = true; - options.RemoteRefreshUrl = configuration["AuthServerUrl"] + options.RemoteRefreshUrl; - }); -} -```` - -> The `RemoteRefreshUrl` is already configured inside methods `AddAbpOpenIdConnect` and `AddAbpJwtBearer`. - - -### The Dynamic Claims Middleware - -Add the `DynamicClaims` middleware to all the applications that performs authentication (including the authentication server): - -````csharp -public override void OnApplicationInitialization( - ApplicationInitializationContext context) -{ - //... - app.UseDynamicClaims(); // Add this line before UseAuthorization. - app.UseAuthorization(); - //... -} -```` - -## How It Works - -The `DynamicClaims` middleware will use `IAbpClaimsPrincipalFactory` to dynamically generate claims for the current user(`HttpContext.User`) in each request. - -There are three pre-built implementations of `IAbpDynamicClaimsPrincipalContributor` for different scenarios: - -* `IdentityDynamicClaimsPrincipalContributor`: Provided by the [Identity module](Modules/Identity.md) and generates and overrides the actual dynamic claims, and writes to the distributed cache. Typically works in the authentication server in a distributed system. -* `RemoteDynamicClaimsPrincipalContributor`: For distributed scenarios, this implementation works in the UI application. It tries to get dynamic claim values in the distributed cache. If not found in the distributed cache, it makes an HTTP call to the authentication server and requests filling it by the authentication server. `AbpClaimsPrincipalFactoryOptions.RemoteRefreshUrl` should be properly configure to make it running. -* `WebRemoteDynamicClaimsPrincipalContributor`: Similar to the `RemoteDynamicClaimsPrincipalContributor` but works in the microservice applications. - -### IAbpDynamicClaimsPrincipalContributor - -If you want to add your own dynamic claims contributor, you can create a class that implement the `IAbpDynamicClaimsPrincipalContributor` interface (and register it to the [dependency injection](Dependency-Injection.md) system. ABP Framework will call the `ContributeAsync` method to get the claims. It better to use a kind of cache to improve the performance since that is a frequently executed method (in every HTTP request). - -## AbpClaimsPrincipalFactoryOptions - -`AbpClaimsPrincipalFactoryOptions` is the main options class to configure the behavior of the dynamic claims system. It has the following properties: - -* `IsDynamicClaimsEnabled`: Enable or disable the dynamic claims feature. -* `RemoteRefreshUrl`: The `url ` of the Auth Server to refresh the cache. It will be used by the `RemoteDynamicClaimsPrincipalContributor`. The default value is `/api/account/dynamic-claims/refresh ` and you should provide the full URL in the authentication server, like `http://my-account-server/api/account/dynamic-claims/refresh `. -* `DynamicClaims`: A list of dynamic claim types. Only the claims in that list will be overridden by the dynamic claims system. -* `ClaimsMap`: A dictionary to map the claim types. This is used when the claim types are different between the Auth Server and the client. Already set up for common claim types by default. - -## WebRemoteDynamicClaimsPrincipalContributorOptions - -`WebRemoteDynamicClaimsPrincipalContributorOptions` is the options class to configure the behavior of the `WebRemoteDynamicClaimsPrincipalContributor`. It has the following properties: - -* `IsEnabled`: Enable or disable the `WebRemoteDynamicClaimsPrincipalContributor`. `false` by default. -* `AuthenticationScheme`: The authentication scheme to authenticate the HTTP call to the authentication server. - -## See Also - -* [Authorization](Authorization.md) -* [Claims-based authorization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims) -* [Mapping, customizing, and transforming claims in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims) diff --git a/docs/en/Dynamic-Proxying-Interceptors.md b/docs/en/Dynamic-Proxying-Interceptors.md deleted file mode 100644 index e8b57b80fb..0000000000 --- a/docs/en/Dynamic-Proxying-Interceptors.md +++ /dev/null @@ -1,7 +0,0 @@ -## Dynamic Proxying / Interceptors - -TODO - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/interception) \ No newline at end of file diff --git a/docs/en/Emailing.md b/docs/en/Emailing.md deleted file mode 100644 index 48635b2565..0000000000 --- a/docs/en/Emailing.md +++ /dev/null @@ -1,260 +0,0 @@ -# Email Sending - -ABP Framework provides various services, settings and integrations for sending emails; - -* Provides `IEmailSender` service that is used to send emails. -* Defines [settings](Settings.md) to configure email sending. -* Integrates to the [background job system](Background-Jobs.md) to send emails via background jobs. -* Provides [MailKit integration](MailKit.md) package. - -## Installation - -> This package is already installed if you are using the [application startup template](Startup-Templates/Application.md). - -It is suggested to use the [ABP CLI](CLI.md) to install this package. Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.Emailing -```` - -If you haven't done it yet, you first need to install the ABP CLI. For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Emailing). - -## Sending Emails - -### IEmailSender - -[Inject](Dependency-Injection.md) the `IEmailSender` into any service and use the `SendAsync` method to send emails. - -**Example** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; - -namespace MyProject -{ - public class MyService : ITransientDependency - { - private readonly IEmailSender _emailSender; - - public MyService(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public async Task DoItAsync() - { - await _emailSender.SendAsync( - "target@domain.com", // target email address - "Email subject", // subject - "This is email body..." // email body - ); - } - } -} -```` - -`SendAsync` method has overloads to supply more parameters like; - -* **from**: You can set this as the first argument to set a sender email address. If not provided, the default sender address is used (see the email settings below). -* **to**: You can set the target email address. -* **subject**: You can set the email subject. -* **body**: You can set the email body. -* **isBodyHtml**: Indicates whether the email body may contain HTML tags. **Default: true**. -* **additionalEmailSendingArgs**: This parameter is used to pass additional arguments to the `IEmailSender` implementation. Include: CC(Carbon copy), a list of `EmailAttachment` and an extra properties. - -> `IEmailSender` is the suggested way to send emails, since it makes your code provider independent. - -#### MailMessage - -In addition to primitive parameters, you can pass a **standard `MailMessage` object** ([see](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage)) to the `SendAsync` method to set more options, like adding attachments. - -### ISmtpEmailSender - -Sending emails is implemented by the standard `SmtpClient` class ([see](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient)) by default. The implementation class is the `SmtpEmailSender`. This class also expose the `ISmtpEmailSender` service (in addition to the `IEmailSender`). - -Most of the time you want to directly use the `IEmailSender` to make your code provider independent. However, if you want to create an `SmtpClient` object with the same email settings, you can inject the `ISmtpEmailSender` and use its `BuildClientAsync` method to obtain a `SmtpClient` object and send the email yourself. - -## Queueing Emails / Background Jobs - -`IEmailSender` has a `QueueAsync` method that can be used to add emails to the background job queue to send them in a background thread. In this way, you don't take time of the user by waiting to send the email. `QueueAsync` method gets the same arguments with the `SendAsync` method. - -Queueing emails tolerates errors since the background job system has re-try mechanism to overcome temporary network/server problems. - -See the [background jobs document](Background-Jobs.md) for more about the background job system. - -## Email Settings - -Email sending uses the [setting system](Settings.md) to define settings and get the values of these settings on the runtime. `Volo.Abp.Emailing.EmailSettingNames` defines constants for the setting names, just listed below: - -* **Abp.Mailing.DefaultFromAddress**: Used as the sender's email address when you don't specify a sender when sending emails (just like in the example above). -* **Abp.Mailing.DefaultFromDisplayName**: Used as the sender's display name when you don't specify a sender when sending emails (just like in the example above). -* **Abp.Mailing.Smtp.Host**: The IP/Domain of the SMTP server (default: 127.0.0.1). -* **Abp.Mailing.Smtp.Port**: The Port of the SMTP server (default: 25). -* **Abp.Mailing.Smtp.UserName**: Username, if the SMTP server requires authentication. -* **Abp.Mailing.Smtp.Password**: Password, if the SMTP server requires authentication. **This value is encrypted **(see the section below). -* **Abp.Mailing.Smtp.Domain**: Domain for the username, if the SMTP server requires authentication. -* **Abp.Mailing.Smtp.EnableSsl**: A value that indicates if the SMTP server uses SSL or not ("true" or "false". Default: "false"). -* **Abp.Mailing.Smtp.UseDefaultCredentials**: If true, uses default credentials instead of the provided username and password ("true" or "false". Default: "true"). - -Email settings can be managed from the *Settings Page* of the [Setting Management](Modules/Setting-Management.md) module: - -![email-settings](images/email-settings.png) - -> Setting Management module is already installed if you've created your solution from the ABP Startup template. - -If you don't use the Setting Management module, you can simply define the settings inside your `appsettings.json` file: - -````json -"Settings": { - "Abp.Mailing.Smtp.Host": "127.0.0.1", - "Abp.Mailing.Smtp.Port": "25", - "Abp.Mailing.Smtp.UserName": "", - "Abp.Mailing.Smtp.Password": "", - "Abp.Mailing.Smtp.Domain": "", - "Abp.Mailing.Smtp.EnableSsl": "false", - "Abp.Mailing.Smtp.UseDefaultCredentials": "true", - "Abp.Mailing.DefaultFromAddress": "noreply@abp.io", - "Abp.Mailing.DefaultFromDisplayName": "ABP application" -} -```` - -You can set/change these settings programmatically using the `ISettingManager` and store values in a database. See the [setting system document](Settings.md) to understand the setting system better. - -### Encrypt the SMTP Password - -*Abp.Mailing.Smtp.Password* must be an **encrypted** value. If you use the `ISettingManager` to set the password, you don't have to worry. It internally encrypts the values on set and decrypts on get. - -If you use the `appsettings.json` to store the password, you should manually inject the `ISettingEncryptionService` and use its `Encrypt` method to obtain an encrypted value. This can be done by creating a simple code in your application. Then you can delete the code. As better, you can create a UI in your application to configure the email settings. In this case, you can directly use the `ISettingManager` without worrying the encryption. - -### ISmtpEmailSenderConfiguration - -If you don't want to use the setting system to store the email sending configuration, you can replace the `ISmtpEmailSenderConfiguration` service with your own implementation to get the configuration from any other source. `ISmtpEmailSenderConfiguration` is implemented by the `SmtpEmailSenderConfiguration` by default, which gets the configuration from the setting system as explained above. - -## Text Template Integration - -ABP Framework provides a strong and flexible [text templating system](Text-Templating.md). You can use the text templating system to create dynamic email contents. Inject the `ITemplateRenderer` and use the `RenderAsync` to render a template. Then use the result as the email body. - -While you can define and use your own text templates, email sending system provides two simple built-in text templates. - -**Example: Use the standard and simple message template to send emails** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace Acme.BookStore.Web -{ - public class MyService : ITransientDependency - { - private readonly IEmailSender _emailSender; - private readonly ITemplateRenderer _templateRenderer; - - public MyService( - IEmailSender emailSender, - ITemplateRenderer templateRenderer) - { - _emailSender = emailSender; - _templateRenderer = templateRenderer; - } - - public async Task DoItAsync() - { - var body = await _templateRenderer.RenderAsync( - StandardEmailTemplates.Message, - new - { - message = "This is email body..." - } - ); - - await _emailSender.SendAsync( - "target-address@domain.com", - "Email subject", - body - ); - } - } -} -```` - -The resulting email body will be shown below: - -````html - - - - - - - This is email body... - - -```` - -Emailing system defines the built-in text templates with the given names: - -"**Abp.StandardEmailTemplates.Message**" is simplest template that has a text message: - -````html -{%{{{model.message}}}%} -```` - -This template uses the "Abp.StandardEmailTemplates.Layout" as its layout. - -"**Abp.StandardEmailTemplates.Layout**" is a simple template to provide an HTML document layout: - -````html - - - - - - - {%{{{content}}}%} - - -```` - -The final rendered message was shown above. - -> These template names are contants defined in the `Volo.Abp.Emailing.Templates.StandardEmailTemplates` class. - -### Overriding/Replacing the Standard Templates - -You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](Virtual-File-System.md) (VFS) or replace them in your own template definition provider. - -Pathes of the templates in the virtual file system are shown below: - -* `/Volo/Abp/Emailing/Templates/Layout.tpl` -* `/Volo/Abp/Emailing/Templates/Message.tpl` - -If you add files to the same location in the virtual file system, your files will override them. - -Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural. - -See the [text templating system](Text-Templating.md) document for details. - -> Notice that you can define and use your own templates for your application, rather than using the standard simple templates. These standard templates are mostly for reusable modules where they don't define their own templates but rely on the built-in ones. This makes easy to customize emails sent by the used modules, by just overriding the standard email layout template. - -## NullEmailSender - -`NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the [standard log system](Logging.md), rathen than actually sending the emails. - -This class can be useful especially in development time where you generally don't want to send real emails. The [application startup template](Startup-Templates/Application.md) already uses this class in the **DEBUG mode** with the following configuration in the domain layer: - -````csharp -#if DEBUG - context.Services.Replace(ServiceDescriptor.Singleton()); -#endif -```` - -So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent as expected on production (RELEASE mode). Remove these lines if you want to send real emails on DEBUG too. - -## See Also - -* [MailKit integration for sending emails](MailKit.md) diff --git a/docs/en/Entities.md b/docs/en/Entities.md deleted file mode 100644 index b5498c4adb..0000000000 --- a/docs/en/Entities.md +++ /dev/null @@ -1,441 +0,0 @@ -# Entities - -Entities are one of the core concepts of DDD (Domain Driven Design). Eric Evans describes it as "*An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity*". - -An entity is generally mapped to a table in a relational database. - -## Entity Class - -Entities are derived from the `Entity` class as shown below: - -```C# -public class Book : Entity -{ - public string Name { get; set; } - - public float Price { get; set; } -} -``` - -> If you do not want to derive your entity from the base `Entity` class, you can directly implement `IEntity` interface. - -`Entity` class just defines an `Id` property with the given primary **key type**, which is `Guid` in the example above. It can be other types like `string`, `int`, `long`, or whatever you need. - -### Entities with GUID Keys - -If your entity's Id type is `Guid`, there are some good practices to implement: - -* Create a constructor that gets the Id as a parameter and passes to the base class. - * If you don't set a GUID Id, **ABP Framework sets it on save**, but it is good to have a valid Id on the entity even before saving it to the database. -* If you create an entity with a constructor that takes parameters, also create a `private` or `protected` empty constructor. This is used while your database provider reads your entity from the database (on deserialization). -* Don't use the `Guid.NewGuid()` to set the Id! **Use [the `IGuidGenerator` service](Guid-Generation.md)** while passing the Id from the code that creates the entity. `IGuidGenerator` optimized to generate sequential GUIDs, which is critical for clustered indexes in the relational databases. - -An example entity: - -````csharp -public class Book : Entity -{ - public string Name { get; set; } - - public float Price { get; set; } - - protected Book() - { - - } - - public Book(Guid id) - : base(id) - { - - } -} -```` - -Example usage in an [application service](Application-Services.md): - -````csharp -public class BookAppService : ApplicationService, IBookAppService -{ - private readonly IRepository _bookRepository; - - public BookAppService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task CreateAsync(CreateBookDto input) - { - await _bookRepository.InsertAsync( - new Book(GuidGenerator.Create()) - { - Name = input.Name, - Price = input.Price - } - ); - } -} -```` - -* `BookAppService` injects the default [repository](Repositories.md) for the book entity and uses its `InsertAsync` method to insert a `Book` to the database. -* `GuidGenerator` is type of `IGuidGenerator` which is a property defined in the `ApplicationService` base class. ABP defines such frequently used base properties as pre-injected for you, so you don't need to manually [inject](Dependency-Injection.md) them. -* If you want to follow the DDD best practices, see the *Aggregate Example* section below. - -### Entities with Composite Keys - -Some entities may need to have **composite keys**. In that case, you can derive your entity from the non-generic `Entity` class. Example: - -````C# -public class UserRole : Entity -{ - public Guid UserId { get; set; } - - public Guid RoleId { get; set; } - - public DateTime CreationTime { get; set; } - - public UserRole() - { - - } - - public override object[] GetKeys() - { - return new object[] { UserId, RoleId }; - } -} -```` - -For the example above, the composite key is composed of `UserId` and `RoleId`. For a relational database, it is the composite primary key of the related table. Entities with composite keys should implement the `GetKeys()` method as shown above. - -> Notice that you also need to define keys of the entity in your **object-relational mapping** (ORM) configuration. See the [Entity Framework Core](Entity-Framework-Core.md) integration document for example. - -> Also note that Entities with Composite Primary Keys cannot utilize the `IRepository` interface since it requires a single Id property. However, you can always use `IRepository`. See [repositories documentation](Repositories.md) for more. - -### EntityEquals - -`Entity.EntityEquals(...)` method is used to check if two Entity Objects are equals. - -Example: - -```csharp -Book book1 = ... -Book book2 = ... - -if (book1.EntityEquals(book2)) //Check equality -{ - ... -} -``` - -## AggregateRoot Class - -"*Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it's useful to treat the order (together with its line items) as a single aggregate.*" (see the [full description](http://martinfowler.com/bliki/DDD_Aggregate.html)) - -`AggregateRoot` class extends the `Entity` class. So, it also has an `Id` property by default. - -> Notice that ABP creates default repositories only for aggregate roots by default. However, it's possible to include all entities. See the [repositories documentation](Repositories.md) for more. - -ABP does not force you to use aggregate roots, you can in fact use the `Entity` class as defined before. However, if you want to implement the [Domain Driven Design](Domain-Driven-Design.md) and want to create aggregate root classes, there are some best practices you may want to consider: - -* An aggregate root is responsible for preserving its own integrity. This is also true for all entities, but the aggregate root has responsibility for its sub-entities too. So, the aggregate root must always be in a valid state. -* An aggregate root can be referenced by its `Id`. Do not reference it by its navigation property. -* An aggregate root is treated as a single unit. It's retrieved and updated as a single unit. It's generally considered as a transaction boundary. -* Work with sub-entities over the aggregate root- do not modify them independently. - -See the [entity design best practice guide](Best-Practices/Entities.md) if you want to implement DDD in your application. - -### Aggregate Example - -This is a full sample of an aggregate root with a related sub-entity collection: - -````C# -public class Order : AggregateRoot -{ - public virtual string ReferenceNo { get; protected set; } - - public virtual int TotalItemCount { get; protected set; } - - public virtual DateTime CreationTime { get; protected set; } - - public virtual List OrderLines { get; protected set; } - - protected Order() - { - - } - - public Order(Guid id, string referenceNo) - { - Check.NotNull(referenceNo, nameof(referenceNo)); - - Id = id; - ReferenceNo = referenceNo; - - OrderLines = new List(); - } - - public void AddProduct(Guid productId, int count) - { - if (count <= 0) - { - throw new ArgumentException( - "You can not add zero or negative count of products!", - nameof(count) - ); - } - - var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId); - - if (existingLine == null) - { - OrderLines.Add(new OrderLine(this.Id, productId, count)); - } - else - { - existingLine.ChangeCount(existingLine.Count + count); - } - - TotalItemCount += count; - } -} - -public class OrderLine : Entity -{ - public virtual Guid OrderId { get; protected set; } - - public virtual Guid ProductId { get; protected set; } - - public virtual int Count { get; protected set; } - - protected OrderLine() - { - - } - - internal OrderLine(Guid orderId, Guid productId, int count) - { - OrderId = orderId; - ProductId = productId; - Count = count; - } - - internal void ChangeCount(int newCount) - { - Count = newCount; - } - - public override object[] GetKeys() - { - return new Object[] {OrderId, ProductId}; - } -} -```` - -> If you do not want to derive your aggregate root from the base `AggregateRoot` class, you can directly implement the `IAggregateRoot` interface. - -`Order` is an **aggregate root** with `Guid` type `Id` property. It has a collection of `OrderLine` entities. `OrderLine` is another entity with a composite primary key (`OrderId` and ` ProductId`). - -While this example may not implement all the best practices of an aggregate root, it still follows some good practices: - -* `Order` has a public constructor that takes **minimal requirements** to construct an `Order` instance. So, it's not possible to create an order without an id and reference number. The **protected/private** constructor is only necessary to **deserialize** the object while reading from a data source. -* `OrderLine` constructor is internal, so it is only allowed to be created by the domain layer. It's used inside of the `Order.AddProduct` method. -* `Order.AddProduct` implements the business rule to add a product to an order. -* All properties have `protected` setters. This is to prevent the entity from arbitrary changes from outside of the entity. For example, it would be dangerous to set `TotalItemCount` without adding a new product to the order. Its value is maintained by the `AddProduct` method. - -ABP Framework does not force you to apply any DDD rule or patterns. However, it tries to make it possible and easier when you do want to apply them. The documentation also follows the same principle. - -### Aggregate Roots with Composite Keys - -While it's not common (and not suggested) for aggregate roots, it is in fact possible to define composite keys in the same way as defined for the mentioned entities above. Use non-generic `AggregateRoot` base class in that case. - -### BasicAggregateRoot Class - -`AggregateRoot` class implements the `IHasExtraProperties` and `IHasConcurrencyStamp` interfaces which brings two properties to the derived class. `IHasExtraProperties` makes the entity extensible (see the *Extra Properties* section below) and `IHasConcurrencyStamp` adds a `ConcurrencyStamp` property that is managed by the ABP Framework to implement the [optimistic concurrency](https://docs.microsoft.com/en-us/ef/core/saving/concurrency). In most cases, these are wanted features for aggregate roots. - -However, if you don't need these features, you can inherit from the `BasicAggregateRoot` (or `BasicAggregateRoot`) for your aggregate root. - -## Base Classes & Interfaces for Audit Properties - -There are some properties like `CreationTime`, `CreatorId`, `LastModificationTime`... which are very common in all applications. ABP Framework provides some interfaces and base classes to **standardize** these properties and also **sets their values automatically**. - -### Auditing Interfaces - -There are a lot of auditing interfaces, so you can implement the one that you need. - -> While you can manually implement these interfaces, you can use **the base classes** defined in the next section to simplify it. - -* `IHasCreationTime` defines the following properties: - * `CreationTime` -* `IMayHaveCreator` defines the following properties: - * `CreatorId` -* `ICreationAuditedObject` inherits from the `IHasCreationTime` and the `IMayHaveCreator`, so it defines the following properties: - * `CreationTime` - * `CreatorId` -* `IHasModificationTime` defines the following properties: - * `LastModificationTime` -* `IModificationAuditedObject` extends the `IHasModificationTime` and adds the `LastModifierId` property. So, it defines the following properties: - * `LastModificationTime` - * `LastModifierId` -* `IAuditedObject` extends the `ICreationAuditedObject` and the `IModificationAuditedObject`, so it defines the following properties: - * `CreationTime` - * `CreatorId` - * `LastModificationTime` - * `LastModifierId` -* `ISoftDelete` (see the [data filtering document](Data-Filtering.md)) defines the following properties: - * `IsDeleted` -* `IHasDeletionTime` extends the `ISoftDelete` and adds the `DeletionTime` property. So, it defines the following properties: - * `IsDeleted` - * `DeletionTime` -* `IDeletionAuditedObject` extends the `IHasDeletionTime` and adds the `DeleterId` property. So, it defines the following properties: - * `IsDeleted` - * `DeletionTime` - * `DeleterId` -* `IFullAuditedObject` inherits from the `IAuditedObject` and the `IDeletionAuditedObject`, so it defines the following properties: - * `CreationTime` - * `CreatorId` - * `LastModificationTime` - * `LastModifierId` - * `IsDeleted` - * `DeletionTime` - * `DeleterId` - -Once you implement any of the interfaces, or derive from a class defined in the next section, ABP Framework automatically manages these properties wherever possible. - -> Implementing `ISoftDelete`, `IDeletionAuditedObject` or `IFullAuditedObject` makes your entity **soft-delete**. See the [data filtering document](Data-Filtering.md) to learn about the soft-delete pattern. - -### Auditing Base Classes - -While you can manually implement any of the interfaces defined above, it is suggested to inherit from the base classes defined here: - -* `CreationAuditedEntity` and `CreationAuditedAggregateRoot` implement the `ICreationAuditedObject` interface. -* `AuditedEntity` and `AuditedAggregateRoot` implement the `IAuditedObject` interface. -* `FullAuditedEntity` and `FullAuditedAggregateRoot` implement the `IFullAuditedObject` interface. - -All these base classes also have non-generic versions to take `AuditedEntity` and `FullAuditedAggregateRoot` to support the composite primary keys. - -All these base classes also have `...WithUser` pairs, like `FullAuditedAggregateRootWithUser` and `FullAuditedAggregateRootWithUser`. This makes possible to add a navigation property to your user entity. However, it is not a good practice to add navigation properties between aggregate roots, so this usage is not suggested (unless you are using an ORM, like EF Core, that well supports this scenario and you really need it - otherwise remember that this approach doesn't work for NoSQL databases like MongoDB where you must truly implement the aggregate pattern). Also, if you add navigation properties to the AppUser class that comes with the startup template, consider to handle (ignore/map) it on the migration dbcontext (see [the EF Core migration document](Entity-Framework-Core-Migrations.md)). - -## Caching Entities - -ABP Framework provides a [Distributed Entity Cache System](Entity-Cache.md) for caching entities. It is useful if you want to use caching for quicker access to the entity rather than repeatedly querying it from the database. - -It's designed as read-only and automatically invalidates a cached entity if the entity is updated or deleted. - -> See the [Entity Cache](Entity-Cache.md) documentation for more information. - -## Versioning Entities - -ABP defines the `IHasEntityVersion` interface for automatic versioning of your entities. It only provides a single `EntityVersion` property, as shown in the following code block: - -````csharp -public interface IHasEntityVersion -{ - int EntityVersion { get; } -} -```` - -If you implement the `IHasEntityVersion` interface, ABP automatically increases the `EntityVersion` value whenever you update your entity. The initial `EntityVersion` value will be `0`, when you first create an entity and save to the database. - -> ABP can not increase the version if you directly execute SQL `UPDATE` commands in the database. It is your responsibility to increase the `EntityVersion` value in that case. Also, if you are using the aggregate pattern and change sub-collections of an aggregate root, it is your responsibility if you want to increase the version of the aggregate root object. - -## Extra Properties - -ABP defines the `IHasExtraProperties` interface that can be implemented by an entity to be able to dynamically set and get properties for the entity. `AggregateRoot` base class already implements the `IHasExtraProperties` interface. If you've derived from this class (or one of the related audit class defined above), you can directly use the API. - -### GetProperty & SetProperty Extension Methods - -These extension methods are the recommended way to get and set data for an entity. Example: - -````csharp -public class ExtraPropertiesDemoService : ITransientDependency -{ - private readonly IIdentityUserRepository _identityUserRepository; - - public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository) - { - _identityUserRepository = identityUserRepository; - } - - public async Task SetTitle(Guid userId, string title) - { - var user = await _identityUserRepository.GetAsync(userId); - - //SET A PROPERTY - user.SetProperty("Title", title); - - await _identityUserRepository.UpdateAsync(user); - } - - public async Task GetTitle(Guid userId) - { - var user = await _identityUserRepository.GetAsync(userId); - - //GET A PROPERTY - return user.GetProperty("Title"); - } -} -```` - -* Property's **value is object** and can be any type of object (string, int, bool... etc). -* `GetProperty` returns `null` if given property was not set before. -* You can store more than one property at the same time by using different **property names** (like `Title` here). - -It would be a good practice to **define a constant** for the property name to prevent typo errors. It would be even a better practice to **define extension methods** to take the advantage of the intellisense. Example: - -````csharp -public static class IdentityUserExtensions -{ - private const string TitlePropertyName = "Title"; - - public static void SetTitle(this IdentityUser user, string title) - { - user.SetProperty(TitlePropertyName, title); - } - - public static string GetTitle(this IdentityUser user) - { - return user.GetProperty(TitlePropertyName); - } -} -```` - -Then you can directly use `user.SetTitle("...")` and `user.GetTitle()` for an `IdentityUser` object. - -### HasProperty & RemoveProperty Extension Methods - -* `HasProperty` is used to check if the object has a property set before. -* `RemoveProperty` is used to remove a property from the object. You can use this instead of setting a `null` value. - -### How it is Implemented? - -`IHasExtraProperties` interface requires to define a `Dictionary` property, named `ExtraProperties`, for the implemented class. - -So, you can directly use the `ExtraProperties` property to use the dictionary API, if you like. However, `SetProperty` and `GetProperty` methods are the recommended ways since they also check for `null`s. - -#### How is it Stored? - -The way to store this dictionary in the database depends on the database provider you're using. - -* For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations; - * By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. - * If you want, you can use the `ObjectExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `ObjectExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `ObjectExtensionManager`. -* For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system. - -### Discussion for the Extra Properties - -Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. - -You typically **don't need** to use this system for your own entities, because it has the following drawbacks: - -* It is **not fully type safe** since it works with strings as property names. -* It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects. - -### Extra Properties Behind Entities - -`IHasExtraProperties` is not restricted to be used with entities. You can implement this interface for any kind of class and use the `GetProperty`, `SetProperty` and other related methods. - -## See Also - -* [Best practice guide to design the entities](Best-Practices/Entities.md) -* [Video tutorial](https://abp.io/video-courses/essentials/entities) diff --git a/docs/en/Entity-Cache.md b/docs/en/Entity-Cache.md deleted file mode 100644 index 6eaa186e53..0000000000 --- a/docs/en/Entity-Cache.md +++ /dev/null @@ -1,131 +0,0 @@ -# Entity Cache - -ABP Framework provides an entity caching system that works on top of the [distributed caching](Caching.md) system. It does the following operations on behalf of you: - -* Gets the entity from the database (by using the [repositories](Repositories.md)) in its first call and then gets it from the cache in subsequent calls. -* Automatically invalidates the cached entity if the entity is updated or deleted. Thus, it will be retrieved from the database in the next call and will be re-cached. - -## Caching Entity Objects - -`IEntityCache` is a simple service provided by the ABP Framework for caching entities. Assume that you have a `Product` entity as shown below: - -```csharp -public class Product : AggregateRoot -{ - public string Name { get; set; } - public string Description { get; set; } - public float Price { get; set; } - public int StockCount { get; set; } -} -``` - -If you want to cache this entity, you should first configure the [dependency injection](Dependency-Injection.md) system to register the `IEntityCache` service in the `ConfigureServices` method of your [module class](Module-Development-Basics.md): - -```csharp -context.Services.AddEntityCache(); -``` - -Now you can inject the `IEntityCache` service wherever you need: - -```csharp -public class ProductAppService : ApplicationService, IProductAppService -{ - private readonly IEntityCache _productCache; - - public ProductAppService(IEntityCache productCache) - { - _productCache = productCache; - } - - public async Task GetAsync(Guid id) - { - var product = await _productCache.GetAsync(id); - return ObjectMapper.Map(product); - } -} -``` - -> Note that we've used the `ObjectMapper` service to map from `Product` to `ProductDto`. You should configure that [object mapping](Object-To-Object-Mapping.md) to make that example service properly work. - -That's all. The cache name (in the distributed cache server) will be the full name (with namespace) of the `Product` class. You can use the `[CacheName]` attribute to change it. Please refer to the [caching document](Caching.md) for details. - -## Using a Cache Item Class - -In the previous section, we've directly cached the `Product` entity. In that case, the `Product` class must be serializable to JSON (and deserializable from JSON). Sometimes that might not be possible or you may want to use another class to store the cache data. For example, we may want to use the `ProductDto` class instead of the `Product` class for the cached object of the `Product` entity. - -Assume that we've created a `ProductDto` class as shown below: - -```csharp -public class ProductDto : EntityDto -{ - public string Name { get; set; } - public string Description { get; set; } - public float Price { get; set; } - public int StockCount { get; set; } -} -``` - -Now, we can register the entity cache services to [dependency injection](Dependency-Injection.md) in the `ConfigureServices` method of your [module class](Module-Development-Basics.md) with three generic parameters, as shown below: - -```csharp -context.Services.AddEntityCache(); -``` - -Since the entity cache system will perform the [object mapping](Object-To-Object-Mapping.md) (from `Product` to `ProductDto`), we should configure the object map. Here, an example configuration with [AutoMapper](https://automapper.org/): - -```csharp -public class MyMapperProfile : Profile -{ - public MyMapperProfile() - { - CreateMap(); - } -} -``` - -Now, you can inject the `IEntityCache` service wherever you want: - -```csharp -public class ProductAppService : ApplicationService, IProductAppService -{ - private readonly IEntityCache _productCache; - - public ProductAppService(IEntityCache productCache) - { - _productCache = productCache; - } - - public async Task GetAsync(Guid id) - { - return await _productCache.GetAsync(id); - } -} -``` - -Notice that the `_productCache.GetAsync` method already returns a `ProductDto` object, so we could directly return it from our application service. - -## Configuration - -All of the `context.Services.AddEntityCache()` methods get an optional `DistributedCacheEntryOptions` parameter where you can easily configure the caching options: - -```csharp -context.Services.AddEntityCache( - new DistributedCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromMinutes(30) - } -); -``` - -> The default cache duration is **2 minutes** with the `AbsoluteExpirationRelativeToNow` configuration. - -## Additional Notes - -* Entity classes should be serializable/deserializable to/from JSON to be cached (because it's serialized to JSON when saving in the [Distributed Cache](Caching.md)). If your entity class is not serializable, you can consider using a cache-item/DTO class instead, as explained before. -* Entity Caching System is designed as **read-only**. You should use the standard [repository](Repositories.md) methods to manipulate the entity if you need to. If you need to manipulate (update) the entity, do not get it from the entity cache. Instead, read it from the repository, change it and update using the repository. - -## See Also - -* [Distributed caching](Caching.md) -* [Entities](Entities.md) -* [Repositories](Repositories.md) diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md deleted file mode 100644 index 9ac7e76f24..0000000000 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ /dev/null @@ -1,557 +0,0 @@ -# EF Core Database Migrations - -This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application. - -> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Part-1.md). - -### Source Code - -You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. - -## About the EF Core Code First Migrations - -Entity Framework Core provides an easy to use and powerful [database migration system](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). ABP Framework [startup templates](Startup-Templates/Index.md) take the advantage of this system to allow you to develop your application in a standard way. - -However, EF Core migration system is **not so good in a modular environment** where each module maintains its **own database schema** while two or more modules may **share a single database** in practical. - -Since ABP Framework cares about modularity in all aspects, it provides a **solution** to this problem. It is important to understand this solution if you need to **customize your database structure**. - -> See [EF Core's own documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to fully learn the EF Core Code First Migrations and why you need to such a system. - -## The Default Solution & Database Configuration - -When you [create a new web application](https://abp.io/get-started) (with EF Core, which is the default database provider), your solution structure will be similar to the picture below: - -![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) - -Actual solution structure may be a bit different based on your preferences, but the database part will be same. - -> This document will use the `Acme.BookStore` example project name to refer the projects and classes. You need to find the corresponding class/project in your solution. - -### The Database Structure - -The startup template has some [application modules](Modules/Index.md) pre-installed. Each layer of the solution has corresponding module **package references**. So, the `.EntityFrameworkCore` project has the NuGet references for the `.EntityFrameworkCore` packages of the used modules: - -![bookstore-efcore-dependencies](images/bookstore-efcore-dependencies.png) - -In this way, you collect all the **EF Core dependencies** under the `.EntityFrameworkCore` project. - -> In addition to the module references, it references to the `Volo.Abp.EntityFrameworkCore.SqlServer` package since the startup template is pre-configured for the **SQL Server**. See the documentation if you want to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). - -While every module has its own `DbContext` class by design and can use its **own physical database**, the solution is configured to use a **single shared database** as shown in the figure below: - -![single-database-usage](images/single-database-usage.png) - -This is **the simplest configuration** and suitable for most of the applications. `appsettings.json` file has a **single connection string**, named `Default`: - -````json -"ConnectionStrings": { - "Default": "..." -} -```` - -So, you have a **single database schema** which contains all the tables of the modules **sharing** this database. - -ABP Framework's [connection string](Connection-Strings.md) system allows you to easily **set a different connection string** for a desired module: - -````json -"ConnectionStrings": { - "Default": "...", - "AbpAuditLogging": "..." -} -```` - -The example configuration tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md) (if you don't specify connection string for a module, it uses the `Default` connection string). - -**However, this can work only if the audit log database with the given connection string is available**. So, you need to create the second database, create audit log tables inside it and maintain the database tables. No problem if you manually do all these. However, the recommended approach is the code first migrations. One of the main purposes of this document is to guide you on such **database separation** scenarios. - -#### Module Tables - -Every module uses its **own databases tables**. For example, the [Identity Module](Modules/Identity.md) has some tables to manage the users and roles in the system. - -##### Table Prefixes - -Since it is allowed to share a single database by all modules (it is the default configuration), a module typically uses a **table name prefix** to group its own tables. - -The fundamental modules, like [Identity](Modules/Identity.md), [Tenant Management](Modules/Tenant-Management.md) and [Audit Logs](Modules/Audit-Logging.md), use the `Abp` prefix, while some other modules use their own prefixes. [Identity Server](Modules/IdentityServer.md) module uses the `IdentityServer` prefix for example. - -If you want, you can **change the database table name prefix** for a module for your application. Example: - -````csharp -Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; -```` - -This code changes the prefix of the [Identity Server](Modules/IdentityServer.md) module. Write this code **at the very beginning** in your application. - -> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage. - -### .EntityFrameworkCore Project - -The solution contains a project, which's name ends with `.EntityFrameworkCore`. This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application. - -**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices and hide your data access code behind the repositories). It is almost an empty `DbContext` since your application don't have any entities at the beginning: - -````csharp -[ReplaceDbContext(typeof(IIdentityDbContext))] -[ReplaceDbContext(typeof(ITenantManagementDbContext))] -[ConnectionStringName("Default")] -public class BookStoreDbContext : - AbpDbContext, - IIdentityDbContext, - ITenantManagementDbContext -{ - /* Add DbSet properties for your Aggregate Roots / Entities here. */ - - /* DbSet for entities from the replaced DbContexts */ - - public BookStoreDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureBackgroundJobs(); - builder.ConfigureAuditLogging(); - builder.ConfigureIdentity(); - builder.ConfigureIdentityServer(); - builder.ConfigureFeatureManagement(); - builder.ConfigureTenantManagement(); - - /* Configure your own tables/entities here. Example: */ - //builder.Entity(b => - //{ - // b.ToTable("YourEntities"); - // b.ConfigureByConvention(); //auto configure for the base properties - // //... - //}); - } -} -```` - -This `DbContext` class needs some explanations: - -* It defines `[ReplaceDbContext]` attributes for `IIdentityDbContext` and `ITenantManagementDbContext` those replaces Identity and Tenant Management module's `DbContext`s by your `DbContext` on runtime. This allows us to easily perform LINQ queries by joining your entities with the entities (over the repositories) coming from those modules. -* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`. -* It inherits from the `AbpDbContext` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext` base class implements some conventions of the ABP Framework to automate some common tasks for you. -* It declares `DbSet` properties for entities from the replaced `DbContext`s (by implementing the corresponding interfaces). These `DbSet` properties are not shown above (for the sake of brevity), but you can find in your application's code in a `region`. -* The constructor takes a `DbContextOptions` instance. -* It overrides the `OnModelCreating` method to define the EF Core mappings. - * It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us. - * It then calls some `builder.ConfigureXXX()` methods for the used modules. This makes possible to add database mappings for these modules to this `DbContext`, so it creates the database tables of the modules when we add a new EF Core database migration. - * You can configure the mappings for your own entities as commented in the example code. At this point, you can also change mappings for the modules you are using. - -### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path - -As mentioned before, in the `.EntityFrameworkCore` project, we merge all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. - -An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more modular in the beginning, it has some drawbacks in practical: - -* **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code. If you can depend on a specific DBMS in your module, that's not an issue for you, however all pre-built ABP modules are DBMS agnostic. -* It would be harder to **customize/enhance** the mapping and the resulting migration code, in the final application. -* It would be harder to track and **apply changes** to database when you use multiple modules. - -## Using Multiple Databases - -The default startup template is organized to use a **single database** used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases. - -This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database. - -The resulting structure will be like the figure below: - -![single-database-usage](images/multiple-database-usage.png) - -### Change the Connection Strings Section - -First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that: - -````json -"ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True" -} -```` - -Change it as shown below: - -````json -"ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True", - "AbpPermissionManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True", - "AbpSettingManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True", - "AbpAuditLogging": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True" -} -```` - -Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all the same). For example, `AbpPermissionManagement` is the connection string name used by the permission management module. - -The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string. - -### Create a Second DbContext - -Defining the connection strings as explained above is enough **on runtime**. However, `BookStore_SecondDb` database doesn't exist yet. You need to create the database and the tables for the related modules. - -Just like the main database, we want to use the EF Core Code First migration system to create and maintain the second database. So, create a new `DbContext` class inside the `.EntityFrameworkCore` project: - -````csharp -using Microsoft.EntityFrameworkCore; -using Volo.Abp.AuditLogging.EntityFrameworkCore; -using Volo.Abp.Data; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.PermissionManagement.EntityFrameworkCore; -using Volo.Abp.SettingManagement.EntityFrameworkCore; - -namespace BookStore.EntityFrameworkCore -{ - [ConnectionStringName("AbpPermissionManagement")] - public class BookStoreSecondDbContext : - AbpDbContext - { - public BookStoreSecondDbContext( - DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureAuditLogging(); - } - } -} -```` - -> `[ConnectionStringName(...)]` attribute is important here and tells to the ABP Framework which connection string should be used for this `DbContext`. We've used `AbpPermissionManagement`, but all are the same. - -We need to register this `BookStoreSecondDbContext` class to the dependency injection system. Open the `BookStoreEntityFrameworkCoreModule` class in the `BookStore.EntityFrameworkCore` project and add the following line into the `ConfigureServices` method: - -````csharp -context.Services.AddAbpDbContext(); -```` - -We should also create a **Design Time Db Factory** class, that is used by the EF Core tooling (by `Add-Migration` and `Update-Database` PCM commands for example): - -````csharp -using System.IO; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Configuration; - -namespace BookStore.EntityFrameworkCore -{ - /* This class is needed for EF Core console commands - * (like Add-Migration and Update-Database commands) */ - public class BookStoreSecondDbContextFactory - : IDesignTimeDbContextFactory - { - public BookStoreSecondDbContext CreateDbContext(string[] args) - { - var configuration = BuildConfiguration(); - var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement")); - return new BookStoreSecondDbContext(builder.Options); - } - - private static IConfigurationRoot BuildConfiguration() - { - var builder = new ConfigurationBuilder() - .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../BookStore.DbMigrator/")) - .AddJsonFile("appsettings.json", optional: false); - - return builder.Build(); - } - } -} -```` - -Now, you can open the Package Manager Console, select the `.EntityFrameworkCore` project as the default project (make sure the `.Web` project is still the startup project) and run the following command: - -````bash -Add-Migration "Initial" -OutputDir "SecondDbMigrations" -Context BookStoreSecondDbContext -```` - -This will add a `SecondDbMigrations` folder in the `.EntityFrameworkCore` project and a migration class inside it. `OutputDir` and `Context` parameters are required since we currently have two `DbContext` class and two migrations folder in the same project. - -You can now run the following command to create the database and the tables inside it: - -````bash -Update-Database -Context BookStoreSecondDbContext -```` - -A new database, named `BookStore_SecondDb` should be created. - -### Remove Modules from the Main Database - -We've **created a second database** that contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy. - -First, remove the following lines from the `BookStoreDbContext` class: - -````csharp -builder.ConfigurePermissionManagement(); -builder.ConfigureSettingManagement(); -builder.ConfigureAuditLogging(); -```` - -Open the Package Manager Console, select the `.EntityFrameworkCore` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command: - -```` -Add-Migration "Removed_Audit_Setting_Permission_Modules" -Context BookStoreDbContext -```` - -This command will create a new migration class as shown below: - -````csharp -public partial class Removed_Audit_Setting_Permission_Modules : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AbpAuditLogActions"); - - migrationBuilder.DropTable( - name: "AbpEntityPropertyChanges"); - - migrationBuilder.DropTable( - name: "AbpPermissionGrants"); - - migrationBuilder.DropTable( - name: "AbpSettings"); - - migrationBuilder.DropTable( - name: "AbpEntityChanges"); - - migrationBuilder.DropTable( - name: "AbpAuditLogs"); - } - - ... -} -```` - -Be careful in this step: - -* If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables. -* If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history. - -Run the following command to delete the tables from your main database: - -````bash -Update-Database -Context BookStoreDbContext -```` - -Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database. - -### Automate the Second Database Schema Migration - -`.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not apply the EF Core Code First Migrations for the database of the `BookStoreSecondDbContext`. Now, you will see how to configure the console migration application to handle both databases. - -`EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using BookStore.Data; -using Volo.Abp.DependencyInjection; - -namespace BookStore.EntityFrameworkCore -{ - public class EntityFrameworkCoreBookStoreDbSchemaMigrator - : IBookStoreDbSchemaMigrator, ITransientDependency - { - private readonly IServiceProvider _serviceProvider; - - public EntityFrameworkCoreBookStoreDbSchemaMigrator( - IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task MigrateAsync() - { - /* We intentionally resolving the BookStoreDbContext - * from IServiceProvider (instead of directly injecting it) - * to properly get the connection string of the current tenant in the - * current scope. - */ - - await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); - } - } -} -```` - -Add the following code inside the `MigrateAsync` method: - -````csharp -await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); -```` - -So, the `MigrateAsync` method should look like the following: - -````csharp -public async Task MigrateAsync() -{ - /* We intentionally resolving the BookStoreDbContext - * from IServiceProvider (instead of directly injecting it) - * to properly get the connection string of the current tenant in the - * current scope. - */ - - await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); - - await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); -} -```` - -That's all. You can now run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases. - -### Fixing the Tests - -Creating a new DbContext will break the integration tests. It is easy to fix. Open the `BookStoreEntityFrameworkCoreTestModule` class in the `BookStore.EntityFrameworkCore.Tests` project, find the `CreateDatabaseAndGetConnection` method. It should be like that: - -````csharp -private static SqliteConnection CreateDatabaseAndGetConnection() -{ - var connection = new SqliteConnection("Data Source=:memory:"); - connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - using (var context = new BookStoreDbContext(options)) - { - context.GetService().CreateTables(); - } - - return connection; -} -```` - -Change it as the following: - -````csharp -private static SqliteConnection CreateDatabaseAndGetConnection() -{ - var connection = new SqliteConnection("Data Source=:memory:"); - connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - using (var context = new BookStoreDbContext(options)) - { - context.GetService().CreateTables(); - } - - // Add the following code -------------- - var optionsForSecondDb = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - - using (var context = new BookStoreSecondDbContext(optionsForSecondDb)) - { - context.GetService().CreateTables(); - } - //-------------------------------------- - - return connection; -} -```` - -Integration tests now will work. I've used the same database in the tests to keep it simple. - -## Separating Host & Tenant Database Schemas - -In a multi-tenant solution, you may want to separate your database schemas, so host-related tables don't locate in the tenant databases when tenants have separate databases. - -Some pre-built ABP modules are related only with the host side, like the [Tenant Management](Modules/Tenant-Management.md) module. So, in the tenant `DbContext` class you don't call `modelBuilder.ConfigureTenantManagement()` and that's all. - -Some modules, like the [Identity](Modules/Identity.md) module, is both used in host and tenant sides. It stores tenant users in the tenant database and host users in the host database. However, it stores some entities, like `IdentityClaimType`, only in the host side. In this case, you don't want to add these tables in the tenant database, even if they are not used and will always be empty for tenants. - -ABP provides a simple way to set the multi-tenancy side for a `DbContext`, so the modules can check it and decide to map tables to the database, or not. - -````csharp -public class MyTenantDbContext : AbpDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.SetMultiTenancySide(MultiTenancySides.Tenant); - - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureIdentity(); - modelBuilder.ConfigureFeatureManagement(); - modelBuilder.ConfigureAuditLogging(); - } -} -```` - -The first line in the `OnModelCreating` sets multi-tenancy side to `Tenant`. For this example, Feature management tables are not created (because all the tables are host-specific), so calling `modelBuilder.ConfigureFeatureManagement()` has no effect. Also, `ConfigureIdentity()` call respects to the multi-tenancy side and doesn't create host-specific tables for this database. - -`SetMultiTenancySide` can get the following values: - -* `MultiTenancySides.Both` (**default value**): This `DbContext` (and the related database) is shared by host and tenant. -* `MultiTenancySides.Host`: This `DbContext` (and the related database) is used only by the host side. -* `MultiTenancySides.Tenant`: This `DbContext` (and the related database) is only for tenants. - -If you create a re-usable application module or want to check that value in your application code, you can use `modelBuilder.GetMultiTenancySide()` to check the current side. - -````csharp -var side = modelBuilder.GetMultiTenancySide(); -if (!side.HasFlag(MultiTenancySides.Host)) -{ - ... -} -```` - -Or practically you can use one of the shortcut extension methods: - -````csharp -if (modelBuilder.IsTenantOnlyDatabase()) -{ - ... -} -```` - -There are four methods to check the current side: - -* `IsHostDatabase()`: Returns `true` if you should create host-related tables. It is equivalent of checking `modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Host)`. -* `IsHostOnlyDatabase()`: Returns `true` if you should only create host-related tables, but should not create tenant-related tables. It is equivalent of checking `modelBuilder.GetMultiTenancySide() == MultiTenancySides.Host`. -* `IsTenantDatabase()`: Returns `true` if you should create tenant-related tables. It is equivalent of checking `modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Tenant)`. -* `IsTenantOnlyDatabase()`: Returns `true` if you should only create tenant-related tables, but should not create host-related tables. It is equivalent of checking `modelBuilder.GetMultiTenancySide() == MultiTenancySides.Tenant`. - -All pre-built ABP [modules](Modules/Index.md) checks this value in their `modelBuilder.ConfigureXXX()` methods. - -## Conclusion - -This document explains how to split your databases and manage your database migrations of your solution for Entity Framework Core. In brief, you need to have a separate migration project per different databases. - -## Source Code - -You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). You can also find the changes explained in this document as a [single commit](https://github.com/abpframework/abp-samples/pull/95/commits/c2ffd76175e0a6fdfcf6477bbaea23dc2793fedd). \ No newline at end of file diff --git a/docs/en/Entity-Framework-Core-MySQL.md b/docs/en/Entity-Framework-Core-MySQL.md deleted file mode 100644 index ccfdd89da3..0000000000 --- a/docs/en/Entity-Framework-Core-MySQL.md +++ /dev/null @@ -1,43 +0,0 @@ -# Switch to EF Core MySQL Provider - -> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with MySQL. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to MySQL after creating the solution. - -This document explains how to switch to the **MySQL** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package - -`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.MySQL](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.MySQL) package. - -## Replace the Module Dependency - -Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreMySQLModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.MySQL;`). - -## UseMySQL() - -Find `UseSqlServer()` calls in your solution. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`. -* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.Parse("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`) - -> Depending on your solution structure, you may find more code files need to be changed. - -## Change the Connection Strings - -MySQL connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/mysql/ ) for details of MySQL connection string options. - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core Migrations depend on the selected DBMS provider. So, changing the DBMS provider will cause the migration fails. - -* Delete the Migrations folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will create a database migration with all database objects (tables) configured. - -Run the `.DbMigrator` project to create the database and seed the initial data. - -## Run the Application - -It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core-Oracle-Devart.md b/docs/en/Entity-Framework-Core-Oracle-Devart.md deleted file mode 100644 index 92cecadec9..0000000000 --- a/docs/en/Entity-Framework-Core-Oracle-Devart.md +++ /dev/null @@ -1,137 +0,0 @@ -# Switch to EF Core Oracle Devart Provider - -This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -> This document uses a paid library of [Devart](https://www.devart.com/dotconnect/oracle/) company, See [this document](Entity-Framework-Core-Oracle.md) for other options. - -> Before switching your provider, please ensure your Oracle version is **v12.2+**. In the earlier versions of Oracle, there were long identifier limitations that prevents creating a database table, column or index longer than 30 bytes. With [v12.2](https://docs.oracle.com/en/database/oracle/oracle-database/12.2/newft/new-features.html#GUID-64283AD6-0939-47B0-856E-5E9255D7246B) "The maximum length of identifiers is increased to 128 bytes". **v12.2** and later versions, you can use the database tables, columns and indexes provided by ABP without any problems. - -## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package - -`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) package. - -## Replace the Module Dependency - -Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleDevartModule)` - -Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`. - -## UseOracle() - -Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. - - -In the `CreateDbContext()` method of the *YourProjectName*DbContextFactory.cs, replace the following code block - -```csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -``` - -with this one -```csharp -var builder = (DbContextOptionsBuilder) - new DbContextOptionsBuilder().UseOracle - ( - configuration.GetConnectionString("Default") - ); -``` - -> Depending on your solution structure, you may find more code files need to be changed. - -## Change the Connection Strings - -Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options. - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default. -EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations. - -* Delete the `Migrations` folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will scaffold a new migration for Oracle. - -Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data. - -## ORA-12899: value too large for column fix - -Oracle limits strings to `NVARCHAR2(2000)` when the migration is created. Some of the entity properties may extend it. You can check the known and reported properties of ABP modules entities that can extend this limit. To prevent this problem, you need to convert the `string` type to `long` type first and generate a new migration. Then convert the `long` type to `clob` type with maximum length. - -### First Migration - -Update you application DbContext `OnModelCreating` method: - -```csharp -protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.Entity(b => - { - b.Property(q => q.Payload).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - builder.Entity(b => - { - b.Property(x => x.Exceptions).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - builder.Entity(b => - { - b.Property(x => x.Parameters).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - /* Configure your own tables/entities inside here */ - } -``` - -Create a new migration using dotnet tooling under EntityFrameworkCore layer of your solution: `dotnet ef migrations add Oracle_Long_Conversion` - -### Second Migration - -Update you application DbContext `OnModelCreating` method: - -```csharp -protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.Entity(b => - { - b.Property(q => q.Payload).HasColumnType("clob").HasMaxLength(4000); - }); - - builder.Entity(b => - { - b.Property(x => x.Exceptions).HasColumnType("clob").HasMaxLength(4000); - }); - - builder.Entity(b => - { - b.Property(x => x.Parameters).HasColumnType("clob").HasMaxLength(4000); - }); - - /* Configure your own tables/entities inside here */ - } -``` - -Create a new migration using dotnet tooling under EntityFrameworkCore layer of your solution: `dotnet ef migrations add Oracle_Clob_Conversion` - -Run DbMigrator (or use dotnet tooling) to migrate the oracle database. - - - -## Run the Application - -It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core-Oracle-Official.md b/docs/en/Entity-Framework-Core-Oracle-Official.md deleted file mode 100644 index fcc776f2c1..0000000000 --- a/docs/en/Entity-Framework-Core-Oracle-Official.md +++ /dev/null @@ -1,130 +0,0 @@ -# Switch to EF Core Oracle Provider - -This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -> Before switching your provider, please ensure your Oracle version is **v12.2+**. In the earlier versions of Oracle, there were long identifier limitations that prevents creating a database table, column or index longer than 30 bytes. With [v12.2](https://docs.oracle.com/en/database/oracle/oracle-database/12.2/newft/new-features.html#GUID-64283AD6-0939-47B0-856E-5E9255D7246B) "The maximum length of identifiers is increased to 128 bytes". **v12.2** and later versions, you can use the database tables, columns and indexes provided by ABP without any problems. - -## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package - -`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle) package. - -## Replace the Module Dependency - -Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleModule)` - -Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle;`. - -## UseOracle() - -Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. - - -In the `CreateDbContext()` method of the *YourProjectName*DbContextFactory.cs, replace the following code block - -```csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -``` - -with this one (just changes `UseSqlServer(...)` to `UseOracle(...)`) -```csharp -var builder = new DbContextOptionsBuilder() - .UseOracle(configuration.GetConnectionString("Default")); -``` - -> Depending on your solution structure, you may find more code files need to be changed. - -## Change the Connection Strings - -Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options. - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default. -EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations. - -* Delete the `Migrations` folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will scaffold a new migration for Oracle. - -Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data. - -## ORA-12899: value too large for column fix - -Oracle limits strings to `NVARCHAR2(2000)` when the migration is created. Some of the entity properties may extend it. You can check the known and reported properties of ABP modules entities that can extend this limit. To prevent this problem, you need to convert the `string` type to `long` type first and generate a new migration. Then convert the `long` type to `clob` type with maximum length. - -### First Migration - -Update you application DbContext `OnModelCreating` method: - -```csharp -protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.Entity(b => - { - b.Property(q => q.Payload).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - builder.Entity(b => - { - b.Property(x => x.Exceptions).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - builder.Entity(b => - { - b.Property(x => x.Parameters).HasColumnType("long").HasMaxLength(int.MaxValue); - }); - - /* Configure your own tables/entities inside here */ - } -``` - -Create a new migration using dotnet tooling under EntityFrameworkCore layer of your solution: `dotnet ef migrations add Oracle_Long_Conversion` - -### Second Migration - -Update you application DbContext `OnModelCreating` method: - -```csharp -protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.Entity(b => - { - b.Property(q => q.Payload).HasColumnType("clob").HasMaxLength(4000); - }); - - builder.Entity(b => - { - b.Property(x => x.Exceptions).HasColumnType("clob").HasMaxLength(4000); - }); - - builder.Entity(b => - { - b.Property(x => x.Parameters).HasColumnType("clob").HasMaxLength(4000); - }); - - /* Configure your own tables/entities inside here */ - } -``` - -Create a new migration using dotnet tooling under EntityFrameworkCore layer of your solution: `dotnet ef migrations add Oracle_Clob_Conversion` - -Run DbMigrator (or use dotnet tooling) to migrate the oracle database. - -## Run the Application - -It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core-Oracle.md b/docs/en/Entity-Framework-Core-Oracle.md deleted file mode 100644 index 161f1abf45..0000000000 --- a/docs/en/Entity-Framework-Core-Oracle.md +++ /dev/null @@ -1,12 +0,0 @@ -# Switch to EF Core Oracle Provider - -> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with Oracle. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to Oracle after creating the solution. - -This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -ABP Framework provides integrations for two different Oracle packages. See one of the following documents based on your provider decision: - -* **[`Volo.Abp.EntityFrameworkCore.Oracle`](Entity-Framework-Core-Oracle-Official.md)** package uses the official & free oracle driver. -* **[`Volo.Abp.EntityFrameworkCore.Oracle.Devart`](Entity-Framework-Core-Oracle-Devart.md)** package uses the commercial (paid) driver of [Devart](https://www.devart.com/) company. - -> You can choose one of the package you want. If you don't know the differences of the packages, please search for it. ABP Framework only provides integrations it doesn't provide support for such 3rd-party libraries. diff --git a/docs/en/Entity-Framework-Core-Other-DBMS.md b/docs/en/Entity-Framework-Core-Other-DBMS.md deleted file mode 100644 index 93c3a6276f..0000000000 --- a/docs/en/Entity-Framework-Core-Other-DBMS.md +++ /dev/null @@ -1,112 +0,0 @@ -# Switch to Another DBMS for Entity Framework Core - -[ABP CLI](CLI.md) provides a `-dbms` option to allow you to choose your Database Management System (DBMS) while creating a new solution. It accepts the following values: - -- `SqlServer` (default) -- `MySQL` -- `SQLite` -- `Oracle` -- `Oracle-Devart` -- `PostgreSQL` - -So, if you want to use MySQL for your solution, you can use the `-dbms MySQL` option while using the `abp new` command. Example: - -````bash -abp new BookStore -dbms MySQL -```` - -Also, the [Get Started page](https://abp.io/get-started) on the ABP website allows you to select one of the providers. - -> **This document provides guidance for who wants to manually change their DBMS after creating the solution.** - -You can use the following documents to learn how to **switch to your favorite DBMS**: - -* [MySQL](Entity-Framework-Core-MySQL.md) -* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) -* [Oracle](Entity-Framework-Core-Oracle.md) -* [SQLite](Entity-Framework-Core-SQLite.md) - -You can also configure your DBMS provider **without** these integration packages. While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it yourself if there is no integration package for your DBMS provider. - -For an example, this document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md). - -## Replace the SQL Server Dependency - -* Remove the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package dependency from the `.EntityFrameworkCore` project. -* Add the [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql/) NuGet package dependency to your `.EntityFrameworkCore` project. - -## Remove the Module Dependency - -Remove the `AbpEntityFrameworkCoreSqlServerModule` from the dependency list of your ***YourProjectName*EntityFrameworkCoreModule** class. - -## Change the UseSqlServer() Calls - -Find the following code part inside the *YourProjectName*EntityFrameworkCoreModule class: - -````csharp -Configure(options => -{ - options.UseSqlServer(); -}); -```` - -Replace it with the following code part: - -````csharp -Configure(options => -{ - options.Configure(ctx => - { - if (ctx.ExistingConnection != null) - { - ctx.DbContextOptions.UseMySql(ctx.ExistingConnection); - } - else - { - ctx.DbContextOptions.UseMySql(ctx.ConnectionString); - } - }); -}); -```` - -* `UseMySql` calls in this code is defined by the Pomelo.EntityFrameworkCore.MySql package and you can use its additional options if you need. -* This code first checks if there is an existing (active) connection to the same database in the current request and reuses it if possible. This allows to share a single transaction among different DbContext types. ABP handles the rest of the things. -* It uses `ctx.ConnectionString` and passes to the `UseMySql` if there is no active connection (which will cause to create a new database connection). Using the `ctx.ConnectionString` is important here. Don't pass a static connection string (or a connection string from a configuration). Because ABP [dynamically determines the correct connection string](Connection-Strings.md) in a multi-database or [multi-tenant](Multi-Tenancy.md) environment. - -## Change the Connection Strings - -MySQL connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/mysql/ ) for details of MySQL connection string options. - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## DBMS restrictions - -MySQL DBMS has some slight differences than the SQL Server. Some module database mapping configuration (especially the field lengths) causes problems with MySQL. For example, some of the the [IdentityServer module](Modules/IdentityServer.md) tables has such problems and it provides an option to configure the fields based on your DBMS. - -The module may provide some built-in solutions. You can configure it via `ModelBuilder`. eg: `Auth Server` module. - -```csharp -builder.ConfigureIdentityServer(options => -{ - options.DatabaseProvider = EfCoreDatabaseProvider.MySql; -}); -``` - -Then `ConfigureIdentityServer()` method will set the field lengths to not exceed the MySQL limits. Refer to related module documentation if you have any problem while creating or executing the database migrations. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core Migrations depend on the selected DBMS provider. So, changing the DBMS provider will cause the migration fails. - -* Delete the Migrations folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will create a database migration with all database objects (tables) configured. - -Run the `.DbMigrator` project to create the database and seed the initial data. - -## Run the Application - -It is ready. Just run the application and enjoy coding. - -Related discussions: https://github.com/abpframework/abp/issues/1920 \ No newline at end of file diff --git a/docs/en/Entity-Framework-Core-PostgreSQL.md b/docs/en/Entity-Framework-Core-PostgreSQL.md deleted file mode 100644 index 9cdaeae34c..0000000000 --- a/docs/en/Entity-Framework-Core-PostgreSQL.md +++ /dev/null @@ -1,43 +0,0 @@ -# Switch to EF Core PostgreSQL Provider - -> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with PostgreSQL. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to PostgreSQL after creating the solution. - -This document explains how to switch to the **PostgreSQL** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package - -`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.PostgreSql](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.PostgreSql) package. - -## Replace the Module Dependency - -Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). - -## UseNpgsql() - -Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UseNpgsql()`. - - -Find `UseSqlServer()` call in *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project and replace with `UseNpgsql()`. - -> Depending on your solution structure, you may find more `UseSqlServer()` calls that needs to be changed. - -## Change the Connection Strings - -PostgreSql connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/postgresql/ ) for details of PostgreSql connection string options. - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core Migrations depend on the selected DBMS provider. So, changing the DBMS provider will cause the migration fails. - -* Delete the Migrations folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will create a database migration with all database objects (tables) configured. - -Run the `.DbMigrator` project to create the database and seed the initial data. - -## Run the Application - -It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core-SQLite.md b/docs/en/Entity-Framework-Core-SQLite.md deleted file mode 100644 index 5015320af1..0000000000 --- a/docs/en/Entity-Framework-Core-SQLite.md +++ /dev/null @@ -1,53 +0,0 @@ -# Switch to EF Core SQLite Provider - -> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with SQLite. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to SQLite after creating the solution. - -This document explains how to switch to the **SQLite** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured. - -## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package - -`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Sqlite](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Sqlite) package. - -## Replace the Module Dependency - -Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreSqliteModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Sqlite;`). - -## UseSqlite() - -Find `UseSqlServer()` calls in your solution, replace with `UseSqlite()`. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. - -> Depending on your solution structure, you may find more code files need to be changed. - -## Change the Connection Strings - -SQLite connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/sqlite/ ) for details of SQLite connection string options. - -An example connection string is - -``` -{ - "ConnectionStrings": { - "Default": "Filename=./MySQLiteDBFile.sqlite" - } -} -``` - -You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. - -## Re-Generate the Migrations - -The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). EF Core Migrations depend on the selected DBMS provider. So, changing the DBMS provider will cause the migration fails. - -* Delete the Migrations folder under the `.EntityFrameworkCore` project and re-build the solution. -* Run `Add-Migration "Initial"` on the Package Manager Console (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore` project as the default project in the Package Manager Console). - -This will create a database migration with all database objects (tables) configured. - -Run the `.DbMigrator` project to create the database and seed the initial data. - -## Run the Application - -It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md deleted file mode 100644 index 53de9eb8cd..0000000000 --- a/docs/en/Entity-Framework-Core.md +++ /dev/null @@ -1,965 +0,0 @@ -# Entity Framework Core Integration - -This document explains how to integrate EF Core as an ORM provider to ABP based applications and how to configure it. - -## Installation - -`Volo.Abp.EntityFrameworkCore` is the main NuGet package for the EF Core integration. Install it to your project (for a layered application, to your data/infrastructure layer): - -```` shell -abp add-package Volo.Abp.EntityFrameworkCore -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.EntityFrameworkCore). -> - -> Note: Instead, you can directly download a [startup template](https://abp.io/Templates) with EF Core pre-installed. - -### Database Management System Selection - -Entity Framework Core supports various database management systems ([see all](https://docs.microsoft.com/en-us/ef/core/providers/)). ABP framework and this document don't depend on any specific DBMS. If you are creating a [reusable application module](Modules/Index.md), avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS. - -> See [Switch to Another DBMS for Entity Framework Core](Entity-Framework-Core-Other-DBMS.md) document to learn how to switch the DBMS. - -## Creating DbContext - -Your `DbContext` class should be derived from `AbpDbContext` as shown below: - -````C# -using Microsoft.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -namespace MyCompany.MyProject -{ - public class MyDbContext : AbpDbContext - { - //...your DbSet properties here - - public MyDbContext(DbContextOptions options) - : base(options) - { - } - } -} -```` - -### About the EF Core Fluent Mapping - -The [application startup template](Startup-Templates/Application.md) has been configured to use the [EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/) to map your entities to your database tables. - -You can still use the **data annotation attributes** (like `[Required]`) on the properties of your entity while the ABP documentation generally follows the **fluent mapping API** approach. It is up to you. - -ABP Framework has some **base entity classes** and **conventions** (see the [entities document](Entities.md)) and it provides some useful **extension methods** to configure the properties inherited from the base entity classes. - -#### ConfigureByConvention Method - -`ConfigureByConvention()` is the main extension method that **configures all the base properties** and conventions for your entities. So, it is a **best practice** to call this method for all your entities, in your fluent mapping code. - -**Example**: Assume that you've a `Book` entity derived from `AggregateRoot` base class: - -````csharp -public class Book : AuditedAggregateRoot -{ - public string Name { get; set; } -} -```` - -You can override the `OnModelCreating` method in your `DbContext` and configure the mapping as shown below: - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - //Always call the base method - base.OnModelCreating(builder); - - builder.Entity(b => - { - b.ToTable("Books"); - - //Configure the base properties - b.ConfigureByConvention(); - - //Configure other properties (if you are using the fluent API) - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - }); -} -```` - -* Calling `b.ConfigureByConvention()` is important here to properly **configure the base properties**. -* You can configure the `Name` property here or you can use the **data annotation attributes** (see the [EF Core document](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)). - -> While there are many extension methods to configure your base properties, `ConfigureByConvention()` internally calls them if necessary. So, it is enough to call it. - -### Configure the Connection String Selection - -If you have multiple databases in your application, you can configure the connection string name for your `DbContext` using the `[ConnectionStringName]` attribute. Example: - -```csharp -[ConnectionStringName("MySecondConnString")] -public class MyDbContext : AbpDbContext -{ - -} -``` - -If you don't configure, the `Default` connection string is used. If you configure a specific connection string name, but not define this connection string name in the application configuration then it fallbacks to the `Default` connection string (see [the connection strings document](Connection-Strings.md) for more information). - -### AbpDbContextOptions - -`AbpDbContextOptions` is used to configure the `DbContext` options. When you create a new solution with the ABP's application startup template, you will see a simple configuration (in the `EntityFrameworkCore` integration project's module class) as shown below: - -````csharp -Configure(options => -{ - options.UseSqlServer(); -}); -```` - -That configuration configures the default DBMS as SQL Server for all the `DbContext`s of the application. That configuration was a shorthand notation and it can be done with the following code block: - -````csharp -Configure(options => -{ - options.Configure(opts => - { - opts.UseSqlServer(); - }); -}); -```` - -`options.Configure(...)` method has more options to configure. For example, you can set `DbContextOptions` (EF Core's native options) as shown below: - -````csharp -Configure(options => -{ - options.Configure(opts => - { - opts.DbContextOptions.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - }); -}); -```` - -Add actions for the `ConfigureConventions` and `OnModelCreating` methods of the `DbContext` as shown below: - -````csharp -options.DefaultConventionAction = (dbContext, builder) => -{ - // This action is called for ConfigureConventions method of all DbContexts. -}; - -options.ConfigureConventions((dbContext, builder) => -{ - // This action is called for ConfigureConventions method of specific DbContext. -}); - -options.DefaultOnModelCreatingAction = (dbContext, builder) => -{ - // This action is called for OnModelCreating method of all DbContexts. -}; - -options.ConfigureOnModelCreating((dbContext, builder) => -{ - // This action is called for OnModelCreating method of specific DbContext. -}); -```` - -If you have a single `DbContext` or you have multiple `DbContext`s but want to use the same DBMS and configuration for all, you can leave it as is. However, if you need to configure a different DBMS or customize the configuration for a specific `DbContext`, you can specify it as shown below: - -````csharp -Configure(options => -{ - // Default configuration for all DbContexts - options.Configure(opts => - { - opts.UseSqlServer(); - }); - - // Customized configuration for a specific DbContext - options.Configure(opts => - { - opts.UseMySQL(); - }); -}); -```` - -> See [Switch to Another DBMS for Entity Framework Core](Entity-Framework-Core-Other-DBMS.md) document to learn how to configure the DBMS. - -## Registering DbContext To Dependency Injection - -Use `AddAbpDbContext` method in your module to register your `DbContext` class for [dependency injection](Dependency-Injection.md) system. - -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpEntityFrameworkCoreModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(); - - //... - } - } -} -```` - -### Add Default Repositories - -ABP can automatically create default [generic repositories](Repositories.md) for the entities in your DbContext. Just use `AddDefaultRepositories()` option on the registration: - -````C# -services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); -}); -```` - -This will create a repository for each [aggregate root entity](Entities.md) (classes derived from `AggregateRoot`) by default. If you want to create repositories for other entities too, then set `includeAllEntities` to `true`: - -````C# -services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(includeAllEntities: true); -}); -```` - -Then you can inject and use `IRepository` in your services. Assume that you have a `Book` entity with `Guid` primary key: - -```csharp -public class Book : AggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } -} -``` - -(`BookType` is a simple `enum` here and not important) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): - -````csharp -public class BookManager : DomainService -{ - private readonly IRepository _bookRepository; - - //inject default repository to the constructor - public BookManager(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task CreateBook(string name, BookType type) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var book = new Book - { - Id = GuidGenerator.Create(), - Name = name, - Type = type - }; - - //Use a standard repository method - await _bookRepository.InsertAsync(book); - - return book; - } -} -```` - -This sample uses `InsertAsync` method to insert a new entity to the database. - -### Add Custom Repositories - -Default generic repositories are powerful enough in most cases (since they implement `IQueryable`). However, you may need to create a custom repository to add your own repository methods. Assume that you want to delete all books by type. - -It's suggested to define an interface for your custom repository: - -````csharp -public interface IBookRepository : IRepository -{ - Task DeleteBooksByType(BookType type); -} -```` - -You generally want to derive from the `IRepository` to inherit standard repository methods (while, you don't have to do). Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`EntityFrameworkCore` project in a [startup template](https://abp.io/Templates)). - -Example implementation of the `IBookRepository` interface: - -````csharp -public class BookRepository - : EfCoreRepository, IBookRepository -{ - public BookRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteBooksByType(BookType type) - { - var dbContext = await GetDbContextAsync(); - await dbContext.Database.ExecuteSqlRawAsync( - $"DELETE FROM Books WHERE Type = {(int)type}" - ); - } -} -```` - -Now, it's possible to [inject](Dependency-Injection.md) the `IBookRepository` and use the `DeleteBooksByType` method when needed. - -#### Override the Default Generic Repository - -Even if you create a custom repository, you can still inject the default generic repository (`IRepository` for this example). Default repository implementation will not use the class you have created. - -If you want to replace default repository implementation with your custom repository, do it inside the `AddAbpDbContext` options: - -````csharp -context.Services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); - - //Replaces IRepository - options.AddRepository(); -}); -```` - -This is especially important when you want to **override a base repository method** to customize it. For instance, you may want to override `DeleteAsync` method to delete a specific entity in a more efficient way: - -````csharp -public async override Task DeleteAsync( - Guid id, - bool autoSave = false, - CancellationToken cancellationToken = default) -{ - //TODO: Custom implementation of the delete method -} -```` - -## Loading Related Entities - -Assume that you've an `Order` with a collection of `OrderLine`s and the `OrderLine` has a navigation property to the `Order`: - -````csharp -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Volo.Abp.Auditing; -using Volo.Abp.Domain.Entities; - -namespace MyCrm -{ - public class Order : AggregateRoot, IHasCreationTime - { - public Guid CustomerId { get; set; } - public DateTime CreationTime { get; set; } - - public ICollection Lines { get; set; } //Sub collection - - public Order() - { - Lines = new Collection(); - } - } - - public class OrderLine : Entity - { - public Order Order { get; set; } //Navigation property - public Guid OrderId { get; set; } - - public Guid ProductId { get; set; } - public int Count { get; set; } - public double UnitPrice { get; set; } - } -} - -```` - -And defined the database mapping as shown below: - -````csharp -builder.Entity(b => -{ - b.ToTable("Orders"); - b.ConfigureByConvention(); - - //Define the relation - b.HasMany(x => x.Lines) - .WithOne(x => x.Order) - .HasForeignKey(x => x.OrderId) - .IsRequired(); -}); - -builder.Entity(b => -{ - b.ToTable("OrderLines"); - b.ConfigureByConvention(); -}); -```` - -When you query an `Order`, you may want to **include** all the `OrderLine`s in a single query or you may want to **load them later** on demand. - -> Actually these are not directly related to the ABP Framework. You can follow the [EF Core documentation](https://docs.microsoft.com/en-us/ef/core/querying/related-data/) to learn all the details. This section will cover some topics related to the ABP Framework. - -### Eager Loading / Load With Details - -You have different options when you want to load the related entities while querying an entity. - -#### Repository.WithDetails - -`IRepository.WithDetailsAsync(...)` can be used to get an `IQueryable` by including one relation collection/property. - -**Example: Get an order with lines** - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace AbpDemo.Orders -{ - public class OrderManager : DomainService - { - private readonly IRepository _orderRepository; - - public OrderManager(IRepository orderRepository) - { - _orderRepository = orderRepository; - } - - public async Task TestWithDetails(Guid id) - { - //Get a IQueryable by including sub collections - var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines); - - //Apply additional LINQ extension methods - var query = queryable.Where(x => x.Id == id); - - //Execute the query and get the result - var order = await AsyncExecuter.FirstOrDefaultAsync(query); - } - } -} -```` - -> `AsyncExecuter` is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use `await query.FirstOrDefaultAsync()`. But, this time you depend on the EF Core in your domain layer. See the [repository document](Repositories.md) to learn more. - -**Example: Get a list of orders with their lines** - -````csharp -public async Task TestWithDetails() -{ - //Get a IQueryable by including sub collections - var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines); - - //Execute the query and get the result - var orders = await AsyncExecuter.ToListAsync(queryable); -} -```` - -> `WithDetailsAsync` method can get more than one expression parameter if you need to include more than one navigation property or collection. - -#### DefaultWithDetailsFunc - -If you don't pass any expression to the `WithDetailsAsync` method, then it includes all the details using the `DefaultWithDetailsFunc` option you provide. - -You can configure `DefaultWithDetailsFunc` for an entity in the `ConfigureServices` method of your [module](Module-Development-Basics.md) in your `EntityFrameworkCore` project. - -**Example: Include `Lines` while querying an `Order`** - -````csharp -Configure(options => -{ - options.Entity(orderOptions => - { - orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines); - }); -}); -```` - -> You can fully use the EF Core API here since this is located in the EF Core integration project. - -Then you can use the `WithDetails` without any parameter: - -````csharp -public async Task TestWithDetails() -{ - //Get a IQueryable by including all sub collections - var queryable = await _orderRepository.WithDetailsAsync(); - - //Execute the query and get the result - var orders = await AsyncExecuter.ToListAsync(queryable); -} -```` - -`WithDetailsAsync()` executes the expression you've setup as the `DefaultWithDetailsFunc`. - -#### Repository Get/Find Methods - -Some of the standard [Repository](Repositories.md) methods have optional `includeDetails` parameters; - -* `GetAsync` and `FindAsync` gets `includeDetails` with default value is `true`. -* `GetListAsync` and `GetPagedListAsync` gets `includeDetails` with default value is `false`. - -That means, the methods return a **single entity includes details** by default while list returning methods don't include details by default. You can explicitly pass `includeDetails` to change the behavior. - -> These methods use the `DefaultWithDetailsFunc` option that is explained above. - -**Example: Get an order with details** - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id); -} -```` - -**Example: Get an order without details** - -````csharp -public async Task TestWithoutDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id, includeDetails: false); -} -```` - -**Example: Get list of entities with details** - -````csharp -public async Task TestWithDetails() -{ - var orders = await _orderRepository.GetListAsync(includeDetails: true); -} -```` - -#### Alternatives - -The repository pattern tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options; - -* Create a custom repository method and use the complete EF Core API. -* Reference to the `Volo.Abp.EntityFrameworkCore` package from your project. In this way, you can directly use `Include` and `ThenInclude` in your code. - -See also [eager loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/eager) of the EF Core. - -### Explicit / Lazy Loading - -If you don't include relations while querying an entity and later need to access to a navigation property or collection, you have different options. - -#### EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync - -Repositories provide `EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` extension methods to **explicitly load** a navigation property or sub collection. - -**Example: Load Lines of an Order when needed** - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id, includeDetails: false); - //order.Lines is empty on this stage - - await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines); - //order.Lines is filled now -} -```` - -`EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` methods do nothing if the property or collection was already loaded. So, calling multiple times has no problem. - -See also [explicit loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/explicit) of the EF Core. - -#### Lazy Loading with Proxies - -Explicit loading may not be possible in some cases, especially when you don't have a reference to the `Repository` or `DbContext`. Lazy Loading is a feature of the EF Core that loads the related properties / collections when you first access to it. - -To enable lazy loading; - -1. Install the [Microsoft.EntityFrameworkCore.Proxies](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies/) package into your project (typically to the EF Core integration project) -2. Configure `UseLazyLoadingProxies` for your `DbContext` (in the `ConfigureServices` method of your module in your EF Core project). Example: - -````csharp -Configure(options => -{ - options.PreConfigure(opts => - { - opts.DbContextOptions.UseLazyLoadingProxies(); //Enable lazy loading - }); - - options.UseSqlServer(); -}); -```` - -3. Make your navigation properties and collections `virtual`. Examples: - -````csharp -public virtual ICollection Lines { get; set; } //virtual collection -public virtual Order Order { get; set; } //virtual navigation property -```` - -Once you enable lazy loading and arrange your entities, you can freely access to the navigation properties and collections: - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id); - //order.Lines is empty on this stage - - var lines = order.Lines; - //order.Lines is filled (lazy loaded) -} -```` - -Whenever you access to a property/collection, EF Core automatically performs an additional query to load the property/collection from the database. - -> Lazy loading should be carefully used since it may cause performance problems in some specific cases. - -See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) of the EF Core. - -## Read-Only Repositories - -ABP Framework provides read-only [repository](Repositories.md) interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`) to explicitly indicate that your purpose is to query data, but not change it. If so, you can inject these interfaces into your services. - -Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. If you need to track the entities, you can still use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document). - -> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. - -## Enabling / Disabling the Change Tracking - -In addition to the read-only repositories, ABP allows to manually control the change tracking behavior for querying objects. Please see the *Enabling / Disabling the Change Tracking* section of the [Repositories documentation](Repositories.md) to learn how to use it. - -## Access to the EF Core API - -In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: - -````csharp -public async Task TestAsync() -{ - var dbContext = await _orderRepository.GetDbContextAsync(); - var dbSet = await _orderRepository.GetDbSetAsync(); - //var dbSet = dbContext.Set(); //Alternative, when you have the DbContext -} -```` - -* `GetDbContextAsync` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it if you need. However, you don't need it in most cases. - -> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the `DbContext`. This breaks encapsulation, but this is what you want in that case. - -## Extra Properties & Object Extension Manager - -Extra Properties system allows you to set/get dynamic properties to entities those implement the `IHasExtraProperties` interface. It is especially useful when you want to add custom properties to the entities defined in an [application module](Modules/Index.md), when you use the module as package reference. - -By default, all the extra properties of an entity are stored as a single `JSON` object in the database. - -Entity extension system allows you to to store desired extra properties in separate fields in the related database table. For more information about the extra properties & the entity extension system, see the following documents: - -* [Customizing the Application Modules: Extending Entities](Customizing-Application-Modules-Extending-Entities.md) -* [Entities](Entities.md) - -This section only explains the EF Core related usage of the `ObjectExtensionManager`. - -### ObjectExtensionManager.Instance - -`ObjectExtensionManager` implements the singleton pattern, so you need to use the static `ObjectExtensionManager.Instance` to perform all the operations. - -### MapEfCoreProperty - -`MapEfCoreProperty` is a shortcut extension method to define an extension property for an entity and map to the database. - -**Example**: Add `Title` property (database field) to the `IdentityRole` entity: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreProperty( - "Title", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(64); - } - ); -```` - -### MapEfCoreEntity - -`MapEfCoreEntity` is a shortcut extension method to configure the `Entity`. - -**Example**: Set the max length of `Name` to the `IdentityRole` entity: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreEntity(builder => - { - builder.As>().Property(x => x.Name).HasMaxLength(200); - }); -```` - -### MapEfCoreDbContext - -`MapEfCoreDbContext` is a shortcut extension method to configure the `DbContext`. - -**Example**: Set the max length of `Name` to the `IdentityRole` entity of `IdentityDbContext`: - -````csharp -ObjectExtensionManager.Instance.MapEfCoreDbContext(b => -{ - b.Entity().Property(x => x.Name).HasMaxLength(200); -}); -```` - -If the related module has implemented this feature(explained below), then the new property is added to the model or the DbContext/Entity configure changed. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field. - -> The `MapEfCoreProperty`, `MapEfCoreEntity` and `MapEfCoreDbContext` methods must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEfCoreEntityExtensionMappings` class that is safe to use this method inside. - -### ConfigureEfCoreEntity, ApplyObjectExtensionMappings and TryConfigureObjectExtensions - -If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureEfCoreEntity`, `ApplyObjectExtensionMappings` and `TryConfigureObjectExtensions` extension methods in your entity mapping. - -**Example**: -````csharp -public static class QADbContextModelCreatingExtensions -{ - public static void ConfigureQA( - this ModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new QAModelBuilderConfigurationOptions( - QADatabaseDbProperties.DbTablePrefix, - QADatabaseDbProperties.DbSchema - ); - - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "Questions", options.Schema); - b.ConfigureByConvention(); - //... - - //Call this in the end of buildAction. - b.ApplyObjectExtensionMappings(); - }); - - //... - - //Call this in the end of ConfigureQA. - builder.TryConfigureObjectExtensions(); - } -} -```` - -> If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` for this example), ABP Framework internally calls the `ConfigureObjectExtensions` and `ConfigureEfCoreEntity` methods. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention. - -> The `Object Extension` feature need the `Change Tracking`, which means you can't use the read-only repositories for the entities that have `extension properties(MapEfCoreProperty)`, Please see the [Repositories documentation](Repositories.md) to learn the change tracking behavior. - -See the "*ConfigureByConvention Method*" section above for more information. - -## Advanced Topics - -### Controlling the Multi-Tenancy - -If your solution is [multi-tenant](Multi-Tenancy.md), tenants may have **separate databases**, you have **multiple** `DbContext` classes in your solution and some of your `DbContext` classes should be usable **only from the host side**, it is suggested to add `[IgnoreMultiTenancy]` attribute on your `DbContext` class. In this case, ABP guarantees that the related `DbContext` always uses the host [connection string](Connection-Strings.md), even if you are in a tenant context. - -**Example:** - -````csharp -[IgnoreMultiTenancy] -public class MyDbContext : AbpDbContext -{ - ... -} -```` - -Do not use the `[IgnoreMultiTenancy]` attribute if any one of your entities in your `DbContext` can be persisted in a tenant database. - -> When you use repositories, ABP already uses the host database for the entities don't implement the `IMultiTenant` interface. So, most of time you don't need to `[IgnoreMultiTenancy]` attribute if you are using the repositories to work with the database. - -### Set Default Repository Classes - -Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations. - -First, define your default repository classes like that: - -```csharp -public class MyRepositoryBase - : EfCoreRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} - -public class MyRepositoryBase - : EfCoreRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -First one is for [entities with composite keys](Entities.md), second one is for entities with single primary key. - -It's suggested to inherit from the `EfCoreRepository` class and override methods if needed. Otherwise, you will have to implement all the standard repository methods manually. - -Now, you can use `SetDefaultRepositoryClasses` option: - -```csharp -context.Services.AddAbpDbContext(options => -{ - options.SetDefaultRepositoryClasses( - typeof(MyRepositoryBase<,>), - typeof(MyRepositoryBase<>) - ); - - //... -}); -``` - -### Set Base DbContext Class or Interface for Default Repositories - -If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example: - -````csharp -public interface IBookStoreDbContext : IEfCoreDbContext -{ - DbSet Books { get; } -} -```` - -`IBookStoreDbContext` is implemented by the `BookStoreDbContext` class. Then you can use generic overload of the `AddDefaultRepositories`: - -````csharp -context.Services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); - //... -}); -```` - -Now, your custom `BookRepository` can also use the `IBookStoreDbContext` interface: - -````csharp -public class BookRepository : EfCoreRepository, IBookRepository -{ - //... -} -```` - -One advantage of using an interface for a DbContext is then it will be replaceable by another implementation. - -### Replace Other DbContextes - -Once you properly define and use an interface for DbContext, then any other implementation can use the following ways to replace it: - -#### ReplaceDbContext Attribute - -```csharp -[ReplaceDbContext(typeof(IBookStoreDbContext))] -public class OtherDbContext : AbpDbContext, IBookStoreDbContext -{ - //... -} -``` - -#### ReplaceDbContext Option - -````csharp -context.Services.AddAbpDbContext(options => -{ - //... - options.ReplaceDbContext(); -}); -```` - -In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. - -#### Replacing with Multi-Tenancy - -It is also possible to replace a DbContext based on the [multi-tenancy](Multi-Tenancy.md) side. `ReplaceDbContext` attribute and `ReplaceDbContext` method can get a `MultiTenancySides` option with a default value of `MultiTenancySides.Both`. - -**Example:** Replace DbContext only for tenants, using the `ReplaceDbContext` attribute - -````csharp -[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)] -```` - -**Example:** Replace DbContext only for the host side, using the `ReplaceDbContext` method - -````csharp -options.ReplaceDbContext(MultiTenancySides.Host); -```` - -### Split Queries - -ABP enables [split queries](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries) globally by default for better performance. You can change it as needed. - -**Example** - -````csharp -Configure(options => -{ - options.UseSqlServer(optionsBuilder => - { - optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); - }); -}); -```` - -### Customize Bulk Operations - -If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IEfCoreBulkOperationProvider`. - -- You may use example template below: - -```csharp -public class MyCustomEfCoreBulkOperationProvider - : IEfCoreBulkOperationProvider, ITransientDependency -{ - public async Task DeleteManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task InsertManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task UpdateManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } -} -``` - -## See Also - -* [Entities](Entities.md) -* [Repositories](Repositories.md) -* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core) \ No newline at end of file diff --git a/docs/en/Event-Bus.md b/docs/en/Event-Bus.md deleted file mode 100644 index a0e6859465..0000000000 --- a/docs/en/Event-Bus.md +++ /dev/null @@ -1,10 +0,0 @@ -# Event Bus - -An event bus is a mediator that transfers a message from a sender to a receiver. In this way, it provides a loosely coupled communication way between objects, services and applications. - -## Event Bus Types - -ABP Framework provides two type of event buses; - -* **[Local Event Bus](Local-Event-Bus.md)** is suitable for in-process messaging. -* **[Distributed Event Bus](Distributed-Event-Bus.md)** is suitable for inter-process messaging, like microservices publishing and subscribing to distributed events. \ No newline at end of file diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md deleted file mode 100644 index 32f45b6b28..0000000000 --- a/docs/en/Exception-Handling.md +++ /dev/null @@ -1,343 +0,0 @@ -# Exception Handling - -ABP provides a built-in infrastructure and offers a standard model for handling exceptions. - -* Automatically **handles all exceptions** and sends a standard **formatted error message** to the client for an API/AJAX request. -* Automatically hides **internal infrastructure errors** and returns a standard error message. -* Provides an easy and configurable way to **localize** exception messages. -* Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map custom exceptions. - -## Automatic Exception Handling - -`AbpExceptionFilter` handles an exception if **any of the following conditions** are met: - -* Exception is thrown by a **controller action** which returns an **object result** (not a view result). -* The request is an AJAX request (`X-Requested-With` HTTP header value is `XMLHttpRequest`). -* Client explicitly accepts the `application/json` content type (via `accept` HTTP header). - -If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client. - -### Error Message Format - -Error Message is an instance of the `RemoteServiceErrorResponse` class. The simplest error JSON has a **message** property as shown below: - -````json -{ - "error": { - "message": "This topic is locked and can not add a new message" - } -} -```` - -There are **optional fields** those can be filled based upon the exception that has occurred. - -##### Error Code - -Error **code** is an optional and unique string value for the exception. Thrown `Exception` should implement the `IHasErrorCode` interface to fill this field. Example JSON value: - -````json -{ - "error": { - "code": "App:010042", - "message": "This topic is locked and can not add a new message" - } -} -```` - -Error code can also be used to localize the exception and customize the HTTP status code (see the related sections below). - -##### Error Details - -Error **details** in an optional field of the JSON error message. Thrown `Exception` should implement the `IHasErrorDetails` interface to fill this field. Example JSON value: - -```json -{ - "error": { - "code": "App:010042", - "message": "This topic is locked and can not add a new message", - "details": "A more detailed info about the error..." - } -} -``` - -##### Validation Errors - -**validationErrors** is a standard field that is filled if the thrown exception implements the `IHasValidationErrors` interface. - -````json -{ - "error": { - "code": "App:010046", - "message": "Your request is not valid, please correct and try again!", - "validationErrors": [{ - "message": "Username should be minimum length of 3.", - "members": ["userName"] - }, - { - "message": "Password is required", - "members": ["password"] - }] - } -} -```` - -`AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic. - -### Logging - -Caught exceptions are automatically logged. - -#### Log Level - -Exceptions are logged with the `Error` level by default. The Log level can be determined by the exception if it implements the `IHasLogLevel` interface. Example: - -````C# -public class MyException : Exception, IHasLogLevel -{ - public LogLevel LogLevel { get; set; } = LogLevel.Warning; - - //... -} -```` - -#### Self Logging Exceptions - -Some exception types may need to write additional logs. They can implement the `IExceptionWithSelfLogging` if needed. Example: - -````C# -public class MyException : Exception, IExceptionWithSelfLogging -{ - public void Log(ILogger logger) - { - //...log additional info - } -} -```` - -> `ILogger.LogException` extension methods is used to write exception logs. You can use the same extension method when needed. - -## Business Exceptions - -Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception. - -`BusinessException` implements the `IBusinessException` interface in addition to the `IHasErrorCode`, `IHasErrorDetails` and `IHasLogLevel` interfaces. The default log level is `Warning`. - -Usually you have an error code related to a particular business exception. For example: - -````C# -throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer); -```` - -`QaErrorCodes.CanNotVoteYourOwnAnswer` is just a `const string`. The following error code format is recommended: - -```` -: -```` - -**code-namespace** is a **unique value** specific to your module/application. Example: - -```` -Volo.Qa:010002 -```` - -`Volo.Qa` is the code-namespace here. code-namespace is then will be used while **localizing** exception messages. - -* You can **directly throw** a `BusinessException` or **derive** your own exception types from it when needed. -* All properties are optional for the `BusinessException` class. But you generally set either `ErrorCode` or `Message` property. - -## Exception Localization - -One problem with throwing exceptions is how to localize error messages while sending it to the client. ABP offers two models and their variants. - -### User Friendly Exception - -If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client. - -`UserFriendlyException` class is the built-in implementation of the `IUserFriendlyException` interface. Example usage: - -````C# -throw new UserFriendlyException( - "Username should be unique!" -); -```` - -In this way, there is **no need for localization** at all. If you want to localize the message, you can inject and use the standard **string localizer** (see the [localization document](Localization.md)). Example: - -````C# -throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage"]); -```` - -Then define it in the **localization resource** for each language. Example: - -````json -{ - "culture": "en", - "texts": { - "UserNameShouldBeUniqueMessage": "Username should be unique!" - } -} -```` - -String localizer already supports **parameterized messages**. For example: - -````C# -throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage", "john"]); -```` - -Then the localization text can be: - -````json -"UserNameShouldBeUniqueMessage": "Username should be unique! '{0}' is already taken!" -```` - -* The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class. - -### Using Error Codes - -`UserFriendlyException` is fine, but it has a few problems in advanced usages: - -* It requires you to **inject the string localizer** everywhere and always use it while throwing exceptions. -* However, in some of the cases, it may **not be possible** to inject the string localizer (in a static context or in an entity method). - -Instead of localizing the message while throwing the exception, you can separate the process using **error codes**. - -First, define the **code-namespace** to **localization resource** mapping in the module configuration: - -````C# -services.Configure(options => -{ - options.MapCodeNamespace("Volo.Qa", typeof(QaResource)); -}); -```` - -Then any of the exceptions with `Volo.Qa` namespace will be localized using their given localization resource. The localization resource should always have an entry with the error code key. Example: - -````json -{ - "culture": "en", - "texts": { - "Volo.Qa:010002": "You can not vote your own answer!" - } -} -```` - -Then a business exception can be thrown with the error code: - -````C# -throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer); -```` - -* Throwing any exception implementing the `IHasErrorCode` interface behaves the same. So, the error code localization approach is not unique to the `BusinessException` class. -* Defining localized string is not required for an error message. If it's not defined, ABP sends the default error message to the client. It does not use the `Message` property of the exception! if you want that, use the `UserFriendlyException` (or use an exception type that implements the `IUserFriendlyException` interface). - -#### Using Message Parameters - -If you have a parameterized error message, then you can set it with the exception's `Data` property. For example: - -````C# -throw new BusinessException("App:010046") -{ - Data = - { - {"UserName", "john"} - } -}; - -```` - -Fortunately there is a shortcut way to code this: - -````C# -throw new BusinessException("App:010046") - .WithData("UserName", "john"); -```` - -Then the localized text can contain the `UserName` parameter: - -````json -{ - "culture": "en", - "texts": { - "App:010046": "Username should be unique. '{UserName}' is already taken!" - } -} -```` - -* `WithData` can be chained with more than one parameter (like `.WithData(...).WithData(...)`). - -## HTTP Status Code Mapping - -ABP tries to automatically determine the most suitable HTTP status code for common exception types by following these rules: - -* For the `AbpAuthorizationException`: - * Returns `401` (unauthorized) if user has not logged in. - * Returns `403` (forbidden) if user has logged in. -* Returns `400` (bad request) for the `AbpValidationException`. -* Returns `404` (not found) for the `EntityNotFoundException`. -* Returns `403` (forbidden) for the `IBusinessException` (and `IUserFriendlyException` since it extends the `IBusinessException`). -* Returns `501` (not implemented) for the `NotImplementedException`. -* Returns `500` (internal server error) for other exceptions (those are assumed as infrastructure exceptions). - -The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP status code. The default implementation is the `DefaultHttpExceptionStatusCodeFinder` class. It can be replaced or extended as needed. - -### Custom Mappings - -Automatic HTTP status code determination can be overrided by custom mappings. For example: - -````C# -services.Configure(options => -{ - options.Map("Volo.Qa:010002", HttpStatusCode.Conflict); -}); -```` - -## Subscribing to the Exceptions - -It is possible to be informed when the ABP Framework **handles an exception**. It automatically **logs** all the exceptions to the standard [logger](Logging.md), but you may want to do more. - -In this case, create a class derived from the `ExceptionSubscriber` class in your application: - -````csharp -public class MyExceptionSubscriber : ExceptionSubscriber -{ - public async override Task HandleAsync(ExceptionNotificationContext context) - { - //TODO... - } -} -```` - -The `context` object contains necessary information about the exception occurred. - -> You can have multiple subscribers, each gets a copy of the exception. Exceptions thrown by your subscriber is ignored (but still logged). - -## Built-In Exceptions - -Some exception types are automatically thrown by the framework: - -- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](Authorization.md) for more. -- `AbpValidationException` is thrown if the input of the current request is not valid. See [validation](Validation.md) for more. -- `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](Repositories.md). - -You can also throw these type of exceptions in your code (although it's rarely needed). - -## AbpExceptionHandlingOptions - -`AbpExceptionHandlingOptions` is the main [options object](Options.md) to configure the exception handling system. You can configure it in the `ConfigureServices` method of your [module](Module-Development-Basics.md): - -````csharp -Configure(options => -{ - options.SendExceptionsDetailsToClients = true; - options.SendStackTraceToClients = false; -}); -```` - -Here, a list of the options you can configure: - -* `SendExceptionsDetailsToClients` (default: `false`): You can enable or disable sending exception details to the client. -* `SendStackTraceToClients` (default: `true`): You can enable or disable sending the stack trace of exception to the client. If you want to send the stack trace to the client, you must set both `SendStackTraceToClients` and `SendExceptionsDetailsToClients` options to `true` otherwise, the stack trace will not be sent to the client. - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/exception-handling) \ No newline at end of file diff --git a/docs/en/Extension-Methods-And-Helpers.md b/docs/en/Extension-Methods-And-Helpers.md deleted file mode 100644 index 8dac0a56ee..0000000000 --- a/docs/en/Extension-Methods-And-Helpers.md +++ /dev/null @@ -1,3 +0,0 @@ -# Extension Methods & Helpers - -TODO \ No newline at end of file diff --git a/docs/en/Features.md b/docs/en/Features.md deleted file mode 100644 index e38a8c2a5d..0000000000 --- a/docs/en/Features.md +++ /dev/null @@ -1,440 +0,0 @@ -# Features - -ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**. - -The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature. - -Feature system was originally designed to control the tenant features in a **[multi-tenant](Multi-Tenancy.md)** application. However, it is **extensible** and capable of determining the features by any condition. - -> The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). - -## Checking for the Features - -Before explaining to define features, let's see how to check a feature value in your application code. - -### RequiresFeature Attribute - -`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is `true` (enabled) or not. It is a useful shortcut for the `boolean` features. - -**Example: Check if the "PDF Reporting" feature enabled** - -```csharp -public class ReportingAppService : ApplicationService, IReportingAppService -{ - [RequiresFeature("MyApp.PdfReporting")] - public async Task GetPdfReportAsync() - { - //TODO... - } -} -``` - -* `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) is thrown and a proper response is returned to the client side. -* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the methods of that class require the given feature. -* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if any of the features enabled. Use `RequiresAll` option, like `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to check all of the features to be enabled. -* Multiple usage of `[RequiresFeature]` attribute is supported for a method or class. ABP checks all of them in that case. - -> Feature name can be any arbitrary string. It should be unique for a feature. - -#### About the Interception - -ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class (application services, controllers...) that is injected from the [dependency injection](Dependency-Injection.md). - -However, there are **some rules should be followed** in order to make it working; - -* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work. -* Only `async` methods (methods returning a `Task` or `Task`) are intercepted. - -> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP Framework uses the action/page filters to implement the feature checking in this case. - -### IFeatureChecker Service - -`IFeatureChecker` allows to check a feature in your application code. - -#### IsEnabledAsync - -Returns `true` if the given feature is enabled. So, you can conditionally execute your business flow. - -**Example: Check if the "PDF Reporting" feature enabled** - -```csharp -public class ReportingAppService : ApplicationService, IReportingAppService -{ - private readonly IFeatureChecker _featureChecker; - - public ReportingAppService(IFeatureChecker featureChecker) - { - _featureChecker = featureChecker; - } - - public async Task GetPdfReportAsync() - { - if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) - { - //TODO... - } - else - { - //TODO... - } - } -} -``` - -`IsEnabledAsync` has overloads to check multiple features in one method call. - -#### GetOrNullAsync - -Gets the current value for a feature. This method returns a `string`, so you store any kind of value inside it, by converting to or from `string`. - -**Example: Check the maximum product count allowed** - -```csharp -public class ProductController : AbpController -{ - private readonly IFeatureChecker _featureChecker; - - public ProductController(IFeatureChecker featureChecker) - { - _featureChecker = featureChecker; - } - - public async Task Create(CreateProductModel model) - { - var currentProductCount = await GetCurrentProductCountFromDatabase(); - - //GET THE FEATURE VALUE - var maxProductCountLimit = - await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount"); - - if (currentProductCount >= Convert.ToInt32(maxProductCountLimit)) - { - throw new BusinessException( - "MyApp:ReachToMaxProductCountLimit", - $"You can not create more than {maxProductCountLimit} products!" - ); - } - - //TODO: Create the product in the database... - } - - private async Task GetCurrentProductCountFromDatabase() - { - throw new System.NotImplementedException(); - } -} -``` - -This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application. - -Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method: - -```csharp -var maxProductCountLimit = await _featureChecker.GetAsync("MyApp.MaxProductCount"); -``` - -#### Extension Methods - -There are some useful extension methods for the `IFeatureChecker` interface; - -* `Task GetAsync(string name, T defaultValue = default)`: Used to get a value of a feature with the given type `T`. Allows to specify a `defaultValue` that is returned when the feature value is `null`. -* `CheckEnabledAsync(string name)`: Checks if given feature is enabled. Throws an `AbpAuthorizationException` if the feature was not `true` (enabled). - -## Defining the Features - -A feature should be defined to be able to check it. - -### FeatureDefinitionProvider - -Create a class inheriting the `FeatureDefinitionProvider` to define features. - -**Example: Defining features** - -```csharp -using Volo.Abp.Features; - -namespace FeaturesDemo -{ - public class MyFeatureDefinitionProvider : FeatureDefinitionProvider - { - public override void Define(IFeatureDefinitionContext context) - { - var myGroup = context.AddGroup("MyApp"); - - myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false"); - myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10"); - } - } -} -``` - -> ABP automatically discovers this class and registers the features. No additional configuration required. - -> This class is generally created in the `Application.Contracts` project of your solution. - -* In the `Define` method, you first need to add a **feature group** for your application/module or get an existing group then add **features** to this group. -* First feature, named `MyApp.PdfReporting`, is a `boolean` feature with `false` as the default value. -* Second feature, named `MyApp.MaxProductCount`, is a numeric feature with `10` as the default value. - -Default value is used if there is no other value set for the current user/tenant. - -### Other Feature Properties - -While these minimal definitions are enough to make the feature system working, you can specify the **optional properties** for the features; - -* `DisplayName`: A localizable string that will be used to show the feature name on the user interface. -* `Description`: A longer localizable text to describe the feature. -* `ValueType`: Type of the feature value. Can be a class implementing the `IStringValueType`. Built-in types: - * `ToggleStringValueType`: Used to define `true`/`false`, `on`/`off`, `enabled`/`disabled` style features. A checkbox is shown on the UI. - * `FreeTextStringValueType`: Used to define free text values. A textbox is shown on the UI. - * `SelectionStringValueType`: Used to force the value to be selected from a list. A dropdown list is shown on the UI. -* `IsVisibleToClients` (default: `true`): Set false to hide the value of this feature from clients (browsers). Sharing the value with the clients helps them to conditionally show/hide/change the UI parts based on the feature value. -* `Properties`: A dictionary to set/get arbitrary key-value pairs related to this feature. This can be a point for customization. - -So, based on these descriptions, it would be better to define these features as shown below: - -```csharp -using FeaturesDemo.Localization; -using Volo.Abp.Features; -using Volo.Abp.Localization; -using Volo.Abp.Validation.StringValues; - -namespace FeaturesDemo -{ - public class MyFeatureDefinitionProvider : FeatureDefinitionProvider - { - public override void Define(IFeatureDefinitionContext context) - { - var myGroup = context.AddGroup("MyApp"); - - myGroup.AddFeature( - "MyApp.PdfReporting", - defaultValue: "false", - displayName: LocalizableString - .Create("PdfReporting"), - valueType: new ToggleStringValueType() - ); - - myGroup.AddFeature( - "MyApp.MaxProductCount", - defaultValue: "10", - displayName: LocalizableString - .Create("MaxProductCount"), - valueType: new FreeTextStringValueType( - new NumericValueValidator(0, 1000000)) - ); - } - } -} -``` - -* `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) for details about the localization system. -* First feature is set to `ToggleStringValueType`, while the second one is set to `FreeTextStringValueType` with a numeric validator that allows to the values from `0` to `1,000,000`. - -Remember to define the localization the keys in your localization file: - -````json -"PdfReporting": "PDF Reporting", -"MaxProductCount": "Maximum number of products" -```` - -See the [localization document](Localization.md) for details about the localization system. - -### Feature Management Modal - -The [application startup template](Startup-Templates/Application.md) comes with the [tenant management](Modules/Tenant-Management.md) and the [feature management](Modules/Feature-Management.md) modules pre-installed. - -Whenever you define a new feature, it will be available on the **feature management modal**. To open this modal, navigate to the **tenant management page** and select the `Features` action for a tenant (create a new tenant if there is no tenant yet): - -![features-action](images/features-action.png) - -This action opens a modal to manage the feature values for the selected tenant: - -![features-modal](images/features-modal.png) - -So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application. - -See the *Feature Management* section below to learn more about managing the features. - -### Child Features - -A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled. - -**Example: Defining child features** - -```csharp -using FeaturesDemo.Localization; -using Volo.Abp.Features; -using Volo.Abp.Localization; -using Volo.Abp.Validation.StringValues; - -namespace FeaturesDemo -{ - public class MyFeatureDefinitionProvider : FeatureDefinitionProvider - { - public override void Define(IFeatureDefinitionContext context) - { - var myGroup = context.AddGroup("MyApp"); - - var reportingFeature = myGroup.AddFeature( - "MyApp.Reporting", - defaultValue: "false", - displayName: LocalizableString - .Create("Reporting"), - valueType: new ToggleStringValueType() - ); - - reportingFeature.CreateChild( - "MyApp.PdfReporting", - defaultValue: "false", - displayName: LocalizableString - .Create("PdfReporting"), - valueType: new ToggleStringValueType() - ); - - reportingFeature.CreateChild( - "MyApp.ExcelReporting", - defaultValue: "false", - displayName: LocalizableString - .Create("ExcelReporting"), - valueType: new ToggleStringValueType() - ); - } - } -} -``` - -The example above defines a *Reporting* feature with two children: *PDF Reporting* and *Excel Reporting*. - -### Changing Features Definitions of a Depended Module - -A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing feature definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. - -**Example: Manipulate an existing feature definition** - -```csharp -var someGroup = context.GetGroupOrNull("SomeModule"); -var feature = someGroup.Features.FirstOrDefault(f => f.Name == "SomeFeature"); -if (feature != null) -{ - feature.Description = ... - feature.CreateChild(...); -} -``` - -## Check a Feature in the Client Side - -A feature value is available at the client side too, unless you set `IsVisibleToClients` to `false` on the feature definition. The feature values are exposed from the [Application Configuration API](API/Application-Configuration.md) and usable via some services on the UI. - -See the following documents to learn how to check features in different UI types: - -* [ASP.NET Core MVC / Razor Pages / JavaScript API](UI/AspNetCore/JavaScript-API/Features.md) -* [Angular](UI/Angular/Features.md) - -**Blazor** applications can use the same `IFeatureChecker` service as explained above. - -## Feature Management - -Feature management is normally done by an admin user using the feature management modal: - -![features-modal](images/features-modal.png) - -This modal is available on the related entities, like tenants in a multi-tenant application. To open it, navigate to the **Tenant Management** page (for a multi-tenant application), click to the **Actions** button left to the Tenant and select the **Features** action. - -If you need to manage features by code, inject the `IFeatureManager` service. - -**Example: Enable PDF reporting for a tenant** - -```csharp -public class MyService : ITransientDependency -{ - private readonly IFeatureManager _featureManager; - - public MyService(IFeatureManager featureManager) - { - _featureManager = featureManager; - } - - public async Task EnablePdfReporting(Guid tenantId) - { - await _featureManager.SetForTenantAsync( - tenantId, - "MyApp.PdfReporting", - true.ToString() - ); - } -} -``` - -`IFeatureManager` is defined by the Feature Management module. It comes pre-installed with the application startup template. See the [feature management module documentation](Modules/Feature-Management.md) for more information. - -## Advanced Topics - -### Feature Value Providers - -Feature system is extensible. Any class derived from `FeatureValueProvider` (or implements `IFeatureValueProvider`) can contribute to the feature system. A value provider is responsible to **obtain the current value** of a given feature. - -Feature value providers are **executed one by one**. If one of them return a non-null value, then this feature value is used and the other providers are not executed. - -There are three pre-defined value providers, executed by the given order: - -* `TenantFeatureValueProvider` tries to get if the feature value is explicitly set for the **current tenant**. -* `EditionFeatureValueProvider` tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (`ICurrentPrincipalAccessor`) with the claim name `editionid` (a constant defined as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](Modules/Tenant-Management.md) module. You can implement it yourself or consider to use the [SaaS module](https://commercial.abp.io/modules/Volo.Saas) of the ABP Commercial. -* `DefaultValueFeatureValueProvider` gets the default value of the feature. - -You can write your own provider by inheriting the `FeatureValueProvider`. - -**Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value** - -```csharp -using System.Threading.Tasks; -using Volo.Abp.Features; -using Volo.Abp.Security.Claims; -using Volo.Abp.Validation.StringValues; - -namespace FeaturesDemo -{ - public class SystemAdminFeatureValueProvider : FeatureValueProvider - { - public override string Name => "SA"; - - private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; - - public SystemAdminFeatureValueProvider( - IFeatureStore featureStore, - ICurrentPrincipalAccessor currentPrincipalAccessor) - : base(featureStore) - { - _currentPrincipalAccessor = currentPrincipalAccessor; - } - - public override Task GetOrNullAsync(FeatureDefinition feature) - { - if (feature.ValueType is ToggleStringValueType && - _currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") - { - return Task.FromResult("true"); - } - - return null; - } - } -} -``` - -If a provider returns `null`, then the next provider is executed. - -Once a provider is defined, it should be added to the `AbpFeatureOptions` as shown below: - -```csharp -Configure(options => -{ - options.ValueProviders.Add(); -}); -``` - -Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) class. - -### Feature Store - -`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information diff --git a/docs/en/FluentValidation.md b/docs/en/FluentValidation.md deleted file mode 100644 index c087f8a185..0000000000 --- a/docs/en/FluentValidation.md +++ /dev/null @@ -1,58 +0,0 @@ -# FluentValidation Integration - -ABP [Validation](Validation.md) infrastructure is extensible. [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package extends the validation system to work with the [FluentValidation](https://fluentvalidation.net/) library. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.FluentValidation -```` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package to your project: - - ```` - Install-Package Volo.Abp.FluentValidation - ```` - -2. Add the `AbpFluentValidationModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpFluentValidationModule) //Add the FluentValidation module - )] -public class YourModule : AbpModule -{ -} -```` - -## Using the FluentValidation - -Follow [the FluentValidation documentation](https://fluentvalidation.net/) to create validator classes. Example: - -````csharp -public class CreateUpdateBookDtoValidator : AbstractValidator -{ - public CreateUpdateBookDtoValidator() - { - RuleFor(x => x.Name).Length(3, 10); - RuleFor(x => x.Price).ExclusiveBetween(0.0f, 999.0f); - } -} -```` - -ABP will automatically find this class and associate with the `CreateUpdateBookDto` on object validation. - -## See Also - -* [Validation System](Validation.md) \ No newline at end of file diff --git a/docs/en/Getting-Started-Angular-Template.md b/docs/en/Getting-Started-Angular-Template.md deleted file mode 100644 index 9beb84bcef..0000000000 --- a/docs/en/Getting-Started-Angular-Template.md +++ /dev/null @@ -1,8 +0,0 @@ -# Getting Started with the Startup Templates - -See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: - -* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) -* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) - - \ No newline at end of file diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md deleted file mode 100644 index 0a6424ac3b..0000000000 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ /dev/null @@ -1,140 +0,0 @@ -# Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application - -This tutorial explains how to start ABP from scratch with minimal dependencies. You generally want to start with the **[startup template](Getting-Started-AspNetCore-MVC-Template.md)**. - -## Create a New Project - -1. Create a new AspNet Core Web Application with Visual Studio 2022 (17.0.0+): - -![](images/create-new-aspnet-core-application-v2.png) - -2. Configure your new project: - -![](images/select-empty-web-application-v2.png) - -3. Press the create button: - -![create-aspnet-core-application](images/create-aspnet-core-application.png) - -## Install Volo.Abp.AspNetCore.Mvc Package - -You can use the [ABP CLI](CLI.md) to install the Volo.Abp.AspNetCore.Mvc package to your project. Execute the following command in the folder of the .csproj file that you want to install the package on: - -````bash -abp add-package Volo.Abp.AspNetCore.Mvc -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.AspNetCore.Mvc). - -## Create the First ABP Module - -ABP is a modular framework and it requires a **startup (root) module** class derived from ``AbpModule``: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - // Configure the HTTP request pipeline. - if (env.IsDevelopment()) - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseConfiguredEndpoints(); - } - } -} -```` - -``AppModule`` is a good name for the startup module for an application. - -ABP packages define module classes and a module can depend on another. In the code above, the ``AppModule`` depends on the ``AbpAspNetCoreMvcModule`` (defined by the [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc) package). It's common to add a ``DependsOn`` attribute after installing a new ABP NuGet package. - -Instead of the Startup class, we are configuring an ASP.NET Core pipeline in this module class. - -## The Program Class - -Next step is to modify the Program class to integrate to the ABP module system: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -await builder.AddApplicationAsync(); - -var app = builder.Build(); - -await app.InitializeApplicationAsync(); -await app.RunAsync(); -```` - -``builder.AddApplicationAsync();`` adds all services defined in all modules starting from the ``AppModule``. - -``app.InitializeApplicationAsync()`` initializes and starts the application. - -## Run the Application! - -That's all! Run the application, it will just work as expected. - -## Using Autofac as the Dependency Injection Framework - -While ASP.NET Core's Dependency Injection (DI) system is fine for basic requirements, [Autofac](https://autofac.org/) provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features. - -Replacing ASP.NET Core's DI system by Autofac and integrating to ABP is pretty easy. - -1. Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) package - -```` -Install-Package Volo.Abp.Autofac -```` - -2. Add the ``AbpAutofacModule`` Dependency - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] //Add dependency to ABP Autofac module -public class AppModule : AbpModule -{ - ... -} -```` - -3. Update `Program.cs` to use Autofac: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -builder.Host.UseAutofac(); //Add this line - -await builder.AddApplicationAsync(); - -var app = builder.Build(); - -await app.InitializeApplicationAsync(); -await app.RunAsync(); -```` - -## Source Code - -Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). diff --git a/docs/en/Getting-Started-AspNetCore-MVC-Template.md b/docs/en/Getting-Started-AspNetCore-MVC-Template.md deleted file mode 100644 index 9beb84bcef..0000000000 --- a/docs/en/Getting-Started-AspNetCore-MVC-Template.md +++ /dev/null @@ -1,8 +0,0 @@ -# Getting Started with the Startup Templates - -See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: - -* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) -* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) - - \ No newline at end of file diff --git a/docs/en/Getting-Started-Create-Solution-Single-Layer.md b/docs/en/Getting-Started-Create-Solution-Single-Layer.md deleted file mode 100644 index d2fbadd1b5..0000000000 --- a/docs/en/Getting-Started-Create-Solution-Single-Layer.md +++ /dev/null @@ -1,62 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Running the solution", - "Path": "Getting-Started-Running-Solution-Single-Layer" - }, - "Previous": { - "Name": "Setup Your Development Environment", - "Path": "Getting-Started-Setup-Environment-Single-Layer" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Create a New Project - -We will use the ABP CLI to create a new ABP project. - -> You can also use the ABP CLI Command Generator on the [ABP Framework website](https://abp.io/get-started) by easily selecting all options from the page. - -Use the `new` command of the ABP CLI to create a new project: - -````shell -abp new Acme.BookStore -t app-nolayers{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{else if UI == "BlazorServer"}} -u blazor-server{{end}}{{if DB == "Mongo"}} -d mongodb{{end}} -```` - -*You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.* - -> [ABP CLI document](./CLI.md) covers all of the available commands and options. - -## The Solution Structure - -The solution structure is based on the [Single-Layer Startup Template](Startup-Templates/Application-Single-Layer.md) where everything is in one project instead of the [Domain Driven Design](Domain-Driven-Design.md). You can check its [documentation](Startup-Templates/Application-Single-Layer.md) for more details. - -{{ if DB == "Mongo" }} - -## MongoDB Transactions - -The [startup template](Startup-Templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable it in the *YourProjectModule* class's `ConfigureMongoDB` method: - - ```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled; //or UnitOfWorkTransactionBehavior.Auto -}); - ``` - -> Or you can delete that code since `Auto` is already the default behavior. - -{{ end }} \ No newline at end of file diff --git a/docs/en/Getting-Started-Create-Solution.md b/docs/en/Getting-Started-Create-Solution.md deleted file mode 100644 index 96740abf89..0000000000 --- a/docs/en/Getting-Started-Create-Solution.md +++ /dev/null @@ -1,82 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` -````json -//[doc-nav] -{ - "Next": { - "Name": "Running the solution", - "Path": "Getting-Started-Running-Solution" - }, - "Previous": { - "Name": "Setup Your Development Environment", - "Path": "Getting-Started-Setup-Environment" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Create a New Project - -We will use the ABP CLI to create a new ABP project. - -> Alternatively, you can **create and download** projects from the [ABP Framework website](https://abp.io/get-started) by easily selecting all options from the page. - -Use the `new` command of the ABP CLI to create a new project: - -````shell -abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{else if UI == "BlazorServer"}} -u blazor-server{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC" || UI == "BlazorServer"}} --tiered{{else}} --separate-auth-server{{end}}{{end}} -```` - -*You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.* - -{{ if Tiered == "Yes" }} - -{{ if UI == "MVC" || UI == "BlazorServer" }} - -* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. - -{{ else }} - -* `--separate-auth-server` argument is used to separate the Auth Server application from the API host application. If not specified, you will have a single endpoint on the server. - -{{ end }} - -{{ end }} - -> [ABP CLI document](./CLI.md) covers all of the available commands and options. - -### Mobile Development - -If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. - -See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. - -## The Solution Structure - -The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. - -{{ if DB == "Mongo" }} - -## MongoDB Transactions - -The [startup template](Startup-Templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable it in the *YourProjectMongoDbModule* class's `ConfigureServices` method: - - ```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; -}); - ``` - -> Or you can delete that code since `Auto` is already the default behavior. - -{{ end }} \ No newline at end of file diff --git a/docs/en/Getting-Started-Overall.md b/docs/en/Getting-Started-Overall.md deleted file mode 100644 index ef88f5f4b5..0000000000 --- a/docs/en/Getting-Started-Overall.md +++ /dev/null @@ -1,8 +0,0 @@ -# Getting Started: Overall - -## Select the Solution Architecture - -This tutorial has multiple versions. Please select the one that fits you the best: - -* **[Single-Layer Solution](Getting-Started-Single-Layered.md)**: Creates a single-project solution. Recommended for building an application with a **simpler and easy to understand** architecture. -* **[Layered Solution Architecture](Getting-Started.md)**: A fully layered (multiple projects) solution based on [Domain Driven Design](Domain-Driven-Design.md) practices. Recommended for long-term projects that need a **maintainable and extensible** codebase. diff --git a/docs/en/Getting-Started-React-Native.md b/docs/en/Getting-Started-React-Native.md deleted file mode 100644 index 767aba6d7e..0000000000 --- a/docs/en/Getting-Started-React-Native.md +++ /dev/null @@ -1,129 +0,0 @@ -````json -//[doc-params] -{ - "Tiered": ["No", "Yes"] -} -```` - -# Getting Started with the React Native - -ABP platform provide basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated to your ABP based backends**. - -![React Native gif](./images/react-native-introduction.gif) - -## How to Prepare Development Environment - -Please follow the steps below to prepare your development environment for React Native. - -1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v16 or v18 installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. -2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. -3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to React Native projects. -4. **Install an Emulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/) or [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. - -## How to Start a New React Native Project - -You have multiple options to initiate a new React Native project that works with ABP: - -### 1. Using ABP CLI - -ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with a React Native application. Simply [install the ABP CLI](CLI.md) and run the following command in your terminal: - -```shell -abp new MyCompanyName.MyProjectName -csf -u -m react-native -``` - -> To see further options in the CLI, please visit the [CLI manual](CLI.md). - -This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. - -### 2. Generating a CLI Command from Get Started Page - -You can generate a CLI command on the [get started page of the abp.io website](https://abp.io/get-started). Then, use the command on your terminal to create a new [Startup Template](./Startup-Templates/Index.md). - -## How to Configure & Run the Backend - -> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. - -> When you are using OpenIddict, You should remove 'clientSecret' on Environment.js (if exists) and disable "HTTPS-only" settings. (Openiddict has default since Version 6.0) - -A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application on your **local IP address**. - -{{ if Tiered == "No"}} -![React Native host project local IP entry](images/rn-host-local-ip.png) - -- Open the `appsettings.json` file in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address. -- Open the `launchSettings.json` file in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. - -{{ else if Tiered == "Yes" }} - -![React Native tiered project local IP entry](images/rn-tiered-local-ip.png) - -- Open the `appsettings.json` file in the `.AuthServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address. -- Open the `launchSettings.json` file in the `.AuthServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. -- Open the `appsettings.json` file in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address. -- Open the `launchSettings.json` file in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. - -{{ end }} - -Run the backend application as described in the [getting started document](Getting-Started.md). - -> You should turn off the "Https Restriction" if you're using OpenIddict as a central identity management solution. Because the IOS Simulator doesn't support self-signed certificates and OpenIddict is set to only work with HTTPS by default. - -## How to disable the Https-only settings of OpenIddict - -Open the {{ if Tiered == "No" }}`MyProjectNameHttpApiHostModule`{{ else if Tiered == "Yes" }}`MyProjectNameAuthServerModule`{{ end }} project and copy-paste the below code-block to the `PreConfigureServices` method: - -```csharp -#if DEBUG - PreConfigure(options => - { - options.UseAspNetCore() - .DisableTransportSecurityRequirement(); - }); -#endif -``` - -## How to Configure & Run the React Native Application - -1. Make sure the [database migration is complete](./Getting-Started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](./Getting-Started?UI=NG&DB=EF&Tiered=No#run-the-application). -2. Open `react-native` folder and run `yarn` or `npm install` if you have not already. -3. Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: - -![react native environment local IP](images/rn-environment-local-ip.png) - -{{ if Tiered == "Yes" }} - -> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. - -{{else}} - -> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. - -{{ end }} - -4. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the opitons. - -> The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. - -![expo-cli-options](images/rn-options.png) - -In the above image, you can start the application with an Android emulator, an iOS simulator or a physical phone by scanning the QR code with the [Expo Client](https://expo.io/tools#client) or choosing the option. - -### Expo - -![React Native login screen on iPhone 11](images/rn-login-iphone.png) - -### Android Studio - -1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command. -2. Press **a** to open in Android Studio. - -![React Native login screen on iPhone 11](images/rn-login-android-studio.png) - -Enter **admin** as the username and **1q2w3E\*** as the password to login to the application. - -The application is up and running. You can continue to develop your application based on this startup template. - -## See Also - -- [React Native project structure](./Startup-Templates/Application#react-native) diff --git a/docs/en/Getting-Started-Running-Solution-Single-Layer.md b/docs/en/Getting-Started-Running-Solution-Single-Layer.md deleted file mode 100644 index 18b2316cf6..0000000000 --- a/docs/en/Getting-Started-Running-Solution-Single-Layer.md +++ /dev/null @@ -1,138 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Previous": { - "Name": "Creating a new solution", - "Path": "Getting-Started-Create-Solution-Single-Layer" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Create the Database - -### Connection String - -Check the **connection string** in the `appsettings.json` file under the `YourProject` project. - -{{ if DB == "EF" }} - -````json -"ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True" -} -```` - -> **About the Connection Strings and Database Management Systems** -> -> The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. However, if you've selected another DBMS using the `-dbms` parameter on the ABP CLI `new` command (like `-dbms MySQL`), the connection string might be different for you. -> -> EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers and you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md) if you need later. - -{{ else if DB == "Mongo" }} - -````json -"ConnectionStrings": { - "Default": "mongodb://localhost:27017/BookStore" -} -```` - -The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. - -{{ end }} - -### Seed Initial Data - -Before running the application, you need to create the database and seed the initial data. To do that, you can run the following command in the directory of your project (in the same folder of the `.csproj` file): - -```bash -dotnet run --migrate-database -``` - -## Before Running the Application - -### Installing the Client-Side Packages - -[ABP CLI](CLI.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. - -However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. - -For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: - -```bash -abp install-libs -``` - -> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. - -{{if UI=="Blazor" || UI=="BlazorServer"}} - -### Bundling and Minification - -`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](CLI.md). - -However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your blazor application: - -```bash -abp bundle -``` - -> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md). - -{{end}} - -## Run the Application - -{{if UI=="MVC" || UI=="BlazorServer"}} - -Running the application is pretty straight-forward, you can run the application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project: - -{{else if UI=="Blazor"}} - -Running the application is pretty straight-forward, you just need to run the `TodoApp.Host` application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project. - -> **Note:** The `host` application hosts and serves the `blazor` application. Therefore, you should run the `host` application only. - -After the application runs, open the application in your default browser. - -{{else if UI=="NG"}} - -The solution has two main applications: - -* `TodoApp` (in the .NET solution) hosts the server-side HTTP API, so the Angular application can consume it. (server-side application) -* `angular` folder contains the Angular application. (client-side application) - -Firstly, run the `TodoApp` project in your favorite IDE (or run the `dotnet run` CLI command on your project directory) to see the server-side HTTP API on [Swagger UI](https://swagger.io/tools/swagger-ui/). - -![swagger-ui](images/swagger-ui.png) - -You can explore and test your HTTP API with this UI. If it works, then we can run the Angular client application. - -You can run the application using the following (or `yarn start`) command: - -````bash -npm start -```` - -This command takes time, but eventually runs and opens the application in your default browser. - -{{end}} - -After running the project, the index page should be seen as below: - -![single-layer-index-page](images/single-layer-index-page.png) - -Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. - -![bookstore-login-2](images/bookstore-login-2.png) \ No newline at end of file diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md deleted file mode 100644 index f522016278..0000000000 --- a/docs/en/Getting-Started-Running-Solution.md +++ /dev/null @@ -1,271 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -````json -//[doc-nav] -{ - "Previous": { - "Name": "Creating a new solution", - "Path": "Getting-Started-Create-Solution" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Create the Database - -### Connection String - -Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.AuthServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else if UI=="BlazorServer"}}`.Blazor` project{{else}}`.HttpApi.Host` project{{end}}{{end}}. - -{{ if DB == "EF" }} - -````json -"ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True" -} -```` - -> **About the Connection Strings and Database Management Systems** -> -> The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. However, if you've selected another DBMS using the `-dbms` parameter on the ABP CLI `new` command (like `-dbms MySQL`), the connection string might be different for you. -> -> EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers and you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md) if you need later. - -### Database Migrations - -The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). It comes with a `.DbMigrator` console application which **applies the migrations** and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -### The Initial Migration - -`.DbMigrator` application automatically **creates the Initial migration** on first run. - -**If you are using Visual Studio, you can skip to the *Running the DbMigrator* section.** However, other IDEs (e.g. Rider) may have problems for the first run since it adds the initial migration and compiles the project. In this case, open a command line terminal in the folder of the `.DbMigrator` project and run the following command: - -````bash -dotnet run -```` - -For the next time, you can just run it in your IDE as you normally do. - -### Running the DbMigrator - -Right click to the `.DbMigrator` project and select **Set as StartUp Project** - -![set-as-startup-project](images/set-as-startup-project.png) - - Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - - ![db-migrator-output](images/db-migrator-output.png) - -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. - -{{ else if DB == "Mongo" }} - -````json -"ConnectionStrings": { - "Default": "mongodb://localhost:27017/BookStore" -} -```` - -The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. - -### Seed Initial Data - -The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -Right click to the `.DbMigrator` project and select **Set as StartUp Project** - -![set-as-startup-project](images/set-as-startup-project.png) - - Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - - ![db-migrator-output](images/db-migrator-output.png) - -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. - -{{ end }} - -## Before Running the Application - -### Installing the Client-Side Packages - -[ABP CLI](CLI.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. - -However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. - -For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: - -```bash -abp install-libs -``` - -> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. - -{{if UI=="Blazor" || UI=="BlazorServer"}} - -### Bundling and Minification - -`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](CLI.md). - -However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your blazor application: - -```bash -abp bundle -``` - -> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md). - -{{end}} - -## Run the Application - -{{ if UI == "MVC" || UI == "BlazorServer" }} - -> **Note**: Before starting the application, run `abp install-libs` command in your Web directory to restore the client-side libraries. This will populate the `libs` folder. - -{{ if UI == "BlazorServer" }} - -> **Important:** The `.AuthServer` application serves as the **Authentication Server** for the `.Blazor` application. It is essential to have the `.AuthServer` application running in the background to ensure the proper functioning of the `.Blazor` application. - -To do this, open terminal in `.AuthServer` project folder and run the following command. - -````bash -dotnet run -```` - -Once the `.AuthServer`application has started, it is time to run `.HttpApi.Host` application. - -> **Important:** Prior to launching the `.Blazor` project, it is essential to execute the `.HttpApi.Host` application as well. - -To do this, open terminal in `.HttpApi.Host` project folder and run the following command. - -````bash -dotnet run -```` -Once the `.AuthServer` and `.HttpApi.Host` applications has started, you can proceed to run the `.Blazor` project. - -{{ end # UI }} - -{{ if Tiered == "Yes" }} - -> Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. - -1. Ensure that the `.AuthServer` project is the startup project. Run this application that will open a **login** page in your browser. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -You can login, but you cannot enter to the main application here. This is **just the authentication server**. - -2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. - -![swagger-ui](images/swagger-ui.png) - -This is the HTTP API that is used by the web application. - -3. Lastly, ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the startup project and run the application which will open a **welcome** page in your browser - -![mvc-tiered-app-home](images/bookstore-home-2.png) - -Click to the **login** button which will redirect you to the *authentication server* to login to the application: - -![bookstore-login](images/bookstore-login-2.png) - -{{ else # Tiered != "Yes" }} - -Ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the startup project. Run the application which will open the **login** page in your browser: - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -![bookstore-login](images/bookstore-login-2.png) - -{{ end # Tiered }} - -{{ else # UI != MVC || BlazorServer }} - -### Running the HTTP API Host (Server Side) - -{{ if Tiered == "Yes" }} - -> Tiered solutions use Redis as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. - -Ensure that the `.AuthServer` project is the startup project. Run the application which will open a **login** page in your browser. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -You can login, but you cannot enter to the main application here. This is **just the authentication server**. - -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: - -{{ else # Tiered == "No" }} - -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -{{ end # Tiered }} - -![swagger-ui](images/swagger-ui.png) - -You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. - -{{ end # UI }} - -{{ if UI == "Blazor" }} - -### Running the Blazor Application (Client Side) - -> **Important:** The `.HttpApi.Host` application serves as the **Authentication Server** for the `.Blazor` application. It is essential to have the `.HttpApi.Host` application running in the background to ensure the proper functioning of the `.Blazor` application. - -To do this, you can open terminal in `.HttpApi.Host` project folder and run the following command. - -````bash -dotnet run -```` - -Once the `.HttpApi.Host` application has started, you can proceed to run the `.Blazor` application. - -Ensure that the `.Blazor` project is the startup project and run the application. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: - -![bookstore-login](images/bookstore-login-2.png) - -{{ else if UI == "NG" }} - -### Running the Angular Application (Client Side) - -Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) - -```bash -yarn -``` - -Once all node modules are loaded, execute `yarn start` (or `npm start`) command: - -```bash -yarn start -``` - -It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. - -![bookstore-login](images/bookstore-login-2.png) - -{{ end }} - -Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. \ No newline at end of file diff --git a/docs/en/Getting-Started-Setup-Environment-Single-Layer.md b/docs/en/Getting-Started-Setup-Environment-Single-Layer.md deleted file mode 100644 index 3652fc5701..0000000000 --- a/docs/en/Getting-Started-Setup-Environment-Single-Layer.md +++ /dev/null @@ -1,55 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Creating a new solution", - "Path": "Getting-Started-Create-Solution-Single-Layer" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Setup Your Development Environment - -First things first! Let's setup your development environment before creating the project. - -### Pre-Requirements - -The following tools should be installed on your development machine: - -* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. - {{ if UI != "Blazor" }} -* [Node v16+](https://nodejs.org/) -* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [1](#f-yarn) or npm v6+ (already installed with Node) - {{ end }} - -{{ if UI != "Blazor" }} - -1 _Yarn v2 works differently and is not supported._ [↩](#a-yarn) - -{{ end }} - -### Install the ABP CLI - -[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. First, you need to install the ABP CLI using the following command: - -````shell -dotnet tool install -g Volo.Abp.Cli -```` - -If you've already installed, you can update it using the following command: - -````shell -dotnet tool update -g Volo.Abp.Cli -```` \ No newline at end of file diff --git a/docs/en/Getting-Started-Setup-Environment.md b/docs/en/Getting-Started-Setup-Environment.md deleted file mode 100644 index 0d33e8abe2..0000000000 --- a/docs/en/Getting-Started-Setup-Environment.md +++ /dev/null @@ -1,58 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` -````json -//[doc-nav] -{ - "Next": { - "Name": "Creating a new solution", - "Path": "Getting-Started-Create-Solution" - } -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -## Setup Your Development Environment - -First things first! Let's setup your development environment before creating the project. - -### Pre-Requirements - -The following tools should be installed on your development machine: - -* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. -{{ if UI != "Blazor" }} -* [Node v16+](https://nodejs.org/) -* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [1](#f-yarn) or npm v6+ (already installed with Node) -{{ end }} -{{ if Tiered == "Yes" }} -* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). -{{ end }} - -{{ if UI != "Blazor" }} - -1 _Yarn v2 works differently and is not supported._ [↩](#a-yarn) - -{{ end }} - -### Install the ABP CLI - -[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. First, you need to install the ABP CLI using the following command: - -````shell -dotnet tool install -g Volo.Abp.Cli -```` - -If you've already installed, you can update it using the following command: - -````shell -dotnet tool update -g Volo.Abp.Cli -```` \ No newline at end of file diff --git a/docs/en/Getting-Started-Single-Layered.md b/docs/en/Getting-Started-Single-Layered.md deleted file mode 100644 index 5ccad79649..0000000000 --- a/docs/en/Getting-Started-Single-Layered.md +++ /dev/null @@ -1,17 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -This tutorial explains how to **create and run** a new Single-Layered web application using the ABP Framework. Follow the steps below: - -1. [Setup your development environment](Getting-Started-Setup-Environment-Single-Layer.md) -2. [Creating a new solution](Getting-Started-Create-Solution-Single-Layer.md) -3. [Running the solution](Getting-Started-Running-Solution-Single-Layer.md) diff --git a/docs/en/Getting-Started-With-Startup-Templates.md b/docs/en/Getting-Started-With-Startup-Templates.md deleted file mode 100644 index add08b8ebd..0000000000 --- a/docs/en/Getting-Started-With-Startup-Templates.md +++ /dev/null @@ -1,3 +0,0 @@ -This document has been [moved to here](Getting-Started.md). - - diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md deleted file mode 100644 index 6352f43bc7..0000000000 --- a/docs/en/Getting-Started.md +++ /dev/null @@ -1,18 +0,0 @@ -# Getting Started - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -This tutorial explains how to **create and run** a new web application using the ABP Framework. Follow the steps below; - -1. [Setup your development environment](Getting-Started-Setup-Environment.md) -2. [Creating a new solution](Getting-Started-Create-Solution.md) -3. [Running the solution](Getting-Started-Running-Solution.md) \ No newline at end of file diff --git a/docs/en/Global-Features.md b/docs/en/Global-Features.md deleted file mode 100644 index 0029127eb0..0000000000 --- a/docs/en/Global-Features.md +++ /dev/null @@ -1,143 +0,0 @@ -# Global Features -Global Feature system is used to enable/disable an application feature on development time. It is done on the development time, because some **services** (e.g. controllers) are removed from the application model and **database tables** are not created for the disabled features, which is not possible on runtime. - -Global Features system is especially useful if you want to develop a reusable application module with optional features. If the final application doesn't want to use some of the features, it can disable these features. - -> If you are looking for a system to enable/disable features based on current tenant or any other condition, please see the [Features](Features.md) document. - -## Installation -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.GlobalFeatures -``` - -## Defining a Global Feature - -A feature class is something like that: - -```csharp -[GlobalFeatureName("Shopping.Payment")] -public class PaymentFeature -{ - -} -``` - -## Enable/Disable Global Features - -Use `GlobalFeatureManager.Instance` to enable/disable a global feature. - -```csharp -// Able to Enable/Disable with generic type parameter. -GlobalFeatureManager.Instance.Enable(); -GlobalFeatureManager.Instance.Disable(); - -// Also able to Enable/Disable with string feature name. -GlobalFeatureManager.Instance.Enable("Shopping.Payment"); -GlobalFeatureManager.Instance.Disable("Shopping.Payment"); -``` - -> Global Features are disabled unless they are explicitly enabled. - -### Where to Configure Global Features? - -Global Features have to be configured before application startup. Since the `GlobalFeatureManager.Instance` is a singleton object, one-time, static configuration is enough. It is suggested to enable/disable global features in `PreConfigureServices` method of your module. You can use the `OneTimeRunner` utility class to make sure it runs only once: - -```csharp -private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - OneTimeRunner.Run(() => - { - GlobalFeatureManager.Instance.Enable(); - }); -} -``` - -## Check for a Global Feature - -```csharp -GlobalFeatureManager.Instance.IsEnabled() -GlobalFeatureManager.Instance.IsEnabled("Shopping.Payment") -``` - -Both methods return `bool`. So, you can write conditional logic as shown below: - -```csharp -if (GlobalFeatureManager.Instance.IsEnabled()) -{ - // Some strong payment codes here... -} -``` - -### RequiresGlobalFeature Attribute - -Beside the manual check, there is `[RequiresGlobalFeature]` attribute to check it declaratively for a controller or page. ABP returns HTTP Response `404` if the related feature was disabled. - -```csharp -[RequiresGlobalFeature(typeof(PaymentFeature))] -public class PaymentController : AbpController -{ - -} -``` - -## Grouping Features of a Module - -It is common to group global features of a module to allow the final application developer easily discover and configure the features. Following example shows how to group features of a module. - -Assume that we've defined a global feature for `Subscription` feature of an `Ecommerce` module: - -```csharp -[GlobalFeatureName("Ecommerce.Subscription")] -public class SubscriptionFeature : GlobalFeature -{ - public SubscriptionFeature(GlobalModuleFeatures module) - : base(module) - { - } -} -``` - -You can define as many features as you need in your module. Then define a class to group these features together: - -```csharp -public class GlobalEcommerceFeatures : GlobalModuleFeatures -{ - public const string ModuleName = "Ecommerce"; - - public SubscriptionFeature Subscription => GetFeature(); - - public GlobalEcommerceFeatures(GlobalFeatureManager featureManager) - : base(featureManager) - { - AddFeature(new SubscriptionFeature(this)); - } -} -``` - -Finally, you can create an extension method on `GlobalModuleFeaturesDictionary`: - -```csharp -public static class GlobalModuleFeaturesDictionaryEcommerceExtensions -{ - public static GlobalEcommerceFeatures Ecommerce( - this GlobalModuleFeaturesDictionary modules) - { - return modules.GetOrAdd( - GlobalEcommerceFeatures.ModuleName, - _ => new GlobalEcommerceFeatures(modules.FeatureManager) - ) as GlobalEcommerceFeatures; - } -``` - -Then `GlobalFeatureManager.Instance.Modules.Ecommerce()` can be used to access the global features of your module. Examples usages: - -```csharp -GlobalFeatureManager.Instance.Modules.Ecommerce().Subscription.Enable(); -GlobalFeatureManager.Instance.Modules.Ecommerce().EnableAll(); -``` - diff --git a/docs/en/Guid-Generation.md b/docs/en/Guid-Generation.md deleted file mode 100644 index 0990f5b6dd..0000000000 --- a/docs/en/Guid-Generation.md +++ /dev/null @@ -1,111 +0,0 @@ -# GUID Generation - -GUID is a common **primary key type** that is used in database management systems. ABP Framework prefers GUID as the primary for pre-built [application modules](Modules/Index.md). Also, `ICurrentUser.Id` property ([see](CurrentUser.md)) is type of GUID, that means the ABP Framework assumes that the User Id is always GUID. - -## Why Prefer GUID? - -GUID has advantages and disadvantages. You can find many articles on the web related to this topic, so we will not discuss all again, but will list the most fundamental advantages: - -* It is **usable** in all database providers. -* It allows to **determine the primary key** on the client side, without needing a **database round trip** to generate the Id value. This can be more performant while inserting new records to the database and allows us to know the PK before interacting to the database. -* GUIDs are **naturally unique** which has some advantages in the following situations if; - * You need to integrate to **external** systems. - * You need to **split or merge** different tables. - * You are creating **distributed systems**. -* GUIDs are impossible to guess, so they can be **more secure** compared to auto-increment Id values in some cases. - -While there are some disadvantages (just search it on the web), we found these advantages much more important while designing the ABP Framework. - -## IGuidGenerator - -The most important problem with GUID is that it is **not sequential by default**. When you use the GUID as the primary key and set it as the **clustered index** (which is default) for your table, it brings a significant **performance problem on insert** (because inserting new record may need to re-order the existing records). - -So, **never use `Guid.NewGuid()` to create Ids** for your entities! - -One good solution to this problem is to generate **sequential GUIDs**, which is provided by the ABP Framework out of the box. `IGuidGenerator` service creates sequential GUIDs (implemented by the `SequentialGuidGenerator` by default). Use `IGuidGenerator.Create()` when you need to manually set Id of an [entity](Entities.md). - -**Example: An entity with GUID primary key and creating the entity** - -Assume that you've a `Product` [entity](Entities.md) that has a `Guid` key: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - private Product() { /* This constructor is used by the ORM/database provider */ } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - } -} -```` - -And you want to create a new product: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace AbpDemo -{ - public class MyProductService : ITransientDependency - { - private readonly IRepository _productRepository; - private readonly IGuidGenerator _guidGenerator; - - public MyProductService( - IRepository productRepository, - IGuidGenerator guidGenerator) - { - _productRepository = productRepository; - _guidGenerator = guidGenerator; - } - - public async Task CreateAsync(string productName) - { - var product = new Product(_guidGenerator.Create(), productName); - - await _productRepository.InsertAsync(product); - } - } -} -```` - -This service injects the `IGuidGenerator` in the constructor. If your class is an [application service](Application-Services.md) or deriving from one of the other base classes, you can directly use the `GuidGenerator` base property which is a pre-injected `IGuidGenerator` instance. - -## Options - -### AbpSequentialGuidGeneratorOptions - -`AbpSequentialGuidGeneratorOptions` is the [option class](Options.md) that is used to configure the sequential GUID generation. It has a single property: - -* `DefaultSequentialGuidType` (`enum` of type `SequentialGuidType`): The strategy used while generating GUID values. - -Database providers behaves differently while processing GUIDs, so you should set it based on your database provider. `SequentialGuidType` has the following `enum` members: - -* `SequentialAtEnd` (**default**) works well with the [SQL Server](Entity-Framework-Core.md). -* `SequentialAsString` is used by [MySQL](Entity-Framework-Core-MySQL.md) and [PostgreSQL](Entity-Framework-Core-PostgreSQL.md). -* `SequentialAsBinary` is used by [Oracle](Entity-Framework-Core-Oracle.md). - -Configure this option in the `ConfigureServices` method of your [module](Module-Development-Basics.md), as shown below: - -````csharp -Configure(options => -{ - options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; -}); -```` - -> EF Core [integration packages](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS) sets this option to a proper value for the related DBMS. So, most of the times, you don't need to set this option if you are using these integration packages. \ No newline at end of file diff --git a/docs/en/IdentityServer-Integration.md b/docs/en/IdentityServer-Integration.md deleted file mode 100644 index da82d4bc6a..0000000000 --- a/docs/en/IdentityServer-Integration.md +++ /dev/null @@ -1,3 +0,0 @@ -# IdentityServer Integration - -TODO \ No newline at end of file diff --git a/docs/en/Image-Manipulation.md b/docs/en/Image-Manipulation.md deleted file mode 100644 index 9d946e31dd..0000000000 --- a/docs/en/Image-Manipulation.md +++ /dev/null @@ -1,401 +0,0 @@ -# Image Manipulation -ABP Framework provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/) and [Magick.NET](https://github.com/dlemstra/Magick.NET) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library. - -> The image resizer/compressor system is designed to be extensible. You can implement your own image resizer/compressor contributor and use it in your application. - -## Installation - -You can add this package to your application by either using the [ABP CLI](CLI.md) or manually installing it. Using the [ABP CLI](CLI.md) is the recommended approach. - -### Using the ABP CLI - -Open a command line terminal in the folder of your project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Imaging.Abstractions -``` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Imaging.Abstractions](https://www.nuget.org/packages/Volo.Abp.Imaging.Abstractions) NuGet package to your project: - -``` -Install-Package Volo.Abp.Imaging.Abstractions -``` - -2. Add the `AbpImagingAbstractionsModule` to the dependency list of your module: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpImagingAbstractionsModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -``` - -## Providers - -ABP Framework provides two image resizer/compressor implementations out of the box: - -* [Magick.NET](#magick-net-provider) -* [ImageSharp](#imagesharp-provider) - -You should install one of these provides to make it actually working. - -> If none of the provider packages installed into your application, compress/resize operations return the untouched input image. - -## IImageResizer - -You can [inject](Dependency-Injection.md) the `IImageResizer` service and use it for image resize operations. Here is the available methods of the `IImageResizer` service: - -```csharp -public interface IImageResizer -{ - /* Works with a Stream object that represents an image */ - Task> ResizeAsync( - Stream stream, - ImageResizeArgs resizeArgs, - string mimeType = null, - CancellationToken cancellationToken = default - ); - - /* Works with a byte array that contains an image file */ - Task> ResizeAsync( - byte[] bytes, - ImageResizeArgs resizeArgs, - string mimeType = null, - CancellationToken cancellationToken = default - ); -} -``` - -**Example usage:** - -```csharp -var resizeResult = await _imageResizer.ResizeAsync( - imageStream, /* A stream object that represents an image */ - new ImageResizeArgs - { - Width = 100, - Height = 100, - Mode = ImageResizeMode.Crop - }, - mimeType: "image/jpeg" -); -``` - -> **Note:** If `resizeResult.State` returns 'Done', then it means that the resize operation was successful. However, if it returns any other state than 'Done', the stream you're using might be corrupted. Therefore, you can perform a check like the one below and assign the correct stream to the main stream: - -```csharp -if (resizeResult.Result is not null && imageStream != resizeResult.Result && resizeResult.Result.CanRead) -{ - await imageStream.DisposeAsync(); - imageStream = resizeResult.Result; -} -``` - -> You can use `MimeTypes.Image.Jpeg` constant instead of the `image/jpeg` magic string used in that example. - -### ImageResizeArgs - -The `ImageResizeArgs` is a class that is used to define the resize operation parameters. It has the following properties: - -* `Width`: The width of the resized image. -* `Height`: The height of the resized image. -* `Mode`: The resize mode (see the [ImageResizeMode](#imageresizemode) section for more information). - -### ImageResizeMode - -The `ImageResizeMode` is an enum that is used to define the resize mode. It has the following values: - -```csharp -public enum ImageResizeMode : byte -{ - None = 0, - Stretch = 1, - BoxPad = 2, - Min = 3, - Max = 4, - Crop = 5, - Pad = 6, - Default = 7 -} -``` - -> See the [ImageSharp documentation](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Processing.ResizeMode.html) for more information about the resize modes. - -### ImageResizeResult - -The `ImageResizeResult` is a generic class that is used to return the result of the image resize operations. It has the following properties: - -* `Result`: The resized image (stream or byte array). -* `State`: The result of the resize operation (type: `ImageProcessState`). - -### ImageProcessState - -The `ImageProcessState` is an enum that is used to return the the result of the image resize operations. It has the following values: - -```csharp -public enum ImageProcessState : byte -{ - Done = 1, - Canceled = 2, - Unsupported = 3, -} -``` - -### ImageResizeOptions - -`ImageResizeOptions` is an [options object](Options.md) that is used to configure the image resize system. It has the following properties: - -* `DefaultResizeMode`: The default resize mode. (Default: `ImageResizeMode.None`) - -## IImageCompressor - -You can [inject](Dependency-Injection.md) the `IImageCompressor` service and use it for image compression operations. Here is the available methods of the `IImageCompressor` service: - -```csharp -public interface IImageCompressor -{ - /* Works with a Stream object that represents an image */ - Task> CompressAsync( - Stream stream, - string mimeType = null, - CancellationToken cancellationToken = default - ); - - /* Works with a byte array that contains an image file */ - Task> CompressAsync( - byte[] bytes, - string mimeType = null, - CancellationToken cancellationToken = default - ); -} -``` - -**Example usage:** - -```csharp -var compressResult = await _imageCompressor.CompressAsync( - imageStream, /* A stream object that represents an image */ - mimeType: "image/jpeg" -); -``` - -> **Note:** If `compressResult.State` returns 'Done', then it means that the compression operation was successful. However, if it returns any other state than 'Done', the stream you're using might be corrupted. Therefore, you can perform a check like the one below and assign the correct stream to the main stream: - -```csharp - -if (compressResult.Result is not null && imageStream != compressResult.Result && compressResult.Result.CanRead) -{ - await imageStream.DisposeAsync(); - imageStream = compressResult.Result; -} -``` - -### ImageCompressResult - -The `ImageCompressResult` is a generic class that is used to return the result of the image compression operations. It has the following properties: - -* `Result`: The compressed image (stream or byte array). -* `State`: The result of the compress operation (type: `ImageProcessState`). - -### ImageProcessState - -The `ImageProcessState` is an enum that is used to return the the result of the image compress operations. It has the following values: - -```csharp -public enum ImageProcessState : byte -{ - Done = 1, - Canceled = 2, - Unsupported = 3, -} -``` - -## Magick.NET Provider - -`Volo.Abp.Imaging.MagickNet` NuGet package implements the image operations using the [Magick.NET](https://github.com/dlemstra/Magick.NET) library. - -## Installation - -You can add this package to your application by either using the [ABP CLI](CLI.md) or manually installing it. Using the [ABP CLI](CLI.md) is the recommended approach. - -### Using the ABP CLI - -Open a command line terminal in the folder of your project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Imaging.MagickNet -``` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Imaging.MagickNet](https://www.nuget.org/packages/Volo.Abp.Imaging.MagickNet) NuGet package to your project: - -``` -Install-Package Volo.Abp.Imaging.MagickNet -``` - -2. Add `AbpImagingMagickNetModule` to your [module](Module-Development-Basics.md)'s dependency list: - -```csharp -[DependsOn(typeof(AbpImagingMagickNetModule))] -public class MyModule : AbpModule -{ - //... -} -``` - -### Configuration - -`MagickNetCompressOptions` is an [options object](Options.md) that is used to configure the Magick.NET image compression system. It has the following properties: - -* `OptimalCompression`: Indicates whether the optimal compression is enabled or not. (Default: `false`) -* `IgnoreUnsupportedFormats`: Indicates whether the unsupported formats are ignored or not. (Default: `false`) -* `Lossless`: Indicates whether the lossless compression is enabled or not. (Default: `false`) - -## ImageSharp Provider - -`Volo.Abp.Imaging.ImageSharp` NuGet package implements the image operations using the [ImageSharp](https://github.com/SixLabors/ImageSharp) library. - -## Installation - -You can add this package to your application by either using the [ABP CLI](CLI.md) or manually installing it. Using the [ABP CLI](CLI.md) is the recommended approach. - -### Using the ABP CLI - -Open a command line terminal in the folder of your project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Imaging.ImageSharp -``` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Imaging.ImageSharp](https://www.nuget.org/packages/Volo.Abp.Imaging.ImageSharp) NuGet package to your project: - -``` -Install-Package Volo.Abp.Imaging.ImageSharp -``` - -2. Add `AbpImagingImageSharpModule` to your [module](Module-Development-Basics.md)'s dependency list: - - -```csharp -[DependsOn(typeof(AbpImagingImageSharpModule))] -public class MyModule : AbpModule -{ - //... -} -``` - -### Configuration - -`ImageSharpCompressOptions` is an [options object](Options.md) that is used to configure the ImageSharp image compression system. It has the following properties: - -* `DefaultQuality`: The default quality of the JPEG and WebP encoders. (Default: `75`) -* [`JpegEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder.html): The JPEG encoder. (Default: `JpegEncoder` with `Quality` set to `DefaultQuality`) -* [`PngEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Png.PngEncoder.html): The PNG encoder. (Default: `PngEncoder` with `IgnoreMetadata` set to `true` and `CompressionLevel` set to `PngCompressionLevel.BestCompression`) -* [`WebPEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Webp.WebpEncoder.html): The WebP encoder. (Default: `WebPEncoder` with `Quality` set to `DefaultQuality`) - -**Example usage:** - -```csharp -Configure(options => -{ - options.JpegEncoder = new JpegEncoder - { - Quality = 60 - }; - options.PngEncoder = new PngEncoder - { - CompressionLevel = PngCompressionLevel.BestCompression - }; - options.WebPEncoder = new WebPEncoder - { - Quality = 65 - }; -}); -``` - -## ASP.NET Core Integration - -`Volo.Abp.Imaging.AspNetCore` NuGet package defines attributes for controller actions that can automatically compress and/or resize uploaded files. - -## Installation - -You can add this package to your application by either using the [ABP CLI](CLI.md) or manually installing it. Using the [ABP CLI](CLI.md) is the recommended approach. - -### Using the ABP CLI - -Open a command line terminal in the folder of your project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Imaging.AspNetCore -``` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Imaging.AspNetCore](https://www.nuget.org/packages/Volo.Abp.Imaging.AspNetCore) NuGet package to your project: - -``` -Install-Package Volo.Abp.Imaging.AspNetCore -``` - -2. Add `AbpImagingAspNetCoreModule` to your [module](Module-Development-Basics.md)'s dependency list: - -```csharp -[DependsOn(typeof(AbpImagingAspNetCoreModule))] -public class MyModule : AbpModule -{ - //... -} -``` - -### CompressImageAttribute - -The `CompressImageAttribute` is used to compress the image before. `IFormFile`, `IRemoteStreamContent`, `Stream` and `IEnumrable` types are supported. It has the following properties: - -* `Parameters`: Names of the the parameters that are used to configure the image compression system. This is useful if your action has some non-image parameters. If you don't specify the parameters names, all of the method parameters are considered as image. - -**Example usage:** - -```csharp -[HttpPost] -[CompressImage] /* Compresses the given file (automatically determines the file mime type) */ -public async Task Upload(IFormFile file) -{ - //... -} -``` - -### ResizeImageAttribute - -The `ResizeImageAttribute` is used to resize the image before requesting the action. `IFormFile`, `IRemoteStreamContent`, `Stream` and `IEnumrable` types are supported. It has the following properties: - -* `Parameters`: Names of the the parameters that are used to configure the image resize system. This is useful if your action has some non-image parameters. If you don't specify the parameters names, all of the method parameters are considered as image. -* `Width`: Target width of the resized image. -* `Height`: Target height of the resized image. -* `Mode`: The resize mode (see the [ImageResizeMode](#imageresizemode) section for more information). - -**Example usage:** - -```csharp -[HttpPost] -[ResizeImage(Width = 100, Height = 100, Mode = ImageResizeMode.Crop)] -public async Task Upload(IFormFile file) -{ - //... -} -``` diff --git a/docs/en/Index.md b/docs/en/Index.md deleted file mode 100644 index bcb370677f..0000000000 --- a/docs/en/Index.md +++ /dev/null @@ -1,97 +0,0 @@ -# ABP Documentation - -ABP Framework offers an **opinionated architecture** to build enterprise software solutions with **best practices** on top of the **.NET** and the **ASP.NET Core** platforms. It provides the fundamental infrastructure, production-ready startup templates, modules, themes, tooling, guides and documentation to implement that architecture properly and **automate the details** and repetitive works as much as possible. - -## Getting Started - -* [Quick Start](Tutorials/Todo/Overall.md) is a single-part, quick-start tutorial to build a simple application with the ABP Framework. Start with this tutorial if you want to quickly understand how ABP works. -* [Getting Started](Getting-Started.md) guide can be used to create and run ABP based solutions with different options and details. -* [Web Application Development Tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack web application with all aspects of a real-life solution. - -### UI Framework Options - -ABP Framework can work with any UI framework, while the following frameworks are supported out of the box: - -ui options - -### Database Provider Options - -ABP Framework can work with any database provider, while the following providers are supported out of the box: - -ABP Database Providers - -## Exploring the Documentation - -ABP has a **comprehensive documentation** that not only explains the ABP Framework, but also includes **guides** and **samples** to help you on creating a **maintainable solution** by introducing and discussing common **software development principle and best practices**. - -### Architecture - -ABP offers a complete, modular and layered software architecture based on [Domain Driven Design](Domain-Driven-Design.md) principles and patterns. It also provides the necessary infrastructure to implement this architecture. - -* See the [Modularity](Module-Development-Basics.md) document to understand the module system. -* [Implementing Domain Driven Design book](https://abp.io/books/implementing-domain-driven-design?ref=doc) is an ultimate guide for who want to understand and implement the DDD with the ABP Framework. -* [Microservice Architecture](Microservice-Architecture.md) document explains how ABP helps to create a microservice solution. -* [Multi-Tenancy](Multi-Tenancy.md) document introduces multi-tenancy and explores the ABP multi-tenancy infrastructure. - -### Infrastructure - -There are a lot of features provided by the ABP Framework to achieve real world scenarios easier, like [Event Bus](Event-Bus.md), [Background Job System](Background-Jobs.md), [Audit Logging](Audit-Logging.md), [BLOB Storing](Blob-Storing.md), [Data Seeding](Data-Seeding.md), [Data Filtering](Data-Filtering.md). - -### Cross Cutting Concerns - -ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](Exception-Handling.md), [Validation](Validation.md), [Authorization](Authorization.md), [Localization](Localization.md), [Caching](Caching.md), [Dependency Injection](Dependency-Injection.md), [Setting Management](Settings.md), etc. - -### Application Modules - -Application Modules provides pre-built application functionalities; - -* [**Account**](Modules/Account.md): Provides UI for the account management and allows user to login/register to the application. -* **[Identity](Modules/Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. -* [**OpenIddict**](Modules/OpenIddict.md): Integrates to OpenIddict. -* [**Tenant Management**](Modules/Tenant-Management.md): Manages tenants for a [multi-tenant](Multi-Tenancy.md) (SaaS) application. - -See the [Application Modules](Modules/Index.md) document for all pre-built modules. - -### Startup Templates - -The [Startup templates](Startup-Templates/Index.md) are pre-built Visual Studio solution templates. You can create your own solution based on these templates to **immediately start your development**. - -## Books - -### Mastering ABP Framework - -Mastering ABP Framework - Halil İbrahim Kalkan - -*Mastering ABP Framework* is an ultimate guide to get started and expertise with the ABP Framework. It is authored by Halil İbrahim Kalkan, the creator and the lead developer of the ABP Framework. - -**[You can order it from Amazon now](https://www.amazon.com/Mastering-ABP-Framework-maintainable-implementing-dp-1801079242/dp/1801079242)!** - -### Free E-Book: Implementing Domain Driven Design - -Implementing Domain Driven Design - -A **practical guide** for implementing Domain Driven Design (DDD). While the implementation details are **based on the ABP Framework** infrastructure, the basic concepts, principles and models can be applied to any solution, even if it is not a .NET solution. - -**[Click here to get your free copy](https://abp.io/books/implementing-domain-driven-design?ref=doc).** - -## ABP Community - -### The Source Code - -ABP is hosted on GitHub. See [the source code](https://github.com/abpframework). - -### ABP Community Web Site - -The [ABP Community](https://community.abp.io/) is a website to publish articles and share knowledge about the ABP Framework. You can also create content for the community! - -### Blog - -Follow the [ABP Blog](https://blog.abp.io/) to learn the latest happenings in the ABP Framework. - -### Samples - -See the [sample projects](Samples/Index.md) built with the ABP Framework. - -### Want to Contribute? - -ABP is a community-driven open source project. See [the contribution guide](Contribution/Index.md) if you want to be a part of this project. diff --git a/docs/en/Integration-Services.md b/docs/en/Integration-Services.md deleted file mode 100644 index 6c31fb69bf..0000000000 --- a/docs/en/Integration-Services.md +++ /dev/null @@ -1,116 +0,0 @@ -# Integration Services - -The *Integration Service* concept was created to distinguish the [application services](Application-Services.md) that are built for inter-module (or inter-microservice) communication from the application services that are intended to be consumed from a UI or a client application. - -The following figure shows a few microservices behind an API Gateway that is consumed by a UI application and 3rd-party client applications: - -![integration-services](images/integration-services.png) - -HTTP requests coming from out of the API Gateway can be called as *external request*, while the HTTP requests performed between microservices can be considered as *internal requests*. The application services that are designed to respond to these internal requests are called as *integration services*, because their purpose is to integrate microservices in the system, rather than respond to user requests. - -## Marking an Application Service as Integration Service - -Assume that you have an application service named `ProductAppService`, and you want to use that application service as an integration service. In that case, you can use the `[IntegrationService]` attribute on top of the application service class as shown below: - -```csharp -[IntegrationService] -public class ProductAppService : ApplicationService, IProductAppService -{ - // ... -} -``` - -If your application service has an interface, like `IProductService` in this example, you can use it on the service interface: - -```csharp -[IntegrationService] -public interface IProductAppService : IApplicationService -{ - // ... -} -``` - -> If you've used the `[IntegrationService]` on top of your service interface, it is *not needed* to use on the service class too. - -That's all. From now, ABP will handle your application service as integration service and implement the followings by convention: - -* That service is **not exposed** by default, unless you explicitly set `ExposeIntegrationServices` options (see the *Exposing Integration Services* section). -* If you are using the [Auto API Controllers](API/Auto-API-Controllers.md) feature in your application, the **URL prefix** will be `/integration-api` instead of `/api` for your integration services. Thus, you can distinguish internal and external service communications and take additional actions, such as preventing REST API calls for integration services out of API Gateway. -* **Audit logging** is disabled by default for the integration services. See the next section if you want to enable it. - -## Marking an MVC Controller as Integration Service - -In addition to application services, you can mark a regular MVC Controller as integration service, using the same `IntegrationService` attribute, or inheriting an interface that has the `IntegrationService` attribute. - -**Example:** - -````csharp -[IntegrationService] // Mark as integration service -[Route("integration-api/products")] -public class ProductController : AbpControllerBase -{ - //... -} -```` - -When you use the `IntegrationService` attribute, ABP will handle your controller as integration service and implement the followings by convention: - -* That controller is **not exposed** to clients by default, unless you explicitly set `ExposeIntegrationServices` options (see the *Exposing Integration Services* section). -* **Audit logging** is disabled by default for controller. See the next section if you want to enable it. - -## Configuration - -### Exposing Integration Services - -Integration services and controllers are not exposed by default for security reasons. They typically don't require authorization, so you should **carefully and explicitly** allow them to be visible and usable to client applications. - -To expose integration services and controllers, set `AbpAspNetCoreMvcOptions.ExposeIntegrationServices` to `true` in the `ConfigureServices` method of your [module class](Module-Development-Basics.md): - -````csharp -Configure(options => -{ - options.ExposeIntegrationServices = true; -}); -```` - -> Hiding integration services is useful when you are building reusable application modules, where they may be used in a monolith application or in a microservice system. In a monolith application, integration services don't need to be exposed outside since the modules may in-process communicate with each other. On the other hand, if you build a microservice solution and use that module as a service, it will be proper to expose the integration services, so other microservices can consume them remotely inside your private network (or Kubernetes cluster). In that case, be careful to not accidently expose the integration services out of your private network. Configuring your API Gateway so that it blocks requests to `integration-api` prefixed URLs from outside of your network will be a good option. - -### Enabling/Disabling the Audit Logging - -Audit Logging is disabled by default for integration services but it can be enabled by configuring the `AbpAuditingOptions` [options class](Options.md) in the `ConfigureServices` method of your [module class](Module-Development-Basics.md): - -```csharp -Configure(options => -{ - options.IsEnabledForIntegrationService = true; -}); -``` - -> Please refer to the [audit logging document](Audit-Logging.md) for other options and details. - -### Filtering Auto API Controllers - -You can filter integration services (or non-integration services) while creating [Auto API Controllers](API/Auto-API-Controllers.md), using the `ApplicationServiceTypes` option of the `ConventionalControllerSetting` by configuring the `AbpAspNetCoreMvcOptions` as shown below: - -```csharp -PreConfigure(options => -{ - options.ConventionalControllers.Create( - typeof(MyApplicationModule).Assembly, - conventionalControllerSetting => - { - conventionalControllerSetting.ApplicationServiceTypes = - ApplicationServiceTypes.IntegrationServices; - }); -}); -``` - -Tip: You can call the `options.ConventionalControllers.Create` multiple times to configure regular application services and integration services with different options. - -> Please refer to the [Auto API Controllers document](API/Auto-API-Controllers.md) for more information about the Auto API Controller system. - -## See Also - -* [Application Services](Application-Services.md) -* [Auto API Controllers](API/Auto-API-Controllers.md) -* [Audit Logging](Audit-Logging.md) diff --git a/docs/en/Integration-Tests.md b/docs/en/Integration-Tests.md deleted file mode 100644 index 9ad4741822..0000000000 --- a/docs/en/Integration-Tests.md +++ /dev/null @@ -1 +0,0 @@ -This document has been [moved to here](Testing.md). \ No newline at end of file diff --git a/docs/en/JSON.md b/docs/en/JSON.md deleted file mode 100644 index d4fc550982..0000000000 --- a/docs/en/JSON.md +++ /dev/null @@ -1,74 +0,0 @@ -# JSON -The ABP Framework provides an abstraction to work with JSON. Having such an abstraction has some benefits; - -* You can write library independent code. Therefore, you can change the underlying library with the minimum effort and code change. -* You can use the predefined converters defined in the ABP without worrying about the underlying library's internal details. - -> The JSON serialization system is implemented with the [Volo.Abp.Json](https://www.nuget.org/packages/Volo.Abp.Json) NuGet package([Volo.Abp.Json.SystemTextJson](https://www.nuget.org/packages/Volo.Abp.Json.SystemTextJson) is the default implementation). Most of the time, you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Json) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). - -## IJsonSerializer - -You can inject `IJsonSerializer` and use it for JSON operations. Here is the available operations in the `IJsonSerializer` interface. - -```csharp -public interface IJsonSerializer -{ - string Serialize(object obj, bool camelCase = true, bool indented = false); - T Deserialize(string jsonString, bool camelCase = true); - object Deserialize(Type type, string jsonString, bool camelCase = true); -} -``` -Usage Example: - -```csharp -public class ProductManager -{ - public IJsonSerializer JsonSerializer { get; } - - public ProductManager(IJsonSerializer jsonSerializer) - { - JsonSerializer = jsonSerializer; - } - - public void SendRequest(Product product) - { - var json= JsonSerializer.Serialize(product); - // Left blank intentionally for demo purposes... - } -} -``` - -## Configuration - -### AbpJsonOptions - -`AbpJsonOptions` type provides options for the JSON operations in the ABP Framework. - -Properties: -* **InputDateTimeFormats(`List`)**: Formats of input JSON date, Empty string means default format. You can provide multiple formats to parse the date. -* **OutputDateTimeFormat(`string`)**: Format of output json date, Null or empty string means default format. - -## System Text Json - -### AbpSystemTextJsonSerializerOptions - -- **JsonSerializerOptions(`System.Text.Json.JsonSerializerOptions`)**: Global options for System.Text.Json library operations. See [here](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) for reference. - -### AbpSystemTextJsonSerializerModifiersOptions - -- **Modifiers(`List>`)**: Configure `Modifiers` of `DefaultJsonTypeInfoResolver`. See [here](https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-6/#json-contract-customization) for reference. - - -## Newtonsoft - -Add [Volo.Abp.Json.Newtonsoft](https://www.nuget.org/packages/Volo.Abp.Json.Newtonsoft) package and depends on `AbpJsonNewtonsoftModule` to replace the `System Text Json`. - -#### AbpNewtonsoftJsonSerializerOptions - -- **JsonSerializerSettings(`Newtonsoft.Json.JsonSerializerSettings`)**: Global options for Newtonsoft library operations. See [here](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonSerializerSettings.htm) for reference. - -## Configuring JSON options in ASP.NET Core - -You can change the JSON behavior in ASP.NET Core by configuring [JsonOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions) or -[MvcNewtonsoftJsonOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions)(if you use `Newtonsoft.Json`) - diff --git a/docs/en/Json-Serialization.md b/docs/en/Json-Serialization.md deleted file mode 100644 index 7d6072c041..0000000000 --- a/docs/en/Json-Serialization.md +++ /dev/null @@ -1,3 +0,0 @@ -# JSON - -TODO \ No newline at end of file diff --git a/docs/en/Local-Event-Bus.md b/docs/en/Local-Event-Bus.md deleted file mode 100644 index 5c5d967f5f..0000000000 --- a/docs/en/Local-Event-Bus.md +++ /dev/null @@ -1,247 +0,0 @@ -# Local Event Bus - -The Local Event Bus allows services to publish and subscribe to **in-process events**. That means it is suitable if two services (publisher and subscriber) are running in the same process. - -## Publishing Events - -There are two ways of publishing local events explained in the following sections. - -### Publishing Events Using the ILocalEventBus - -`ILocalEventBus` can be [injected](Dependency-Injection.md) and used to publish a local event. - -**Example: Publish a local event when the stock count of a product changes** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Local; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly ILocalEventBus _localEventBus; - - public MyService(ILocalEventBus localEventBus) - { - _localEventBus = localEventBus; - } - - public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) - { - //TODO: IMPLEMENT YOUR LOGIC... - - //PUBLISH THE EVENT - await _localEventBus.PublishAsync( - new StockCountChangedEvent - { - ProductId = productId, - NewCount = newCount - } - ); - } - } -} -```` - -`PublishAsync` method gets a single parameter: the event object, which is responsible to hold the data related to the event. It is a simple plain class: - -````csharp -using System; - -namespace AbpDemo -{ - public class StockCountChangedEvent - { - public Guid ProductId { get; set; } - - public int NewCount { get; set; } - } -} -```` - -Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case). - -### Publishing Events Inside Entity / Aggregate Root Classes - -[Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish local events inside entity / aggregate root classes. - -**Example: Publish a local event inside an aggregate root method** - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - public int StockCount { get; private set; } - - private Product() { } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - - public void ChangeStockCount(int newCount) - { - StockCount = newCount; - - //ADD an EVENT TO BE PUBLISHED - AddLocalEvent( - new StockCountChangedEvent - { - ProductId = Id, - NewCount = newCount - } - ); - } - } -} -```` - -`AggregateRoot` class defines the `AddLocalEvent` to add a new local event, that is published when the aggregate root object is saved (created, updated or deleted) into the database. - -> Tip: If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event. - -#### IGeneratesDomainEvents Interface - -Actually, adding local events are not unique to the `AggregateRoot` class. You can implement `IGeneratesDomainEvents` for any entity class. But, `AggregateRoot` implements it by default and makes it easy for you. - -> It is not suggested to implement this interface for entities those are not aggregate roots, since it may not work for some database providers for such entities. It works for EF Core, but not works for MongoDB for example. - -#### How It Was Implemented? - -Calling the `AddLocalEvent` doesn't immediately publish the event. The event is published when you save changes to the database; - -* For EF Core, it is published on `DbContext.SaveChanges`. -* For MongoDB, it is published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). - -## Subscribing to Events - -A service can implement the `ILocalEventHandler` to handle the event. - -**Example: Handle the `StockCountChangedEvent` defined above** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus; - -namespace AbpDemo -{ - public class MyHandler - : ILocalEventHandler, - ITransientDependency - { - public async Task HandleEventAsync(StockCountChangedEvent eventData) - { - //TODO: your code that does something on the event - } - } -} -```` - -That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic in your handler class. - -* **One or more handlers** can subscribe to the same event. -* A single event handler class can **subscribe to multiple events** by implementing the `ILocalEventHandler` interface for each event type. - -If you perform **database operations** and use the [repositories](Repositories.md) inside the event handler, you may need to create a [unit of work](Unit-Of-Work.md), because some repository methods need to work inside an **active unit of work**. Make the handle method `virtual` and add a `[UnitOfWork]` attribute for the method, or manually use the `IUnitOfWorkManager` to create a unit of work scope. - -> The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options. - -### LocalEventHandlerOrder Attribute - -`LocalEventHandlerOrder` attribute can be used to set the execution order for the event handlers, which can be helpful if you want to handle your event handlers in a specific order. - -````csharp -[LocalEventHandlerOrder(-1)] -public class MyHandler - : ILocalEventHandler, - ITransientDependency -{ - public async Task HandleEventAsync(StockCountChangedEvent eventData) - { - //TODO: your code that does something on the event - } -} -```` - -> By default, all event handlers have an order value of 0. Thus, if you want to take certain event handlers to be executed before other event handlers, you can set the order value as a negative value. - -#### LocalEventHandlerOrderAttribute Properties - -* `Order` (`int`): Used to set the execution order for a certain event handler. - -### Transaction & Exception Behavior - -Event handlers are always executed in the same [unit of work](Unit-Of-Work.md) scope, that means in the same database transaction with the code that published the event. If an event handler throws an exception, the unit of work (database transaction) is rolled back. So, **use try-catch yourself** in the event handler if you want to hide the error. - -When you call `ILocalEventBus.PublishAsync`, the event handlers are not immediately executed. Instead, they are executed just before the current unit of work completed (an unhandled exception in the handler still rollbacks the current unit of work). If you want to immediately execute the handlers, set the optional `onUnitOfWorkComplete` parameter to `false`. - -> Keeping the default behavior is recommended unless you don't have a unique requirement. `onUnitOfWorkComplete` option is not available when you publish events inside entity / aggregate root classes (see the *Publishing Events Inside Entity / Aggregate Root Classes* section). - -## Pre-Built Events - -It is very common to **publish events on entity create, update and delete** operations. ABP Framework **automatically** publish these events for all entities. You can just subscribe to the related event. - -**Example: Subscribe to an event that published when a user was created** - -````csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities.Events; -using Volo.Abp.EventBus; - -namespace AbpDemo -{ - public class MyHandler - : ILocalEventHandler>, - ITransientDependency - { - public async Task HandleEventAsync( - EntityCreatedEventData eventData) - { - var userName = eventData.Entity.UserName; - var email = eventData.Entity.Email; - //... - } - } -} -```` - -This class subscribes to the `EntityCreatedEventData`, which is published just after a user was created (but before the current transaction is completed). For example, you may want to send a "Welcome" email to the new user. - -The pre-built event types are; - -* `EntityCreatedEventData` is published just after an entity was successfully created. -* `EntityUpdatedEventData` is published just after an entity was successfully updated. -* `EntityDeletedEventData` is published just after an entity was successfully deleted. -* `EntityChangedEventData` is published just after an entity was successfully created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events. - -### How It Was Implemented? - -Pre-build events are published when you save changes to the database; - -* For EF Core, they are published on `DbContext.SaveChanges`. -* For MongoDB, they are published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). - -#### AbpEntityChangeOptions - -There is a `PublishEntityUpdatedEventWhenNavigationChanges` option in the `AbpEntityChangeOptions` class with a default value of `true`. -If you set it to `false`, the `EntityUpdatedEventData` will not be published when a navigation property changes. - -> This option is only used for the EF Core. - -## See Also - -* [Distributed Event Bus](Distributed-Event-Bus.md) diff --git a/docs/en/Localization.md b/docs/en/Localization.md deleted file mode 100644 index 5b04940025..0000000000 --- a/docs/en/Localization.md +++ /dev/null @@ -1,258 +0,0 @@ -# Localization - -ABP's localization system is seamlessly integrated to the `Microsoft.Extensions.Localization` package and compatible with the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization). It adds some useful features and enhancements to make it easier to use in real life application scenarios. - -## Installation - -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. - -You can use the [ABP CLI](CLI.md) to install the Volo.Abp.Localization package to your project. Execute the following command in the folder of the .csproj file that you want to install the package on: - -```bash -abp add-package Volo.Abp.Localization -``` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Localization). - -Then you can add **AbpLocalizationModule** dependency to your module: - -```c# -using Volo.Abp.Modularity; -using Volo.Abp.Localization; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpLocalizationModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -## Creating A Localization Resource - -A localization resource is used to group related localization strings together and separate them from other localization strings of the application. A [module](Module-Development-Basics.md) generally defines its own localization resource. Localization resource is just a plain class. Example: - -````C# -public class TestResource -{ -} -```` - -Then it should be added using `AbpLocalizationOptions` as shown below: - -````C# -[DependsOn(typeof(AbpLocalizationModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - // "YourRootNameSpace" is the root namespace of your project. It can be empty if your root namespace is empty. - options.FileSets.AddEmbedded("YourRootNameSpace"); - }); - - Configure(options => - { - //Define a new localization resource (TestResource) - options.Resources - .Add("en") - .AddVirtualJson("/Localization/Resources/Test"); - }); - } -} -```` - -In this example; - -* Added a new localization resource with "en" (English) as the default culture. -* Used JSON files to store the localization strings. -* JSON files are embedded into the assembly using `AbpVirtualFileSystemOptions` (see [virtual file system](Virtual-File-System.md)). - -JSON files are located under "/Localization/Resources/Test" project folder as shown below: - -![localization-resource-json-files](images/localization-resource-json-files.png) - -A JSON localization file content is shown below: - -````json -{ - "culture": "en", - "texts": { - "HelloWorld": "Hello World!" - } -} -```` - -* Every localization file should define the `culture` code for the file (like "en" or "en-US"). -* `texts` section just contains key-value collection of the localization strings (keys may have spaces too). - -> ABP will ignore (skip) the JSON file if the `culture` section is missing. - -### Default Resource - -`AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified: - -````csharp -Configure(options => -{ - options.DefaultResourceType = typeof(TestResource); -}); -```` - -> The [application startup template](Startup-Templates/Application.md) sets `DefaultResourceType` to the localization resource of the application. - -### Short Localization Resource Name - -Localization resources are also available in the client (JavaScript) side. So, setting a short name for the localization resource makes it easy to use localization texts. Example: - -````C# -[LocalizationResourceName("Test")] -public class TestResource -{ -} -```` - -See the Getting Localized Test / Client Side section below. - -### Inherit From Other Resources - -A resource can inherit from other resources which makes possible to re-use existing localization strings without referring the existing resource. Example: - -````C# -[InheritResource(typeof(AbpValidationResource))] -public class TestResource -{ -} -```` - -Alternative inheritance by configuring the `AbpLocalizationOptions`: - -````C# -services.Configure(options => -{ - options.Resources - .Add("en") //Define the resource by "en" default culture - .AddVirtualJson("/Localization/Resources/Test") //Add strings from virtual json files - .AddBaseTypes(typeof(AbpValidationResource)); //Inherit from an existing resource -}); -```` - -* A resource may inherit from multiple resources. -* If the new resource defines the same localized string, it overrides the string. - -### Extending Existing Resource - -Inheriting from a resource creates a new resource without modifying the existing one. In some cases, you may want to not create a new resource but directly extend an existing resource. Example: - -````C# -services.Configure(options => -{ - options.Resources - .Get() - .AddVirtualJson("/Localization/Resources/Test/Extensions"); -}); -```` - -* If an extension file defines the same localized string, it overrides the string. - -## Getting the Localized Texts - -Getting the localized text is pretty standard. - -### Simplest Usage In A Class - -Just inject the `IStringLocalizer` service and use it like shown below: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IStringLocalizer _localizer; - - public MyService(IStringLocalizer localizer) - { - _localizer = localizer; - } - - public void Foo() - { - var str = _localizer["HelloWorld"]; - } -} -```` - -##### Format Arguments - -Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`. - -> Refer to the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) for details about using the localization. - -### Using In A Razor View/Page - -Use `IHtmlLocalizer` in razor views/pages; - -````c# -@inject IHtmlLocalizer Localizer - -

@Localizer["HelloWorld"]

-```` - -### Special Base Classes - -Some ABP Framework base classes provide a `L` property to use the localizer even easier. - -**Example: Localize a text in an application service method** - -```csharp -using System.Threading.Tasks; -using MyProject.Localization; -using Volo.Abp.Application.Services; - -namespace MyProject -{ - public class TestAppService : ApplicationService - { - public TestAppService() - { - LocalizationResource = typeof(MyProjectResource); - } - - public async Task DoIt() - { - var str = L["HelloWorld"]; - } - } -} -``` - -When you set the `LocalizationResource` in the constructor, the `ApplicationService` class uses that resource type when you use the `L` property, just like in the `DoIt()` method. - -Setting `LocalizationResource` in every application service can be tedious. You can create an abstract base application service class, set it there and derive your application services from that base class. This is already implemented when you create a new project with the [startup templates](Startup-Templates/Application.md). So, you can simply inherit from the base class directly use the `L` property: - -```csharp -using System.Threading.Tasks; - -namespace MyProject -{ - public class TestAppService : MyProjectAppService - { - public async Task DoIt() - { - var str = L["HelloWorld"]; - } - } -} -``` - -The `L` property is also available for some other base classes like `AbpController` and `AbpPageModel`. - -## The Client Side - -See the following documents to learn how to reuse the same localization texts in the JavaScript side; - -* [Localization for the MVC / Razor Pages UI](UI/AspNetCore/JavaScript-API/Localization.md) -* [Localization for the Blazor UI](UI/Blazor/Localization.md) -* [Localization for the Angular UI](UI/Angular/Localization.md) -* [Video tutorial](https://abp.io/video-courses/essentials/localization) \ No newline at end of file diff --git a/docs/en/Logging.md b/docs/en/Logging.md deleted file mode 100644 index 7fa45c03b9..0000000000 --- a/docs/en/Logging.md +++ /dev/null @@ -1,6 +0,0 @@ -# Logging - -ABP Framework doesn't implement any logging infrastructure. It uses the [ASP.NET Core's logging system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging). - -> .NET Core's logging system is actually independent from the ASP.NET Core. It is usable in any type of application. - diff --git a/docs/en/MailKit.md b/docs/en/MailKit.md deleted file mode 100644 index b05a28f9ba..0000000000 --- a/docs/en/MailKit.md +++ /dev/null @@ -1,48 +0,0 @@ -# MailKit Integration - -[MailKit](http://www.mimekit.net/) is a cross-platform, popular open source mail client library for .net. ABP Framework provides an integration package to use the MailKit as the [email sender](Emailing.md). - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.MailKit -```` - -If you haven't done it yet, you first need to install the ABP CLI. For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.MailKit). - -## Sending Emails - -### IEmailSender - -[Inject](Dependency-Injection.md) the standard `IEmailSender` into any service and use the `SendAsync` method to send emails. See the [email sending document](Emailing.md) for details. - -> `IEmailSender` is the suggested way to send emails even if you use MailKit, since it makes your code provider independent. - -### IMailKitSmtpEmailSender - -MailKit package also exposes the `IMailKitSmtpEmailSender` service that extends the `IEmailSender` by adding the `BuildClientAsync()` method. This method can be used to obtain a `MailKit.Net.Smtp.SmtpClient` object that can be used to perform MailKit specific operations. - -## Configuration - -MailKit integration package uses the same settings defined by the email sending system. So, refer to the [email sending document](Emailing.md) for the settings. - -In addition to the standard settings, this package defines `AbpMailKitOptions` as a simple [options](Options.md) class. This class defines only one options: - -* **SecureSocketOption**: Used to set one of the `SecureSocketOptions`. Default: `null` (uses the defaults). - -**Example: Use *SecureSocketOptions.SslOnConnect*** - -````csharp -Configure(options => -{ - options.SecureSocketOption = SecureSocketOptions.SslOnConnect; -}); -```` - -Refer to the [MailKit documentation](http://www.mimekit.net/) to learn more about this option. - -## See Also - -* [Email sending](Emailing.md) \ No newline at end of file diff --git a/docs/en/Microservice-Architecture.md b/docs/en/Microservice-Architecture.md deleted file mode 100644 index b98323908d..0000000000 --- a/docs/en/Microservice-Architecture.md +++ /dev/null @@ -1,30 +0,0 @@ -# Microservice Architecture - -*"Microservices are a software development technique—a variant of the **service-oriented architecture** (SOA) architectural style that structures an application as a collection of **loosely coupled services**. In a microservices architecture, services are **fine-grained** and the protocols are **lightweight**. The benefit of decomposing an application into different smaller services is that it improves **modularity**. This makes the application easier to understand, develop, test, and become more resilient to architecture erosion. It **parallelizes development** by enabling small autonomous teams to **develop, deploy and scale** their respective services independently. It also allows the architecture of an individual service to emerge through **continuous refactoring**. Microservices-based architectures enable **continuous delivery and deployment**."* - -— [Wikipedia](https://en.wikipedia.org/wiki/Microservices) - -## Introduction - -One of the major goals of the ABP framework is to provide a convenient infrastructure to create microservice solutions. To make this possible, - -* Provides a [module system](Module-Development-Basics.md) that allows you to split your application into modules where each module may have its own database, entities, services, APIs, UI components/pages... etc. -* Offers an [architectural model](Best-Practices/Module-Architecture.md) to develop your modules to be compatible to microservice development and deployment. -* Provides [best practices guide](Best-Practices/Index.md) to develop your module standards-compliance. -* Provides base infrastructure to implement [Domain Driven Design](Domain-Driven-Design.md) in your microservices. -* Provide services to [automatically create REST-style APIs](API/Auto-API-Controllers.md) from your application services. -* Provide services to [automatically create C# API clients](API/Dynamic-CSharp-API-Clients.md) that makes easy to consume your services from another service/application. -* Provides a [distributed event bus](Event-Bus.md) to communicate your services. -* Provides many other services to make your daily development easier. - -## Microservice for New Applications - -One common advise to start a new solution is **always to start with a monolith**, keep it modular and split into microservices once the monolith becomes a problem. This makes your progress fast in the beginning especially if your team is small and you don't want to deal with challenges of the microservice architecture. - -However, developing such a well-modular application can be a problem since it is **hard to keep modules isolated** from each other as you would do it for microservices (see [Stefan Tilkov's article](https://martinfowler.com/articles/dont-start-monolith.html) about that). Microservice architecture naturally forces you to develop well isolated services, but in a modular monolithic application it's easy to tight couple modules to each other and design **weak module boundaries** and API contracts. - -ABP can help you in that point by offering a **microservice-compatible, strict module architecture** where your module is split into multiple layers/projects and developed in its own VS solution completely isolated and independent from other modules. Such a developed module is a natural microservice yet it can be easily plugged-in a monolithic application. See the [module development best practice guide](Best-Practices/Index.md) that offers a **microservice-first module design**. All [standard ABP modules](https://github.com/abpframework/abp/tree/master/modules) are developed based on this guide. So, you can use these modules by embedding into your monolithic solution or deploy them separately and use via remote APIs. They can share a single database or can have their own database based on your simple configuration. - -## Microservice Demo Solution: eShopOnAbp - -The [eShopOnAbp project](https://github.com/abpframework/eShopOnAbp) demonstrates a complete microservice solution based on the ABP framework. diff --git a/docs/en/Migration-Guides/Abp-4_0-Angular.md b/docs/en/Migration-Guides/Abp-4_0-Angular.md deleted file mode 100644 index 6095a0f447..0000000000 --- a/docs/en/Migration-Guides/Abp-4_0-Angular.md +++ /dev/null @@ -1,121 +0,0 @@ -# Angular UI 3.3 to 4.0 Migration Guide - -## Angular v11 - -The new ABP Angular UI is based on Angular v11 and TypeScript v4. The difference between v10 and v11 is non-breaking so you do not have to update right away but it is recommended. Nevertheless, ABP modules will keep working with Angular v10. Therefore, if your project is Angular v10, you do not need to update to Angular 11. The update is usually very easy though. - -You can read more about Angular v11 [here](https://blog.angular.io/version-11-of-angular-now-available-74721b7952f7) - -## **Breaking Changes** - -### **Localization** - -Prior to ABP 4.x, we'd handled what locale files of Angular should be created to load them lazily. However, this made it impossible to add new locale files (to be lazily loaded) for our users. With ABP 4.x, we enabled an option to pass a function to `CoreModule`. - -The quickest solution is as follows: - -```typescript -// app.module.ts - -import { registerLocale } from '@abp/ng.core/locale'; -// or -// import { registerLocale } from '@volo/abp.ng.language-management/locale'; -// if you have commercial license - - -@NgModule({ -imports: [ - // ... - CoreModule.forRoot({ - // ...other options, - registerLocaleFn: registerLocale() - }), - //... -] -export class AppModule {} -``` - -You can find the related issue [here](https://github.com/abpframework/abp/issues/6066) -Also, please refer to [the docs](https://docs.abp.io/en/abp/latest/UI/Angular/Localization#registering-a-new-locale) for more information. - -### **Removed the Angular Account Module Public UI** - -With ABP 4.x, we have retired `@abp/ng.account`, it is no longer a part of our framework. There won't be any newer versions of this package as well. Therefore, you can delete anything related to this package. -There should be a config in `app-routing.module` for path `account` and `AccountConfigModule` import in `app.module` - -However, if you are using the commercial version of this package, a.k.a `@volo/abp.ng.account`, this package will continue to exist because it contains `AccountAdminModule` which is still being maintained and developed. You only need to delete the route config from `app-routing.module` - -You can find the related issue [here](https://github.com/abpframework/abp/issues/5652) - -Angular UI is using the Authorization Code Flow to authenticate since version 3.1.0 by default. Starting from version 4.0, this is becoming the only option, because it is the recommended way of authenticating SPAs. - -If you haven't done it yet, see [this post](https://blog.abp.io/abp/ABP-Framework-v3.1-RC-Has-Been-Released) to change the authentication of your application. - -### State Management - -In the ABP Angular UI, we've been using `NGXS` for state management. However, we've decided that the Angular UI should be agnostic with regard to state management. Our users should be able to handle the state in any way they prefer. They should be able to use any library other than `NGXS` or no library at all. That's why we have created our internal store in version 3.2. It is a simple utility class that employs `BehaviorSubject` internally. - -You can examine it [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts) - -With version 4.0, we will keep utilizing our `InternalStore` instead of `@ngxs/store` in our services and move away from `@ngxs/store`. We plan to remove any dependency of `NGXS` by version 5.0. - -With this in mind, we've already deprecated some services and implemented some breaking changes. - -#### Removed the `SessionState` - -Use `SessionStateService` instead of the `SessionState`. See [this issue](https://github.com/abpframework/abp/issues/5606) for details. - -#### Deprecated the `ConfigState` - -`ConfigState` is now deprecated and should not be used. - -`ConfigState` reference removed from `CoreModule`. If you want to use the `ConfigState` (not recommended), you should pass the state to `NgxsModule` as shown below: - -```typescript -//app.module.ts - -import { ConfigState } from '@abp/ng.core'; - -// ... - -imports: [ - NgxsModule.forRoot([ConfigState]), -// ... -``` - -Moving away from the global store, we create small services with a single responsibility. There are two new services available in version 4.0 which are `EnvironmentService` and `PermissionService`. - -See [the related issue](https://github.com/abpframework/abp/issues/6154) - -Please refer to the following docs for detail information and examples -- [`ConfigStateService`](../UI/Angular/Config-State-Service) -- [`EnvironmentService`](../UI/Angular/Environment#EnvironmentService) -- [`PermissionService`](../UI/Angular/Permission-Management#) - -### Deprecated Interfaces - - Some interfaces have long been marked as deprecated and now they are removed. - -- Removed replaceable components state. -- Removed legacy identity types and service. -- Removed legacy tenant management types and service. -- Removed legacy feature management types and services. -- Removed legacy permission management types and service. - -### Deprecated commercial interfaces -- Removed legacy audit logging types and services. -- Removed legacy identity types and services. -- Removed legacy language management types and services. -- Removed legacy saas types and services. - -### Identity Server [COMMERCIAL] - -With the new version of Identity Server, there happened some breaking changes in the backend (also in the database). We've implemented those in the Angular UI. -If you are just using the package `@volo/abp.ng.identity-server` as is, you will not need to do anything. -However, there are a couple of breaking changes we need to mention. - -- As we have stated above, we want to remove the dependency of `NGXS`. Thus, we have deleted all of the actions defined in `identity-server.actions`. Those actions are not needed anymore and the state is managed locally. With the actions gone, `IdentityServerStateService` became unused and got deleted as well. - -- `ApiScope` is also available as a new entity (It was part of `ApiResource` before). It provides tokens for entity prop, entity actions, toolbar, edit and create form contributors like the existing ones which are `Client`, `IdentityResource` and `ApiResource` - -- There were some deprecated interfaces within the `IdentityServer` namespace. Those are no longer being used, instead, their replacements were generated by `ABP Cli` using the `generate-proxy` command. diff --git a/docs/en/Migration-Guides/Abp-4_0.md b/docs/en/Migration-Guides/Abp-4_0.md deleted file mode 100644 index 9e1bd4e816..0000000000 --- a/docs/en/Migration-Guides/Abp-4_0.md +++ /dev/null @@ -1,280 +0,0 @@ -# ABP Framework 3.3 to 4.0 Migration Guide - -This document introduces the breaking changes done in the ABP Framework 4.0 and explains how to fix your 3.x based solutions while upgrading to the ABP Framework 4.0. - -> See [the blog post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0) to learn what's new with the ABP Framework 4.0. This document only focuses on the breaking changes. - -## Overall - -Here, the overall list of the changes; - -* Upgraded to the .NET 5.0 [(#6118](https://github.com/abpframework/abp/issues/6118)). -* Moved from Newtonsoft.Json to System.Text.Json [(#1198](https://github.com/abpframework/abp/issues/1198)). -* Upgraded to the Identity Server 4.1.1 ([#4461](https://github.com/abpframework/abp/issues/4461)). -* Switched to `kebab-case` for conventional URLs for the auto API controller routes ([#5325](https://github.com/abpframework/abp/issues/5325)). -* Removed Retry for the Dynamic HTTP Client Proxies ([#6090](https://github.com/abpframework/abp/issues/6090)). -* Creation audit properties of the entities made read-only ([#6020](https://github.com/abpframework/abp/issues/6020)). -* Changed type of the IHasExtraProperties.ExtraProperties ([#3751](https://github.com/abpframework/abp/issues/3751)). -* Use IBrandingProvider in the Volo.Abp.UI package and remove the one in the Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared ([#5375](https://github.com/abpframework/abp/issues/5375)). -* Removed the Angular Account Module Public UI (login, register... pages) since they are not being used in the default (authorization code) flow ([#5652](https://github.com/abpframework/abp/issues/5652)). -* Removed the SessionState in the @abp/ng.core package ([#5606](https://github.com/abpframework/abp/issues/5606)). -* Made some API revisions & startup template changes for the Blazor UI. - -## Upgraded to .NET 5.0 - -ABP Framework has been moved to .NET 5.0. So, if you want to upgrade to the ABP Framework 4.0, you also need to upgrade to .NET 5.0. - -See the [Migrate from ASP.NET Core 3.1 to 5.0](https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50) document to learn how to upgrade your solution to .NET 5.0. - -## Moved to System.Text.Json - -ABP Framework 4.0 uses the System.Text.Json by default as the JSON serialization library. It, actually, using a hybrid approach: Continues to use the Newtonsoft.Json when it needs to use features not supported by the System.Text.Json. - -### Unsupported Types - -If you want to use the Newtonsoft.Json to serialize/deserialize for some specific types, you can configure the `AbpSystemTextJsonSerializerOptions` in your module's `ConfigureServices` method. - -**Example: Use Newtonsoft.Json for `MySpecialClass`** - -````csharp -Configure(options => -{ - options.UnsupportedTypes.AddIfNotContains(typeof(MySpecialClass)); -}); -```` - -### Always Use the Newtonsoft.Json - -If you want to continue to use the Newtonsoft.Json library for all the types, you can set `UseHybridSerializer` to false in the `PreConfigureServices` method of your module class: - -````csharp -PreConfigure(options => -{ - options.UseHybridSerializer = false; -}); -```` - -## Upgraded to Identity Server 4.1.1 - -ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.1.1 with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes. Some of them are **breaking changes in the data structure**. - -### Entity Changes - -Entity changes don't directly affect your application; however, it is good to know. - -#### ApiScope - -As the **most critical breaking change**; Identity Server 4.x defines the `ApiScope` as an independent aggregate root. Previously, it was the child entity of the `ApiResource`. This change requires manual operation. See the _Database Changes_ section. - -Also, added `Enabled(string)` and `Description(bool,true)` properties. - -#### ApiResource - -- Added `AllowedAccessTokenSigningAlgorithms (string)` and `ShowInDiscoveryDocument(bool, default: true)` properties - -#### Client - -- Added `RequireRequestObject ` and `AllowedIdentityTokenSigningAlgorithms ` properties. -- Changed the default value of `RequireConsent` from `true` to `false`. -- Changed the default value of `RequirePkce` from `false` to `true`. - -#### DeviceFlowCodes - -- Added `SessionId ` and `Description ` properties. - -#### PersistedGrant - -- Added `SessionId `, `Description ` and `ConsumedTime ` properties - -### Database Changes - -> Attention: **Please backup your database** before the migration! - -**If you are upgrading from 3.x, then there are some steps should be done in your database.** - -#### Database Schema Migration - -If you are using **Entity Framework Core**, you need to add a new database migration, using the `Add-Migration` command, and apply changes to the database. Please **review the migration** script and read the sections below to understand if it affects your existing data. Otherwise, you may **lose some of your configuration**, which may not be easy to remember and re-configure. - -#### Seed Code - -If you haven't customized the `IdentityServerDataSeedContributor` and haven't customized the initial data inside the `IdentityServer*` tables; - -1. Update `IdentityServerDataSeedContributor` class by comparing to [the latest code](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/IdentityServer/IdentityServerDataSeedContributor.cs). You probably only need to add the `CreateApiScopesAsync` method and the code related to it. -2. Then you can simply clear all the **data** in these tables then execute the `DbMigrator` application to fill it with the new configuration. - -#### Migrating the Configuration Data - -If you've customized your IdentityServer configuration in the database or in the seed data, you should understand the changes and upgrade your code/data accordingly. Especially, the following changes will affect your application: - -- `IdentityServerApiScopes` table's `Enabled` field is dropped and re-created. So, you need to enable the API scopes again manually. -- `IdentityServerApiResourceScopes` table is dropped and recreated. So, you need to backup and move your current data to the new table. -- `IdentityServerIdentityResourceClaims` table is dropped and recreated. So, you need to backup and move your current data to the new table. - -You may need to perform additional steps based on how much you made custom configurations. - -### Other IdentityServer Changes - -IdentityServer has removed the [public origin option](https://github.com/IdentityServer/IdentityServer4/pull/4335). It was resolving HTTP/HTTPS conversion issues, but they decided to leave this to the developer. This is especially needed if you use a reverse proxy where your external protocol is HTTPS but internal protocol is HTTP. - -One simple solution is to add such a middleware at the begingning of your ASP.NET Core pipeline. - -```csharp -app.Use((httpContext, next) => -{ - httpContext.Request.Scheme = "https"; - return next(); -}); -``` - -> This sample is obtained from the [ASP.NET Core documentation](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer#scenarios-and-use-cases). You can use it if you always use HTTPS in all environments. - -### Related Resources - -- https://leastprivilege.com/2020/06/19/announcing-identityserver4-v4-0/ -- https://github.com/IdentityServer/IdentityServer4/issues/4592 - -## Auto API Controller Route Changes - -The route calculation for the [Auto API Controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) is changing with the ABP Framework version 4.0 ([#5325](https://github.com/abpframework/abp/issues/5325)). Before v4.0 the route paths were **camelCase**. After version 4.0, it's changed to **kebab-case** route paths where it is possible. - -**A typical auto API before v4.0** - -![route-before-4](images/route-before-4.png) - -**camelCase route parts become kebab-case with 4.0** - -![route-4](images/route-4.png) - -### How to Fix? - -You may not take any action for the MVC & Blazor UI projects. - -For the Angular UI, this change may effect your client UI. If you have used the [ABP CLI Service Proxy Generation](../UI/Angular/Service-Proxies.md), you can run the server side and re-generate the service proxies. If you haven't used this tool, you should manually update the related URLs in your application. - -If there are other type of clients (e.g. 3rd-party companies) using your APIs, they also need to update the URLs. - -### Use the v3.x style URLs - -If it is hard to change it in your application, you can still to use the version 3.x route strategy, by following one of the approaches; - -- Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: - -```csharp -options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.UseV3UrlStyle = true; - }); -``` - -This approach affects only the controllers for the `BookStoreApplicationModule`. - -- Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: - -```csharp -Configure(options => -{ - options.UseV3UrlStyle = true; -}); -``` - -Setting it globally affects all the modules in a modular application. - -## Removed Retry for the Dynamic HTTP Client Proxies - -[Dynamic C# HTTP Client Proxies](../API/Dynamic-CSharp-API-Clients.md) were trying up to 3 times if a request fails using the [Polly](https://github.com/App-vNext/Polly) library. Starting from the version 4.0, this logic has been removed. If you need it, you should configure it in your own application, by configuring the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module. - -**Example: Retry 3 times on failure by incremental waiting between tries** - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => - { - clientBuilder.AddTransientHttpErrorPolicy( - policyBuilder => policyBuilder - .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i))) - ); - }); - }); -} -```` - -This example uses the Microsoft.Extensions.Http.Polly NuGet package. - -If you create a new solution, you can find the same configuration in the `.HttpApi.Client.ConsoleTestApp` project's module class, as an example. - -## Creation Audit Properties Made Read-Only - -Removed setters from the `IHasCreationTime.CreationTime`, ` IMustHaveCreator.CreatorId` and `IMayHaveCreator.CreatorId` properties to accidently set the creation properties while updating an existing entity. - -Since the ABP Framework automatically sets these properties, you normally don't need to directly set them. If you want to set them, as a best practice, it is suggested to make it in the constructor to not provide a way to change it later. - -These properties implemented with `protected set` in the `Entity` and `AggregateRoot` base classes. That means you can still set in a derived class, if you need it. Alternatively, you can use reflection to set them (Or use `ObjectHelper.TrySetProperty` which internally uses reflection) out of the class if you have to do. - -## Changed type of the IHasExtraProperties.ExtraProperties - -`IHasExtraProperties.ExtraProperties` was a regular `Dictionary`. With the version 4.0, it is replaced with `ExtraPropertyDictionary` class which inherits the `Dictionary`. - -Most of the applications don't be affected by this change. If you've directly implemented this interface, replace the standard dictionary to the `ExtraPropertyDictionary`. - -## Other Changes - -### IdentityOptions Usage - -Previously, when you inject `IOptions`, you get a dynamically overridden options value. For example, when you get `IdentityOptions.Password.RequiredLength`, the value is being changed based on the setting (`IdentitySettingNames.Password.RequiredLength`) of the current tenant. That means `IdentityOptions` changes per tenant. However, this caused an [issue](https://github.com/abpframework/abp/issues/6318) and we [had to change](https://github.com/abpframework/abp/pull/6333) the usage. - -With the version 4.0, you need to inject `IOptions` and call the new `SetAsync` method before using it, to be able to override the options by the settings. Otherwise, you get the default (statically configured) values of the options. - -Example usage: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IOptions _options; - - public MyService(IOptions options) - { - _options = options; - } - - public async Task DoItAsync() - { - await _options.SetAsync(); - - var requiredLength = _options.Value.Password.RequiredLength; - } -} -```` - -Pre-built modules already handles this. However, if you have used `IdentityOptions` directly in your code, you also need to follow this new pattern. -Please make sure that the injected `IOptions` service and the service consuming it are in the same scope of dependency injection container. - -### LDAP module full async - -In order to solve the problem of async over sync, `ILdapManager` uses async method instead of sync. And use [`ldap4net`](https://github.com/flamencist/ldap4net) to replace [`Novell.Directory.Ldap.NETStandard`](https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard) package. - -### Dynamic external login provider system - -You need to change the `WithDynamicOptions` method and pass the `Handler` class of the external login provider. -Use the `goto definition` function in Visual Studio or Rider to check `Handler` in the extension method like `AddGoogle`. - -```csharp -- WithDynamicOptions() -+ WithDynamicOptions() -```` - -## ASP.NET Core MVC / Razor Pages UI - -See the [ASP.NET Core MVC / Razor Pages UI Migration Guide](Abp-4_0-MVC-Razor-Pages.md). - -## Angular UI - -See the [Angular UI Migration Guide](Abp-4_0-Angular.md). - -## Blazor UI - -See the [Blazor UI Migration Guide](Abp-4_0-Blazor.md). diff --git a/docs/en/Migration-Guides/Abp-4_2.md b/docs/en/Migration-Guides/Abp-4_2.md deleted file mode 100644 index 084ae24caa..0000000000 --- a/docs/en/Migration-Guides/Abp-4_2.md +++ /dev/null @@ -1,101 +0,0 @@ -# ABP version 4.2 Migration Guide - -This version has no breaking changes but there is an important change on the repositories that should be applied for your application for an important performance and scalability gain. - -## IRepository.GetQueryableAsync - -`IRepository` interface inherits `IQueryable`, so you can directly use the standard LINQ extension methods, like `Where`, `OrderBy`, `First`, `Sum`... etc. - -**Example: Using LINQ directly over the repository object** - -````csharp -public class BookAppService : ApplicationService, IBookAppService -{ - private readonly IRepository _bookRepository; - - public BookAppService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task DoItInOldWayAsync() - { - //Apply any standard LINQ extension method - var query = _bookRepository - .Where(x => x.Price > 10) - .OrderBy(x => x.Name); - - //Execute the query asynchronously - var books = await AsyncExecuter.ToListAsync(query); - } -} -```` - -*See [the documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) if you wonder what is the `AsyncExecuter`.* - -**Beginning from the version 4.2, the recommended way is using `IRepository.GetQueryableAsync()` to obtain an `IQueryable`, then use the LINQ extension methods over it.** - -**Example: Using the new GetQueryableAsync method** - -````csharp -public async Task DoItInNewWayAsync() -{ - //Use GetQueryableAsync to obtain the IQueryable first - var queryable = await _bookRepository.GetQueryableAsync(); - - //Then apply any standard LINQ extension method - var query = queryable - .Where(x => x.Price > 10) - .OrderBy(x => x.Name); - - //Finally, execute the query asynchronously - var books = await AsyncExecuter.ToListAsync(query); -} -```` - -ABP may start a database transaction when you get an `IQueryable` (If current [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) is transactional). In this new way, it is possible to **start the database transaction in an asynchronous way**. Previously, we could not get the advantage of asynchronous while starting the transactions. - -> **The new way has a significant performance and scalability gain. The old usage (directly using LINQ over the repositories) will be removed in the next major version (5.0).** You have a lot of time for the change, but we recommend to immediately take the action since the old usage has a big **scalability problem**. - -### Actions to Take - -* Use the repository's queryable feature as explained before. -* If you've overridden `CreateFilteredQuery` in a class derived from `CrudAppService`, you should override the `CreateFilteredQueryAsync` instead and remove the `CreateFilteredQuery` in your class. -* If you've overridden `WithDetails` in your custom repositories, remove it and override `WithDetailsAsync` instead. -* If you've used `DbContext` or `DbSet` properties in your custom repositories, use `GetDbContextAsync()` and `GetDbSetAsync()` methods instead of them. - -You can re-build your solution and check the `Obsolete` warnings to find some of the usages need to change. - -#### About IRepository Async Extension Methods - -Using IRepository Async Extension Methods has no such a problem. The examples below are pretty fine: - -````csharp -var countAll = await _personRepository - .CountAsync(); - -var count = await _personRepository - .CountAsync(x => x.Name.StartsWith("A")); - -var book1984 = await _bookRepository - .FirstOrDefaultAsync(x => x.Name == "John"); -```` - -See the [repository documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) to understand the relation between `IQueryable` and asynchronous operations. - -## .NET Package Upgrades - -ABP uses the latest 5.0.* .NET packages. If your application is using 5.0.0 packages, you may get an error on build. We recommend to depend on the .NET packages like `5.0.*` in the `.csproj` files to use the latest patch versions. - -Example: - -````xml - -```` - -## Blazorise Library Upgrade - -If you are upgrading to 4.2, you also need also upgrade the following packages in your Blazor application; - -* `Blazorise.Bootstrap` to `0.9.3-preview6` -* `Blazorise.Icons.FontAwesome` to `0.9.3-preview6` \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-4_3.md b/docs/en/Migration-Guides/Abp-4_3.md deleted file mode 100644 index 22d1282715..0000000000 --- a/docs/en/Migration-Guides/Abp-4_3.md +++ /dev/null @@ -1,43 +0,0 @@ -# ABP Framework 4.x to 4.3 Migration Guide - -This version comes with some changes in the startup template, mostly related to Blazor UI. This document explains the breaking changes. However, **it is suggested to [compare the startup templates manually](Upgrading-Startup-Template.md) to see all the changes** and apply to your solution. - -## Common - -* `app.UseVirtualFiles()` has been marked as **obsolete**. Use `app.UseStaticFiles()` instead. ABP will handle the virtual file system integrated to the static files middleware. - -## Blazor UI - -Implemented the Blazor Server Side support with this release. It required some packages and namespaces arrangements. **Existing Blazor (WebAssembly) applications should done the changes explained in this section**. - -### Namespace Changes - -- `AbpBlazorMessageLocalizerHelper` -> moved to Volo.Abp.AspNetCore.Components.Web -- `AbpRouterOptions` -> moved to Volo.Abp.AspNetCore.Components.Web.Theming.Routing -- `AbpToolbarOptions` and `IToolbarContributor` -> moved to Volo.Abp.AspNetCore.Components.Web.Theming.Toolbars -- `IAbpUtilsService` -> moved to Volo.Abp.AspNetCore.Components.Web -- `PageHeader` -> moved to `Volo.Abp.AspNetCore.Components.Web.Theming.Layout`. - -In practice, if your application is broken because of the `Volo.Abp.AspNetCore.Components.WebAssembly.*` namespace, please try to switch to `Volo.Abp.AspNetCore.Components.Web.*` namespace. - -Remember to change namespaces in the `_Imports.razor` files. - -### Package Changes - -No change on the framework packages, but **module packages are separated as Web Assembly & Server**; - -* Use `Volo.Abp.Identity.Blazor.WebAssembly` NuGet package instead of `Volo.Abp.Identity.Blazor` package. Also, change `AbpIdentityBlazorModule` usage to `AbpIdentityBlazorWebAssemblyModule` in the `[DependsOn]` attribute on your module class. -* Use `Volo.Abp.TenantManagement.Blazor.WebAssembly` NuGet package instead of `Volo.Abp.TenantManagement.Blazor` package. Also, change `AbpTenantManagementBlazorModule` usage to `AbpTenantManagementBlazorWebAssemblyModule` in the `[DependsOn]` attribute on your module class. -* Use `Volo.Abp.PermissionManagement.Blazor.WebAssembly` NuGet package instead of `Volo.Abp.PermissionManagement.Blazor` package. Also, change `AbpPermissionManagementBlazorModule` usage to `AbpPermissionManagementBlazorWebAssemblyModule` in the `[DependsOn]` attribute on your module class. -* Use `Volo.Abp.SettingManagement.Blazor.WebAssembly` NuGet package instead of `Volo.Abp.SettingManagement.Blazor` package. Also, change `AbpSettingManagementBlazorModule` usage to `AbpSettingManagementBlazorWebAssemblyModule` in the `[DependsOn]` attribute on your module class. -* Use `Volo.Abp.FeatureManagement.Blazor.WebAssembly` NuGet package instead of `Volo.Abp.FeatureManagement.Blazor` package. Also, change `AbpFeatureManagementBlazorModule` usage to `AbpFeatureManagementBlazorWebAssemblyModule` in the `[DependsOn]` attribute on your module class. - -### Other Changes - -* `EntityAction.RequiredPermission` has been marked as **obsolete**, because of performance reasons. It is suggested to use the `Visible` property by checking the permission/policy yourself and assigning to a variable. - -### Resource Reference Changes - -Open `BundleContributor.cs` and replace `context.Add("main.css");` to `context.Add("main.css", true);` - -Then run `abp bundle` command in the `blazor` folder to update resource references. \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-5-0-Blazor.md b/docs/en/Migration-Guides/Abp-5-0-Blazor.md deleted file mode 100644 index 09d0f8e682..0000000000 --- a/docs/en/Migration-Guides/Abp-5-0-Blazor.md +++ /dev/null @@ -1,19 +0,0 @@ -# ABP Blazor UI v4.x to v5.0 Migration Guide - -> This document is for the Blazor UI. See also [the main migration guide](Abp-5_0.md). - -## Upgrading to the latest Blazorise - -ABP 5.0 uses the latest version of the [Blazorise](https://blazorise.com/) library. Please upgrade the Blazorise NuGet packages in your solution. - -## Bootstrap 5 - -ABP 5.0 now works with Bootstrap 5. For details, please refer to the official [migration guide](https://getbootstrap.com/docs/5.0/migration/) provided by Bootstrap. - -Replace `Blazorise.Bootstrap` with `Blazorise.Bootstrap5` package. - -Replace `AddBootstrapProviders()` with `AddBootstrap5Providers()`. - -## Update Bundle - -Use `abp bundle` command to update bundles if you are using Blazor WebAssembly. diff --git a/docs/en/Migration-Guides/Abp-5-0-MVC.md b/docs/en/Migration-Guides/Abp-5-0-MVC.md deleted file mode 100644 index f2c863c634..0000000000 --- a/docs/en/Migration-Guides/Abp-5-0-MVC.md +++ /dev/null @@ -1,12 +0,0 @@ -# ABP MVC / Razor Pages UI v4.x to v5.0 Migration Guide - -> This document is for the ABP MVC / Razor Pages UI. See also [the main migration guide](Abp-5_0.md). - -## Use install-libs by default - -Removed the Gulp dependency from the MVC / Razor Pages UI projects in favor of `abp install-libs` command ([see](https://docs.abp.io/en/abp/5.0/UI/AspNetCore/Client-Side-Package-Management#install-libs-command)) of the ABP CLI. You should run this command whenever you change/upgrade your client-side package dependencies via `package.json`. - -## Switched to SweetAlert2 - -Switched from SweetAlert to SweetAlert2. Run the `abp install-libs` command (in the root directory of the web project) after upgrading your solution. See [#9607](https://github.com/abpframework/abp/pull/9607). - diff --git a/docs/en/Migration-Guides/Abp-5_0-Angular.md b/docs/en/Migration-Guides/Abp-5_0-Angular.md deleted file mode 100644 index b7e55dad38..0000000000 --- a/docs/en/Migration-Guides/Abp-5_0-Angular.md +++ /dev/null @@ -1,154 +0,0 @@ -# Angular UI v4.x to v5.0 Migration Guide - -> This document is for the Angular UI. See also [the main migration guide](Abp-5_0.md). - -## Overall - -See the overall list of breaking changes: - -- Bootstrap 5 implementation [#10067](https://github.com/abpframework/abp/issues/10067) -- Remove NGXS dependency & states [#9952](https://github.com/abpframework/abp/issues/9952) -- Install @angular/localize package to startup templates [#10099](https://github.com/abpframework/abp/issues/10099) -- Create new secondary entrypoints and move the related proxies to there [#10060](https://github.com/abpframework/abp/issues/10060) -- Move SettingTabsService to @abp/ng.setting-management/config package from @abp/ng.core [#10061](https://github.com/abpframework/abp/issues/10061) -- Make the @abp/ng.account dependent on @abp/ng.identity [#10059](https://github.com/abpframework/abp/issues/10059) -- Set default abp-modal size medium [#10118](https://github.com/abpframework/abp/issues/10118) -- Update all dependency versions to the latest [#9806](https://github.com/abpframework/abp/issues/9806) -- Chart.js big include with CommonJS warning [#7472](https://github.com/abpframework/abp/issues/7472) - -## Angular v12 - -The new ABP Angular UI is based on Angular v12. We started to compile Angular UI packages with the Ivy compilation. Therefore, **new packages only work with Angular v12**. If you are still on the older version of Angular v12, you have to update to Angular v12. The update is usually very easy. See [Angular Update Guide](https://update.angular.io/?l=2&v=11.0-12.0) for further information. - -> **ABP Angular UI is not yet compatible with Angular v13 due to some issues.** - -## Bootstrap 5 - -ABP 5.0 now works with Bootstrap 5. For details, please refer to the official [migration guide](https://getbootstrap.com/docs/5.0/migration/) provided by Bootstrap. - -We have updated dependencies of the `ThemeShared` package, therefore when you update `@abp/ng.theme.shared`, it will install the necessary dependencies. - -### RTL - -Bootstrap 5 provides its own CSS file for RTL(right-to-left) languages. Therefore, we have removed `bootstrap-rtl.min.css` from `@abp/ng.theme.shared`. - -In angular.json, make the following change: - -Replace - -`"node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css"` with - -`"node_modules/bootstrap/dist/css/bootstrap.rtl.min.css"` - -```js -{ - // ... - "styles": [{ - "input": "node_modules/bootstrap/dist/css/bootstrap.rtl.min.css", - "inject": false, - "bundleName": "bootstrap-rtl.min" - }] -} -``` - -That's it for open source templates. - -### Commercial - -Starting from version 5.0, Lepton styles get bundled with Bootstrap. That's why you don't need to provide `bootstrap` styles in `angular.json` anymore. - -Remove the following two objects from the `styles` array in `angular.json` - -```js -{ - // ... - "styles": [{ - "input": "node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css", - "inject": false, - "bundleName": "bootstrap-rtl.min" - }, - { - "input": "node_modules/bootstrap/dist/css/bootstrap.min.css", - "inject": true, - "bundleName": "bootstrap-ltr.min" - }] -} -``` - -After you have implemented the necessary changes explained by Bootstrap, it should be good to go. - -## NGXS has been removed - -We aim to make the ABP Framework free of any state-management solutions. ABP developers should be able to use the ABP Framework with any library/framework of their choice. So, we decided to remove NGXS from ABP packages. - -If you'd like to use NGXS after upgrading to v5.0, you have to install the NGXS to your project. The package can be installed with the following command: - -```bash -npm install @ngxs/store - -# or - -yarn add @ngxs/store -``` - -NGXS states and actions, some namespaces have been removed. See [this issue](https://github.com/abpframework/abp/issues/9952) for the details. - -If you don't want to use the NGXS, you should remove all NGXS related imports, injections, etc., from your project. - -## @angular/localize package - -[`@angular/localize`](https://angular.io/api/localize) dependency has been removed from `@abp/ng.core` package. The package must be installed in your app. Run the following command to install: - -```bash -npm install @angular/localize@12 - -# or - -yarn add @angular/localize@12 -``` - -> ABP Angular UI packages are not dependent on the `@angular/localize` package. However, some packages (like `@ng-bootstrap/ng-bootstrap`) depend on the package. Thus, this package needs to be installed in your project. - -## Proxy endpoints - -New endpoints named proxy have been created, related proxies have moved. -For example; before v5.0, `IdentityUserService` could be imported from `@abp/ng.identity`. As of v5.0, the service can be imported from `@abp/ng.identity/proxy`. See an example: - -```ts -import { IdentityUserService } from '@abp/ng.identity/proxy'; - -@Component({}) -export class YourComponent { - constructor(private identityUserService: IdentityUserService) {} -} -``` - -Following proxies have been affected: - -- `@abp/ng.account` to `@abp/ng.account.core/proxy` -- `@abp/ng.feature-management` to `@abp/ng.feature-management/proxy` -- `@abp/ng.identity` to `@abp/ng.identity/proxy` -- `@abp/ng.permission-management` to `@abp/ng.permission-management/proxy` -- `@abp/ng.tenant-management` to `@abp/ng.tenant-management/proxy` -- **ProfileService** is deleted from `@abp/ng.core`. Instead, you can import it from `@abp/ng.identity/proxy` - -## SettingTabsService - -**SettingTabsService** has moved from `@abp/ng.core` to `@abp/ng.setting-management/config`. - -## ChartComponent - -[`ChartComponent`](../UI/Angular/Chart-Component.md) has moved from `@abp/ng.theme.shared` to `@abp/ng.components/chart.js`. To use the component, you need to import the `ChartModule` to your module as follows: - -```ts -import { ChartModule } from '@abp/ng.components/chart.js'; - -@NgModule({ - imports: [ - ChartModule, - // ... - ], - // ... -}) -export class YourFeatureModule {} -``` diff --git a/docs/en/Migration-Guides/Abp-5_0.md b/docs/en/Migration-Guides/Abp-5_0.md deleted file mode 100644 index 758f2b91ff..0000000000 --- a/docs/en/Migration-Guides/Abp-5_0.md +++ /dev/null @@ -1,121 +0,0 @@ -# ABP Framework v4.x to v5.0 Migration Guide - -This document is a guide for upgrading ABP 4.x solutions to ABP 5.0. Please read them all since 5.0 has some important breaking changes. - -## .NET 6.0 - -ABP 5.0 runs on .NET 6.0. So, please upgrade your solution to .NET 6.0 if you want to use ABP 5.0. You can see [Microsoft's migration guide](https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60). - -## Bootstrap 5 - -ABP 5.0 uses the Bootstrap 5 as the fundamental HTML/CSS framework. We've migrated all the UI themes, tag helpers, UI components and the pages of the pre-built application modules. You may need to update your own pages by following the [Bootstrap's migration guide](https://getbootstrap.com/docs/5.0/migration/). - -## Startup Template Changes - -The startup template has changed. You don't need to apply all the changes, but it is strongly suggested to follow [this guide](Upgrading-Startup-Template.md) and make the necessary changes for your solution. - -## ABP Framework - -This section contains breaking changes in the ABP Framework. - -### MongoDB - -ABP Framework will serialize the datetime based on [AbpClockOptions](https://docs.abp.io/en/abp/latest/Timing#clock-options) starting from ABP v5.0. It was saving `DateTime` values as UTC in MongoDB. Check out [MongoDB Datetime Serialization Options](https://mongodb.github.io/mongo-csharp-driver/2.13/reference/bson/mapping/#datetime-serialization-options). - -If you want to revert back this feature, set `UseAbpClockHandleDateTime` to `false` in `AbpMongoDbOptions`: - -```cs -Configure(x => x.UseAbpClockHandleDateTime = false); -``` - -### Publishing Auto-Events in the Same Unit of Work - -Local and distributed auto-events are handled in the same unit of work now. That means the event handles are executed in the same database transaction and they can rollback the transaction if they throw any exception. The new behavior may affect your previous assumptions. See [#9896](https://github.com/abpframework/abp/issues/9896) for more. - -#### Deprecated EntityCreatingEventData, EntityUpdatingEventData, EntityDeletingEventData and EntityChangingEventData - -As a side effect of the previous change, `EntityCreatingEventData`, `EntityUpdatingEventData`, `EntityDeletingEventData` and `EntityChangingEventData` is not necessary now, because `EntityCreatedEventData`, `EntityUpdatedEventData`, `EntityDeletedEventData` and `EntityChangedEventData` is already taken into the current unit of work. Please switch to `EntityCreatedEventData`, `EntityUpdatedEventData`, `EntityDeletedEventData` and `EntityChangedEventData` if you've used the deprecated events. See [#9897](https://github.com/abpframework/abp/issues/9897) to learn more. - -### Removed ModelBuilderConfigurationOptions classes - -If you've used these classes, please remove their usages and use the static properties to customize the module's database mappings. See [#8887](https://github.com/abpframework/abp/issues/8887) for more. - -### Removed Obsolete APIs - -* `IRepository` doesn't inherit from `IQueryable` anymore. It was [made obsolete in 4.2](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_2#irepository-getqueryableasync). - -### Automatically Setting the TenantId for New Entities - -Beginning from the version 5.0, ABP automatically sets the `TenantId` for you when you create a new entity object (that implements the `IMultiTenant` interface). It is done in the constructor of the base `Entity` class (all other base entity and aggregate root classes are derived from the `Entity` class). The `TenantId` is set from the current value of the `ICurrentTenant.Id` property. - -This can be a breaking change in rare cases (for example, if you create host side entities from a tenant context and do not explicitly set host entity's `TenantId` to `null`). - -### Other Breaking Changes - -* [#9549](https://github.com/abpframework/abp/pull/9549) `IObjectValidator` methods have been changed to asynchronous. -* [#9940](https://github.com/abpframework/abp/pull/9940) Use ASP NET Core's authentication scheme to handle `AbpAuthorizationException`. -* [#9180](https://github.com/abpframework/abp/pull/9180) Use `IRemoteContentStream` without form content headers. - -## UI Providers - -* [Angular UI 4.x to 5.0 Migration Guide](Abp-5_0-Angular.md) -* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide](Abp-5-0-MVC.md) -* [Blazor UI 4.x to 5.0 Migration Guide](Abp-5-0-Blazor.md) - -## Modules - -This section contains breaking and important changes in the application modules. - -### Identity - -#### User Active/Passive - -An `IsActive` (`bool`) property is added to the `IdentityUser` entity. This flag will be checked during the authentication of the users. EF Core developers need to add a new database migration and update their databases. - -**After the database migration, set this property to `true` for the existing users: `UPDATE AbpUsers SET IsActive=1`**. Otherwise, none of the users can login to the application. - -Alternatively, you can set `defaultValue` to `true` in the migration class (after adding the migration). -This will add the column with `true` value for the existing records. - -```cs -public partial class AddIsActiveToIdentityUser : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsActive", - table: "AbpUsers", - type: "bit", - nullable: false, - defaultValue: true); // Default is false, change it to true. - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsActive", - table: "AbpUsers"); - } -} -``` - -For MongoDB, you need to update the `IsActive` field for the existing users in the database. - -You can use following script in MongoShell: -```js -db.AbpUsers.updateMany({},{$set:{ IsActive : true }}) -``` - -#### Identity -> Account API Changes - -`IProfileAppService` (and the implementation and the related DTOs) are moved to the Account module from the Identity module (done with [this PR](https://github.com/abpframework/abp/pull/10370/files)). - -### IdentityServer - -`IApiScopeRepository.GetByNameAsync` method renamed as `FindByNameAsync`. - -## See Also - -* [Angular UI 4.x to 5.0 Migration Guide](Abp-5_0-Angular.md) -* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide](Abp-5-0-MVC.md) -* [Blazor UI 4.x to 5.0 Migration Guide](Abp-5-0-Blazor.md) diff --git a/docs/en/Migration-Guides/Abp-5_2.md b/docs/en/Migration-Guides/Abp-5_2.md deleted file mode 100644 index aeb79e22a6..0000000000 --- a/docs/en/Migration-Guides/Abp-5_2.md +++ /dev/null @@ -1,52 +0,0 @@ -# ABP Version 5.2 Migration Guide - -This document is a guide for upgrading ABP v5.x solutions to ABP v5.2. Please read them all since v5.2 has some changes you should take care. - - -## MongoDB - -- `IMongoDbRepositoryFilterer.AddGlobalFilters()` method is replaced with async one `IMongoDbRepositoryFilterer.AddGlobalFiltersAsync()` - -## Blazor UI -If you use Blazor WASM or Blazor Server UI, you should follow this section. - -### Blazorise 1.0 -We've upgraded to Blazorise 1.0 stable version. So there is some breaking changes that you have to apply in your project. - -Also You can review that pull request [#11649 - Blazorise 1.0 Migration](https://github.com/abpframework/abp/pull/11649) - -- `NumericEdit` is now made around the native `input type="number"` so a lot of its formatting features are moved to the new `NumericPicker` component. Replace NumericEdit with NumericPicker. -- Rename `DecimalsSeparator` to `DecimalSeparator` on the `DataGridColumn` and `NumericPicker`. -- Rename `MaxMessageSize` to `MaxChunkSize`. -- Remove `Fullscreen` parameter on `` and replace it with `Size="ModalSize.Fullscreen"` parameter. -- Remove `NotificationType`, `Message`, and `Title` parameter from `` component. -- Move `RightAligned` parameter from `` to `` component. -- Rename any usage of the `ChangeTextOnKeyPress` parameter into `Immediate`. -- Rename any usage of `DelayTextOnKeyPress` parameter into `Debounce` and `DelayTextOnKeyPressInterval` into DebounceInterval. -- Replace all `Left` and `Right` enums with `Start` and `End` for the following enum types: `Direction`, `Float`, `Placement`, `NotificationLocation`, `Side`, `SnackbarLocation`, `SnackbarStackLocation`, `TabPosition`, and `TextAlignment`. -- Replace all `FromLeft`, `FromRight`, `RoundedLeft`, and `RoundedRight` enums with `FromStart`, `FromEnd`, `RoundedStart`, and `RoundedEnd` for the `Border` utilities. -- Replace all `FromLeft` and `FromRight` with `FromStart`, `FromEnd` for the Margin and `Padding` utilities. -- Replace all `AddLabel` with `AddLabels` method on chart instance. -- Change enum value from `None` to `Default` for the following enum types: `Color`, `Background`, `TextColor`, `Alignment`, `BorderRadius`, `BorderSize`, `Direction`, `DisplayDirection`, `FigureSize`, `IconSize`, `JustifyContent`, `OverflowType`, `SnackbarColor`, `Target`, `TextAlignment`, `TextOverflow`, `TextTransform`, `TextWeight`, `VerticalAlignment`, `Visibility`, `Size`, and `SnackbarLocation`. -- Obsolete typography parameters `Alignment`, `Color`, `Transform`, and `Weight` are removed in favor of `TextAlignment`, `TextColor`, `TextTransform`, and `TextWeight`. -- Remove any use of an obsolete component ``. -- The Datagrid's obsolete `Direction` parameter has now been removed. Instead, please use the `SortDirection` parameter if you weren't already.. -- Rename `` `Mode` parameter into `RenderMode`. - -> _Check out [Blazorise Release Notes](https://preview.blazorise.com/news/release-notes/100) for more information._ - -## MVC - Razor Pages UI - -If you use MVC Razor Pages UI, you should follow this section. - -### Client libraries -The `libs` folder no longer exists in templates after v5.2. That change greatly reduced the size of templates and brought some other advantages. - -You can use `abp install-libs` command for installing or updating client libraries. You should run this command after updating v5.2. - -> If you're creating a new project, you don't have to be concerned about it, ABP CLI installs client libraries after automatically. - -## See Also - -* [Official blog post for the 5.2 release](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published) - diff --git a/docs/en/Migration-Guides/Abp-5_3.md b/docs/en/Migration-Guides/Abp-5_3.md deleted file mode 100644 index acc69504f0..0000000000 --- a/docs/en/Migration-Guides/Abp-5_3.md +++ /dev/null @@ -1,11 +0,0 @@ -# ABP Version 5.3 Migration Guide - -This document is a guide for upgrading ABP v5.2 solutions to ABP v5.3. There is a change in this version that may effect your applications, please read it carefully and apply the necessary changes to your application. - -## AutoMapper Upgraded to v11.0.1 - -AutoMapper library upgraded to **v11.0.1** in this version. So, you need to change your project's target SDK that use the **AutoMapper** library (typically your `*.Application` project). You can change it from `netstandard2.0` to `netstandard2.1` or `net6` if needed. Please see [#12189](https://github.com/abpframework/abp/pull/12189) for more info. - -## See Also - -* [Official blog post for the 5.3 release](https://blog.abp.io/abp/ABP.IO-Platform-5.3-RC-Has-Been-Published) diff --git a/docs/en/Migration-Guides/Abp-6_0.md b/docs/en/Migration-Guides/Abp-6_0.md deleted file mode 100644 index 21eb6023b8..0000000000 --- a/docs/en/Migration-Guides/Abp-6_0.md +++ /dev/null @@ -1,29 +0,0 @@ -# ABP Version 6.0 Migration Guide - -This document is a guide for upgrading ABP v5.3 solutions to ABP v6.0. There is a change in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## The IsActive property is Added - -`IsActive` property is added to `IUserData`. This property is set to **true** by default. **Cmskit** and **Blog** modules are affected by this change. You need to add a new migration to your existing application if you are using any of these modules. Please see [#11417](https://github.com/abpframework/abp/pull/11417) for more info. - -## Default behavior change in MultiTenancyMiddlewareErrorPageBuilder - -If you have customized the `MultiTenancyMiddlewareErrorPageBuilder` of the `AbpMultiTenancyOptions`, the pipeline now returns **true** to stop the pipeline as the default behavior. See [AbpMultiTenancyOptions: Handle inactive and non-existent tenants](https://github.com/abpframework/abp/blob/dev/docs/en/Multi-Tenancy.md#abpmultitenancyoptions-handle-inactive-and-non-existent-tenants) for more info. - -## Migrating to LeptonX Lite - -LeptonX Lite is now being introduced and you can follow the guides below to migrate your existing applications: - -- [Migrating to LeptonX MVC UI](../Themes/LeptonXLite/AspNetCore.md) -- [Migrating to LeptonX Angular UI](../Themes/LeptonXLite/Angular.md) -- [Migrating to LeptonX Blazor UI](../Themes/LeptonXLite/Blazor.md) - -## Migrating to OpenIddict - -After the [announcement of plan to replace the IdentityServer](https://github.com/abpframework/abp/issues/11989), we have successfully implemented [Openiddict](https://github.com/openiddict/openiddict-core) as a replacement for IdentityServer4 as an OpenID-Provider. - -You can follow the [IdentityServer to OpenIddict Step by Step Guide](OpenIddict-Step-by-Step.md) for migrating your existing application in detail with a sample project. - -## See Also - -* [Official blog post for the 6.0 release](https://blog.abp.io/abp/ABP.IO-Platform-6.0-RC-Has-Been-Published) diff --git a/docs/en/Migration-Guides/Abp-7_0.md b/docs/en/Migration-Guides/Abp-7_0.md deleted file mode 100644 index 65a1614603..0000000000 --- a/docs/en/Migration-Guides/Abp-7_0.md +++ /dev/null @@ -1,213 +0,0 @@ -# ABP Version 7.0 Migration Guide - -This document is a guide for upgrading ABP v6.x solutions to ABP v7.0. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -> ABP Framework upgraded to .NET 7.0, so you need to move your solutions to .NET 7.0 if you want to use the ABP 7.0. You can check the [Migrate from ASP.NET Core 6.0 to 7.0](https://learn.microsoft.com/en-us/aspnet/core/migration/60-70?view=aspnetcore-7.0) documentation. - -## `FormTenantResolveContributor` Removed from the `AbpTenantResolveOptions` - -`FormTenantResolveContributor` has been removed from the `AbpTenantResolveOptions`. Thus, if you need to get tenant info from `HTTP Request From`, please add a custom `TenantResolveContributor` to implement it. - -## `IHybridServiceScopeFactory` Removed - -`IHybridServiceScopeFactory` has been removed. Please use the `IServiceScopeFactory` instead. - -## Hybrid JSON was removed. - -Since [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview) library supports more custom features in NET 7, ABP no longer need the hybrid Json feature. - -### Previous Behavior - -There is a `Volo.Abp.Json` package which contains the `AbpJsonModule` module. -`Serialization/deserialization` features of [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview) and [Newtonsoft](https://www.newtonsoft.com/json/help/html/SerializingJSON.htm) are implemented in this module. - -We use [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview) first, More custom cases can be handled with [Newtonsoft](https://www.newtonsoft.com/json/help/html/SerializingJSON.htm) by configuring `UnsupportedTypes` of `AbpSystemTextJsonSerializerOptions`. - -### New Behavior - -We created `Volo.Abp.Json.SystemTextJson` and `Volo.Abp.Json.Newtonsoft` as separate packages, which means you can only use one of them in your project. The default is to use `SystemTextJson`. If you want `Newtonsoft`, please also use `Volo.Abp.AspNetCore.Mvc.NewtonsoftJson` in your web project. - -* Volo.Abp.Json.Abstractions -* Volo.Abp.Json.Newtonsoft -* Volo.Abp.Json.SystemTextJson -* Volo.Abp.Json (Depends on `Volo.Abp.Json.SystemTextJson` by default to prevent breaking) -* Volo.Abp.AspNetCore.Mvc.NewtonsoftJson - -The `AbpJsonOptions` now has only two properties, which are - -* `InputDateTimeFormats(List)`: Formats of input JSON date, Empty string means default format. You can provide multiple formats to parse the date. -* `OutputDateTimeFormat(string)`: Format of output json date, Null or empty string means default format. - -Please remove all `UnsupportedTypes` add custom `Modifiers` to control serialization/deserialization behavior. - -Check the docs to see the more info: https://github.com/abpframework/abp/blob/dev/docs/en/JSON.md#configuration - -Check the docs to see how to customize a JSON contract: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/custom-contracts - -## "Manage Host Features" Moved to the Settings Page - -"Manage Host Features" button has been moved from Tenants page to Settings page. - -See https://github.com/abpframework/abp/pull/13359 for more info. - -## Removed the `setter` from the Auditing Interfaces - -`AuditedEntity` and other base entity classes will continue to have public setters. If you want to make them private, don't derive from these base classes, but implement the interfaces yourself. - -See https://github.com/abpframework/abp/issues/12229#issuecomment-1191384798 for more info. - -## Added Abp prefix to DbProperties Classes - -Please update the database migration and related connection string names. - -## `EntityCreatingEventData`, `EntityUpdatingEventData`, `EntityDeletingEventData` and `EntityChangingEventData` has been removed. - -They are deprecated don't use them anymore. - -## LayoutHookInfo.cs, LayoutHookViewModel.cs, LayoutHooks.cs, AbpLayoutHookOptions.cs classes have been moved under the Volo.Abp.Ui.LayoutHooks namespace. - -See https://github.com/abpframework/abp/pull/13903 for more info. - -## Removed `abp.auth.policies` - -`abp.auth.polices` has been removed, use `abp.auth.grantedPolicies` instead. - -## Static C# Proxy Generation - -The `abp generate-proxy -t csharp ..` command will generate all the `classes/enums/other types` in the client side (including application service interfaces) behalf of you. - -If you have reference to the target contracts package, then you can pass a parameter `--without-contracts (shortcut: -c)`. - -See https://github.com/abpframework/abp/issues/13613#issue-1333088953 for more info. - -## Dynamic Permissions - -* `IPermissionDefinitionManager` methods are converted to asynchronous, and renamed (added Async postfix). -* Removed `MultiTenancySides` from permission groups. -* Inherit `MultiTenancySides` enum from byte (default was int). -* Needs to add migration for new entities in the Permission Management module. - -See https://github.com/abpframework/abp/pull/13644 for more info. - -## External Localization Infrastructure - -* Introduced `LocalizationResourceBase` that is base for localization resources. `LocalizationResource` inherits from it for typed (static) localization resources (like before). Also introduced `NonTypedLocalizationResource` that inherits from `LocalizationResourceBase` for dynamic/external localization resources. We are using `LocalizationResourceBase` for most of the places where we were using `LocalizationResource` before and that can be a breaking change for some applications. -* All layouts in all MVC UI themes should add this line just before the **ApplicationConfigurationString** line: - -```html - -``` - -We've already done this for our themes. - -See https://github.com/abpframework/abp/pull/13845 for more info. - -## Replaced `BlogPostPublicDto` with `BlogPostCommonDto` - -- In the CMS Kit Module, `BlogPostPublicDto` has been moved to `Volo.CmsKit.Common.Application.Contracts` from `Volo.CmsKit.Public.Application.Contracts` and renamed to `BlogPostCommonDto`. - -- See the [PR#13499](https://github.com/abpframework/abp/pull/13499) for more information. - -> You can ignore this if you don't use CMS Kit Module. - -## Data migration environment - -Please call `AddDataMigrationEnvironment` method in the migration project. - -```cs -using (var application = await AbpApplicationFactory.CreateAsync(options => -{ - //... - options.AddDataMigrationEnvironment(); -})) -{ - //... -} -``` - -```cs -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDataMigrationEnvironment(); -// Call AddDataMigrationEnvironment before AddApplicationAsync -await builder.AddApplicationAsync(); -//... -``` - -See https://github.com/abpframework/abp/pull/13985 for more info. - -## Devart.Data.Oracle.EFCore - -The `Devart.Data.Oracle.EFCore` package do not yet support EF Core 7.0, If you use `AbpEntityFrameworkCoreOracleDevartModule(Volo.Abp.EntityFrameworkCore.Oracle.Devart)` may not work as expected, We will release new packages as soon as they are updated. - -See https://github.com/abpframework/abp/issues/14412 for more info. -# Changes on Angular Apps -## Added a new package `@abp/ng.oauth` -OAuth Functionality moved to a seperate package named `@abp/ng.oauth`, so ABP users should add the `@abp/ng.oauth` packages on app.module.ts. -Add the new npm package to your app. -``` -yarn add @abp/ng.oauth -// or npm i ---save @abp/ng.oauth -``` - -```typescript -// app.module.ts -import { AbpOAuthModule } from "@abp/ng.oauth"; -// ... -@NgModule({ - // ... - imports: [ - AbpOAuthModule.forRoot(), // <-- Add This - // ... - ], - // ... -}) -export class AppModule {} - -``` -## Lepton X Google-Font -If you are using LeptonX that has google fonts, the fonts were built-in the Lepton file. It's been moved to a seperate file. So the ABP user should add font-bundle in angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' ) - -// for LeptonX Lite -```json - { - input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.rtl.css', - inject: false, - bundleName: 'font-bundle.rtl', - }, - { - input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.css', - inject: false, - bundleName: 'font-bundle', - }, -``` - -// for LeptonX -```json - { - input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.css', - inject: false, - bundleName: 'font-bundle', - }, - { - input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.rtl.css', - inject: false, - bundleName: 'font-bundle.rtl', - }, -``` - -## Updated Side Menu Layout - -In side menu layout, eThemeLeptonXComponents.Navbar has been changed to eThemeLeptonXComponents.Toolbar, and -eThemeLeptonXComponents.Sidebar to eThemeLeptonXComponents.Navbar. - -And also added new replaceable component like Logo Component, Language Component etc. - -If you are using replaceable component system you can check [documentation](https://docs.abp.io/en/commercial/latest/themes/lepton-x/angular#customization). - - -## ng-zorro-antd-tree.css - -ng-zorro-antd-tree.css file should be in angular.json if the user uses AbpTree component or Abp-commercial. The ABP User should add this style definition on angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' ) - -{ "input": "node_modules/ng-zorro-antd/tree/style/index.min.css", "inject": false, "bundleName": "ng-zorro-antd-tree" }, - diff --git a/docs/en/Migration-Guides/Abp-7_1.md b/docs/en/Migration-Guides/Abp-7_1.md deleted file mode 100644 index 711f520800..0000000000 --- a/docs/en/Migration-Guides/Abp-7_1.md +++ /dev/null @@ -1,21 +0,0 @@ -# ABP Version 7.1 Migration Guide - -This document is a guide for upgrading ABP v7.0 solutions to ABP v7.1. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -> **Note**: Entity Framework developers may need to add a new code-first database migration to their projects since we made some improvements to the existing entities of some application modules. - -## Navigation Menu - `CustomData` type changed to `Dictionary` - -`ApplicationMenu` and `ApplicationMenuItem` classes' `CustomData` property type has been changed to `Dictionary`. So, if you use the optional `CustomData` property of these classes, change it accordingly. See [#15608](https://github.com/abpframework/abp/pull/15608) for more information. - -*Old usage:* - -```csharp -var menu = new ApplicationMenu("Home", L["Home"], "/", customData: new MyCustomData()); -``` - -*New usage:* - -```csharp -var menu = new ApplicationMenu("Home", L["Home"], "/").WithCustomData("CustomDataKey", new MyCustomData()); -``` diff --git a/docs/en/Migration-Guides/Abp-7_2.md b/docs/en/Migration-Guides/Abp-7_2.md deleted file mode 100644 index 5c99e8e604..0000000000 --- a/docs/en/Migration-Guides/Abp-7_2.md +++ /dev/null @@ -1,13 +0,0 @@ -# ABP Version 7.2 Migration Guide - -This document is a guide for upgrading ABP v7.1 solutions to ABP v7.2. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## `LastPasswordChangeTime` and `ShouldChangePasswordOnNextLogin` Properties Added to the `IdentityUser` Class - -In this version, two new properties, which are `LastPasswordChangeTime` and `ShouldChangePasswordOnNextLogin` have been added to the `IdentityUser` class and to the corresponding entity. Therefore, you may need to create a new migration and apply it to your database. - -## Renamed `OnRegistered` Method - -There was a typo in an extension method, named as `OnRegistred`. In this version, we have fixed the typo and renamed the method as `OnRegistered`. Also, we have updated the related places in our modules that use this method. - -However, if you have used this method in your projects, you need to rename it as `OnRegistered` in your code. \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-7_3.md b/docs/en/Migration-Guides/Abp-7_3.md deleted file mode 100644 index 1d8e5b1e36..0000000000 --- a/docs/en/Migration-Guides/Abp-7_3.md +++ /dev/null @@ -1,31 +0,0 @@ -# ABP Version 7.3 Migration Guide - -This document is a guide for upgrading ABP v7.2 solutions to ABP v7.3. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## OpenIddict - Refactoring of `ClaimsPrincipal` - -There are some changes that you might need to fix in your code. You can see the following list of the required changes: - -* `AbpOpenIddictClaimDestinationsManager` was renamed as `AbpOpenIddictClaimsPrincipalManager`. -* Use `AbpOpenIddictClaimsPrincipalManager.HandleAsync` instead of `AbpOpenIddictClaimDestinationsManager.SetAsync`, which is removed. -* `AbpDefaultOpenIddictClaimDestinationsProvider` was renamed as `AbpDefaultOpenIddictClaimsPrincipalHandler`. -* `IAbpOpenIddictClaimDestinationsProvider` was renamed as `IAbpOpenIddictClaimsPrincipalHandler`. -* Use `IAbpOpenIddictClaimsPrincipalHandler.HandleAsync` instead of `IAbpOpenIddictClaimDestinationsProvider.SetAsync`, which is removed. -* `AbpOpenIddictClaimDestinationsOptions` was renamed as `AbpOpenIddictClaimsPrincipalOptions`. - -Please check [this PR](https://github.com/abpframework/abp/pull/16537) if you encounter any problems related to OpenIddict Module. - -## Nonce attribute support for Content Security Policy (CSP) - -ABP Framework supports adding unique value to nonce attribute for script tags which can be used by Content Security Policy to determine whether or not a given fetch will be allowed to proceed for a given element. In other words, it provides a mechanism to execute only correct script tags with the correct nonce value. - -> See the [Security Headers](../UI/AspNetCore/Security-Headers.md) documentation for more information. - -This feature comes with a small restriction. If you use any C# code used inside the script tag, it may cause errors (Because a new `NonceScriptTagHelper` has been added, and it replaces script tags in the HTML contents). - -For example, `` will no longer work. However, you can use the C# code for an attribute of script tag, for example, `` is completely valid and won't cause any problem. - -> Note: You should not use any C# code used inside the script tag, even if you don't use this feature. Because it might cause errors. - -## Angular UI -We would like to inform you that ABP Framework version 7.3 uses Angular version 16. Please migrate your applications to Angular 16. [Update angular](https://update.angular.io/) \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-7_4.md b/docs/en/Migration-Guides/Abp-7_4.md deleted file mode 100644 index ee72da812f..0000000000 --- a/docs/en/Migration-Guides/Abp-7_4.md +++ /dev/null @@ -1,119 +0,0 @@ -# ABP Version 7.4 Migration Guide - -This document is a guide for upgrading ABP v7.3 solutions to ABP v7.4. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## Bumped the `Microsoft.Extensions.FileProviders.Embedded` Package Version To v7.0.10 - -In this version, the `Microsoft.Extensions.FileProviders.Embedded` (and other `Microsoft.*` packages) upgraded to the latest version, which is v7.0.10. Therefore, in your solution, you should update the `Microsoft.Extensions.FileProviders.Embedded` package (and other `Microsoft.*` packages) version to v7.0.10. This package typically would be in your `Domain.Shared` project and other projects that have embedded resource(s). So, search this package through your solution and update it accordingly. - -> You can check [this issue](https://github.com/abpframework/abp/pull/17516) to see the updated package versions. - -## Renamed the `AddGlobalFilters<>` method as `FilterQueryable<>` in `IMongoDbRepositoryFilterer` - -ABP Framework provides services to automatically filter data on querying from a database. Prior to this version, creating a new class that derives from the `MongoDbRepositoryFilterer` and overriding its `AddGlobalFilters` method was needed for implementing a data filter for [MongoDB](../MongoDB.md). - -In this version, the `AddGlobalFilters<>` method is renamed as `FilterQueryable<>`. Therefore, you need to update the method name if you have used data filtering for MongoDB, in your application. - -## Exposing Integration Services - -[Integration Services](../Integration-Services.md) are now not being exposed by default. In a monolith application, integration services don't need to be exposed outside since the modules would probably be in-process communication with each other. Therefore, they don't need to be exposed for most of the time. - -If you build a microservice solution or you need to access an integration service via a network call from any other application, you will probably need to expose the integration services so the other applications can consume them. - -To expose integration services and controllers, you can configure the `AbpAspNetCoreMvcOptions` and set the `ExposeIntegrationServices` property as *true* in the `ConfigureServices` method of your [module class](../Module-Development-Basics.md): - -```csharp -Configure(options => -{ - options.ExposeIntegrationServices = true; -}); -``` - -## `LocalizationResource` property removed from the `TemplateDefinition` class - -In this version, the `LocalizationResource` property was removed from the `TemplateDefinition` class and instead, the `LocalizationResourceName` property has been added. - -```diff -- public Type LocalizationResource { get; set; } -+ public string LocalizationResourceName { get; set; } -``` - -## Changed the method signature for `ICorrelationIdProvider.Get()` - -Prior to this version, the `ICorrelationIdProvider.Get()` method used to return a non nullable string that represented a *correlationId* (a unique key that is used in distributed applications to trace requests across multiple services/operations). In this version, this method may return `null` if it hasn't been generated by `AbpCorrelationIdMiddleware` before. - -```diff -public interface ICorrelationIdProvider -{ - -- [NotNull] string Get(); -+ string? Get(); - - //other methods - -} -``` - -Therefore, if you've used this method in your application, you might want to make a null check and update the method signature where it's used. - -> See [#16795](https://github.com/abpframework/abp/pull/16795) for more information. - -## Dynamic Setting Store - Setting Management Module - -In this version, ABP Framework introduces Dynamic Setting Store, which is an important feature that allows us to collect and get all setting definitions from a single point. This feature requires some actions that need to be taken care of as the following: - -* You need to create a new migration and apply it to your database because a new database table has been added. -* `ISettingDefinitionManager`'s sync methods have been removed and instead, asynchronous versions of the existing methods have been added. - -```diff -public interface ISettingDefinitionManager -{ -- SettingDefinition Get([NotNull] string name); -+ Task GetAsync([NotNull] string name); - -- IReadOnlyList GetAll(); -+ Task> GetAllAsync(); - -- SettingDefinition? GetOrNull(string name); -+ Task GetOrNullAsync([NotNull] string name); -} -``` - -## `IdentityUserIntegrationService` - Identity Module - -In this version, ABP Framework introduces the `IdentityUserIntegrationService`, which is designed to get the current user's information, such as his/her role names within a non-authorized integration service. - -> For more information, see the related PR: [#16962](https://github.com/abpframework/abp/pull/16962) - -This is a breaking change for microservice solutions because of the following two reasons and it should be considered: - -* `IdentityUserIntegrationService` provides non-authorized services. This is not breaking the application but should be taken care of. Since, everyone can use the service to retrieve some information for a certain user (for example, the role names of a user). -* Secondly, since integration services are not exposed by default anymore as explained in the *Exposing Integration Services* section above, you should explicitly enable exposing integration services. Otherwise, the operation will fail and you'll get a `404` error from the identity microservice. - -To expose integration services and controllers, you can configure the `AbpAspNetCoreMvcOptions` and set the `ExposeIntegrationServices` property as *true* in the `ConfigureServices` method of your [module class](../Module-Development-Basics.md): - -```csharp -Configure(options => -{ - options.ExposeIntegrationServices = true; -}); -``` - -## Blazor UI -If you use Blazor WASM or Blazor Server UI, you should follow this section. - -### Bumped the `Blazorise` dependencies to `1.3.1` -In this version, the `Blazorise` dependencies are upgraded to the `1.3.1` version. you should upgrade Blazorise packages to `1.3.1` in your `Blazor.csproj` file. -The following packages are included in the templates by default: -- `Blazorise.Bootstrap5` -- `Blazorise.Icons.FontAwesome` -- `Blazorise.Components` -> _If your project depends on more blazorise packages, then you should upgrade all of them._ -> You should execute `dotnet build` & `abp bundle` commands in the Blazor project if you are using the Blazor WebAssembly. - -### Bumped the `Microsoft.AspNetCore.Components.*` dependency to `7.0.10` - -In this version, the `Microsoft.AspNetCore.Components.*` dependencies are upgraded to the `7.0.10` version. Therefore, you should upgrade the `Microsoft.AspNetCore.Components.Web` and `Microsoft.AspNetCore.Components.WebAssembly` packages to `7.0.10` in your `Blazor.csproj` file. - -## Angular UI -We would like to inform you that ABP Framework version 7.4 uses Angular version 16. Please migrate your applications to Angular 16. [Update angular](https://update.angular.io/) \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-8-2-Blazor-Web-App.md b/docs/en/Migration-Guides/Abp-8-2-Blazor-Web-App.md deleted file mode 100644 index 239bbf70c8..0000000000 --- a/docs/en/Migration-Guides/Abp-8-2-Blazor-Web-App.md +++ /dev/null @@ -1,223 +0,0 @@ -# Migrating to Blazor Web App - -ASP.NET Blazor in .NET 8 allows you to use a single powerful component model to handle all of your web UI needs, including server-side rendering, client-side rendering, streaming rendering, progressive enhancement, and much more! - -ABP v8.2.x supports the new Blazor Web App template, in this guide, we will introduce some new changes and features in the new Blazor Web App template. - -## Create a new Blazor Web App - -> Please make sure you have installed the 8.2.x version of the ABP CLI. - -You can create a new Blazor Web App using the `abp new BookStore -t app -u blazor-webapp` command. The `-u blazor-webapp` option is used to select the Blazor Web App template. - -Of course, you can also create Blazor WASM and Blazor Server applications. We have changed them to use the new Blazor Web App mode: - -````csharp -abp new BookStore -t app -u blazor -abp new BookStore -t app -u blazor-server -```` - -## Render modes - -The template project use different render modes for different types of projects in the `App.razor` component. - -| Type | Render mode -|----------------|------------------ -| WASM | InteractiveWebAssembly(prerender: false) -| Server | InteractiveServer -| WebApp | InteractiveAuto - -## The key changes of the new Blazor Web App template - -The new Web App template has two projects, each containing a system of [ABP modules](https://docs.abp.io/en/abp/latest/Modules/Index). - -- MyCompanyName.MyProjectName.Blazor.WebApp -- MyCompanyName.MyProjectName.Blazor.WebApp.Client - -### MyCompanyName.MyProjectName.Blazor.WebApp - -The `Blazor.WebApp` is the startup project, and there is an `App.razor` component in the `Blazor.WebApp` project, which is the root component of the Blazor application. - -The main differences between it, and a regular Blazor server project are: - -1. You need to `PreConfigure` the `IsBlazorWebApp` to `true` in `AbpAspNetCoreComponentsWebOptions`: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.IsBlazorWebApp = true; - }); -} -```` - -2. Add related services to the container. Add assembly of `MyProjectNameBlazorClientModule` to the `AdditionalAssemblies` by configuring `AbpRouterOptions`: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Add services to the container. - context.Services.AddRazorComponents() - .AddInteractiveServerComponents() - .AddInteractiveWebAssemblyComponents(); - - Configure(options => - { - options.AppAssembly = typeof(MyProjectNameBlazorModule).Assembly; - options.AdditionalAssemblies.Add(typeof(MyProjectNameBlazorClientModule).Assembly); - }); -} -```` - -3. Add `UseAntiforgery` middleware and `MapRazorComponents/AddInteractiveServer/WebAssemblyRenderMode/AddAdditionalAssemblies` in the `OnApplicationInitialization` method. - -````csharp -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - var env = context.GetEnvironment(); - var app = context.GetApplicationBuilder(); - // ... - - app.UseAntiforgery(); - app.UseAuthorization(); - - app.UseConfiguredEndpoints(builder => - { - builder.MapRazorComponents() - .AddInteractiveServerRenderMode() - .AddInteractiveWebAssemblyRenderMode() - .AddAdditionalAssemblies(builder.ServiceProvider.GetRequiredService>().Value.AdditionalAssemblies.ToArray()); - }); -} -```` - -### MyCompanyName.MyProjectName.Blazor.WebApp.Client - -There is a `Routers.razor` component in the `Blazor.WebApp.Client` project, which is used by the `App.razor` component. - -The main differences between it and a regular Blazor WASM project are: - -1. You need to `PreConfigure` the `IsBlazorWebApp` to `true` in `AbpAspNetCoreComponentsWebOptions`: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.IsBlazorWebApp = true; - }); -} -```` - -2. Use `AddBlazorWebAppServices` to replace `Authentication` code: - -````csharp -private static void ConfigureAuthentication(WebAssemblyHostBuilder builder) -{ - builder.Services.AddBlazorWebAppServices(); -} -```` - -3. Remove the `builder.RootComponents.Add("#ApplicationContainer");` code. - -### MyCompanyName.MyProjectName.Blazor.WebApp.Tiered and MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client - -The tiered projects are the same as the WebApp projects, but the authentication configuration is different. - -We need share the `access_token` to `Client` project. - -Add code block to `App.razor` of `MyCompanyName.MyProjectName.Blazor.WebApp.Tiered` as below: - -````csharp -@code{ - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - [Inject] - private PersistentComponentState PersistentComponentState { get; set; } - - private string? Token { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - if (HttpContext.User?.Identity?.IsAuthenticated == true) - { - Token = await HttpContext.GetTokenAsync("access_token"); - } - - PersistentComponentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); - } - - async Task OnPersistingAsync() - { - if (!Token.IsNullOrWhiteSpace()) - { - PersistentComponentState.PersistAsJson(PersistentAccessToken.Key, new PersistentAccessToken - { - AccessToken = Token - }); - } - - await Task.CompletedTask; - } -} -```` - -Add `ConfigureAuthentication` to `MyProjectNameBlazorClientModule` of `MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client` as below: - -````csharp -private static void ConfigureAuthentication(WebAssemblyHostBuilder builder) -{ - builder.Services.AddBlazorWebAppTieredServices(); -} -```` - -## ABP Bundle - -You need set `IsBlazorWebApp` and `InteractiveAuto` to `true` in the `appsettings.json` file of the `MyCompanyName.MyProjectName.Blazor.WebApp.Client` project: - -````json -{ - "AbpCli": { - "Bundle": { - "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ - "Name": "global", - "IsBlazorWebApp": true, - "InteractiveAuto": true, - "Parameters": { - - } - } - } -} -```` - -For Blazor WASM and Blazor Server applications, you need to set `IsBlazorWebApp` to `true` and not need to change the `InteractiveAuto`: - -````json -{ - "AbpCli": { - "Bundle": { - "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ - "Name": "global", - "IsBlazorWebApp": true, - "Parameters": { - - } - } - } -} -```` - -Then run the `abp bundle` command to under the `MyCompanyName.MyProjectName.Blazor.WebApp.Client` project to generate the `global.css` and `global.js` files. - -## Troubleshooting - -If you encounter any problems during the migration, please create a new template project and compare the differences between the new and old projects. - -# References - -- [ASP.NET Core Blazor render modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0) -- [Migrate from ASP.NET Core Blazor 7.0 to 8.0](https://learn.microsoft.com/en-us/aspnet/core/migration/70-80?view=aspnetcore-8.0&tabs=visual-studio#blazor) -- [Full stack web UI with Blazor in .NET 8 | .NET Conf 2023](https://www.youtube.com/watch?v=YwZdtLEtROA) diff --git a/docs/en/Migration-Guides/Abp-8_0.md b/docs/en/Migration-Guides/Abp-8_0.md deleted file mode 100644 index 0cba42d759..0000000000 --- a/docs/en/Migration-Guides/Abp-8_0.md +++ /dev/null @@ -1,303 +0,0 @@ -# ABP Version 8.0 Migration Guide - -This document is a guide for upgrading ABP v7.x solutions to ABP v8.0. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -> ABP Framework upgraded to .NET 8.0, so you need to move your solutions to .NET 8.0 if you want to use the ABP 8.0. You can check the [Migrate from ASP.NET Core 7.0 to 8.0](https://learn.microsoft.com/en-us/aspnet/core/migration/70-80) documentation. - -## Upgraded to OpenIddict to 5.0.0 - -The 5.0 release of OpenIddict is a major release that introduces breaking changes. - -See [OpenIddict 4.x to 5.x Migration Guide](OpenIddict4-to-5.md) for more information. - -## Injected the `IDistributedEventBus` Dependency into the `IdentityUserManager` - -In this version, `IDistributedEventBus` service has been injected to the `IdentityUserManager` service, to publish a distributed event when the email or username is changed for a user, this was needed because sometimes there may be scenarios where the old email/username is needed for the synchronization purposes. - -Therefore, you might need to update the `IdentityUserManager`'s constructor if you have overridden the class and are using it. - -> See the issue for more information: https://github.com/abpframework/abp/pull/17990 - -## Updated Method Signatures in the Bundling System - -In this version, ABP Framework introduced the CDN support for bundling. During the development, we have made some improvements on the bundling system and changed some method signatures. - -See https://github.com/abpframework/abp/issues/17864 for more information. - -## Replaced `IdentityUserLookupAppService` with the `IIdentityUserIntegrationService` - -[Integration Services](../Integration-Services.md) are built for module-to-module (or microservice-to-microservice) communication rather than consumed from a UI or a client application as [Application Services](../Application-Services.md) are intended to do. - -In that regard, we are discarding the `IIdentityUserLookupAppService` in the Identity Module and moving its functionality to the `IIdentityUserIntegrationService`. Therefore, if you have used that application service directly, use the integration service (`IIdentityUserIntegrationService`) instead. `IIdentityUserLookupAppService` will be removed in thes next versions, so you may need to create a similar service in your application. - -> Notice that integration services have no authorization and are not exposed as HTTP API by default. -Also, if you have overridden the `IdentityUserLookupAppService` and `IdentityUserIntegrationService` classes in your application, you should update these classes' constructors as follows: - -*IdentityUserLookupAppService.cs* -```csharp - public IdentityUserLookupAppService(IIdentityUserIntegrationService identityUserIntegrationService) - { - IdentityUserIntegrationService = identityUserIntegrationService; - } -``` - -*IdentityUserIntegrationService.cs* - -```diff - public IdentityUserIntegrationService( - IUserRoleFinder userRoleFinder, -+ IdentityUserRepositoryExternalUserLookupServiceProvider userLookupServiceProvider) - { - UserRoleFinder = userRoleFinder; -+ UserLookupServiceProvider = userLookupServiceProvider; - } -``` - -## MongoDB Event Bus Enhancements - -In this version, we have made some enhancements in the transactional inbox/outbox pattern implementation and defined two new methods: `ConfigureEventInbox` and `ConfigureEventOutbox` for MongoDB Event Box collections. - -If you call one of these methods in your DbContext class, then this introduces a breaking-change because if you do it, MongoDB collection names will be changed. Therefore, it should be carefully done since existing (non-processed) event records are not automatically moved to new collection and they will be lost. Existing applications with event records should rename the collection manually while deploying their solutions. - -See https://github.com/abpframework/abp/pull/17723 for more information. Also, check the documentation for the related configurations: [Distributed Event Bus](../Distributed-Event-Bus.md) - -## Moved the CMS Kit Pages Feature's Routing to a `DynamicRouteValueTransformer` - -In this version, we have made some improvements in the [CMS Kit's Pages Feature](../Modules/Cms-Kit/Pages.md), such as moving the routing logic to a `DynamicRouteValueTransformer` and etc... - -These enhancements led to some breaking changes as listed below that should be taken care of: - -* Page routing has been moved to **DynamicRouteValueTransformer**. If you use `{**slug}` pattern in your routing, it might conflict with new CMS Kit routing. -* `PageConsts.UrlPrefix` has been removed, instead, the default prefix is *pages* for now. Still `/pages/{slug}` route works for backward compatibility alongside with `/{slug}` route. - -* **Endpoints changed:** - * `api/cms-kit-public/pages/{slug}` endpoint is changed to `api/cms-kit-public/pages/by-slug?slug={slug}`. Now multiple level of page URLs can be used and `/` characters will be transferred as URL Encoded in querysting to the HTTP API. - * `api/cms-kit-public/pages` changed to `api/cms-kit-public/pages/home` - ->_CmsKit Client Proxies are updated. If you don't send a **custom request** to this endpoint, **you don't need to take an action**_ - -## Added Integration Postfix for Auto Controllers - -With this version on, the `Integration` suffix from controller names while generating [auto controllers](../API/Auto-API-Controllers.md) are not going to be removed, to differ the integration services from application services in the OpenAPI specification: - -![](./images/integration-postfix-not-removed.png) - -> This should not affect most of the applications since you normally do not depend on the controller names in the client side. - -See https://github.com/abpframework/abp/issues/17625 for more information (how to preserve the existing behaviour, etc...). - -## Revised the reCaptcha Generator for CMS Kit's Comment Feature - -In this version, we have made improvements on the [CMS Kit's Comment Feature](../Modules/Cms-Kit/Comments.md) and revised the reCaptcha generation process, and made a performance improvement. - -This introduced some breaking changes that you should aware of: - -* Lifetime of the `SimpleMathsCaptchaGenerator` changed from singleton to transient, -* Changed method signatures for `SimpleMathsCaptchaGenerator` class. (all of its methods are now async) - -If you haven't override the comment view component, then you don't need to make any changes, however if you have overriden the component and used the `SimpleMathsCaptchaGenerator` class, then you should make the required changes as described. - -## Disabled Logging for `HEAD` HTTP Methods - -HTTP GET requests should not make any change in the database normally and audit log system of ABP Framework doesn't save audit log objects for GET requests by default. You can configure the `AbpAuditingOptions` and set the `IsEnabledForGetRequests` to **true** if you want to record _GET_ requests as described in [the documentation](../Audit-Logging.md). - -Prior to this version, only the _GET_ requests were not saved as audit logs. From this version on, also the _HEAD_ requests will not be saved as audit logs, if the `IsEnabledForGetRequests` explicitly set as **true**. - -You don't need to make any changes related to that, however it's important to know this change. - -## Obsolete the `AbpAspNetCoreIntegratedTestBase` Class - -In this version, `AbpAspNetCoreAsyncIntegratedTestBase` class has been set as `Obsolete` and it's recommended to use `AbpWebApplicationFactoryIntegratedTest` instead. - -## Use NoTracking for Readonly Repositories for EF Core - -In this version, ABP Framework provides read-only [repository](Repositories.md) interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`) to explicitly indicate that your purpose is to query data, but not change it. If so, you can inject these interfaces into your services. - -Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. - -> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. - -> See the issue for more information: https://github.com/abpframework/abp/pull/17421 - -## Use `IAbpDaprClientFactory` to Obtain `DaprClient` - -From this version on, instead of injecting the `DaprClient` directly, using the `IAbpDaprClientFactory.CreateAsync` method to create `DaprClient` or `HttpClient` objects to perform operations on Dapr is recommended. - -The documentation is already updated according to this suggestion and can be found at https://docs.abp.io/en/abp/8.0/Dapr/Index. So, if you want to learn more you can check the documentation or see the PR: https://github.com/abpframework/abp/pull/18117. - -## Use Newer Versions of the SQL Server (SQL Server 14+) - -Starting with EF Core 8.0, EF now generates SQL that is more efficient, but is unsupported on SQL Server 2014 and below. Therefore, if your database provider is SQL Server, then ensure that it's newer than SQL Server 2014. Otherwise, you may get errors due to database creation or while seeding initial data. - -The error is similar to: `Microsoft.Data.SqlClient.SqlException (0x80131904)`. - -> Check the [Entity Framework Core's Breaking Changes](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/breaking-changes#high-impact-changes) documentation for more info. - -## Angular UI - -### Guards - -From Angular Documentation; - -> Class-based **`Route`** guards are deprecated in favor of functional guards. - -- Angular has been using functional guards since version 14. According to this situation we have moved our guards to functional guards. - -We have modified our modules to adaptate functional guards. - -```diff -- import {AuthGuard, PermissionGuard} from '@abp/ng.core'; -+ import {authGuard, permissionGuard} from '@abp/ng.core'; - -- canActivate: mapToCanActivate([AuthGuard, PermissionGuard]) -+ canActivate: [authGuard, permissionGuard] -``` - -You can still use class based guards but we recommend it to use functional guards like us :) - -## Upgraded NuGet Dependencies - -You can see the following list of NuGet libraries that have been upgraded with .NET 8.0 upgrade, if you are using one of these packages explicitly, you may consider upgrading them in your solution: - -| Package | Old Version | New Version | -| ------------------- | ----------- | ----------- | -| aliyun-net-sdk-sts | 3.1.0 | 3.1.2 | -| AsyncKeyedLock | 6.2.1 | 6.2.2 | -| Autofac | 7.0.0 | 8.0.0 | -| Autofac.Extensions.DependencyInjection | 8.0.0 | 9.0.0 | -| Autofac.Extras.DynamicProxy | 6.0.1 | 7.1.0 | -| AutoMapper | 12.0.0 | 12.0.1 | -| AWSSDK.S3 | 3.7.9.2 | 3.7.300.2 | -| AWSSDK.SecurityToken | 3.7.1.151 | 3.7.300.2 | -| Azure.Messaging.ServiceBus | 7.8.1 | 7.17.0 | -| Azure.Storage.Blobs | 12.15.0 | 12.19.1 | -| Blazorise | 1.3.1 | 1.4.1 | -| Blazorise.Bootstrap5 | 1.3.1 | 1.4.1 | -| Blazorise.Icons.FontAwesome | 1.3.1 | 1.4.1 | -| Blazorise.Components | 1.3.1 | 1.4.1 | -| Blazorise.DataGrid | 1.3.1 | 1.4.1 | -| Blazorise.Snackbar | 1.3.1 | 1.4.1 | -| Confluent.Kafka | 1.8.2 | 2.3.0 | -| Dapper | 2.0.123 | 2.1.21 | -| Dapr.AspNetCore | 1.9.0 | 1.12.0 | -| Dapr.Client | 1.9.0 | 1.12.0 | -| Devart.Data.Oracle.EFCore | 10.1.134.7 | 10.3.10.8 | -| DistributedLock.Core | 1.0.4 | 1.0.5 | -| DistributedLock.Redis | 1.0.1 | 1.0.2 | -| EphemeralMongo.Core | 1.1.0 | 1.1.3 | -| EphemeralMongo6.runtime.linux-x64 | 1.1.0 | 1.1.3 | -| EphemeralMongo6.runtime.osx-x64 | 1.1.0 | 1.1.3 | -| EphemeralMongo6.runtime.win-x64 | 1.1.0 | 1.1.3 | -| FluentValidation | 11.0.1 | 11.8.0 | -| Hangfire.AspNetCore | 1.8.2 | 1.8.6 | -| Hangfire.SqlServer | 1.8.2 | 1.8.6 | -| HtmlSanitizer | 5.0.331 | 8.0.746 | -| HtmlAgilityPack | 1.11.42 | 1.11.54 | -| IdentityModel | 6.0.0 | 6.2.0 | -| IdentityServer4.AspNetIdentity | 4.1.1 | 4.1.2 | -| JetBrains.Annotations | 2022.1.0 | 2023.3.0 | -| LibGit2Sharp | 0.26.2 | 0.28.0 | -| Magick.NET-Q16-AnyCPU | 13.2.0 | 13.4.0 | -| MailKit | 3.2.0 | 4.3.0 | -| Markdig.Signed | 0.26.0 | 0.33.0 | -| Microsoft.AspNetCore.Authentication.JwtBearer | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Authentication.OpenIdConnect | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Authorization | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.Authorization | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.Web | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.WebAssembly | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.WebAssembly.Authentication | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.WebAssembly.DevServer | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Components.WebAssembly.Server | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.Mvc.Versioning | 5.0.0 | 5.1.0 | -| Microsoft.AspNetCore.Razor.Language | 6.0.8 | 6.0.25 | -| Microsoft.AspNetCore.TestHost | 7.0.10 | 8.0.0 | -| Microsoft.AspNetCore.WebUtilities | 2.2.0 | 8.0.0 | -| Microsoft.Bcl.AsyncInterfaces | 7.0.0 | 8.0.0 | -| Microsoft.CodeAnalysis.CSharp | 4.2.0 | 4.5.0 | -| Microsoft.Data.Sqlite | 7.0.0 | 8.0.0 | -| Microsoft.EntityFrameworkCore | 7.0.10 | 8.0.0 | -| Microsoft.EntityFrameworkCore.Design | 7.0.0 | 8.0.0 | -| Microsoft.EntityFrameworkCore.InMemory | 7.0.10 | 8.0.0 | -| Microsoft.EntityFrameworkCore.Proxies | 7.0.10 | 8.0.0 | -| Microsoft.EntityFrameworkCore.Relational | 7.0.10 | 8.0.0 | -| Microsoft.EntityFrameworkCore.Sqlite | 7.0.10 | 8.0.0 | -| Microsoft.EntityFrameworkCore.SqlServer | 7.0.0 | 8.0.0 | -| Microsoft.EntityFrameworkCore.Tools | 7.0.1 | 8.0.0 | -| Microsoft.Extensions.Caching.Memory | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Caching.StackExchangeRedis | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Configuration.Binder | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Configuration.CommandLine | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Configuration.EnvironmentVariables | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Configuration.UserSecrets | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.DependencyInjection | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.DependencyInjection.Abstractions | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.FileProviders.Composite | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.FileProviders.Embedded | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.FileProviders.Physical | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.FileSystemGlobbing | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Hosting | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Hosting.Abstractions | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Http | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Http.Polly| 7.0.10 | 8.0.0 | -| Microsoft.Extensions.Identity.Core | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Localization | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Logging | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Logging.Console | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Options | 7.0.0 | 8.0.0 | -| Microsoft.Extensions.Options.ConfigurationExtensions | 7.0.0 | 8.0.0 | -| Microsoft.NET.Test.Sdk | 17.2.0 | 17.8.0 | -| Microsoft.VisualStudio.Web.CodeGeneration.Design | 7.0.0 | 8.0.0 | -| Minio | 4.0.6 | 6.0.1 | -| MongoDB.Driver | 2.19.1 | 2.22.0 | -| NEST | 7.14.1 | 7.17.5 | -| Newtonsoft.Json | 13.0.1 | 13.0.3 | -| NSubstitute | 4.3.0 | 5.1.0 | -| NuGet.Versioning | 5.11.0 | 6.7.0 | -| NUglify | 1.20.0 | 1.21.0 | -| Npgsql.EntityFrameworkCore.PostgreSQL | 7.0.0 | 8.0.0 | -| NSubstitute.Analyzers.CSharp | 1.0.15 | 1.0.16 | -| Octokit | 0.50.0 | 9.0.0 | -| OpenIddict.Abstractions | 4.8.0 | 5.0.0 | -| OpenIddict.Core | 4.8.0 | 5.0.0 | -| OpenIddict.Server.AspNetCore | 4.8.0 | 5.0.0 | -| OpenIddict.Validation.AspNetCore | 4.8.0 | 5.0.0 | -| OpenIddict.Validation.ServerIntegration | 4.8.0 | 5.0.0 | -| Oracle.EntityFrameworkCore | 7.21.8 | 8.21.121 | -| Polly | 7.2.3 | 8.2.0 | -| Pomelo.EntityFrameworkCore.MySql | 7.0.0 | 8.0.0 | -| Quartz | 3.4.0 | 3.7.0 | -| Quartz.Extensions.DependencyInjection | 3.4.0 | 3.7.0 | -| Quartz.Plugins.TimeZoneConverter | 3.4.0 | 3.7.0 | -| Quartz.Serialization.Json | 3.3.3 | 3.7.0 | -| RabbitMQ.Client | 6.3.0 | 6.6.0 | -| Rebus | 6.6.5 | 8.0.1 | -| Rebus.ServiceProvider | 7.0.0 | 10.0.0 | -| Scriban | 5.4.4 | 5.9.0 | -| Serilog | 2.11.0 | 3.1.1 | -| Serilog.AspNetCore | 5.0.0 | 8.0.0 | -| Serilog.Extensions.Hosting | 3.1.0 | 8.0.0 | -| Serilog.Extensions.Logging | 3.1.0 | 8.0.0 | -| Serilog.Sinks.Async | 1.4.0 | 1.5.0 | -| Serilog.Sinks.Console | 3.1.1 | 5.0.0 | -| Serilog.Sinks.File | 4.1.0 | 5.0.0 | -| SharpZipLib | 1.3.3 | 1.4.2 | -| Shouldly | 4.0.3 | 4.2.1 | -| SixLabors.ImageSharp.Drawing | 2.0.0 | 2.0.1 | -| Slugify.Core | 3.0.0 | 4.0.1 | -| StackExchange.Redis | 2.6.122 | 2.7.4 | -| Swashbuckle.AspNetCore | 6.2.1 | 6.5.0 | -| System.Collections.Immutable | 7.0.0 | 8.0.0 | -| System.Linq.Dynamic.Core | 1.3.3 | 1.3.5 | -| System.Security.Permissions | 7.0.0 | 8.0.0 | -| System.Text.Encoding.CodePages | 7.0.0 | 8.0.0 | -| System.Text.Encodings.Web | 7.0.0 | 8.0.0 | -| System.Text.Json | 7.0.0 | 8.0.0 | -| TimeZoneConverter | 5.0.0 | 6.1.0 | -| xunit | 2.4.1 | 2.6.1 | -| xunit.extensibility.execution | 2.4.1 | 2.6.1 | -| xunit.runner.visualstudio | 2.4.5 | 2.5.3 | diff --git a/docs/en/Migration-Guides/Abp-8_1.md b/docs/en/Migration-Guides/Abp-8_1.md deleted file mode 100644 index f13ac8d209..0000000000 --- a/docs/en/Migration-Guides/Abp-8_1.md +++ /dev/null @@ -1,146 +0,0 @@ -# ABP Version 8.1 Migration Guide - -This document is a guide for upgrading ABP v8.0 solutions to ABP v8.1. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## Added `NormalizedName` property to `Tenant` - -The `Tenant` entity has a new property called `NormalizedName`. It is used to find/cache a tenant by its name in a case-insensitive way. -This property is automatically set when a tenant is created or updated. It gets the normalized name of the tenant name by `UpperInvariantTenantNormalizer(ITenantNormalizer)` service. You can implement this service to change the normalization logic. - -### `ITenantStore` - -The `ITenantStore` will use the `NormalizedName` parameter to get tenants, Please use the `ITenantNormalizer` to normalize the tenant name before calling the `ITenantStore` methods. - -### Update `NormalizedName` in `appsettings.json` - -If your tenants defined in the `appsettings.json` file, you should add the `NormalizedName` property to your tenants. - -````json -"Tenants": [ - { - "Id": "446a5211-3d72-4339-9adc-845151f8ada0", - "Name": "tenant1", - "NormalizedName": "TENANT1" // <-- Add this property - }, - { - "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", - "Name": "tenant2", - "NormalizedName": "TENANT2", // <-- Add this property - "ConnectionStrings": { - "Default": "...tenant2's db connection string here..." - } - } - ] -```` - -### Update `NormalizedName` in the database - -Please add a sql script to your migration to set the `NormalizedName` property of the existing tenants. You can use the following script: - -> This script is for the SQL Server database. You can change it for your database. - -> The table name `SaasTenants` is used for ABP commercial Saas module. `AbpTenants` is for the ABP open-source Tenant Management module. - -```sql -UPDATE SaasTenants SET NormalizedName = UPPER(Name) WHERE NormalizedName IS NULL OR NormalizedName = '' -``` - -```csharp -/// -public partial class Add_NormalizedName : Migration -{ - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "NormalizedName", - table: "SaasTenants", - type: "nvarchar(64)", - nullable: false, - defaultValue: ""); - - migrationBuilder.Sql("UPDATE SaasTenants SET NormalizedName = UPPER(Name) WHERE NormalizedName IS NULL OR NormalizedName = ''"); - - migrationBuilder.CreateIndex( - name: "IX_SaasTenants_NormalizedName", - table: "SaasTenants", - column: "NormalizedName"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_SaasTenants_NormalizedName", - table: "SaasTenants"); - - migrationBuilder.DropColumn( - name: "NormalizedName", - table: "SaasTenants"); - } -} -``` - -See https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#adding-raw-sql to learn how to add raw SQL to migrations. - -## Use `Asp.Versioning.Mvc` to replace `Microsoft.AspNetCore.Mvc.Versioning` - -The Microsoft.AspNetCore.Mvc.Versioning packages are now deprecated and superseded by Asp.Versioning.Mvc. -See the announcement here: https://github.com/dotnet/aspnet-api-versioning/discussions/807 - -The namespace of the `[ControllerName]` attribute has changed to `using Asp.Versioning`, Please update your code to use the new namespace. - -Related PR: https://github.com/abpframework/abp/pull/18380 - -## New asynchronous methods for `IAppUrlProvider` - -The `IsRedirectAllowedUrl` method of `IAppUrlProvider` has been changed to `IsRedirectAllowedUrlAsync` and it is now an async method. -You should update your usage of `IAppUrlProvider` to use the new method. - -Related PR: https://github.com/abpframework/abp/pull/18492 - -## New attribute: `ExposeKeyedServiceAttribute` - -The new `ExposeKeyedServiceAttribute` is used to control which keyed services are provided by the related class. Example: - -````C# -[ExposeKeyedService("taxCalculator")] -[ExposeKeyedService("calculator")] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ -} -```` - -In the example above, the `TaxCalculator` class exposes the `ITaxCalculator` interface with the key `taxCalculator` and the `ICalculator` interface with the key `calculator`. That means you can get keyed services from the `IServiceProvider` as shown below: - -````C# -var taxCalculator = ServiceProvider.GetRequiredKeyedService("taxCalculator"); -var calculator = ServiceProvider.GetRequiredKeyedService("calculator"); -```` - -Also, you can use the [`FromKeyedServicesAttribute`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute?view=dotnet-plat-ext-8.0) to resolve a certain keyed service in the constructor: - -```csharp -public class MyClass -{ - //... - public MyClass([FromKeyedServices("taxCalculator")] ITaxCalculator taxCalculator) - { - TaxCalculator = taxCalculator; - } -} -``` - -> Notice that the `ExposeKeyedServiceAttribute` only exposes the keyed services. So, you can not inject the `ITaxCalculator` or `ICalculator` interfaces in your application without using the `FromKeyedServicesAttribute` as shown in the example above. If you want to expose both keyed and non-keyed services, you can use the `ExposeServicesAttribute` and `ExposeKeyedServiceAttribute` attributes together as shown below: -````C# -[ExposeKeyedService("taxCalculator")] -[ExposeKeyedService("calculator")] -[ExposeServices(typeof(ITaxCalculator), typeof(ICalculator))] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ -} -```` - -This is a small **Breaking Change** because `IOnServiceExposingContext` has changed. You should update your usage of `IOnServiceExposingContext` if you have related code. - -Related PR: https://github.com/abpframework/abp/pull/18819 diff --git a/docs/en/Migration-Guides/Abp-8_2.md b/docs/en/Migration-Guides/Abp-8_2.md deleted file mode 100644 index 5412fe47c0..0000000000 --- a/docs/en/Migration-Guides/Abp-8_2.md +++ /dev/null @@ -1,61 +0,0 @@ -# ABP Version 8.2 Migration Guide - -This document is a guide for upgrading ABP v8.x solutions to ABP v8.2. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. - -## Updated target frameworks to net8.0 for template projects - -`TargetFrameworks` of the following template projects are upgraded to **net8.0**: - -* `*.Application.Contracts` -* `*.Domain.Shared` -* `*.Domain` -* `*.MongoDB` -* `*.HttpApi.Client` -* `*.Host.Shared` (for module template) - -Before this version, all of the projects above were targeting multiple frameworks (**netstandard2.0**, **netstandard2.1** and **net8.0**), with this version, we started to only target **net8.0** for these template projects. Note that, all other shared libraries still target multiple frameworks. - -> This change should not affect your pre-existing solutions and you don't need to make any changes in your application. See the PR for more info: https://github.com/abpframework/abp/pull/19565 - -## Upgraded AutoMapper to 13.0.1 - -In this version, **AutoMapper** library version upgraded to 13.0.1. See [the release notes of AutoMapper v13.0.1](https://github.com/AutoMapper/AutoMapper/releases/tag/v13.0.1) for more information. - -## Added default padding to `.tab-content` class for Basic Theme - -In this version, default padding (padding-top: 1.5rem and padding-bottom: 1.5rem) has been added to the tab-content for the Basic Theme. See [#19475](https://github.com/abpframework/abp/pull/19475) for more information. - -## Moved members page directory for Blogging Module - -With this version on, ABP Framework allows you to use single blog mode, without needing to define a blog and a prefix. With this change, the following namespace changes were done: -* `Volo.Blogging.Pages.Blog` -> `Volo.Blogging.Pages.Blogs` -* `Volo.Blogging.Pages.Members` -> `Volo.Blogging.Pages.Blogs.Members` (members folder) - -> If you haven't overridden the pages above, then you don't need to make any additional changes. See [#19418](https://github.com/abpframework/abp/pull/19418) for more information. - -## Removed `FlagIcon` property from the `ILanguageInfo` - -The `FlagIcon` property has been removed from the `ILanguageInfo` interface since we removed the flag icon library in the earlier versions from all of our themes and none of them using it now. - -If the flag icon has been specified while defining the localization languages, then it should be removed: - -```diff - Configure(options => - { -- options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi", "in")); -+ options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi")); - //... - } -``` - -## Blazor Full-Stack Web UI - -In this version, ABP Framework provides a new UI option called **Blazor Full-Stack WebApp**. We have already created an introduction/migration guide for you to check it: [Migrating to Blazor Web App](Abp-8-2-Blazor-Web-App.md) - -> Please read the documentation carefully if you are considering migrating your existing **Blazor** project to **Blazor WebApp**. - -## Session Management Infrastructure - -The **Session Management** feature allows you to prevent concurrent login and manage user sessions. - -In this version, a new entity called `IdentitySession` has been added to the framework and you should create a new migration and apply it to your database. \ No newline at end of file diff --git a/docs/en/Migration-Guides/BlazorUI-3_3.md b/docs/en/Migration-Guides/BlazorUI-3_3.md deleted file mode 100644 index bb3d07f615..0000000000 --- a/docs/en/Migration-Guides/BlazorUI-3_3.md +++ /dev/null @@ -1,14 +0,0 @@ -# Migration Guide for the Blazor UI from the v3.2 to the v3.3 - -## Startup Template Changes - -* Remove `Volo.Abp.Account.Blazor` NuGet package from your `.Blazor.csproj` and add `Volo.Abp.TenantManagement.Blazor` NuGet package. -* Remove the ``typeof(AbpAccountBlazorModule)`` from the dependency list of *YourProjectBlazorModule* class and add the `typeof(AbpTenantManagementBlazorModule)`. -* Add `@using Volo.Abp.BlazoriseUI` and `@using Volo.Abp.BlazoriseUI.Components` into the `_Imports.razor` file. -* Remove the `div` with `id="blazor-error-ui"` (with its contents) from the `wwwroot/index.html ` file, since the ABP Framework now shows error messages as a better message box. -* Update the`AddOidcAuthentication` options in your *YourProjectBlazorModule* class as described in the issue [#5913](https://github.com/abpframework/abp/issues/5913). - -## BlazoriseCrudPageBase to AbpCrudPageBase - -Renamed `BlazoriseCrudPageBase` to `AbpCrudPageBase`. Just update the usages. It also has some changes, you may need to update method calls/usages manually. - diff --git a/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md b/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md deleted file mode 100644 index aff389ccf8..0000000000 --- a/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md +++ /dev/null @@ -1,230 +0,0 @@ -# Migrating from OpenIddict to IdentityServer4 Step by Step Guide - -ABP startup templates use `OpenIddict` OpenID provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is the default OpenID provider library for ABP templates since v6.0, you may want to keep using [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) library, even it is **archived and no longer maintained by the owners**. ABP doesn't provide support for newer versions of IdentityServer. This guide provides layer-by-layer guidance for migrating your existing [OpenIddict](https://github.com/openiddict/openiddict-core) application to IdentityServer4. - -## IdentityServer4 Migration Steps - -Use the `abp update` command to update your existing application. See [Upgrading docs](../Upgrading.md) for more info. Apply required migrations by following the [Migration Guides](Index.md) based on your application version. - -### Domain.Shared Layer - -- In **MyApplication.Domain.Shared.csproj** replace **project reference**: - -```csharp - -``` - - with - -```csharp - -``` - -- In **MyApplicationDomainSharedModule.cs** replace usings and **module dependencies:** - -```csharp -using Volo.Abp.OpenIddict; -... -typeof(AbpOpenIddictDomainSharedModule) -``` - - with - -```csharp -using Volo.Abp.IdentityServer; -... -typeof(AbpIdentityServerDomainSharedModule) -``` - -### Domain Layer - -- In **MyApplication.Domain.csproj** replace **project references**: - -```csharp - - -``` - - with - -```csharp - - -``` - -- In **MyApplicationDomainModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.OpenIddict; -using Volo.Abp.PermissionManagement.OpenIddict; -... -typeof(AbpOpenIddictDomainModule), -typeof(AbpPermissionManagementDomainOpenIddictModule), -``` - - with - -```csharp -using Volo.Abp.IdentityServer; -using Volo.Abp.PermissionManagement.IdentityServer; -... -typeof(AbpIdentityServerDomainModule), -typeof(AbpPermissionManagementDomainIdentityServerModule), -``` - -#### OpenIddictDataSeedContributor - -DataSeeder is the most important part for starting the application since it seeds the initial data for both OpenID providers. - -- Create a folder named *IdentityServer* under the Domain project and copy the [IdentityServerDataSeedContributor.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.Domain/IdentityServer/IdentityServerDataSeedContributor.cs) under this folder. **Rename** all the `OpenId2Ids` with your project name. -- Delete *OpenIddict* folder that contains `OpenIddictDataSeedContributor.cs` which is no longer needed. - -### EntityFrameworkCore Layer - -If you are using MongoDB, skip this step and check the *MongoDB* layer section. - -- In **MyApplication.EntityFrameworkCore.csproj** replace **project reference**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationEntityFrameworkCoreModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.OpenIddict.EntityFrameworkCore; -... -typeof(AbpOpenIddictEntityFrameworkCoreModule), -``` - - with - -```csharp -using Volo.Abp.IdentityServer.EntityFrameworkCore; -... -typeof(AbpIdentityServerEntityFrameworkCoreModule), -``` - -- In **MyApplicationDbContext.cs** replace usings and **fluent api configurations**: - - ```csharp - using Volo.Abp.OpenIddict.EntityFrameworkCore; - ... - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - ... - builder.ConfigureOpenIddict(); - ``` - - with - - ```csharp - using Volo.Abp.IdentityServer.EntityFrameworkCore; - ... - using Volo.Abp.OpenIddict.EntityFrameworkCore; - ... - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - ... - builder.ConfigureIdentityServer(); - ``` - -> Not: You need to create new migration after updating the fluent api. Navigate to *EntityFrameworkCore* folder and add a new migration. Ex, `dotnet ef migrations add Updated_To_IdentityServer ` - -### MongoDB Layer - -If you are using EntityFrameworkCore, skip this step and check the *EntityFrameworkCore* layer section. - -- In **MyApplication.MongoDB.csproj** replace **project reference**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationMongoDbModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.OpenIddict.MongoDB; -... -typeof(AbpOpenIddictMongoDbModule), -``` - - with - -```csharp -using Volo.Abp.IdentityServer.MongoDB; -... -typeof(AbpIdentityServerMongoDbModule), -``` - -### DbMigrator Project - -- In `appsettings.json` **replace OpenIddict section with IdentityServer** since IdentityServerDataSeeder will be using these information for initial data seeding: - - ```json - "IdentityServer": { // Rename OpenIddict to IdentityServer - "Clients ": { // Rename Applications to Clients - ... - } - } - ``` - - -### Test Project - -- In **MyApplicationTestBaseModule.cs** **add** the IdentityServer related using and PreConfigurations: - - ```csharp - using Volo.Abp.IdentityServer; - ``` - - and - - ```csharp - PreConfigure(options => - { - options.AddDeveloperSigningCredential = false; - }); - - PreConfigure(identityServerBuilder => - { - identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString()); - }); - ``` - - to `PreConfigureServices` to run authentication related unit tests. - -### UI Layer - -You can follow the migrations guides from IdentityServer to OpenIddict in **reverse order** to update your UIs. You can also check the source-code for [Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml) and [Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml.cs) files for **AuthServer** project. - -- [Angular UI Migration](OpenIddict-Angular.md) -- [MVC/Razor UI Migration](OpenIddict-Mvc.md) -- [Blazor-Server UI Migration](OpenIddict-Blazor-Server.md) -- [Blazor-Wasm UI Migration](OpenIddict-Blazor.md) - -## Source code of samples and module - -* [Open source tiered & separate auth server application migrate OpenIddict to Identity Server](https://github.com/abpframework/abp-samples/tree/master/OpenId2Ids) -* [IdentityServer module document](https://docs.abp.io/en/abp/6.0/Modules/IdentityServer) -* [IdentityServer module source code](https://github.com/abpframework/abp/tree/rel-6.0/modules/identityserver) diff --git a/docs/en/Migration-Guides/Index.md b/docs/en/Migration-Guides/Index.md deleted file mode 100644 index 2718bc80fb..0000000000 --- a/docs/en/Migration-Guides/Index.md +++ /dev/null @@ -1,20 +0,0 @@ -# ABP Framework Migration Guides - -The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../Upgrading.md). - -- [8.x to 8.2](Abp-8_2.md) -- [8.0 to 8.1](Abp-8_1.md) -- [7.x to 8.0](Abp-8_0.md) -- [7.3 to 7.4](Abp-7_4.md) -- [7.2 to 7.3](Abp-7_3.md) -- [7.1 to 7.2](Abp-7_2.md) -- [7.0 to 7.1](Abp-7_1.md) -- [6.0 to 7.0](Abp-7_0.md) -- [5.3 to 6.0](Abp-6_0.md) -- [5.2 to 5.3](Abp-5_3.md) -- [5.1 to 5.2](Abp-5_2.md) -- [4.x to 5.0](Abp-5_0.md) -- [4.2 to 4.3](Abp-4_3.md) -- [4.x to 4.2](Abp-4_2.md) -- [3.3.x to 4.0](Abp-4_0.md) -- [2.9.x to 3.0](../UI/Angular/Migration-Guide-v3.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Angular.md b/docs/en/Migration-Guides/OpenIddict-Angular.md deleted file mode 100644 index fe0b3986ac..0000000000 --- a/docs/en/Migration-Guides/OpenIddict-Angular.md +++ /dev/null @@ -1,170 +0,0 @@ -# OpenIddict Angular UI Migration Guide - -## Angular Project - -- In `environment.ts` and `environment.prod.ts` **add a trailing slash at the end of the issuer**: - - ```typescript - oAuthConfig: { - issuer: 'https://localhost:44377/', - ... - }, - ``` - -## Http.Api.Host (Non-Separated IdentityServer) - -- In **MyApplication.HttpApi.Host.csproj** replace **project references**: - - ```csharp - - - ``` - - with - - ```csharp - - ``` - -- In the **MyApplicationHttpApiHostModule.cs** replace usings and **module dependencies**: - - ```csharp - using Volo.Abp.AspNetCore.Authentication.JwtBearer; - ... - typeof(AbpAspNetCoreAuthenticationJwtBearerModule), - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - using OpenIddict.Validation.AspNetCore; - ... - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationHttpApiHostModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In the **MyApplicationHttpApiHostModule.cs** `ConfigureServices` method, **replace the method call**: - - From `ConfigureAuthentication(context, configuration);` to `ConfigureAuthentication(context);` and update the method as: - - ```csharp - private void ConfigureAuthentication(ServiceConfigurationContext context) - { - context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - } - ``` - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, **replace the midware**: - - ```csharp - app.UseJwtTokenMiddleware(); - app.UseIdentityServer(); - ``` - - with - - ```csharp - app.UseAbpOpenIddictValidation(); - ``` - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - - ```json - "AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" - }, - ``` - -## Http.Api.Host (Separated IdentityServer) - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - - ```json - "AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" - }, - ``` - -## IdentityServer - -This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - -- In **MyApplication.IdentityServer.csproj** replace **project references**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In the **MyApplicationIdentityServerModule.cs** replace usings and **module dependencies**: - - ```csharp - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationIdentityServerModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In the **MyApplicationIdentityServerModule.cs** `OnApplicationInitialization` method, **remove the midware**: - - ```csharp - app.UseIdentityServer(); - ``` - -- To use the new AuthServer page, replace **Index.cshtml.cs** with [AuthServer Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml) and **Index.cshtml** file with [AuthServer Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml.cs) and rename **Ids2OpenId** with your application namespace. - - > Note: It can be found under the *Pages* folder. - -## See Also - -* [OpenIddict Step-by-Step Guide](OpenIddict-Step-by-Step.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md b/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md deleted file mode 100644 index 93c9562a4d..0000000000 --- a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md +++ /dev/null @@ -1,175 +0,0 @@ -# OpenIddict Blazor-Server UI Migration Guide - -## Blazor Project (Non-Tiered Solution) - -- In the **MyApplication.Blazor.csproj** replace **project references**: - - ```csharp - - - ``` - - with - - ```csharp - - ``` - -- In the **MyApplicationBlazorModule.cs** replace usings and **module dependencies**: - - ```csharp - using System; - using System.Net.Http; - using Volo.Abp.AspNetCore.Authentication.JwtBearer; - ... - typeof(AbpAspNetCoreAuthenticationJwtBearerModule), - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - using OpenIddict.Validation.AspNetCore; - ... - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationBlazorModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In the **MyApplicationBlazorModule.cs** `ConfigureServices` method, **replace the method call**: - - From `ConfigureAuthentication(context, configuration);` to `ConfigureAuthentication(context);` and update the method as: - - ```csharp - private void ConfigureAuthentication(ServiceConfigurationContext context) - { - context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - } - ``` - -- In the **MyApplicationBlazorModule.cs** `OnApplicationInitialization` method, **replace the midware**: - - ```csharp - app.UseJwtTokenMiddleware(); - app.UseIdentityServer(); - ``` - - with - - ```csharp - app.UseAbpOpenIddictValidation(); - ``` - -## Blazor Project (Tiered Solution) - -- In the **MyApplicationWebModule.cs** update the `AddAbpOpenIdConnect` configurations: - - ```csharp - .AddAbpOpenIdConnect("oidc", options => - { - options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - - options.Scope.Add("roles"); // Replace "role" with "roles" - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("MyApplication"); - }); - ``` - - Replace **role** scope with **roles**. - -## IdentityServer - -This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - -- In **MyApplication.IdentityServer.csproj** replace **project references**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationIdentityServerModule.cs** replace usings and **module dependencies**: - - ```csharp - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationIdentityServerModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In **MyApplicationIdentityServerModule.cs** `OnApplicationInitialization` method **remove IdentityServer midware**: - - ```csharp - app.UseIdentityServer(); - ``` - -## Http.Api.Host - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - -```json -"AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" -}, -``` - -- To use the new AuthServer page, replace **Index.cshtml.cs** with [AuthServer Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml) and **Index.cshtml** file with [AuthServer Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml.cs) and rename **Ids2OpenId** with your application namespace. - - > Note: It can be found under the *Pages* folder. - -## See Also - -* [OpenIddict Step-by-Step Guide](OpenIddict-Step-by-Step.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor.md b/docs/en/Migration-Guides/OpenIddict-Blazor.md deleted file mode 100644 index 09e137a0a6..0000000000 --- a/docs/en/Migration-Guides/OpenIddict-Blazor.md +++ /dev/null @@ -1,189 +0,0 @@ -# OpenIddict Blazor Wasm UI Migration Guide - -## Blazor Project - -- In the **MyApplicationBlazorModule.cs** update the `ConfigureAuthentication` method: - - ```csharp - builder.Services.AddOidcAuthentication(options => - { - ... - options.UserOptions.RoleClaim = JwtClaimTypes.Role; - - options.ProviderOptions.DefaultScopes.Add("role"); - ... - }); - ``` - - Update **UserOptions** and **role scope** as below - - ```csharp - builder.Services.AddOidcAuthentication(options => - { - ... - options.UserOptions.NameClaim = OpenIddictConstants.Claims.Name; - options.UserOptions.RoleClaim = OpenIddictConstants.Claims.Role; - - options.ProviderOptions.DefaultScopes.Add("roles"); - ... - }); - ``` - -## Http.Api.Host (Non-Separated IdentityServer) - -- In the **MyApplication.HttpApi.Host.csproj** replace **project references**: - - ```csharp - - - ``` - - with - - ```csharp - - ``` - -- In the **MyApplicationHttpApiHostModule.cs** replace usings and **module dependencies**: - - ```csharp - using System.Net.Http; - using Volo.Abp.AspNetCore.Authentication.JwtBearer; - ... - typeof(AbpAspNetCoreAuthenticationJwtBearerModule), - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - using OpenIddict.Validation.AspNetCore; - ... - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationHostModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In the **MyApplicationHostModule.cs** `ConfigureServices` method, **replace the method call**: - - From `ConfigureAuthentication(context, configuration);` to `ConfigureAuthentication(context);` and update the method as: - - ```csharp - private void ConfigureAuthentication(ServiceConfigurationContext context) - { - context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - } - ``` - -- In the **MyApplicationHostModule.cs** `OnApplicationInitialization` method, **replace the midware**: - - ```csharp - app.UseJwtTokenMiddleware(); - app.UseIdentityServer(); - ``` - - with - - ```csharp - app.UseAbpOpenIddictValidation(); - ``` - -- Delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - - ```json - "AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" - }, - ``` - -## Http.Api.Host (Separated IdentityServer) - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - - ```json - "AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" - }, - ``` - -## IdentityServer - -This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - -- In **MyApplication.IdentityServer.csproj** replace **project references**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In the **MyApplicationIdentityServerModule.cs** replace usings and **module dependencies**: - - ```csharp - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationIdentityServerModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In the **MyApplicationIdentityServerModule.cs** `OnApplicationInitialization` method, **remove the midware**: - - ```csharp - app.UseIdentityServer(); - ``` - -- To use the new AuthServer page, replace **Index.cshtml.cs** with [AuthServer Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml) and **Index.cshtml** file with [AuthServer Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml.cs) and rename **Ids2OpenId** with your application namespace. - - > Note: It can be found under the *Pages* folder. - -## See Also - -* [OpenIddict Step-by-Step Guide](OpenIddict-Step-by-Step.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Mvc.md b/docs/en/Migration-Guides/OpenIddict-Mvc.md deleted file mode 100644 index 8dd5ec6c94..0000000000 --- a/docs/en/Migration-Guides/OpenIddict-Mvc.md +++ /dev/null @@ -1,183 +0,0 @@ -# OpenIddict MVC/Razor UI Migration Guide - -## Web Project (Non-Tiered Solution) - -- In **MyApplication.Web.csproj** replace **project references**: - - ```csharp - - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationWebModule.cs** replace usings and **module dependencies**: - - ```csharp - using Volo.Abp.AspNetCore.Authentication.JwtBearer; - ... - typeof(AbpAccountWebIdentityServerModule), - typeof(AbpAspNetCoreAuthenticationJwtBearerModule), - ``` - - with - - ```csharp - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In **MyApplicationWebModule.cs** `ConfigureServices` method **update authentication configuration**: - - ```csharp - ConfigureAuthentication(context, configuration); - ``` - - with - - ```csharp - ConfigureAuthentication(context); - ``` - - and update the `ConfigureAuthentication` private method to: - - ```csharp - private void ConfigureAuthentication(ServiceConfigurationContext context) - { - context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - } - ``` - - - In the **MyApplicationWebModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In **MyApplicationWebModule.cs** `OnApplicationInitialization` method **replace IdentityServer and JwtToken midwares**: - - ```csharp - app.UseJwtTokenMiddleware(); - app.UseIdentityServer(); - ``` - - with - - ```csharp - app.UseAbpOpenIddictValidation(); - ``` - - -## Web Project (Tiered Solution) - -- In the **MyApplicationWebModule.cs** update the `AddAbpOpenIdConnect` configurations: - - ```csharp - .AddAbpOpenIdConnect("oidc", options => - { - options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - - options.UsePkce = true; // Add this line - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true - - options.Scope.Add("roles"); // Replace "role" with "roles" - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("MyApplication"); - }); - ``` - -Replace role scope to **roles** and add **UsePkce** and **SignoutScheme** options. - -## IdentityServer - -This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - -- In **MyApplication.IdentityServer.csproj** replace **project references**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationIdentityServerModule.cs** replace usings and **module dependencies**: - - ```csharp - typeof(AbpAccountWebIdentityServerModule), - ``` - - with - - ```csharp - typeof(AbpAccountWebOpenIddictModule), - ``` - -- In the **MyApplicationIdentityServerModule.cs** add `PreConfigureServices` like below with your application name as the audience: - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(builder => - { - builder.AddValidation(options => - { - options.AddAudiences("MyApplication"); // Replace with your application name - options.UseLocalServer(); - options.UseAspNetCore(); - }); - }); - } - ``` - -- In **MyApplicationIdentityServerModule.cs** `OnApplicationInitialization` method **remove IdentityServer midware**: - - ```csharp - app.UseIdentityServer(); - ``` - -- To use the new AuthServer page, replace **Index.cshtml.cs** with [AuthServer Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml) and **Index.cshtml** file with [AuthServer Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.IdentityServer/Pages/Index.cshtml.cs) and rename **Ids2OpenId** with your application namespace. - - > Note: It can be found under the *Pages* folder. - -## Http.Api.Host - -- In the **MyApplicationHttpApiHostModule.cs** `OnApplicationInitialization` method, delete `c.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` in `app.UseAbpSwaggerUI` options configurations which is no longer needed. - -- In `appsettings.json` delete **SwaggerClientSecret** from the *AuthServer* section like below: - - ```json - "AuthServer": { - "Authority": "https://localhost:44345", - "RequireHttpsMetadata": "false", - "SwaggerClientId": "MyApplication_Swagger" - }, - ``` - -## See Also - -* [OpenIddict Step-by-Step Guide](OpenIddict-Step-by-Step.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md b/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md deleted file mode 100644 index 8aff29c84d..0000000000 --- a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md +++ /dev/null @@ -1,266 +0,0 @@ -# Migrating from IdentityServer to OpenIddict Step by Step Guide - -This guide provides layer-by-layer guidance for migrating your existing application to [OpenIddict](https://github.com/openiddict/openiddict-core) from IdentityServer. ABP startup templates use `OpenIddict` OpenId provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is only available with ABP v6.0, you will need to update your existing application in order to apply OpenIddict changes. - -## History -We are not removing Identity Server packages and we will continue to release new versions of IdentityServer-related NuGet/NPM packages. That means you won't have an issue while upgrading to v6.0 when the stable version releases. We will continue to fix bugs in our packages for a while. ABP 7.0 will be based on .NET 7. If Identity Server continues to work with .NET 7, we will also continue to ship NuGet packages for our IDS integration. - -On the other hand, Identity Server ends support for the open-source Identity Server at the end of 2022. The Identity Server team has decided to move to Duende IDS and ABP will not be migrated to the commercial Duende IDS. You can see the Duende Identity Server announcement from [this link](https://blog.duendesoftware.com/posts/20220111_fair_trade). - -## Commercial Template - -If you are using a commercial template, please check [Migrating from IdentityServer to OpenIddict for the Commercial Templates](https://docs.abp.io/en/commercial/6.0/migration-guides/openIddict-step-by-step) guide. -If you are using the microservice template, please check [Migrating the Microservice Template from IdentityServer to OpenIddict](https://docs.abp.io/en/commercial/6.0/migration-guides/openIddict-microservice) guide. - -## OpenIddict Migration Steps - -Use the `abp update` command to update your existing application. See [Upgrading docs](../Upgrading.md) for more info. Apply required migrations by following the [Migration Guides](Index.md) based on your application version. - -### Domain.Shared Layer - -- In **MyApplication.Domain.Shared.csproj** replace **project reference**: - -```csharp - -``` - - with - -```csharp - -``` - -- In **MyApplicationDomainSharedModule.cs** replace usings and **module dependencies:** - -```csharp -using Volo.Abp.IdentityServer; -... -typeof(AbpIdentityServerDomainSharedModule) -``` - - with - -```csharp -using Volo.Abp.OpenIddict; -... -typeof(AbpOpenIddictDomainSharedModule) -``` - -### Domain Layer - -- In **MyApplication.Domain.csproj** replace **project references**: - -```csharp - - -``` - - with - -```csharp - - -``` - -- In **MyApplicationDomainModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.IdentityServer; -using Volo.Abp.PermissionManagement.IdentityServer; -... -typeof(AbpIdentityServerDomainModule), -typeof(AbpPermissionManagementDomainIdentityServerModule), -``` - - with - -```csharp -using Volo.Abp.OpenIddict; -using Volo.Abp.PermissionManagement.OpenIddict; -... -typeof(AbpOpenIddictDomainModule), -typeof(AbpPermissionManagementDomainOpenIddictModule), -``` - -#### OpenIddictDataSeedContributor - -- Create a folder named *OpenIddict* under the Domain project and copy the [OpenIddictDataSeedContributor.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.Domain/OpenIddict/OpenIddictDataSeedContributor.cs) under this folder. **Rename** all the `Ids2OpenId` with your project name. -- Delete *IdentityServer* folder that contains `IdentityServerDataSeedContributor.cs` which is no longer needed. - -You can also create a project with the same name and copy the `OpenIddict` folder of the new project into your project. - -### EntityFrameworkCore Layer - -If you are using MongoDB, skip this step and check the *MongoDB* layer section. - -- In **MyApplication.EntityFrameworkCore.csproj** replace **project reference**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationEntityFrameworkCoreModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.IdentityServer.EntityFrameworkCore; -... -typeof(AbpIdentityServerEntityFrameworkCoreModule), -``` - - with - -```csharp -using Volo.Abp.OpenIddict.EntityFrameworkCore; -... -typeof(AbpOpenIddictEntityFrameworkCoreModule), -``` - -- In **MyApplicationDbContext.cs** replace usings and **fluent api configurations**: - - ```csharp - using Volo.Abp.IdentityServer.EntityFrameworkCore; - ... - using Volo.Abp.OpenIddict.EntityFrameworkCore; - ... - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - ... - builder.ConfigureIdentityServer(); - ``` - - with - - ```csharp - using Volo.Abp.OpenIddict.EntityFrameworkCore; - ... - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - ... - builder.ConfigureOpenIddict(); - ``` - -### MongoDB Layer - -If you are using EntityFrameworkCore, skip this step and check the *EntityFrameworkCore* layer section. - -- In **MyApplication.MongoDB.csproj** replace **project reference**: - - ```csharp - - ``` - - with - - ```csharp - - ``` - -- In **MyApplicationMongoDbModule.cs** replace usings and **module dependencies**: - -```csharp -using Volo.Abp.IdentityServer.MongoDB; -... -typeof(AbpIdentityServerMongoDbModule), -``` - - with - -```csharp -using Volo.Abp.OpenIddict.MongoDB; -... -typeof(AbpOpenIddictMongoDbModule), -``` - -### DbMigrator Project - -- In **MyApplication.DbMigrator.csproj** **add project reference**: - - ```csharp - - ``` - -for creating the host builder. - -- In `appsettings.json` **replace IdentityServer section with OpenIddict:** - - ```json - "OpenIddict": { - "Applications": { - "MyApplication_Web": { - "ClientId": "MyApplication_Web", - "ClientSecret": "1q2w3e*", - "RootUrl": "https://localhost:44384" - }, - "MyApplication_App": { - "ClientId": "MyApplication_App", - "RootUrl": "http://localhost:4200" - }, - "MyApplication_BlazorServerTiered": { - "ClientId": "MyApplication_BlazorServerTiered", - "ClientSecret": "1q2w3e*", - "RootUrl": "https://localhost:44346" - }, - "MyApplication_Swagger": { - "ClientId": "MyApplication_Swagger", - "RootUrl": "https://localhost:44391" - } - } - } - ``` - - Replace **MyApplication** with your application name. - -### Test Project - -- In **MyApplicationTestBaseModule.cs** **remove** the IdentityServer related using and PreConfigurations: - - ```csharp - using Volo.Abp.IdentityServer; - ``` - - and - - ```csharp - PreConfigure(options => - { - options.AddDeveloperSigningCredential = false; - }); - - PreConfigure(identityServerBuilder => - { - identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString()); - }); - ``` - - from `PreConfigureServices`. - -### UI Layer - -- [Angular UI Migration](OpenIddict-Angular.md) -- [MVC/Razor UI Migration](OpenIddict-Mvc.md) -- [Blazor-Server UI Migration](OpenIddict-Blazor-Server.md) -- [Blazor-Wasm UI Migration](OpenIddict-Blazor.md) - -## Source code of samples and module - -* [Open source tiered & separate auth server application migrate Identity Server to OpenIddict](https://github.com/abpframework/abp-samples/tree/master/Ids2OpenId) -* [OpenIddict module document](https://docs.abp.io/en/abp/6.0/Modules/OpenIddict) -* [OpenIddict module source code](https://github.com/abpframework/abp/tree/rel-6.0/modules/openiddict) - -## See Also - -* [ABP Version 6.0 Migration Guide](Abp-6_0.md) diff --git a/docs/en/Migration-Guides/Upgrading-Startup-Template.md b/docs/en/Migration-Guides/Upgrading-Startup-Template.md deleted file mode 100644 index 0b7bbe8aa0..0000000000 --- a/docs/en/Migration-Guides/Upgrading-Startup-Template.md +++ /dev/null @@ -1,88 +0,0 @@ -# Upgrading the Startup Template - -Sometimes we introduce new features/changes that requires to **make changes in the startup template**. We already implement the changes in the startup template for new applications. However, in some cases you need to manually make some minor changes in your existing solution. - -This guide explains a suggested way of upgrading your solution templates, using the WinMerge tool. - -> See also the [Upgrading document](../Upgrading.md) for an overall progress of upgrading. This document focuses on upgrading the startup template. - -## 1) Create Dummy Solutions - -We will create two solutions to compare the changes; - -* The first solution is with your existing version -* The second solution is the version you want to upgrade - -Assume that we are upgrading from the version **4.2.2** to version **4.3.0-rc.1**. First, create two empty folders: - -![upgrade-diff-empty-folders](../images/upgrade-diff-empty-folders.png) - -**A)** Open a command-line terminal inside the `4_2_2` folder and create a new solution with the version `4.2.2` using the ABP [CLI](../CLI.md) (install it if you haven't installed before). - -**Example:** - -````bash -abp new MyCompareApp -u blazor -v 4.2.2 -```` - -> Important: You need to create the solution with the exact configuration of your solution. If your application has Angular UI and MongoDB, you should use the same options here. - -**B)** Then open a command-line terminal inside the `4_3_0-rc1` folder and create a new solution with the version `4.3.0-rc.1` using the ABP [CLI](../CLI.md). - -**Example:** - -````bash -abp new MyCompareApp -u blazor -v 4.3.0-rc.1 -```` - -Now, we have the same application with different versions. - -## 2) Upgrade the Old Application - -If we compare two folders now, we will see unnecessary differences because of NuGet & NPM package differences. It is better to upgrade the old application to the new version before comparing them. - -Open a command-line terminal inside the `4_2_2` folder and type the following command: - -````bash -abp update -v 4.3.0-rc.1 -```` - -This will update all NuGet & NPM packages in your solution. We are ready to compare the folders to see the differences. - -## 3) Compare the Folders - -We will use the [WinMerge](https://winmerge.org/) utility for the comparison. So, please install it if it wasn't installed before. After installation, open the WinMerge application, select the the *File > Open* menu item, select the folders you want to compare: - -![winmerge-open-folders](../images/winmerge-open-folders.png) - -Now, we can click to the *Compare* button to see all the differences. Here, a screenshot from the comparison: - -![winmerge-comparison-result](../images/winmerge-comparison-result.png) - -See the *Comparison result* column or the yellow coloring to understand if two files or folder are different. It shows almost all folders are different. However, don't worry. Generally a few files will be different in a folder and a few lines will be different in a file comparison. - -For example, I select the `MyCompareApp.Blazor.csproj` to understand what's changed in this file: - -![winmerge-file-diff](../images/winmerge-file-diff.png) - -We see that; - -* `Blazorise.Bootstrap` package is upgraded from version `0.9.3-preview6` to version `0.9.3.3`. -* `Blazorise.Icons.FontAwesome` package is upgraded from version `0.9.3-preview6` to version `0.9.3.3`. -* `Volo.Abp.Identity.Blazor` package is replaced by `Volo.Abp.Identity.Blazor.WebAssembly`. -* `Volo.Abp.TenantManagement.Blazor` package is replaced by `Volo.Abp.TenantManagement.Blazor.WebAssembly`. -* `Volo.Abp.SettingManagement.Blazor.WebAssembly` package is newly added. - -In this way, we can understand all the changes. - -## 4) Apply Changes on Your Solution - -Comparison result clearly shows the necessary changes should be done on upgrade. All you need to do is to apply the same changes in your own solution. - -> **It is important you first upgrade your own solution to the new version, using the `abp update` command. Then you can apply the manual changes** - -## Notes - -* Sometimes, you may find some changes are unnecessary for your own solution. You may deleted these or already customized. In these cases, you can just ignore it. -* If you do not upgrade your solution as described in this document, your application will continue to work as long as you implement the breaking changes documented in the [migration guide](Index.md). However, you may not get benefit of some new features those require changes in your solution files. -* Most of the times, there will be a few or no differences on the startup templates. When there are important changes, we write a note to the related migration guide, so you apply them manually. \ No newline at end of file diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md deleted file mode 100644 index 785f631563..0000000000 --- a/docs/en/Module-Development-Basics.md +++ /dev/null @@ -1,194 +0,0 @@ -# Modularity - -## Introduction - -ABP Framework was designed to support to build fully modular applications and systems where every module may have entities, services, database integration, APIs, UI components and so on; - -* This document introduces the basics of the module system. -* [Module development best practice guide](Best-Practices/Index.md) explains some **best practices** to develop **re-usable application modules** based on **DDD** principles and layers. A module designed based on this guide will be **database independent** and can be deployed as a **microservice** if needed. -* [Pre-built application modules](Modules/Index.md) are **ready to use** in any kind of application. -* [Module startup template](Startup-Templates/Module.md) is a jump start way to **create a new module**. -* [ABP CLI](CLI.md) has commands to support modular development. -* All other framework features are compatible to the modularity system. - -## Module Class - -Every module should define a module class. The simplest way of defining a module class is to create a class derived from ``AbpModule`` as shown below: - -````C# -public class BlogModule : AbpModule -{ - -} -```` - -### Configuring Dependency Injection & Other Modules - -#### ConfigureServices Method - -``ConfigureServices`` is the main method to add your services to the dependency injection system and configure other modules. Example: - -> These methods have Async versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //... - } -} -```` - -You can register dependencies one by one as stated in Microsoft's [documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection). But ABP has a **conventional dependency registration system** which automatically register all services in your assembly. See the [dependency Injection](Dependency-Injection.md) documentation for more about the dependency injection system. - -You can also configure other services and modules in this way. Example: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Configure default connection string for the application - Configure(options => - { - options.ConnectionStrings.Default = "......"; - }); - } -} -```` - -> `ConfigureServices` method has an asynchronous version too: `ConfigureServicesAsync`. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one. If you override both asynchronous and synchronous versions, only the asynchronous version will be executed. - -See the [Configuration](Configuration.md) document for more about the configuration system. - -#### Pre & Post Configure Services - -``AbpModule`` class also defines ``PreConfigureServices`` and ``PostConfigureServices`` methods to override and write your code just before and just after ``ConfigureServices``. Notice that the code you have written into these methods will be executed before/after the ``ConfigureServices`` methods of all other modules. - -> These methods have asynchronous versions too. If you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. - -### Application Initialization - -Once all the services of all modules are configured, the application starts by initializing all modules. In this phase, you can resolve services from ``IServiceProvider`` since it's ready and available. - -#### OnApplicationInitialization Method - -You can override ``OnApplicationInitialization`` method to execute code while application is being started. - -**Example:** - -````C# -public class BlogModule : AbpModule -{ - public override void OnApplicationInitialization( - ApplicationInitializationContext context) - { - var myService = context.ServiceProvider.GetService(); - myService.DoSomething(); - } -} -```` - -`OnApplicationInitialization` method has an asynchronous version too. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one. - -**Example:** - -````csharp -public class BlogModule : AbpModule -{ - public override Task OnApplicationInitializationAsync( - ApplicationInitializationContext context) - { - var myService = context.ServiceProvider.GetService(); - await myService.DoSomethingAsync(); - } -} -```` - -> If you override both asynchronous and synchronous versions, only the asynchronous version will be executed. - -``OnApplicationInitialization`` is generally used by the startup module to construct the middleware pipeline for ASP.NET Core applications. - -**Example:** - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -public class AppModule : AbpModule -{ - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMvcWithDefaultRoute(); - } -} -```` - -You can also perform startup logic if your module requires it - -#### Pre & Post Application Initialization - -``AbpModule`` class also defines ``OnPreApplicationInitialization`` and ``OnPostApplicationInitialization`` methods to override and write your code just before and just after ``OnApplicationInitialization``. Notice that the code you have written into these methods will be executed before/after the ``OnApplicationInitialization`` methods of all other modules. - -> These methods have asynchronous versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. - -### Application Shutdown - -Lastly, you can override ``OnApplicationShutdown`` method if you want to execute some code while application is being shutdown. - -> This methods has asynchronous version too. If you want to make asynchronous calls inside this method, override the asynchronous version instead of the synchronous one. - -## Module Dependencies - -In a modular application, it's not unusual for one module to depend upon another module(s). An ABP module must declare a ``[DependsOn]`` attribute if it does have a dependency upon another module, as shown below: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] -public class BlogModule -{ - //... -} -```` - -You can use multiple ``DependsOn`` attribute or pass multiple module types to a single ``DependsOn`` attribute depending on your preference. - -A depended module may depend on another module, but you only need to define your direct dependencies. ABP investigates the dependency graph for the application at startup and initializes/shutdowns modules in the correct order. - -## Additional Module Assemblies - -ABP automatically registers all the services of your module to the [dependency injection](Dependency-Injection.md) system. It finds the service types by scanning types in the assembly that defines your module class. That assembly is considered as the main assembly of your module. - -Typically, every assembly contains a separate module class definition. Then modules depend on each other using the `DependsOn` attribute as explained in the previous section. However, in some rare cases, your module may consist of multiple assemblies, and only one of them defines a module class, and you want to make the other assemblies parts of your module. In that case, you can use the `AdditionalAssembly` attribute as shown below: - -````csharp -[DependsOn(...)] // Your module dependencies as you normally do -[AdditionalAssembly(typeof(BlogService))] // A type in the target assembly -public class BlogModule -{ - //... -} -```` - -In this example, we assume that the `BlogService` class is inside one assembly (`csproj`) and the `BlogModule` class is inside another assembly (`csproj`). With the `AdditionalAssembly` definition, ABP will load the assembly containing the `BlogService` class as a part of the blog module. - -Notice that `BlogService` is only an arbitrary selected type in the target assembly. It is just used to indicate the related assembly. You could use any type in the assembly. - -> WARNING: If you need to use the `AdditionalAssembly`, be sure that you don't design your system in a wrong way. With this example above, the `BlogService` class' assembly should normally have its own module class and the `BlogModule` should depend on it using the `DependsOn` attribute. Do not use the `AdditionalAssembly` attribute when you can already use the `DependsOn` attribute. - -## Framework Modules vs Application Modules - -There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose: - -- **Framework modules**: These are **core modules of the framework** like caching, emailing, theming, security, serialization, validation, EF Core integration, MongoDB integration... etc. They do not have application/business functionalities but makes your daily development easier by providing common infrastructure, integration and abstractions. -- **Application modules**: These modules implement **specific application/business functionalities** like blogging, document management, identity management, tenant management... etc. They generally have their own entities, services, APIs and UI components. See [pre-built application modules](Modules/Index.md). - -## See Also -* [Video tutorial](https://abp.io/video-courses/essentials/modularity) diff --git a/docs/en/Module-Entity-Extensions.md b/docs/en/Module-Entity-Extensions.md deleted file mode 100644 index 93c79120cd..0000000000 --- a/docs/en/Module-Entity-Extensions.md +++ /dev/null @@ -1,512 +0,0 @@ -# Module Entity Extensions - -Module entity extension system is a **high level** extension system that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. - -> The module must be developed the *Module Entity Extensions* system in mind. All the **official modules** supports this system wherever possible. - -## Quick Example - -Open the `YourProjectNameModuleExtensionConfigurator` class inside the `Domain.Shared` project of your solution and change the `ConfigureExtraProperties`method as shown below to add a `SocialSecurityNumber` property to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). - -````csharp -public static void ConfigureExtraProperties() -{ - OneTimeRunner.Run(() => - { - ObjectExtensionManager.Instance.Modules() - .ConfigureIdentity(identity => - { - identity.ConfigureUser(user => - { - user.AddOrUpdateProperty( //property type: string - "SocialSecurityNumber", //property name - property => - { - //validation rules - property.Attributes.Add(new RequiredAttribute()); - property.Attributes.Add( - new StringLengthAttribute(64) { - MinimumLength = 4 - } - ); - - //...other configurations for this property - } - ); - }); - }); - }); -} -```` - ->This method is called inside the `YourProjectNameDomainSharedModule` at the beginning of the application. `OneTimeRunner` is a utility class that guarantees to execute this code only one time per application, since multiple calls are unnecessary. - -* `ObjectExtensionManager.Instance.Modules()` is the starting point to configure a module. `ConfigureIdentity(...)` method is used to configure the entities of the Identity Module. -* `identity.ConfigureUser(...)` is used to configure the user entity of the identity module. Not all entities are designed to be extensible (since it is not needed). Use the intellisense to discover the extensible modules and entities. -* `user.AddOrUpdateProperty(...)` is used to add a new property to the user entity with the `string` type (`AddOrUpdateProperty` method can be called multiple times for the same property of the same entity. Each call can configure the options of the same property, but only one property is added to the entity with the same property name). You can call this method with different property names to add more properties. -* `SocialSecurityNumber` is the name of the new property. -* `AddOrUpdateProperty` gets a second argument (the `property =>` lambda expression) to configure additional options for the new property. - * We can add data annotation attributes like shown here, just like adding a data annotation attribute to a class property. - -#### Create & Update Forms - -Once you define a property, it appears in the create and update forms of the related entity: - -![add-new-property-to-user-form](images/add-new-property-to-user-form.png) - -`SocialSecurityNumber` field comes into the form. Next sections will explain the localization and the validation for this new property. - -### Data Table - -New properties also appear in the data table of the related page: - -![add-new-property-to-user-form](images/add-new-property-to-user-table.png) - -`SocialSecurityNumber` column comes into the table. Next sections will explain the option to hide this column from the data table. - -## Property Options - -There are some options that you can configure while defining a new property. - -### Display Name - -You probably want to set a different (human readable) display name for the property that is shown on the user interface. - -#### Don't Want to Localize? - -If your application is not localized, you can directly set the `DisplayName` for the property to a `FixedLocalizableString` object. Example: - -````csharp -property => -{ - property.DisplayName = new FixedLocalizableString("Social security no"); -} -```` - -#### Localizing the Display Name - -If you want to localize the display name, you have two options. - -##### Localize by Convention - -Instead of setting the `property.DisplayName`, you can directly open your localization file (like `en.json`) and add the following entry to the `texts` section: - -````json -"SocialSecurityNumber": "Social security no" -```` - -Define the same `SocialSecurityNumber` key (the property name you've defined before) in your localization file for each language you support. That's all! - -In some cases, the localization key may conflict with other keys in your localization files. In such cases, you can use the `DisplayName:` prefix for display names in the localization file (`DisplayName:SocialSecurityNumber` as the localization key for this example). Extension system looks for prefixed version first, then fallbacks to the non prefixed name (it then fallbacks to the property name if you haven't localized it). - -> This approach is recommended since it is simple and suitable for most scenarios. - -##### Localize using the `DisplayName` Property - -If you want to specify the localization key or the localization resource, you can still set the `DisplayName` option: - -````csharp -property => -{ - property.DisplayName = - LocalizableString.Create( - "UserSocialSecurityNumberDisplayName" - ); -} -```` - -* `MyProjectNameResource` is the localization resource and `UserSocialSecurityNumberDisplayName` is the localization key in the localization resource. - -> See [the localization document](Localization.md) if you want to learn more about the localization system. - -#### Default Value - -A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. - -There are two ways to override the default value: - -##### DefaultValue Option - -`DefaultValue` option can be set to any value: - -````csharp -property => -{ - property.DefaultValue = 42; -} -```` - -##### DefaultValueFactory Options - -`DefaultValueFactory` can be set to a function that returns the default value: - -````csharp -property => -{ - property.DefaultValueFactory = () => DateTime.Now; -} -```` - -`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . - -> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. - -### Validation - -Entity extension system allows you to define validation for extension properties in a few ways. - -#### Data Annotation Attributes - -`Attributes` is a list of attributes associated to this property. The example code below adds two [data annotation validation attributes](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to the property: - -````csharp -property => -{ - property.Attributes.Add(new RequiredAttribute()); - property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); -} -```` - -When you run the application, you see that the validation works out of the box: - -![add-new-propert-to-user-form](images/add-new-property-to-user-form-validation-error.png) - -Since we've added the `RequiredAttribute`, it doesn't allow to left it blank. The validation system works; - -* On the user interface (with automatic localization). -* On the HTTP API. Even if you directly perform an HTTP request, you get validation errors with a proper HTTP status code. -* On the `SetProperty(...)` method on the entity (see [the document](Entities.md) if you wonder what is the `SetProperty()` method). - -So, it automatically makes a full stack validation. - -> See the [ASP.NET Core MVC Validation document](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to learn more about the attribute based validation. - -##### Default Validation Attributes - -There are some attributes **automatically added** when you create certain type of properties; - -* `RequiredAttribute` is added for **non nullable** primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. If you want to allow nulls, make the property nullable (e.g. `int?`). -* `EnumDataTypeAttribute` is added for **enum types**, to prevent to set invalid enum values. - -Use `property.Attributes.Clear();` if you don't want these attributes. - -#### Validation Actions - -Validation actions allows you to execute a custom code to perform the validation. The example below checks if the `SocialSecurityNumber` starts with `B` and adds a validation error if so: - -````csharp -property => -{ - property.Attributes.Add(new RequiredAttribute()); - property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); - - property.Validators.Add(context => - { - if (((string) context.Value).StartsWith("B")) - { - context.ValidationErrors.Add( - new ValidationResult( - "Social security number can not start with the letter 'B', sorry!", - new[] {"extraProperties.SocialSecurityNumber"} - ) - ); - } - }); - -} -```` - -Using a `RegularExpressionAttribute` might be better in this case, but this is just an example. Anyway, if you enter a value starts with the letter `B` you get the following error **while saving the form**: - -![add-new-propert-to-user-form](images/add-new-property-to-user-form-validation-error-custom.png) - -##### The Context Object - -The `context` object has useful properties that can be used in your custom validation action. For example, you can use the `context.ServiceProvider` to resolve services from the [dependency injection system](Dependency-Injection.md). The example below gets the localizer and adds a localized error message: - -````csharp -if (((string) context.Value).StartsWith("B")) -{ - var localizer = context.ServiceProvider - .GetRequiredService>(); - - context.ValidationErrors.Add( - new ValidationResult( - localizer["SocialSecurityNumberCanNotStartWithB"], - new[] {"extraProperties.SocialSecurityNumber"} - ) - ); -} -```` - ->`context.ServiceProvider` is nullable! It can be `null` only if you use the `SetProperty(...)` method on the object. Because DI system is not available on this time. While this is a rare case, you should perform a fallback logic when `context.ServiceProvider` is `null`. For this example, you would add a non-localized error message. This is not a problem since setting an invalid value to a property generally is a programmer mistake and you mostly don't need to localization in this case. In any way, you would not be able to use localization even in a regular property setter. But, if you are serious about localization, you can throw a business exception (see the [exception handling document](https://docs.abp.io/en/abp/latest/Exception-Handling) to learn how to localize a business exception). - -### UI Visibility - -When you define a property, it appears on the data table, create and edit forms on the related UI page. However, you can control each one individually. Example: - -````csharp -property => -{ - property.UI.OnTable.IsVisible = false; - //...other configurations -} -```` - -Use `property.UI.OnCreateForm` and `property.UI.OnEditForm` to control forms too. If a property is required, but not added to the create form, you definitely get a validation exception, so use this option carefully. But a required property may not be in the edit form if that's your requirement. - -### UI Order - -When you define a property, it appears on the data table, create and edit forms on the related UI page. However, you can control its order. Example: - -````csharp -property => -{ - property.UI.Order = 1; - //...other configurations -} -```` - -Use `property.UI.OnCreateForm` and `property.UI.OnEditForm` to control forms too. If a property is required, but not added to the create form, you definitely get a validation exception, so use this option carefully. But a required property may not be in the edit form if that's your requirement. - -### HTTP API Availability - -Even if you disable a property on UI, it can be still available through the HTTP API. By default, a property is available on all APIs. - -Use the `property.Api` options to make a property unavailable in some API endpoints. - -````csharp -property => -{ - property.Api.OnUpdate.IsAvailable = false; -} -```` - -In this example, Update HTTP API will not allow to set a new value to this property. In this case, you also want to disable this property on the edit form: - -````csharp -property => -{ - property.Api.OnUpdate.IsAvailable = false; - property.UI.OnEditForm.IsVisible = false; -} -```` - -In addition to the `property.Api.OnUpdate`, you can set `property.Api.OnCreate` and `property.Api.OnGet` for a fine control the API endpoint. - -## Special Types - -### Enum - -Module extension system naturally supports the `enum` types. - -An example enum type: - -````csharp -public enum UserType -{ - Regular, - Moderator, - SuperUser -} -```` - -You can add enum properties just like others: - -````csharp -user.AddOrUpdateProperty("Type"); -```` - -An enum properties is shown as combobox (select) in the create/edit forms: - -![add-new-property-enum](images/add-new-property-enum.png) - -#### Localization - -Enum member name is shown on the table and forms by default. If you want to localize it, just create a new entry on your [localization](https://docs.abp.io/en/abp/latest/Localization) file: - -````json -"Enum:UserType.0": "Super user" -```` - -One of the following names can be used as the localization key: - -* `Enum:UserType.0` -* `Enum:UserType.SuperUser` -* `UserType.0` -* `UserType.SuperUser` -* `SuperUser` - -Localization system searches for the key with the given order. Localized text are used on the table and the create/edit forms. - -### Navigation Properties / Foreign Keys - -It is supported to add an extension property to an entity that is Id of another entity (foreign key). - -#### Example: Associate a department to a user - -````csharp -ObjectExtensionManager.Instance.Modules() - .ConfigureIdentity(identity => - { - identity.ConfigureUser(user => - { - user.AddOrUpdateProperty( - "DepartmentId", - property => - { - property.UI.Lookup.Url = "/api/departments"; - property.UI.Lookup.DisplayPropertyName = "name"; - } - ); - }); - }); -```` - -`UI.Lookup.Url` option takes a URL to get list of departments to select on edit/create forms. This endpoint can be a typical controller, an [auto API controller](API/Auto-API-Controllers.md) or any type of endpoint that returns a proper JSON response. - -An example implementation that returns a fixed list of departments (in real life, you get the list from a data source): - -````csharp -[Route("api/departments")] -public class DepartmentController : AbpController -{ - [HttpGet] - public async Task> GetAsync() - { - return new ListResultDto( - new[] - { - new DepartmentDto - { - Id = Guid.Parse("6267f0df-870f-4173-be44-d74b4b56d2bd"), - Name = "Human Resources" - }, - new DepartmentDto - { - Id = Guid.Parse("21c7b61f-330c-489e-8b8c-80e0a78a5cc5"), - Name = "Production" - } - } - ); - } -} -```` - -This API returns such a JSON response: - -````json -{ - "items": [{ - "id": "6267f0df-870f-4173-be44-d74b4b56d2bd", - "name": "Human Resources" - }, { - "id": "21c7b61f-330c-489e-8b8c-80e0a78a5cc5", - "name": "Production" - }] -} -```` - -ABP can now show an auto-complete select component to pick the department while creating or editing a user: - -![extension-navigation-property-form](images/extension-navigation-property-form.png) - -And shows the department name on the data table: - -![extension-navigation-property-form](images/extension-navigation-property-table.png) - -#### Lookup Options - -`UI.Lookup` has the following options to customize how to read the response returned from the `Url`: - -* `Url`: The endpoint to get the list of target entities. This is used on edit and create forms. -* `DisplayPropertyName`: The property in the JSON response to read the display name of the target entity to show on the UI. Default: `text`. -* `ValuePropertyName`: The property in the JSON response to read the Id of the target entity. Default: `id`. -* `FilterParamName`: ABP allows to search/filter the entity list on edit/create forms. This is especially useful if the target list contains a lot of items. In this case, you can return a limited list (top 100, for example) and allow user to search on the list. ABP sends filter text to the server (as a simple query string) with the name of this option. Default: `filter`. -* `ResultListPropertyName`: By default, returned JSON result should contain the entity list in an `items` array. You can change the name of this field. Default: `items`. - -#### Lookup Properties: How Display Name Works? - -You may wonder how ABP shows the department name on the data table above. - -It is easy to understand how to fill the dropdown on edit and create forms: ABP makes an AJAX request to the given URL. It re-requests whenever user types to filter the items. - -However, for the data table, multiple items are shown on the UI and performing a separate AJAX call to get display name of the department for each row would not be so efficient. - -Instead, the display name of the foreign entity is also saved as an extra property of the entity (see *Extra Properties* section of the [Entities](Entities.md) document) in addition to Id of the foreign entity. If you check the database, you can see the `DepartmentId_Text` in the `ExtraProperties` field in the database table: - -````json -{"DepartmentId":"21c7b61f-330c-489e-8b8c-80e0a78a5cc5","DepartmentId_Text":"Production"} -```` - -So, this is a type of *data duplication*. If your target entity's name changes in the database later, there is no automatic synchronization system. The system works as expected, but you see the old name on the data tables. If that's a problem for you, you should care yourself to update this information when display name of your entity changes. - -## Database Mapping - -For relational databases, all extension property values are stored in a single field in the table: - -![add-new-propert-to-user-database-extra-properties](images/add-new-propert-to-user-database-extra-properties.png) - -`ExtraProperties` field stores the properties as a JSON object. While that's fine for some scenarios, you may want to create a dedicated field for your new property. Fortunately, it is very easy to configure. - -If you are using the Entity Framework Core database provider, you can configure the database mapping as shown below: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreProperty( - "SocialSecurityNumber", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(64); - } - ); -```` - -Write this inside the `YourProjectNameEfCoreEntityExtensionMappings` class in your `.EntityFrameworkCore` project. Then you need to use the standard `Add-Migration` and `Update-Database` commands to create a new database migration and apply the change to your database. - -Add-Migration create a new migration as shown below: - -````csharp -public partial class Added_SocialSecurityNumber_To_IdentityUser : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "SocialSecurityNumber", - table: "AbpUsers", - maxLength: 128, - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "SocialSecurityNumber", - table: "AbpUsers"); - } -} -```` - -Once you update your database, you will see that the `AbpUsers` table has the new property as a standard table field: - -![add-new-propert-to-user-database-extra-properties](images/add-new-propert-to-user-database-field.png) - -> If you first created a property without a database table field, then you later needed to move this property to a database table field, it is suggested to execute an SQL command in your migration to copy the old values to the new field. -> -> However, if you don't make it, the ABP Framework seamlessly manages it. It uses the new database field, but fallbacks to the `ExtraProperties` field if it is null. When you save the entity, it moves the value to the new field. - -See the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document for more. - -## More - -See the [Customizing the Modules](Customizing-Application-Modules-Guide.md) guide for an overall index for all the extensibility options. - -Here, a few things you can do: - -* You can create a second entity that maps to the same database table with the extra property as a standard class property (if you've defined the EF Core mapping). For the example above, you can add a `public string SocialSecurityNumber {get; set;}` property to the `AppUser` entity in your application, since the `AppUser` entity is mapped to the same `AbpUser` table. Do this only if you need it, since it brings more complexity to your application. -* You can override a domain or application service to perform custom logics with your new property. -* You can low level control how to add/render a field in the data table on the UI. - -## See Also - -* [Angular UI Extensions](UI/Angular/Extensions-Overall.md) diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md deleted file mode 100644 index 2ae77d532d..0000000000 --- a/docs/en/Modules/Account.md +++ /dev/null @@ -1,80 +0,0 @@ -# Account Module - -Account module implements the basic authentication features like **login**, **register**, **forgot password** and **account management**. - -This module is based on [Microsoft's Identity library](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity) and the [Identity Module](Identity.md). It has [IdentityServer](https://github.com/IdentityServer) integration (based on the [IdentityServer Module](IdentityServer.md)) and [OpenIddict](https://github.com/openiddict) integration (based on the [OpenIddict Module](OpenIddict.md)) to provide **single sign-on**, access control and other advanced authentication features. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/account). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -This section introduces the main pages provided by this module. - -### Login - -`/Account/Login` page provides the login functionality. - -![account-module-login](../images/account-module-login.png) - -Social/external login buttons becomes visible if you setup it. See the *Social/External Logins* section below. Register and Forgot password and links redirect to the pages explained in the next sections. - -### Register - -`/Account/Register` page provides the new user registration functionality. - -![account-module-register](../images/account-module-register.png) - -### Forgot Password & Reset Password - -`/Account/ForgotPassword` page provides a way of sending password reset link to user's email address. The user then clicks to the link and determines a new password. - -![account-module-forgot-password](../images/account-module-forgot-password.png) - -### Account Management - -`/Account/Manage` page is used to change password and personal information of the user. - -![account-module-manage-account](../images/account-module-manage-account.png) - -## OpenIddict Integration - -[Volo.Abp.Account.Web.OpenIddict](https://www.nuget.org/packages/Volo.Abp.Account.Web.OpenIddict) package provides integration for the [OpenIddict](https://github.com/openiddict). This package comes as installed with the [application startup template](../Startup-Templates/Application.md). See the [OpenIddict Module](OpenIddict.md) documentation. - -## IdentityServer Integration - -[Volo.Abp.Account.Web.IdentityServer](https://www.nuget.org/packages/Volo.Abp.Account.Web.IdentityServer) package provides integration for the [IdentityServer](https://github.com/IdentityServer). This package comes as installed with the [application startup template](../Startup-Templates/Application.md). See the [IdentityServer Module](IdentityServer.md) documentation. - -## Social/External Logins - -The Account Module has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. - -### Example: Facebook Authentication - -Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. - -#### Add the NuGet Package - -Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. - -#### Configure the Provider - -Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: - -````csharp -context.Services.AddAuthentication() - .AddFacebook(facebook => - { - facebook.AppId = "..."; - facebook.AppSecret = "..."; - facebook.Scope.Add("email"); - facebook.Scope.Add("public_profile"); - }); -```` - -> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. diff --git a/docs/en/Modules/Audit-Logging.md b/docs/en/Modules/Audit-Logging.md deleted file mode 100644 index 93a50c8387..0000000000 --- a/docs/en/Modules/Audit-Logging.md +++ /dev/null @@ -1,60 +0,0 @@ -# Audit Logging Module - -The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. - -> This document covers only the audit logging module which persists audit logs to a database. See [the audit logging](../Audit-Logging.md) document for more about the audit logging system. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/audit-logging). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## Internals - -### Domain Layer - -#### Aggregates - -- `AuditLog` (aggregate root): Represents an audit log record in the system. - - `EntityChange` (collection): Changed entities of audit log. - - `AuditLogAction` (collection): Executed actions of audit log. - -#### Repositories - -Following custom repositories are defined for this module: - -- `IAuditLogRepository` - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Abp` prefix by default. Set static properties on the `AbpAuditLoggingDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `AbpAuditLogging` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- **AbpAuditLogs** - - AbpAuditLogActions - - AbpEntityChanges - - AbpEntityPropertyChanges - -#### MongoDB - -##### Collections - -- **AbpAuditLogs** - -## See Also - -* [Audit logging system](../Audit-Logging.md) \ No newline at end of file diff --git a/docs/en/Modules/Background-Jobs.md b/docs/en/Modules/Background-Jobs.md deleted file mode 100644 index 5fd57d812c..0000000000 --- a/docs/en/Modules/Background-Jobs.md +++ /dev/null @@ -1,55 +0,0 @@ -# Background Jobs Module - -The Background Jobs module implements the `IBackgroundJobStore` interface and makes possible to use the default background job manager of the ABP Framework. If you don't want to use this module, then you should implement the `IBackgroundJobStore` interface yourself. - -> This document covers only the background jobs module which persists background jobs to a database. See [the background jobs](../Background-Jobs.md) document for more about the background jobs system. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/background-jobs). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## Internals - -### Domain Layer - -#### Aggregates - -- `BackgroundJobRecord` (aggregate root): Represents a background job record. - -#### Repositories - -Following custom repositories are defined for this module: - -- `IBackgroundJobRepository` - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Abp` prefix by default. Set static properties on the `BackgroundJobsDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `AbpBackgroundJobs` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- **AbpBackgroundJobs** - -#### MongoDB - -##### Collections - -- **AbpBackgroundJobs** - -## See Also - -* [Background job system](../Background-Jobs.md) \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Blogging.md b/docs/en/Modules/Cms-Kit/Blogging.md deleted file mode 100644 index eb6619a4bc..0000000000 --- a/docs/en/Modules/Cms-Kit/Blogging.md +++ /dev/null @@ -1,134 +0,0 @@ -# CMS Kit: Blogging - -The blogging feature provides the necessary UI to manage and render blogs and blog posts. - -## Enabling the Blogging Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## User Interface - -### Menu Items - -The following menu items are added by the blogging feature to the admin application: - -* **Blogs**: Blog management page. -* **Blog Posts**: Blog post management page. - -## Pages - -### Blogs - -Blogs page is used to create and manage blogs in your system. - -![blogs-page](../../images/cmskit-module-blogs-page.png) - -A screenshot from the new blog creation modal: - -![blogs-edit](../../images/cmskit-module-blogs-edit.png) - -**Slug** is the URL part of the blog. For this example, the root URL of the blog becomes `your-domain.com/blogs/technical-blog/`. - -- You can change the default slug by using `CmsBlogsWebConsts.BlogRoutePrefix` constant. For example, if you set it to `foo`, the root URL of the blog becomes `your-domain.com/foo/technical-blog/`. - - ```csharp - public override void PreConfigureServices(ServiceConfigurationContext context) - { - CmsBlogsWebConsts.BlogsRoutePrefix = "foo"; - } - ``` - -#### Blog Features - -Blog feature uses some of the other CMS Kit features. You can enable or disable the features by clicking the features action for a blog. - -![blogs-feature-action](../../images/cmskit-module-blogs-feature-action.png) - -You can select/deselect the desired features for blog posts. - -![features-dialog](../../images/cmskit-module-features-dialog-2.png) - -##### Quick Navigation Bar In Blog Post -If you enable "Quick navigation bar in blog posts", it will enabled scroll index as seen below. - -![scroll-index](../../images/cmskit-module-features-scroll-index.png) - -### Blog Post Management - -When you create blogs, you can manage blog posts on this page. - -![blog-posts-page](../../images/cmskit-module-blog-posts-page.png) - -You can create and edit an existing blog post on this page. If you enable specific features such as tags, you can set tags for the blog post on this page. - -![blog-post-edit](../../images/cmskit-module-blog-post-edit.png) - -## Internals - -### Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -- `Blog` _(aggregate root)_: Presents blogs of application. -- `BlogPost`_(aggregate root)_: Presents blog posts in blogs. -- `BlogFeature`:_(aggregate root)_: Presents blog features enabled/disabled state. Such as reactions, ratings, comments, etc. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. The following repositories are defined for this feature: - -- `IBlogRepository` -- `IBlogPostRepository` -- `IBlogFeatureRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -- `BlogManager` -- `BlogPostManager` -- `BlogFeatureManager` - -### Application layer - -#### Application Services - -##### Common - -- `BlogFeatureAppService` _(implements `IBlogFeatureAppService`)_ - -##### Admin - -- `BlogAdminAppService` _(implements `IBlogAdminAppService`)_ -- `BlogFeatureAdminAppService` _(implements `IBlogFeatureAdminAppService`)_ -- `BlogPostAdminAppService` _(implements `IBlogPostAdminAppService`)_ - -##### Public - -- `BlogPostPublicAppService` _(implements `IBlogPostPublicAppService`)_ - -### Database providers - -#### Entity Framework Core - -##### Tables - -- CmsBlogs -- CmsBlogPosts -- CmsBlogFeatures - -#### MongoDB - -##### Collections - -- CmsBlogs -- CmsBlogPosts -- CmsBlogFeatures - -## Entity Extensions - -Check the ["Entity Extensions" section of the CMS Kit Module documentation](Index.md#entity-extensions) to see how to extend entities of the Blogging Feature of the CMS Kit module. \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Comments.md b/docs/en/Modules/Cms-Kit/Comments.md deleted file mode 100644 index ddd39fc675..0000000000 --- a/docs/en/Modules/Cms-Kit/Comments.md +++ /dev/null @@ -1,145 +0,0 @@ -# CMS Kit: Comments - -CMS kit provides a **comment** system to add the comment feature to any kind of resource, like blog posts, products, etc. - -## Enabling the Comment Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## Options - -The comment system provides a mechanism to group comment definitions by entity types. For example, if you want to use the comment system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product`, and add comments under these entity types. - -`CmsKitCommentOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: - -```csharp -Configure(options => -{ - options.EntityTypes.Add(new CommentEntityTypeDefinition("Product")); - options.IsRecaptchaEnabled = true; //false by default - options.AllowedExternalUrls = new Dictionary> - { - { - "Product", - new List - { - "https://abp.io/" - } - } - }; -}); -``` - -> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. - -`CmsKitCommentOptions` properties: - -- `EntityTypes`: List of defined entity types(`CmsKitCommentOptions`) in the comment system. -- `IsRecaptchaEnabled`: This flag enables or disables the reCaptcha for the comment system. You can set it as **true** if you want to use reCaptcha in your comment system. -- `AllowedExternalUrls`: Indicates the allowed external URLs by entity types, which can be included in a comment. If it's specified for a certain entity type, then only the specified external URLs are allowed in the comments. - -`CommentEntityTypeDefinition` properties: - -- `EntityType`: Name of the entity type. - -## The Comments Widget - -The comment system provides a commenting [widget](../../UI/AspNetCore/Widgets.md) to allow users to send comments to resources on public websites. You can simply place the widget on a page like below. - -```csharp -@await Component.InvokeAsync(typeof(CommentingViewComponent), new -{ - entityType = "Product", - entityId = "...", - isReadOnly = false, - referralLinks = new [] {"nofollow"} -}) -``` - -`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. `referralLinks` is an optional parameter. You can use this parameter to add values (such as "nofollow", "noreferrer", or any other values) to the [rel attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) of links. - -## User Interface - -### Menu Items - -The following menu items are added by the commenting feature to the admin application: - -* **Comments**: Opens the comment management page. - -### Pages - -#### Comment Management - -You can view and manage comments on this page. - -![comment-page](../../images/cmskit-module-comment-page.png) - -You can also view and manage replies on this page. - -![comments-detail](../../images/cmskit-module-comments-detail.png) - -## Internals - -### Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -##### Comment - -A comment represents a written comment from a user. - -- `Comment` (aggregate root): Represents a written comment in the system. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -The following custom repositories are defined for this feature: - -- `ICommentRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Comment Manager - -`CommentManager` is used to perform some operations for the `Comment` aggregate root. - -### Application layer - -#### Application services - -- `CommentAdminAppService` (implements `ICommentAdminAppService`): Implements the use cases of the comment management system, like listing or removing comments etc. -- `CommentPublicAppService` (implements `ICommentPublicAppService`): Implements the use cases of the comment management on the public websites, like listing comments, adding comments etc. - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- CmsComments - -#### MongoDB - -##### Collections - -- **CmsComments** - diff --git a/docs/en/Modules/Cms-Kit/Dynamic-Widget.md b/docs/en/Modules/Cms-Kit/Dynamic-Widget.md deleted file mode 100644 index 4ca764b7ea..0000000000 --- a/docs/en/Modules/Cms-Kit/Dynamic-Widget.md +++ /dev/null @@ -1,137 +0,0 @@ -# Dynamic Widget - -CMS kit provides a dynamic [widget](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets) used to render the components previously developed by the software in the content of the pages and blog posts. Its means, that in static content you can use dynamic content. We will mention how you can do it. You have two choices to define the widget in the system: Writing and UI. - -### Adding the widget -Firstly we will show how to use the widget system via writing manually in the page and blog post contents. - -Let's define the view component - -```csharp -[Widget] -[ViewComponent(Name = "CmsToday")] -public class TodayViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("~/ViewComponents/Today.cshtml", - new TodayViewComponent()); - } -} -``` - -```html -@model Volo.CmsKit.ViewComponents.TodayViewComponent - -

Welcome Today Component

-

@DateTime.Now.ToString()

- -``` - -Now configuration time on YourModule.cs -```csharp -Configure(options => - { - options.AddWidget("Today","CmsToday"); - }); -``` - -Now you're ready to add your widget by writing. -[Widget Type="Today"] - -After completing the above steps, you can see the output at the right of the below screenshot. -![cmskit-without-parameter.png](../../images/cmskit-without-parameter.png) - -### Adding by using UI -Now we will mention the second option, using UI. -Once writing these definitions can make some mistakes hence we added a new feature to use the widget system easily. To the right of the editor, you will see the customized `W` button to add a dynamic widget like the below image. Don't forget please this is design mode and you need to view your page in view mode after saving. Also `Preview` tab on the editor will be ready to check your output easily for widget configurations in the next features. - -![cms-kit-page-editor](../../images/cms-kit-page-editor.png) - -### Adding by using UI with parameters -Let's improve the above example by adding a new parameter named format. Via this feature, we can use the widget system with many different scenarios but not prolong the document. Also, these examples can be expandable with dependency injection and getting values from the database, but we will use a basic example. We will add the format parameter to customize the date. - -```csharp -[Widget] -[ViewComponent(Name = "CmsToday")] -public class TodayViewComponent : AbpViewComponent -{ - public string Format { get; set; } - - public IViewComponentResult Invoke(string format) - { - return View("~/ViewComponents/Today.cshtml", - new TodayViewComponent() { Format = format }); - } -} -``` - -```html -@model Volo.CmsKit.ViewComponents.TodayViewComponent - -

Welcome Today Component

-

@DateTime.Now.ToString(Format)

- -``` - -Let's define the format component. -```csharp -[Widget] -[ViewComponent(Name = "Format")] -public class FormatViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("~/ViewComponents/Format.cshtml", - new FormatViewModel()); - } -} - -public class FormatViewModel -{ - [DisplayName("Format your date in the component")] - public string Format { get; set; } -} -``` -> Important Note: To get properties properly you should set the `name` property on the razor page or you may use the ABP component. ABP handles that automatically. - -```html -@using Volo.CmsKit.ViewComponents -@model FormatViewModel - -
- -
-``` - -```csharp -Configure(options => - { - options.AddWidget("Today", "CmsToday", "Format"); - }); -``` - -![cmskit-module-editor-parameter](../../images/cmskit-module-editor-parameter.png) - -In this image, after choosing your widget (on the other case, it changes automatically up to your configuration, mine is `Today`. Its parameter name `parameterWidgetName` and its value is `Format`) you will see the next widget. Enter input values or choose them and click `Add`. You will see the underlined output in the editor. Right of the image, also you can see its previewed output. - -You can edit this output manually if do any wrong coding for that (wrong value or typo) you won't see the widget, even so, your page will be viewed successfully. - -## Options -To configure the widget, you should define the below code in YourModule.cs - -```csharp -Configure(options => - { - options.AddWidget(widgetType: "Today", widgetName: "CmsToday", parameterWidgetName: "Format"); - }); -``` - -Let's look at these parameters in detail -* `widgetType` is used for end-user and more readable names. The following bold word represents widgetType. -[Widget Type="**Today**" Format="yyyy-dd-mm HH:mm:ss"]. - -* `widgetName` is used for your widget name used in code for the name of the `ViewComponent`. - -* `parameterWidgetName` is used the for editor component side to see on the `Add Widget` modal. -After choosing the widget type from listbox (now just defined `Format`) and renders this widget automatically. It's required only to see UI once using parameters \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Global-Resources.md b/docs/en/Modules/Cms-Kit/Global-Resources.md deleted file mode 100644 index 5e0e0154a0..0000000000 --- a/docs/en/Modules/Cms-Kit/Global-Resources.md +++ /dev/null @@ -1,72 +0,0 @@ -# CMS Kit: Global Resources - -CMS Kit Global Resources system allows to add global styles and scripts dynamically. - -## Enabling the Global Resources Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## The User Interface - -### Menu items - -CMS Kit module admin side adds the following items to the main menu, under the *Global Resources* menu item: - -* **Global Resources**: Global resources management page. - -`CmsKitAdminMenus` class has the constants for the menu item names. - -### Global Resources Page - -Global Resources page is used to manage global styles and scripts in the system. - -![cms-kit-global-resources-page](../../images/cmskit-module-global-resources-page.png) - -# Internals - -## Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -- `GlobalResource` (aggregate root): Stores a resource. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -Following custom repositories are defined for this feature: - -- `IGlobalResourceRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Global Resource Manager - -`GlobalResourceManager` is used to perform operations for the `GlobalResource` aggregate root. - -### Application layer - -#### Application services - -- `GlobalResourceAdminAppService` (implements `IGlobalResourceAdminAppService`): Implements the management operations of global resources system. -- `GlobalResourcePublicAppService` (implements `IGlobalResourcePublicAppService`): Implements the public use cases of global resources system. - -#### Database - -#### Entity Framework Core - -##### Tables - -- CmsGlobalResources - -#### MongoDB - -##### Collections - -- CmsGlobalResources \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Index.md b/docs/en/Modules/Cms-Kit/Index.md deleted file mode 100644 index 6c3201d0f9..0000000000 --- a/docs/en/Modules/Cms-Kit/Index.md +++ /dev/null @@ -1,146 +0,0 @@ -# CMS Kit Module - -This module provides CMS (Content Management System) capabilities for your application. It provides **core building blocks** and fully working **sub-systems** to create your own website with CMS features enabled, or use the building blocks in your web sites with any purpose. - -> You can see the live demo at [cms-kit-demo.abpdemo.com](https://cms-kit-demo.abpdemo.com/). - -> **This module currently available only for the MVC / Razor Pages UI**. While there is no official Blazor package, it can also work in a Blazor Server UI since a Blazor Server UI is actually a hybrid application that runs in an ASP.NET Core MVC / Razor Pages application. - -The following features are currently available: - -* Provides a [**page**](Pages.md) management system to manage dynamic pages with dynamic URLs. -* Provides a [**blogging**](Blogging.md) system to create publish blog posts with multiple blog support. -* Provides a [**tagging**](Tags.md) system to tag any kind of resource, like a blog post. -* Provides a [**comment**](Comments.md) system to add comments feature to any kind of resource, like blog post or a product review page. -* Provides a [**reaction**](Reactions.md) system to add reactions (smileys) feature to any kind of resource, like a blog post or a comment. -* Provides a [**rating**](Ratings.md) system to add rating feature to any kind of resource. -* Provides a [**menu**](Menus.md) system to manage public menus dynamically. -* Provides a [**global resources**](Global-Resources.md) system to add global styles and scripts dynamically. -* Provides a [**Dynamic Widget**](Dynamic-Widget.md) system to create dynamic widgets for page and blog posts. - -> You can click on the any feature links above to understand and learn how to use it. - -All features are individually usable. If you disable a feature, it completely disappears from your application, even from the database tables, with the help of the [Global Features](../../Global-Features.md) system. - -## Pre Requirements - -- This module depends on [BlobStoring](../../Blob-Storing.md) module for keeping media content. -> Make sure `BlobStoring` module is installed and at least one provider is configured properly. For more information, check the [documentation](../../Blob-Storing.md). - -- CMS Kit uses [distributed cache](../../Caching.md) for responding faster. -> Using a distributed cache, such as [Redis](../../Redis-Cache.md), is highly recommended for data consistency in distributed/clustered deployments. - -## How to Install - -[ABP CLI](../../CLI.md) allows installing a module to a solution using the `add-module` command. You can install the CMS Kit module in a command-line terminal with the following command: - -```bash -abp add-module Volo.CmsKit --skip-db-migrations -``` - -> By default, Cms-Kit is disabled by `GlobalFeature`. Because of that the initial migration will be empty. So you can skip the migration by adding `--skip-db-migrations` to command when installing if you are using Entity Framework Core. After enabling Cms-Kit global feture, please add new migration. - -After the installation process, open the `GlobalFeatureConfigurator` class in the `Domain.Shared` project of your solution and place the following code into the `Configure` method to enable all the features in the CMS Kit module. - -```csharp -GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => -{ - cmsKit.EnableAll(); -}); -``` - -Instead of enabling all, you may prefer to enable the features one by one. The following example enables only the [tags](Tags.md) and [comments](Comments.md) features: - -````csharp -GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => -{ - cmsKit.Tags.Enable(); - cmsKit.Comments.Enable(); -}); -```` - -> If you are using Entity Framework Core, do not forget to add a new migration and update your database. - -## The Packages - -This module follows the [module development best practices guide](https://docs.abp.io/en/abp/latest/Best-Practices/Index) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. - -CMS kit packages are designed for various usage scenarios. If you check the [CMS kit packages](https://www.nuget.org/packages?q=Volo.CmsKit), you will see that some packages have `Admin` and `Public` suffixes. The reason is that the module has two application layers, considering they might be used in different type of applications. These application layers uses a single domain layer: - - - `Volo.CmsKit.Admin.*` packages contain the functionalities required by admin (back office) applications. - - `Volo.CmsKit.Public.*` packages contain the functionalities used in public websites where users read blog posts or leave comments. - - `Volo.CmsKit.*` (without Admin/Public suffix) packages are called as unified packages. Unified packages are shortcuts for adding Admin & Public packages (of the related layer) separately. If you have a single application for administration and public web site, you can use these packages. - -## Internals - -### Table / collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -## Entity Extensions - -[Module entity extension](https://docs.abp.io/en/abp/latest/Module-Entity-Extensions) system is a **high-level** extension system that allows you to **define new properties** for existing entities of the dependent modules. It automatically **adds properties to the entity**, **database**, **HTTP API, and user interface** in a single point. - -To extend entities of the CMS Kit module, open your `YourProjectNameModuleExtensionConfigurator` class inside of your `DomainShared` project and change the `ConfigureExtraProperties` method like shown below. - -```csharp -public static void ConfigureExtraProperties() -{ - OneTimeRunner.Run(() => - { - ObjectExtensionManager.Instance.Modules() - .ConfigureCmsKit(cmsKit => - { - cmsKit.ConfigureBlog(plan => // extend the Blog entity - { - plan.AddOrUpdateProperty( //property type: string - "BlogDescription", //property name - property => { - //validation rules - property.Attributes.Add(new RequiredAttribute()); //adds required attribute to the defined property - - //...other configurations for this property - } - ); - }); - - cmsKit.ConfigureBlogPost(blogPost => // extend the BlogPost entity - { - blogPost.AddOrUpdateProperty( //property type: string - "BlogPostDescription", //property name - property => { - //validation rules - property.Attributes.Add(new RequiredAttribute()); //adds required attribute to the defined property - property.Attributes.Add( - new StringLengthAttribute(MyConsts.MaximumDescriptionLength) { - MinimumLength = MyConsts.MinimumDescriptionLength - } - ); - - //...other configurations for this property - } - ); - }); - }); - }); -} -``` - -* `ConfigureCmsKit(...)` method is used to configure the entities of the CMS Kit module. - -* `cmsKit.ConfigureBlog(...)` is used to configure the **Blog** entity of the CMS Kit module. You can add or update your extra properties on the **Blog** entity. - -* `cmsKit.ConfigureBlogPost(...)` is used to configure the **BlogPost** entity of the CMS Kit module. You can add or update your extra properties of the **BlogPost** entity. - -* You can also set some validation rules for the property that you defined. In the above sample, `RequiredAttribute` and `StringLengthAttribute` were added for the property named **"BlogPostDescription"**. - -* When you define the new property, it will automatically add to **Entity**, **HTTP API**, and **UI** for you. - * Once you define a property, it appears in the create and update forms of the related entity. - * New properties also appear in the datatable of the related page. - diff --git a/docs/en/Modules/Cms-Kit/Menus.md b/docs/en/Modules/Cms-Kit/Menus.md deleted file mode 100644 index b469c322ab..0000000000 --- a/docs/en/Modules/Cms-Kit/Menus.md +++ /dev/null @@ -1,90 +0,0 @@ -# CMS Kit: Menus - -CMS Kit Menu system allows to manage public menus dynamically. - -## Enabling the Menu Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## The User Interface - -### Menu items - -CMS Kit module admin side adds the following items to the main menu, under the *CMS* menu item: - -* **Menus**: Menu management page. - -`CmsKitAdminMenus` class has the constants for the menu item names. - -### Menus - -#### Menu Management - -Menus page is used to manage dynamic public menus in the system. - -![cms-kit-menus-page](../../images/cmskit-module-menus-page.png) - -The created menu items will be visible on the public-web side, as shown below: - -![cms-kit-public-menus](../../images//cmskit-module-menus-public.png) - -## Internals - -### Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -- `MenuItem` (aggregate root): A Menu Item presents a single node at menu tree. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -Following custom repositories are defined for this feature: - -- `IMenuItemRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Menu Item Manager - -`MenuItemManager` is used to perform some operations for the `MenuItemManager` aggregate root. - -### Application layer - -#### Application services - -- `MenuItemAdminAppService` (implements `IMenuItemAdminAppService`): Implements the management operations of menu system. -- `MenuItemPublicAppService` (implements `IMenuItemPublicAppService`): Implements the public use cases of menu system. - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- CmsMenuItems - -#### MongoDB - -##### Collections - -- CmsMenuItems \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Pages.md b/docs/en/Modules/Cms-Kit/Pages.md deleted file mode 100644 index 04614dd74a..0000000000 --- a/docs/en/Modules/Cms-Kit/Pages.md +++ /dev/null @@ -1,34 +0,0 @@ -# CMS Kit: Pages - -CMS Kit Page system allows you to create dynamic pages by specifying URLs, which is the fundamental feature of a CMS. - -## Enabling the Pages Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## The User Interface - -### Menu items - -CMS Kit module admin side adds the following items to the main menu, under the *CMS* menu item: - -* **Pages**: Page management page. - -`CmsKitAdminMenus` class has the constants for the menu item names. - -### Pages - -#### Page Management - -**Pages** page is used to manage dynamic pages in the system. You can create/edit pages with dynamic routes and contents on this page: - -![pages-edit](../../images/cmskit-module-pages-edit.png) - -After you have created pages, you can set one of them as a *home page*. Then, whenever anyone navigates to your application's homepage, they see the dynamic content of the page that you have defined on this page. - -![pages-page](../../images/cmskit-module-pages-page.png) - -Also when you create a page, you can access the created page via `/{slug}` URL. - diff --git a/docs/en/Modules/Cms-Kit/Ratings.md b/docs/en/Modules/Cms-Kit/Ratings.md deleted file mode 100644 index e6a2ba97fd..0000000000 --- a/docs/en/Modules/Cms-Kit/Ratings.md +++ /dev/null @@ -1,111 +0,0 @@ -# Rating System - -CMS kit provides a **rating** system to to add ratings feature to any kind of resource like blog posts, comments, etc. Here how the rating component looks like on a sample page: - -![ratings](../../images/cmskit-module-ratings.png) - -## Enabling the Rating Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## Options - -The rating system provides a mechanism to group ratings by entity types. For example, if you want to use the rating system for products, you need to define an entity type named `Product` and then add ratings under the defined entity type. - -`CmsKitRatingOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: - -```csharp -Configure(options => -{ - options.EntityTypes.Add(new RatingEntityTypeDefinition("Product")); -}); -``` - -> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. - -`CmsKitRatingOptions` properties: - -- `EntityTypes`: List of defined entity types(`RatingEntityTypeDefinition`) in the rating system. - -`RatingEntityTypeDefinition` properties: - -- `EntityType`: Name of the entity type. - -## The Rating Widget - -The ratings system provides a rating widget to allow users send ratings to resources in public websites. You can simply place the widget on a page like below. - -```csharp -@await Component.InvokeAsync(typeof(RatingViewComponent), new -{ - entityType = "Product", - entityId = "entityId", - isReadOnly = false -}) -``` - -`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. - -# Internals - -## Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -##### Rating - -A rating represents a given rating from a user. - -- `Rating` (aggregate root): Represents a given rating in the system. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -Following custom repositories are defined for this feature: - -- `IRatingRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Reaction Manager - -`RatingManager` is used to perform some operations for the `Rating` aggregate root. - -### Application layer - -#### Application services - -- `RatingPublicAppService` (implements `IRatingPublicAppService`): Implements the use cases of rating system. - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- CmsRatings - -#### MongoDB - -##### Collections - -- **CmsRatings** \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit/Reactions.md b/docs/en/Modules/Cms-Kit/Reactions.md deleted file mode 100644 index 457d5e0f26..0000000000 --- a/docs/en/Modules/Cms-Kit/Reactions.md +++ /dev/null @@ -1,126 +0,0 @@ -# Reaction System - -CMS kit provides a **reaction** system to add reactions feature to any kind of resource, like blog posts or comments. - -Reaction component allows users to react to your content via pre-defined icons/emojis. Here how the reactions component may looks like: - -![reactions](../../images/cmskit-module-reactions.png) - -You can also customize the reaction icons shown in the reaction component. - -## Enabling the Reaction Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## Options - -Reaction system provides a mechanism to group reactions by entity types. For example, if you want to use the reaction system for products, you need to define an entity type named `Product`, and then add reactions under the defined entity type. - -`CmsKitReactionOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: - -```csharp -Configure(options => -{ - options.EntityTypes.Add( - new ReactionEntityTypeDefinition( - "Product", - reactions: new[] - { - new ReactionDefinition(StandardReactions.Smile), - new ReactionDefinition(StandardReactions.ThumbsUp), - new ReactionDefinition(StandardReactions.ThumbsDown), - new ReactionDefinition(StandardReactions.Confused), - new ReactionDefinition(StandardReactions.Eyes), - new ReactionDefinition(StandardReactions.Heart) - })); -}); -``` - -> If you're using the [Comment](Comments.md) or [Blogging](Blogging.md) features, the ABP framework defines predefined reactions for these features automatically. - -`CmsKitReactionOptions` properties: - -- `EntityTypes`: List of defined entity types (`CmsKitReactionOptions`) in the reaction system. - -`ReactionEntityTypeDefinition` properties: - -- `EntityType`: Name of the entity type. -- `Reactions`: List of defined reactions (`ReactionDefinition`) in the entity type. - -## The Reactions Widget - -The reaction system provides a reaction widget to allow users to send reactions to resources. You can place the widget on a page like below: - -```csharp -@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new -{ - entityType = "Product", - entityId = "..." -}) -``` - -`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. - -# Internals - -## Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -##### UserReaction - -A user reaction represents a given reaction from a user. - -- `UserReaction` (aggregate root): Represents a given reaction in the system. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -Following custom repositories are defined for this feature: - -- `IUserReactionRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Reaction Manager - -`ReactionManager` is used to perform some operations for the `UserReaction` aggregate root. - -### Application layer - -#### Application services - -- `ReactionPublicAppService` (implements `IReactionPublicAppService`): Implements the use cases of reaction system. - -### Database providers - -#### Common - -##### Table / collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- CmsUserReactions - -#### MongoDB - -##### Collections - -- **CmsUserReactions** diff --git a/docs/en/Modules/Cms-Kit/Tags.md b/docs/en/Modules/Cms-Kit/Tags.md deleted file mode 100644 index 9f85faeff2..0000000000 --- a/docs/en/Modules/Cms-Kit/Tags.md +++ /dev/null @@ -1,165 +0,0 @@ -# Tag Management - -CMS kit provides a **tag** system to tag any kind of resources, like a blog post. - -## Enabling the Tag Management Feature - -By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. - -> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. - -## Options - -The tag system provides a mechanism to group tags by entity types. For example, if you want to use the tag system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product` and add tags under these entity types. - -`CmsKitTagOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics) class. - -**Example: Adding tagging support for products** - -```csharp -Configure(options => -{ - options.EntityTypes.Add(new TagEntityTypeDefiniton("Product")); -}); -``` - -> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. - -`CmsKitTagOptions` properties: - -- `EntityTypes`: List of defined entity types(`TagEntityTypeDefiniton`) in the tag system. - -`TagEntityTypeDefiniton` properties: - -- `EntityType`: Name of the entity type. -- `DisplayName`: The display name of the entity type. You can use a user-friendly display name to show entity type definition on the admin website. -- `CreatePolicies`: List of policy/permission names allowing users to create tags under the entity type. -- `UpdatePolicies`: List of policy/permission names allowing users to update tags under the entity type. -- `DeletePolicies`: List of policy/permission names allowing users to delete tags under the entity type. - -## The Tag Widget - -The tag system provides a tag [widget](../../UI/AspNetCore/Widgets.md) to display associated tags of a resource that was configured for tagging. You can simply place the widget on a page like the one below: - -```csharp -@await Component.InvokeAsync(typeof(TagViewComponent), new -{ - entityType = "Product", - entityId = "...", - urlFormat = "/products?tagId={TagId}&tagName={TagName}" -}) -``` - -`entityType` was explained in the previous section. In this example, the `entityId` should be the unique id of the product. If you have a `Product` entity, you can use its Id here. `urlFormat` is the string format of the URL which will be generated for each tag. You can use the `{TagId}` and `{TagName}` placeholders to populate the URL. For example, the above URL format will populate URLs like `/products?tagId=1&tagName=tag1`. - -## The Popular Tags Widget - -The tag system provides a popular tags [widget](../../UI/AspNetCore/Widgets.md) to display popular tags of a resource that was configured for tagging. You can simply place the widget on a page as below: - -```csharp -@await Component.InvokeAsync(typeof(PopularTagsViewComponent), new -{ - entityType = "Product", - urlFormat = "/products?tagId={TagId}&tagName={TagName}", - maxCount = 10 -}) -``` - -`entityType` was explained in the previous section. `urlFormat` was explained in the previous section. `maxCount` is the maximum number of tags to be displayed. - -## User Interface - -### Menu Items - -The following menu items are added by the tagging feature to the admin application: - -* **Tags**: Opens the tag management page. - -### Pages - -#### Tag Management - -This page can be used to create, edit and delete tags for the entity types. - -![tags-page](../../images/cmskit-module-tags-page.png) - -You can create or edit an existing tag on this page. - -![tag-edit](../../images/cmskit-module-tag-edit.png) - -## Internals - -### Domain Layer - -#### Aggregates - -This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. - -##### Tag - -A tag represents a tag under the entity type. - -- `Tag` (aggregate root): Represents a tag in the system. - -##### EntityTag - -An entity tag represents a connection between the tag and the tagged entity. - -- `EntityTag`(entity): Represents a connection between the tag and the tagged entity. - -#### Repositories - -This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. - -The following custom repositories are defined for this feature: - -- `ITagRepository` -- `IEntityTagRepository` - -#### Domain services - -This module follows the [Domain Services Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) guide. - -##### Tag Manager - -`TagManager` performs some operations for the `Tag` aggregate root. - -##### Entity Tag Manager - -`EntityTagManager` performs some operations for the `EntityTag` entity. - -### Application layer - -#### Application services - -- `TagAdminAppService` (implements `ITagAdminAppService`). -- `EntityTagAdminAppService` (implements `IEntityTagAdminAppService`). -- `TagAppService` (implements `ITagAppService`). - -### Database providers - -#### Common - -##### Table / Collection prefix & schema - -All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection string - -This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- CmsTags -- CmsEntityTags - -#### MongoDB - -##### Collections - -- **CmsTags** -- **CmsEntityTags** diff --git a/docs/en/Modules/Database-Tables.md b/docs/en/Modules/Database-Tables.md deleted file mode 100644 index 0f00055d13..0000000000 --- a/docs/en/Modules/Database-Tables.md +++ /dev/null @@ -1,575 +0,0 @@ -# Database Tables - -This documentation describes all database tables and their purposes. You can read this documentation to get general knowledge of the database tables that come from each module. - -## [Audit Logging Module](Audit-Logging.md) - -### AbpAuditLogs - -This table stores information about the audit logs in the application. Each record represents an audit log and tracks the actions performed in the application. - -### AbpAuditLogActions - -This table stores information about the actions performed in the application, which are logged for auditing purposes. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpAuditLogs](#abpauditlogs) | Id | Links each action to a specific audit log. | - -### AbpEntityChanges - -This table stores information about entity changes in the application, which are logged for auditing purposes. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpAuditLogs](#abpauditlogs) | Id | Links each entity change to a specific audit log. | - -### AbpEntityPropertyChanges - -This table stores information about property changes to entities in the application, which are logged for auditing purposes. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpEntityChanges](#abpentitychanges) | Id | Links each property change to a specific entity change. | - -## [Background Jobs Module](Background-Jobs.md) - -### AbpBackgroundJobs - -This table stores information about the background jobs in the application and facilitates their efficient management and tracking. Each entry in the table contains details of a background job, including the job name, arguments, try count, next try time, last try time, abandoned status, and priority. - -## [Tenant Management Module](Tenant-Management.md) - -### AbpTenants - -This table stores information about the tenants. Each record represents a tenant and contains information about the tenant, such as name and other details. - -### AbpTenantConnectionStrings - -This table stores information about the tenant database connection strings. When you define a connection string for a tenant, a new record will be added to this table. You can query this database to get connection strings by tenants. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpTenants](#abptenants) | Id | The `Id` column in the `AbpTenants` table is used to associate the tenant connection string with the corresponding tenant. | - -## Blogging Module - -### BlgUsers - -This table stores information about the blog users. When a new identity user is created, a new record will be added to this table. - -### BlgBlogs - -This table serves to store blog information and semantically separates the posts of each blog. - -### BlgPosts - -This table stores information about the blog posts. You can query this table to get blog posts by blogs. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [BlgBlogs](#blgblogs) | Id | To associate the blog post with the corresponding blog. | -### BlgComments - -This table stores information about the comments made on blog posts. You can query this table to get comments by posts. -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [BlgPosts](#blgposts) | Id | Links the comment to the corresponding blog post. | -| [BlgComments](#blgcomments) | Id | Links the comment to the parent comment. | - -### BlgTags - -This table stores information about the tags. When a new tag is used, a new record will be added to this table. You can query this table to get tags by blogs. - -### BlgPostTags - -This table is used to associate tags with blog posts in order to categorize and organize the content. You can query this table to get post tags by posts. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [BlgTags](#blgtags) | Id | Links the post tag to the corresponding tag. | -| [BlgPosts](#blgposts) | Id | Links the post tag to the corresponding blog post. | - -## [CMS Kit Module](Cms-Kit/Index.md) - -### CmsUsers - -This table stores information about the CMS Kit module users. When a new identity user is created, a new record will be added to this table. - -### CmsBlogs - -This table serves to store blog information and semantically separates the posts of each blog. - -### CmsBlogPosts - -This table stores information about the blog posts. You can query this table to get blog posts by blogs. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [CmsUsers](#cmsusers) | Id | Links the blog post to the corresponding author. | - -### CmsBlogFeatures - -This table stores information about the blog features. You can query this table to get blog features by blogs. - -### CmsComments - -This table is utilized by the [CMS Kit Comment system](Cms-Kit/Comments.md) to store comments made on the blog posts. You can query this table to get comments by posts. - -### CmsTags - -This table stores information about the tags. When a new tag is used, a new record will be added to this table. You can query this table to get tags by blogs. - -### CmsEntityTags - -This table is utilized by the [Tag Management system](Cms-Kit/Tags.md) to store tags and their relationship with various entities, thus enabling efficient categorization and organization of content. You can query this table to get entity tags by entities. - -### CmsGlobalResources - -This table is a database table for the [CMS Kit Global Resources system](Cms-Kit/Global-Resources.md), allowing dynamic addition of global styles and scripts. - -### CmsMediaDescriptors - -This table is utilized by the CMS kit module to manage media files by using the [BlobStoring](../Blob-Storing.md) module. - -### CmsMenuItems - -This table is used by the [CMS Kit Menu system](Cms-Kit/Menus.md) to manage and store information about dynamic public menus, including details such as menu item display names, URLs, and hierarchical relationships. - -### CmsPages - -This table is utilized by the [CMS Kit Page system](Cms-Kit/Pages.md) to store dynamic pages within the application, including information such as page URLs, titles, and content. - -### CmsRatings - -This table is utilized by the [CMS Kit Rating system](Cms-Kit/Ratings.md) to store ratings made on blog posts. You can query this table to get ratings by posts. - -### CmsUserReactions - -This table is utilized by the [CMS Kit Reaction system](Cms-Kit/Reactions.md) to store reactions made on blog posts. You can query this table to get reactions by posts. - -## [Docs Module](Docs.md) - -### DocsProjects - -This table stores project information to categorize documents according to different projects. - -### DocsDocuments - -This table retrieves the document if it's not found in the cache. The documentation is being updated when the content is retrieved from the database. - -### DocsDocumentContributors - -This table stores information about the contributors of the documents. You can query this table to get document contributors by documents. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [DocsDocuments](#docsdocuments) | Id | Links the document contributor to the corresponding document. | - -## [Feature Management Module](Feature-Management.md) - -### AbpFeatureGroups - -This table stores information about the feature groups in the application. For example, you can group all the features in the [`AbpFeatures`](#abpfeatures) table related to the `Identity` module under the `Identity` group. - -### AbpFeatures - -This table stores information about the features in the application. You can use the `Name` column to link each feature with its corresponding feature value in the [`AbpFeatureValues`](#abpfeaturevalues) table, so that you can easily manage and organize the features. - -### AbpFeatureValues - -This table stores the values of the features for different providers. You can use the `Name` column to link each feature value with its corresponding feature in the [`AbpFeatures`](#abpfeatures) table, so that you can easily manage and organize the features. - -## [Identity Module](Identity.md) - -### AbpUsers - -This table stores information about the identity users in the application. - -### AbpRoles - -This table stores information about the roles in the application. Roles are used to manage and control access to different parts of the application by assigning permissions and claims to roles and then assigning those roles to users. This table is important for managing and organizing the roles in the application, and for defining the access rights of the users. - -### AbpClaimTypes - -This table stores information about the claim types used in the application. You can use the `Name`, `Regex` columns to filter the claim types by name, and regex pattern respectively, so that you can easily manage and track the claim types in the application. - -### AbpLinkUsers - -This table is useful for linking multiple user accounts across different tenants or applications to a single user, allowing them to easily switch between their accounts. - -### AbpUserClaims - -This table can manage user-based access control by allowing to assign claims to users, which describes the access rights of the individual user. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpUsers](#abpusers) | Id | Links the user claim to the corresponding user. | - -### AbpUserLogins - -This table can store information about the user's external logins such as login with Facebook, Google, etc. and it can also be used to track the login history of users. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpUsers](#abpusers) | Id | Links the user login to the corresponding user. | - -### AbpUserRoles - -This table can manage user-based access control by allowing to assign roles to users, which describe the access rights of the individual user. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpUsers](#abpusers) | Id | Links the user role to the corresponding user. | -| [AbpRoles](#abproles) | Id | Links the user role to the corresponding role. | - -### AbpUserTokens - -This table can store information about user's refresh tokens, access tokens and other tokens used in the application. It can also be used to invalidate or revoke user tokens. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpUsers](#abpusers) | Id | Links the user token to the corresponding user. | - -### AbpOrganizationUnits - -This table is useful for creating and managing a hierarchical structure of the organization, allowing to group users and assign roles based on the organization structure. You can use the `Code`, `ParentId` columns to filter the organization units by code and parent id respectively, so that you can easily manage and track the organization units in the application. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpOrganizationUnits](#abporganizationunits) | ParentId | Links the organization unit to its parent organization unit. | - -### AbpOrganizationUnitRoles - -This table is useful for managing role-based access control at the level of organization units, allowing to assign different roles to different parts of the organization structure. You can use the `OrganizationUnitId`, `RoleId` columns to filter the roles by organization unit id and role id respectively, so that you can easily manage and track the roles assigned to organization units in the application. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpOrganizationUnits](#abporganizationunits) | Id | Links the organization unit role to the corresponding organization unit. | -| [AbpRoles](#abproles) | Id | Links the organization unit role to the corresponding role. | - -### AbpUserOrganizationUnits - -This table stores information about the organization units assigned to the users in the application. This table can manage user-organization unit relationships, and to group users based on the organization structure. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpUsers](#abpusers) | Id | Links the user organization unit to the corresponding user. | -| [AbpOrganizationUnits](#abporganizationunits) | Id | Links the user organization unit to the corresponding organization unit. | - -### AbpRoleClaims - -This table is useful for managing role-based access control by allowing to assign claims to roles, which describes the access rights of the users that belong to that role. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpRoles](#abproles) | Id | Links the role claim to the corresponding role. | - -### AbpSecurityLogs - -This table logs important operations and changes related to user accounts, allowing users to save the security logs for future reference. - -## [Permission Management](Permission-Management.md) - -### AbpPermissionGroups - -This table is important for managing and organizing the permissions in the application, by grouping them into logical categories. - -### AbpPermissions - -This table is important for managing and controlling access to different parts of the application and for defining the granular permissions that make up the larger permissions or roles. - -### AbpPermissionGrants - -The table stores and manage the permissions in the application and to keep track of permissions that are granted, to whom and when. Columns such as `Name`, `ProviderName`, `ProviderKey`, `TenantId` can be used to filter the granted permissions by name, provider name, provider key, and tenant id respectively, so that you can easily manage and track the granted permissions in the application. - -## [Setting Management](Setting-Management.md) - -### AbpSettings - -This table stores key-value pairs of settings for the application, and it allows dynamic configuration of the application without the need for recompilation. - -## [OpenIddict](OpenIddict.md) - -### OpenIddictApplications - -This table can store information about the OpenID Connect applications, including the client id, client secret, redirect URI, and other relevant information. It can also be used to authenticate and authorize clients using OpenID Connect protocol. - -### OpenIddictAuthorizations - -This table stores the OpenID Connect authorization data in the application. It can also be used to manage and validate the authorization grants issued to clients and users. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [OpenIddictApplications](#openiddictapplications) | Id | Links the authorization to the corresponding application. | - -### OpenIddictTokens - -This table can store information about the OpenID Connect tokens, including the token payload, expiration, type, and other relevant information. It can also be used to manage and validate the tokens issued to clients and users, such as access tokens and refresh tokens, and to control access to protected resources. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [OpenIddictApplications](#openiddictapplications) | Id | Links the token to the corresponding application. | -| [OpenIddictAuthorizations](#openiddictauthorizations) | Id | Links the token to the corresponding authorization. | - -### OpenIddictScopes - -This table can store information about the OpenID Connect scopes, including the name and description of the scope. It can also be used to define the permissions or access rights associated with the scopes, which are then used to control access to protected resources. - -## [IdentityServer](IdentityServer.md) - -### IdentityServerApiResources - -This table can store information about the API resources, including the resource name, display name, description, and other relevant information. It can also be used to define the scopes, claims, and properties associated with the API resources, which are then used to control access to protected resources. - -### IdentityServerIdentityResources - -This table can store information about the identity resources, including the name, display name, description, and enabled status. - -### IdentityServerClients - -This table can store information about the clients, including the client id, client name, client URI and other relevant information. It can also be used to define the scopes, claims, and properties associated with the clients, which are then used to control access to protected resources. - -### IdentityServerApiScopes - -This table can store information about the API scopes, including the scope name, display name, description, and other relevant information. It can also be used to define the claims and properties associated with the API scopes, which are then used to control access to protected resources. - -### IdentityServerApiResourceClaims - -This table can store information about the claims of an API resource, including the claim type and API resource id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the claim to the corresponding API resource. | - -### IdentityServerIdentityResourceClaims - -This table can store information about the claims of an identity resource, including the claim type and identity resource id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerIdentityResources](#identityserveridentityresources) | Id | Links the claim to the corresponding identity resource. | - -### IdentityServerClientClaims - -This table can store information about the claims of a client, including the claim type, claim value and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the claim to the corresponding client. | - -### IdentityServerApiScopeClaims - -This table can store information about the claims of an API scope, including the claim type and API scope id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiScopes](#identityserverapiscopes) | Id | Links the claim to the corresponding API scope. | - -### IdentityServerApiResourceProperties - -This table can store information about properties, including the property key and value, and the associated API resource. These properties can store additional metadata or configuration information related to the API resources. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the property to the corresponding API resource. | - -### IdentityServerIdentityResourceProperties - -This table can store information about properties, including the property key and value, and the associated identity resource. These properties can store additional metadata or configuration information related to the identity resources. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerIdentityResources](#identityserveridentityresources) | Id | Links the property to the corresponding identity resource. | - -### IdentityServerClientProperties - -This table can be store information about the properties of a client, including the key, value and client id. These properties can store additional metadata or configuration information related to the clients. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the property to the corresponding client. | - -### IdentityServerApiScopeProperties - -This table can store information about the properties of an API scope, including the key, value and API scope id. These properties can store additional metadata or configuration information related to the API scopes. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiScopes](#identityserverapiscopes) | Id | Links the property to the corresponding API scope. | - -### IdentityServerApiResourceScopes - -This table can store information about the scopes of an API resource, including the scope name and API resource id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the scope to the corresponding API resource. | - -### IdentityServerClientScopes - - This table can store information about the scopes of a client, including the scope and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the scope to the corresponding client. | - -### IdentityServerApiResourceSecrets - -This table can store information about the secrets of an API resource, including the secret value, expiration date, and API resource id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the secret to the corresponding API resource. | - -### IdentityServerClientSecrets - -This table can store information about the secrets of a client, including the secret value, expiration date, and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the secret to the corresponding client. | - -### IdentityServerClientCorsOrigins - -This table can store information about the CORS origins of a client, including the origin and client id. It can also be used to manage and validate the CORS origins of a client. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the CORS origin to the corresponding client. | - -### IdentityServerClientGrantTypes - -This table can store information about the grant types of a client, including the grant type and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the grant type to the corresponding client. | - -### IdentityServerClientIdPRestrictions - -This table can store information about the identity provider restrictions of a client, including the identity provider and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the identity provider restriction to the corresponding client. | - -### IdentityServerClientPostLogoutRedirectUris - -This table can store information about the post logout redirect URIs of a client, including the post logout redirect URI and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the post logout redirect URI to the corresponding client. | - -### IdentityServerClientRedirectUris - -This table can store information about the redirect URIs of a client, including the redirect URI and client id. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [IdentityServerClients](#identityserverclients) | Id | Links the redirect URI to the corresponding client. | - -### IdentityServerDeviceFlowCodes - -This table can store information about the device flow codes, including the user code, device code, subject id, client id, creation time, expiration, data and session id. - -### IdentityServerPersistedGrants - -This table can store information about the persisted grants, including the key, type, subject id, client id, creation time, expiration, and data. - -## Others - -### AbpBlobContainers - -This table is important for providing a better user experience by allowing the application to support multiple containers and providing BLOB-specific features. - -### AbpBlobs - -This table stores the binary data of BLOBs (binary large objects) in the application. Each BLOB is related to a container in the [AbpBlobContainers](#abpblobcontainers) table, where the container name, tenant id and other properties of the container can be found. - -#### Foreign Keys - -| Table | Column | Description | -| --- | --- | --- | -| [AbpBlobContainers](#abpblobcontainers) | Id | Links the BLOB to the corresponding container. | - -### AbpLocalizationResources - -This table stores the localization resources for the application. This table is important for providing a better user experience by allowing the application to support multiple resources and providing localized text and other localization-specific features. - -### AbpLocalizationTexts - -The table contains the resource name, culture name, and a json encoded value which holds the key-value pair of localization text. It allows for efficient storage and management of localization texts and allows for easy update or addition of new translations for specific resources and cultures. diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md deleted file mode 100644 index e52db93a23..0000000000 --- a/docs/en/Modules/Docs.md +++ /dev/null @@ -1,675 +0,0 @@ -# Docs Module - -## What is Docs Module? - -Docs module is an application module for ABP framework. It simplifies software documentation. This module is free and open-source. - -### Integration - -Currently docs module provides you to store your docs both on GitHub and file system. - -### Hosting - -Docs module is an application module and does not offer any hosting solution. You can host your docs on-premise or on cloud. - -### Versioning - -When you use GitHub to store your docs, Docs Module supports versioning. If you have multiple versions for your docs, there will be a combo-box on the UI to switch between versions. If you choose file system to store your docs, it does not support multiple versions. - -[The documents](https://docs.abp.io/) for ABP framework is also using this module. - -> Docs module follows the [module architecture best practices](../Best-Practices/Module-Architecture.md) guide. - -## Installation - -This document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. - -### 1- Creating an application - -If you do not have an existing ABP project, you can either [generate a CLI command from the get started page of the abp.io website](https://abp.io/get-started) and runs it or run the command below: - -```bash -abp new Acme.MyProject -``` - -### 2- Running The Empty Application - -After you created the project, open `Acme.MyProject.sln`. You will see that the solution consists of `Application`, `Application.Contracts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `HttpApi`, `HttpApi.Client` and `Web` projects. Right click on `Acme.MyProject.Web` project and **Set as StartUp Project**. - -![Create a new project](../images/docs-module_solution-explorer.png) - -The database connection string is located in `appsettings.json` of your `Acme.MyProject.Web` project. If you have a different database configuration, change the connection string. - -```json -{ - "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" - } -} -``` - -Run `Acme.MyProject.DbMigrator` project, it will be responsible for applying database migration and seed data. The database `MyProject` will be created in your database server. - -Now an empty ABP project has been created! You can now run your project and see the empty website. - -To login your website enter `admin` as the username and `1q2w3E*` as the password. - -### 3- Installation Module - -Docs module packages are hosted on NuGet. There are 4 packages that needs be to installed to your application. Each package has to be installed to the relevant project. - -#### 3.1- Use ABP CLI - -It is recommended to use the ABP CLI to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command: - -```bash -abp add-module Volo.Docs -``` - -#### 3.2- Manually install - -Or you can also manually install nuget package to each project: - -* Install [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Domain` project. - - ```bash - Install-Package Volo.Docs.Domain - ``` - -* Install [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget package to `Acme.MyProject.EntityFrameworkCore` project. - - ```bash - Install-Package Volo.Docs.EntityFrameworkCore - ``` - -* Install [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget package to `Acme.MyProject.Application` project. - - ```bash - Install-Package Volo.Docs.Application - ``` - -* Install [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Web` project. - - ```bash - Install-Package Volo.Docs.Web - ``` - -##### 3.2.1- Adding Module Dependencies - -An ABP module must declare `[DependsOn]` attribute if it has a dependency upon another module. Each module has to be added in`[DependsOn]` attribute to the relevant project. - -* Open `MyProjectDomainModule.cs`and add `typeof(DocsDomainModule)` as shown below; - - ```csharp - [DependsOn( - typeof(DocsDomainModule), - typeof(AbpIdentityDomainModule), - typeof(AbpAuditingModule), - typeof(BackgroundJobsDomainModule), - typeof(AbpAuditLoggingDomainModule) - )] - public class MyProjectDomainModule : AbpModule - { - //... - } - ``` - -* Open `MyProjectEntityFrameworkCoreModule.cs`and add `typeof(DocsEntityFrameworkCoreModule)` as shown below; - - ```csharp - [DependsOn( - typeof(DocsEntityFrameworkCoreModule), - typeof(MyProjectDomainModule), - typeof(AbpIdentityEntityFrameworkCoreModule), - typeof(AbpPermissionManagementEntityFrameworkCoreModule), - typeof(AbpSettingManagementEntityFrameworkCoreModule), - typeof(AbpEntityFrameworkCoreSqlServerModule), - typeof(BackgroundJobsEntityFrameworkCoreModule), - typeof(AbpAuditLoggingEntityFrameworkCoreModule) - )] - public class MyProjectEntityFrameworkCoreModule : AbpModule - { - //... - } - ``` - -* Open `MyProjectApplicationModule.cs`and add `typeof(DocsApplicationModule)` as shown below; - - ```csharp - [DependsOn( - typeof(DocsApplicationModule), - typeof(MyProjectDomainModule), - typeof(AbpIdentityApplicationModule))] - public class MyProjectApplicationModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.DefinitionProviders.Add(); - }); - - Configure(options => - { - options.AddProfile(); - }); - } - } - ``` - -* Open `MyProjectWebModule.cs`and add `typeof(DocsWebModule)` as shown below; - - ```csharp - [DependsOn( - typeof(DocsWebModule), - typeof(MyProjectApplicationModule), - typeof(MyProjectEntityFrameworkCoreModule), - typeof(AbpAutofacModule), - typeof(AbpIdentityWebModule), - typeof(AbpAccountWebModule), - typeof(AbpAspNetCoreMvcUiBasicThemeModule) - )] - public class MyProjectWebModule : AbpModule - { - //... - } - ``` - -##### 3.2.2- Adding NPM Package - -Open `package.json` and add `@abp/docs": "^5.0.0` as shown below: - - ```json - { - "version": "1.0.0", - "name": "my-app", - "private": true, - "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^5.0.0", - "@abp/docs": "^5.0.0" - } - } - ``` - -Then open the command line terminal in the `Acme.MyProject.Web` project folder and run the following command: - -````bash -abp install-libs -```` - -### 4- Database Integration - -#### 4.1- Entity Framework Integration - -If you choose Entity Framework as your database provider, you need to configure the Docs Module. To do this; - -- Open `MyProjectMigrationsDbContext.cs` and add `builder.ConfigureDocs()` to the `OnModelCreating()`. - - ```csharp - public class MyProjectMigrationsDbContext : AbpDbContext - { - public MyProjectMigrationsDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureBackgroundJobs(); - builder.ConfigureAuditLogging(); - builder.ConfigureIdentity(); - builder.ConfigureIdentityServer(); - builder.ConfigureFeatureManagement(); - builder.ConfigureTenantManagement(); - builder.ConfigureDocs(); //Add this line to configure the Docs Module - - /* Configure customizations for entities from the modules included */ - - builder.Entity(b => - { - b.ConfigureCustomUserProperties(); - }); - - /* Configure your own tables/entities inside the ConfigureQaDoc method */ - - builder.ConfigureMyProject(); - } - } - ``` - -* Open `Package Manager Console` in `Visual Studio` and choose `Acme.MyProject.EntityFrameworkCore` as default project. Then write the below command to add the migration for Docs Module. - - ```csharp - add-migration Added_Docs_Module - ``` - - When the command successfully executes , you will see a new migration file named as `20181221111621_Added_Docs_Module` in the folder `Acme.MyProject.EntityFrameworkCore\Migrations`. - - Now, update the database for Docs module database changes. To do this run the below code on `Package Manager Console` in `Visual Studio`. Be sure `Acme.MyProject.EntityFrameworkCore` is still default project. - - ```csharp - update-database - ``` - - Finally, you can check your database to see the newly created tables. For example you can see `DocsProjects` table must be added to your database. - -### 5- Linking Docs Module - -The default route for Docs module is; - -```txt -/Documents -``` - -To add Docs module link to your application menu; - -* Open `MyProjectMenuContributor.cs` and add the below line to the method `ConfigureMainMenuAsync()`. - - ```csharp - context.Menu.Items.Add(new ApplicationMenuItem("MyProject.Docs", l["Menu:Docs"], "/Documents")); - ``` - - Final look of **MyProjectMenuContributor.cs** - - ```csharp - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - var l = context.ServiceProvider.GetRequiredService>(); - - context.Menu.Items.Insert(0, new ApplicationMenuItem("MyProject.Home", l["Menu:Home"], "/")); - - context.Menu.Items.Add(new ApplicationMenuItem("MyProject.Docs", l["Menu:Docs"], "/Documents")); - } - ``` - -The `Menu:Docs` keyword is a localization key. To localize the menu text, open `Localization\MyProject\en.json` in the project `Acme.MyProject.Domain`. And add the below line - -```json -"Menu:Docs": "Documents" -``` - -Final look of **en.json** - -```json -{ - "culture": "en", - "texts": { - "Menu:Home": "Home", - "Welcome": "Welcome", - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", - "Menu:Docs": "Documents" - } -} -``` - -The new menu item for Docs Module is added to the menu. Run your web application and browse to `http://localhost:YOUR_PORT_NUMBER/documents` URL. - -You will see a warning says; - -```txt -There are no projects yet! -``` - -As we have not added any projects yet, this warning is normal. - -### 6- Adding New Docs Project - -Open `DocsProjects` in your database, and insert a new record with the following field information; - -* **Name**: The display name of the document name which will be shown on the web page. -* **ShortName**: A short and URL friendly name that will be used in your docs URL. -* **Format**: The format of the document (for Markdown: `md`, for HTML: `html`) -* **DefaultDocumentName**: The document for the initial page. -* **NavigationDocumentName**: The document to be used for the navigation menu (Index). -* **MinimumVersion**: The minimum version to show the docs. Below version will not be listed. -* **DocumentStoreType**: The source of the documents (for GitHub:`GitHub`, for file system`FileSystem`) -* **ExtraProperties**: A serialized `JSON` that stores special configuration for the selected `DocumentStoreType`. -* **MainWebsiteUrl**: The URL when user clicks to the logo of the Docs module page. You can simply set as `/` to link to your website root address. -* **LatestVersionBranchName**: This is a config for GitHub. It's the branch name which to retrieve the docs. You can set it as `dev`. - -#### Sample Project Record for "GitHub" - -You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documents to configure your GitHub document store. - -- Name: `ABP framework (GitHub)` - -- ShortName: `abp` - -- Format: `md` - -- DefaultDocumentName: `Index` - -- NavigationDocumentName: `docs-nav.json` - -- MinimumVersion: `` (no minimum version) - -- DocumentStoreType: `GitHub` - -- ExtraProperties: - - ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""} - ``` - - Note that `GitHubAccessToken` is masked with `***`. It's a private token that you must get it from GitHub. See https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - -- MainWebsiteUrl: `/` - -- LatestVersionBranchName: `dev` - -For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table: - -```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName], [ConcurrencyStamp]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"","GitHubUserAgent":""}', N'/', N'dev', N'', N'12f21123e08e4f15bedbae0b2d939659') -``` - -Be aware that `GitHubAccessToken` is masked. It's a private token and you must get your own token and replace the `***` string. - -Now you can run the application and navigate to `/Documents`. - -#### Sample Project Record for "FileSystem" - -You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documents to configure your GitHub document store. - -- Name: `ABP framework (FileSystem)` - -- ShortName: `abp` - -- Format: `md` - -- DefaultDocumentName: `Index` - -- NavigationDocumentName: `docs-nav.json` - -- MinimumVersion: `` (no minimum version) - -- DocumentStoreType: `FileSystem` - -- ExtraProperties: - - ```json - {"Path":"C:\\Github\\abp\\docs"} - ``` - - Note that `Path` must be replaced with your local docs directory. You can fetch the ABP Framework's documents from https://github.com/abpframework/abp/tree/master/docs and copy to the directory `C:\\Github\\abp\\docs` to get it work. - -- MainWebsiteUrl: `/` - -- LatestVersionBranchName: `latest` - -For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table: - -```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName], [ConcurrencyStamp]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', N'latest', N'', N'12f21123e08e4f15bedbae0b2d939659') -``` - -Add one of the sample projects above and run the application. In the menu you will see `Documents` link, click the menu link to open the documents page. - -So far, we have created a new application from abp.io website and made it up and ready for Docs module. - -### 7- Creating a New Document - -In the sample Project records, you see that `Format` is specified as `md` which refers to [Mark Down](https://en.wikipedia.org/wiki/Markdown). You can see the mark down cheat sheet following the below link; - -https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet - -ABP Docs Module can render mark down to HTML. - -Now let's have a look a sample document in markdown format. - -~~~markdown -# This is a header - -Welcome to Docs Module. - -## This is a sub header - - [This is a link](https://abp.io) - -![This is an image](https://abp.io/assets/my-image.png) - -## This is a code block - -```csharp -public class Person -{ - public string Name { get; set; } - - public string Address { get; set; } -} -``` -~~~ - -As an example you can see ABP Framework documentation: - -[https://github.com/abpframework/abp/blob/master/docs/en/](https://github.com/abpframework/abp/blob/master/docs/en/) - -#### Conditional sections feature (Using Scriban) - -Docs module uses [Scriban](https://github.com/lunet-io/scriban/tree/master/doc) for conditionally show or hide some parts of a document. In order to use that feature, you have to create a JSON file as **Parameter document** per every language. It will contain all the key-values, as well as their display names. - -For example, [en/docs-params.json](https://github.com/abpio/abp-commercial-docs/blob/master/en/docs-params.json): - -```json -{ - "parameters": [{ - "name": "UI", - "displayName": "UI", - "values": { - "MVC": "MVC / Razor Pages", - "NG": "Angular" - } - }, - { - "name": "DB", - "displayName": "Database", - "values": { - "EF": "Entity Framework Core", - "Mongo": "MongoDB" - } - }, - { - "name": "Tiered", - "displayName": "Tiered", - "values": { - "No": "Not Tiered", - "Yes": "Tiered" - } - }] -} -``` - -Since not every single document in your projects may not have sections or may not need all of those parameters, you have to declare which of those parameters will be used for sectioning the document, as a JSON block anywhere on the document. - -For example [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): - -``` -..... - -​```json -//[doc-params] -{ - "UI": ["MVC","NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -​``` - -........ -``` - -This section will be automatically deleted during render. And f course, those key values must match with the ones in **Parameter document**. - -![Interface](../images/docs-section-ui.png) - -Now you can use **Scriban** syntax to create sections in your document. - -For example: - -```` -{{ if UI == "NG" }} - -* `-u` argument specifies the UI framework, `angular` in this case. - -{{ end }} - -{{ if DB == "Mongo" }} - -* `-d` argument specifies the database provider, `mongodb` in this case. - -{{ end }} - -{{ if Tiered == "Yes" }} - -* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. - -{{ end }} - -```` - -You can also use variables in a text, adding **_Value** postfix to its key: - -```txt -This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. -``` - -Also, **Document_Language_Code** and **Document_Version** keys are pre-defined if you want to get the language code or the version of the current document (This may be useful for creating links that redirects to another documentation system in another domain). - ------- - -**IMPORTANT NOTICE**: Scriban uses "{{" and "}}" for syntax. Therefore, you must use escape blocks if you are going to use those in your document (an Angular document, for example). See [Scriban docs](https://github.com/lunet-io/scriban/blob/master/doc/language.md#13-escape-block) for more information. - -### 8- Creating the Navigation Document - -Navigation document is the main menu of the documents page. It is located on the left side of the page. It is a `JSON` file. Take a look at the below sample navigation document to understand the structure. - -```json -{ - "items":[ - { - "text":"Sample Menu Item - 1", - "items":[ - { - "text":"Sample Menu Item - 1.1", - "items":[ - { - "text":"Sample Menu Item - 1.1.1", - "path":"SampleMenuItem_1_1_1.md" - } - ] - }, - { - "text":"Sample Menu Item - 1.2", - "items":[ - { - "text":"Sample Menu Item - 1.2.1", - "path":"SampleMenuItem_1_2_1.md" - }, - { - "text":"Sample Menu Item - 1.2.2", - "path":"SampleMenuItem_1_2_2.md" - } - ] - } - ] - }, - { - "text":"Sample Menu Item - 2", - "items":[ - { - "text":"Sample Menu Item - 2.1", - "items":[ - { - "text":"Sample Menu Item - 2.1.1", - "path":"SampleMenuItem_2_1_1.md" - } - ] - } - ] - } - ] -} -``` - -The upper sample `JSON` file renders the below navigation menu as `HTML`. - -![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) - -Finally a new Docs Module is added to your project which is feeded with GitHub. - - -## Full-Text Search(Elastic Search) - -The Docs module supports full-text search using Elastic Search. It is not enabled by default. You can configure `DocsElasticSearchOptions` to enable it. - -```csharp -Configure(options => -{ - options.Enable = true; - options.IndexName = "your_index_name"; //default IndexName is abp_documents -}); -``` - -The `Index` is automatically created after the application starts if the `Index` does not exist. - -`DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`. -If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](../Dependency-Injection.md) system. - -```json -{ - "ElasticSearch": { - "Url": "http://localhost:9200" - } -} -``` - - -## Row Highlighting - -You can apply highlight to specific code lines or a range of sequential lines. -See the following examples: - -``` - ```C# {3, 5} - public class Book : Entity - { - public string Name { get; set; } - public string Surname { get; set; } - } - ``` -``` - -``` - ```C# {2-4} - public class Book : Entity - { - public string Name { get; set; } - public string Surname { get; set; } - } - ``` -``` - -``` - ```C# {1, 2-4} - public class Book : Entity - { - public string Name { get; set; } - public string Surname { get; set; } - } - ``` -``` - ---- - - - -## Next - -Docs Module is also available as a standalone application. Check out [VoloDocs](../Apps/VoloDocs). diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md deleted file mode 100644 index d6cf470391..0000000000 --- a/docs/en/Modules/Feature-Management.md +++ /dev/null @@ -1,106 +0,0 @@ -# Feature Management Module - -The Feature Management module implements the `IFeatureManagementStore` interface defined by the [Feature System](../Features.md). - -> This document covers only the feature management module which persists feature values to a database. See [the features](../Features.md) document for more about the feature system. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/feature-management). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -### Feature Management Dialog - -Feature management module provides a reusable dialog to manage features related to an object. For example, the [Tenant Management Module](Tenant-Management.md) uses it to manage features of tenants in the Tenant Management page. - -![features-module-opening](../images/features-module-opening.png) - -When you click *Actions* -> *Features* for a tenant, the feature management dialog is opened. An example screenshot from this dialog with two features defined: - -![features-modal](../images/features-modal.png) - -In this dialog, you can enable, disable or set values for the features for a tenant. - -## IFeatureManager - -`IFeatureManager` is the main service provided by this module. It is used to read and change the setting values for the tenants in a multi-tenant application. `IFeatureManager` is typically used by the *Feature Management Dialog*. However, you can inject it if you need to set a feature value. - -> If you just want to read feature values, use the `IFeatureChecker` as explained in the [Features document](../Features.md). - -**Example: Get/set a feature's value for a tenant** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.FeatureManagement; - -namespace Demo -{ - public class MyService : ITransientDependency - { - private readonly IFeatureManager _featureManager; - - public MyService(IFeatureManager featureManager) - { - _featureManager = featureManager; - } - - public async Task SetFeatureDemoAsync(Guid tenantId, string value) - { - await _featureManager - .SetForTenantAsync(tenantId, "Feature1", value); - - var currentValue = await _featureManager - .GetOrNullForTenantAsync("Feature1", tenantId); - } - } -} -```` - -## Feature Management Providers - -Features Management Module is extensible, just like the [features system](../Features.md). You can extend it by defining feature management providers. There are 3 pre-built feature management providers registered it the following order: - -* `DefaultValueFeatureManagementProvider`: Gets the value from the default value of the feature definition. It can not set the default value since default values are hard-coded on the feature definition. -* `EditionFeatureManagementProvider`: Gets or sets the feature values for an edition. Edition is a group of features assigned to tenants. Edition system has not implemented by the Tenant Management module. You can implement it yourself or purchase the ABP Commercial [SaaS Module](https://commercial.abp.io/modules/Volo.Saas) which implements it and also provides more SaaS features, like subscription and payment. -* `TenantFeatureManagementProvider`: Gets or sets the features values for tenants. - -`IFeatureManager` uses these providers on get/set methods. Typically, every feature management provider defines extension methods on the `IFeatureManager` service (like `SetForTenantAsync` defined by the tenant feature management provider). - -If you want to create your own provider, implement the `IFeatureManagementProvider` interface or inherit from the `FeatureManagementProvider` base class: - -````csharp -public class CustomFeatureProvider : FeatureManagementProvider -{ - public override string Name => "Custom"; - - public CustomFeatureProvider(IFeatureManagementStore store) - : base(store) - { - } -} -```` - -`FeatureManagementProvider` base class makes the default implementation (using the `IFeatureManagementStore`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each feature value record). - -Once you create your provider class, you should register it using the `FeatureManagementOptions` [options class](../Options.md): - -````csharp -Configure(options => -{ - options.Providers.Add(); -}); -```` - -The order of the providers are important. Providers are executed in the reverse order. That means the `CustomFeatureProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. - -## See Also - -* [Features](../Features.md) - diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md deleted file mode 100644 index f73e8073b2..0000000000 --- a/docs/en/Modules/Identity.md +++ /dev/null @@ -1,322 +0,0 @@ -# Identity Management Module - -Identity module is used to manage roles, users and their permissions, based on the [Microsoft Identity library](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity). - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/identity). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -This module provides [Blazor](../UI/Blazor/Overall.md), [Angular](../UI/Angular/Quick-Start.md) and [MVC / Razor Pages](../UI/AspNetCore/Overall.md) UI options. - -### Menu Items - -This module adds an *Identity management* menu item under the *Administration* menu: - -![identity-module-menu](../images/identity-module-menu.png) - -The menu items and the related pages are authorized. That means the current user must have the related permissions to make them visible. The `admin` role (and the users with this role - like the `admin` user) already has these permissions. If you want to enable permissions for other roles/users, open the *Permissions* dialog on the *Roles* or *Users* page and check the permissions as shown below: - -![identity-module-permissions](../images/identity-module-permissions.png) - -See the [Authorization document](../Authorization.md) to understand the permission system. - -### Pages - -This section introduces the main pages provided by this module. - -#### Users - -This page is used to see the list of users. You can create/edit and delete users, assign users to roles. - -![identity-module-users](../images/identity-module-users.png) - -A user can have zero or more roles. Users inherit permissions from their roles. In addition, you can assign permissions directly to the users (by clicking the *Actions* button, then selecting the *Permissions*). - -#### Roles - -Roles are used to group permissions assign them to users. - -![identity-module-roles](../images/identity-module-roles.png) - -Beside the role name, there are two properties of a role: - -* `Default`: If a role is marked as "default", then that role is assigned to new users by default when they register to the application themselves (using the [Account Module](Account.md)). -* `Public`: A public role of a user can be seen by other users in the application. This feature has no usage in the Identity module, but provided as a feature that you may want to use in your own application. - -## Other Features - -This section covers some other features provided by this module which don't have the UI pages. - -### Organization Units - -Organization Units (OU) can be used to **hierarchically group users and entities**. - -#### OrganizationUnit Entity - -An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are: - -- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. -- **ParentId**: Parent OU's Id. Can be null if this is a root OU. -- **Code**: A hierarchical string code that is unique for a tenant. -- **DisplayName**: Shown name of the OU. - -#### Organization Tree - -Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree; - -- There can be more than one root (where the `ParentId` is `null`). -- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below). - -#### OU Code - -OU code is automatically generated and maintained by the `OrganizationUnitManager` service. It's a string that looks something like this: - -"**00001.00042.00005**" - -This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code (automatically applied when you use `OrganizationUnitManager`): - -- It is **unique** for a [tenant](../Multi-Tenancy.md). -- All the children of the same OU have codes that **start with the parent OU's code**. -- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample. -- While the OU code is unique, it can be **changed** if you move the related OU. - -Notice that you must reference an OU by Id, not Code, because the Code can be changed later. - -#### OrganizationUnit Manager - -The `OrganizationUnitManager` class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are: - -- Create, Update or Delete an OU -- Move an OU in the OU tree. -- Getting information about the OU tree and its items. - -### Identity Security Log - -The security log system records some important operations or changes about your account (like *login* and *change password*). You can also save the security log if needed. - -You can inject and use `IdentitySecurityLogManager` or `ISecurityLogManager` to write security logs. It will create a log object by default and fill in some common values, such as `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant`, etc. Of course, you can override them. - -```cs -await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() -{ - Identity = "IdentityServer", - Action = "ChangePassword" -}); -``` - -Configure `AbpSecurityLogOptions` to provide the application name (in case of you have multiple applications and want to distinguish the applications in the logs) for the log or disable this feature. - -```cs -Configure(options => -{ - options.ApplicationName = "AbpSecurityTest"; -}); -``` - -## Options - -`IdentityOptions` is the standard [options class](../Options.md) provided by the Microsoft [Identity library](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity). So, you can set these options in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) class. - -**Example: Set minimum required length of passwords** - -````csharp -Configure(options => -{ - options.Password.RequiredLength = 5; -}); -```` - -ABP takes these options one step further and allows you to change them on runtime by using the [setting system](../Settings.md). You can [inject](../Dependency-Injection.md) `ISettingManager` and use one of the `Set...` methods to change the option values for a user, a tenant or globally for all users. - -**Example: Change minimum required length of passwords for the current tenant** - -````csharp -public class MyService : ITransientDependency -{ - private readonly ISettingManager _settingManager; - - public MyService(ISettingManager settingManager) - { - _settingManager = settingManager; - } - - public async Task ChangeMinPasswordLength(int minLength) - { - await _settingManager.SetForCurrentTenantAsync( - IdentitySettingNames.Password.RequiredLength, - minLength.ToString() - ); - } -} -```` - -`IdentitySettingNames` class (in the `Volo.Abp.Identity.Settings` namespace) defines constants for the setting names. - -## Distributed Events - -This module defines the following ETOs (Event Transfer Objects) to allow you to subscribe to changes on the entities of the module; - -* `UserEto` is published on changes done on an `IdentityUser` entity. -* `IdentityRoleEto` is published on changes done on an `IdentityRole` entity. -* `IdentityClaimTypeEto` is published on changes done on an `IdentityClaimType` entity. -* `OrganizationUnitEto` is published on changes done on an `OrganizationUnit` entity. - -**Example: Get notified when a new user has been created** - -````csharp -public class MyHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - UserEto user = eventData.Entity; - // TODO: ... - } -} -```` - -`UserEto` and `IdentityRoleEto` are configured to automatically publish the events. You should configure yourself for the others. See the [Distributed Event Bus document](../Distributed-Event-Bus.md) to learn details of the pre-defined events. - -> Subscribing to the distributed events is especially useful for distributed scenarios (like microservice architecture). If you are building a monolithic application, or listening events in the same process that runs the Identity Module, then subscribing to the [local events](../Local-Event-Bus.md) can be more efficient and easier. - -## Internals - -This section covers some internal details of the module that you don't need much, but may need to use in some cases. - -### Domain layer - -#### Aggregates - -##### User - -A user is generally a person logins to and uses the application. - -* `IdentityUser` (aggregate root): Represents a user in the system. - * `IdentityUserRole` (collection): Roles to the user. - * `IdentityUserClaim` (collection): Custom claims of the user. - * `IdentityUserLogin` (collection): External logins of the user. - * `IdentityUserToken` (collection): Tokens of the user (used by the Microsoft Identity services). - -##### Role - -A role is typically a group of permissions to assign to the users. - -* `IdentityRole` (aggregate root): Represents a role in the system. - * `IdentityRoleClaim` (collection): Custom claims of the role. - -##### Claim Type - -A claim type is a definition of a custom claim that can be assigned to other entities (like roles and users) in the system. - -* `IdentityClaimType` (aggregate root): Represents a claim type definition. It contains some properties (e.g. Required, Regex, Description, ValueType) to define the claim type and the validation rules. - -##### Identity Security Log - -A `IdentitySecurityLog` object represents an authentication related operation (like *login*) in the system. - -* `IdentitySecurityLog` (aggregate root): Represents a security log in the system. - -##### OrganizationUnit - -An Organization unit is a entity in a hierarchical structure. - -* ```OrganizationUnit``` (aggregate root): Represents an organization unit in the system. - * ```Roles``` (collection): Roles of the organization unit. - -#### Repositories - -Following custom repositories are defined for this module: - -* `IIdentityUserRepository` -* `IIdentityRoleRepository` -* `IIdentityClaimTypeRepository` -* ```IIdentitySecurityLogRepository``` -* ```IOrganizationUnitRepository``` - -#### Domain services - -##### User manager - -`IdentityUserManager` is used to manage users, their roles, claims, passwords, emails, etc. It is derived from Microsoft Identity's `UserManager` class where `T` is `IdentityUser`. - -##### Role manager - -`IdentityRoleManager` is used to manage roles and their claims. It is derived from Microsoft Identity's `RoleManager` class where `T` is `IdentityRole`. - -##### Claim type manager - -`IdenityClaimTypeManager` is used to perform some operations for the `IdentityClaimType` aggregate root. - -##### Organization unit manager - -```OrganizationUnitManager``` is used to perform some operations for the ```OrganizationUnit``` aggregate root. - -##### Security log manager - -```IdentitySecurityLogManager``` is used to save security logs. - -### Application Layer - -#### Application Services - -* `IdentityUserAppService` (implements `IIdentityUserAppService`): Implements the use cases of the user management UI. -* `IdentityRoleAppService` (implement `IIdentityRoleAppService`): Implements the use cases of the role management UI. -* `IdentityClaimTypeAppService` (implements `IIdentityClaimTypeAppService`): Implements the use cases of the claim type management UI. -* `IdentitySettingsAppService` (implements `IIdentitySettingsAppService`): Used to get and update settings for the Identity module. -* `IdentityUserLookupAppService` (implements `IIdentityUserLookupAppService`): Used to get information for a user by `id` or `userName`. It is aimed to be used internally by the ABP framework. -* `ProfileAppService` (implements `IProfileAppService`): Used to change a user's profile and the password. -* ```IdentitySecurityLogAppService``` (implements ```IIdentitySecurityLogAppService```): Implements the use cases of the security logs UI. -* ```OrganizationUnitAppService``` (implements ```OrganizationUnitAppService```): Implements the use cases of the organization unit management UI. - -### Database Providers - -This module provides [Entity Framework Core](../Entity-Framework-Core.md) and [MongoDB](../MongoDB.md) options for the database. - -#### Entity Framework Core - -[Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore) NuGet package implements the EF Core integration. - -##### Database Tables - -* **AbpRoles** - * AbpRoleClaims -* **AbpUsers** - * AbpUserClaims - * AbpUserLogins - * AbpUserRoles - * AbpUserTokens -* **AbpClaimTypes** -* **AbpOrganizationUnits** - * AbpOrganizationUnitRoles - * AbpUserOrganizationUnits -* **AbpSecurityLogs** - -#### MongoDB - -[Volo.Abp.Identity.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.MongoDB) NuGet package implements the MongoDB integration. - -##### Database Collections - -* **AbpRoles** -* **AbpUsers** -* **AbpClaimTypes** -* **AbpOrganizationUnits** -* **AbpSecurityLogs** - -#### Common Database Properties - -You can set the following properties of the `AbpIdentityDbProperties` class to change the database options: - -* `DbTablePrefix` (`Abp` by default) is the prefix for table/collection names. -* `DbSchema` (`null` by default) is the database schema. -* `ConnectionStringName` (`AbpIdentity` by default) is the [connection string](../Connection-Strings.md) name for this module. - -These are static properties. If you want to set, do it in the beginning of your application (typically, in `Program.cs`). - diff --git a/docs/en/Modules/IdentityServer.md b/docs/en/Modules/IdentityServer.md deleted file mode 100644 index 90c08f6c6b..0000000000 --- a/docs/en/Modules/IdentityServer.md +++ /dev/null @@ -1,175 +0,0 @@ -# IdentityServer Module - -IdentityServer module provides a full integration with the [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) (IDS) framework, which provides advanced authentication features like single sign-on and API access control. This module persists clients, resources and other IDS-related objects to database. **This module is replaced by** [OpenIddict module](https://docs.abp.io/en/abp/latest/Modules/OpenIddict) after ABP v6.0 in the startup templates. - -> Note: You can not use IdentityServer and OpenIddict modules together. They are separate OpenID provider libraries for the same job. - -## How to Install - -You don't need this module when you are using OpenIddict module. However, if you want to keep using IdentityServer4 for your applications, you can install this module and remove the OpenIddict module. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/identityserver). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -This module implements the domain logic and database integrations, but not provides any UI. Management UI is useful if you need to add clients and resources on the fly. In this case, you may build the management UI yourself or consider to purchase the [ABP Commercial](https://commercial.abp.io/) which provides the management UI for this module. - -## Relations to Other Modules - -This module is based on the [Identity Module](Identity.md) and have an [integration package](https://www.nuget.org/packages/Volo.Abp.Account.Web.IdentityServer) with the [Account Module](Account.md). - -## Options - -### AbpIdentityServerBuilderOptions - -`AbpIdentityServerBuilderOptions` can be configured in `PreConfigureServices` method of your Identity Server [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -```` - -`AbpIdentityServerBuilderOptions` properties: - -* `UpdateJwtSecurityTokenHandlerDefaultInboundClaimTypeMap` (default: true): Updates `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap` to be compatible with Identity Server claims. -* `UpdateAbpClaimTypes` (default: true): Updates `AbpClaimTypes` to be compatible with identity server claims. -* `IntegrateToAspNetIdentity` (default: true): Integrate to ASP.NET Identity. -* `AddDeveloperSigningCredential` (default: true): Set false to suppress AddDeveloperSigningCredential() call on the IIdentityServerBuilder. - -`IIdentityServerBuilder` can be configured in `PreConfigureServices` method of your Identity Server [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - builder.AddSigningCredential(...); - }); -} -```` - -## Internals - -### Domain Layer - -#### Aggregates - -##### ApiResource - -API Resources are needed for allowing clients to request access tokens. - -* `ApiResource` (aggregate root): Represents an API resource in the system. - * `ApiSecret` (collection): secrets of the API resource. - * `ApiScope` (collection): scopes of the API resource. - * `ApiResourceClaim` (collection): claims of the API resource. - -##### Client - -Clients represent applications that can request tokens from your Identity Server. - -* `Client` (aggregate root): Represents an Identity Server client application. - * `ClientScope` (collection): Scopes of the client. - * `ClientSecret` (collection): Secrets of the client. - * `ClientGrantType` (collection): Grant types of the client. - * `ClientCorsOrigin` (collection): CORS origins of the client. - * `ClientRedirectUri` (collection): redirect URIs of the client. - * `ClientPostLogoutRedirectUri` (collection): Logout redirect URIs of the client. - * `ClientIdPRestriction` (collection): Provider restrictions of the client. - * `ClientClaim` (collection): Claims of the client. - * `ClientProperty` (collection): Custom properties of the client. - -##### PersistedGrant - -Persisted Grants stores AuthorizationCodes, RefreshTokens and UserConsent. - -* `PersistedGrant` (aggregate root): Represents PersistedGrant for identity server. - -##### IdentityResource - -Identity resources are data like user ID, name, or email address of a user. - -* `IdentityResource` (aggregate root): Represents and Identity Server identity resource. - * `IdentityClaim` (collection): Claims of identity resource. - -#### Repositories - -Following custom repositories are defined for this module: - -* `IApiResourceRepository` -* `IClientRepository` -* `IPersistentGrantRepository` -* `IIdentityResourceRepository` - -#### Domain Services - -This module doesn't contain any domain service but overrides the services below; - -* `AbpProfileService` (Used when `AbpIdentityServerBuilderOptions.IntegrateToAspNetIdentity` is true) -* `AbpClaimsService` -* `AbpCorsPolicyService` - -### Settings - -This module doesn't define any settings. - -### Application Layer - -#### Application Services - -* `ApiResourceAppService` (implements `IApiResourceAppService`): Implements the use cases of the API resource management UI. -* `IdentityServerClaimTypeAppService` (implement `IIdentityServerClaimTypeAppService`): Used to get list of claims. -* `ApiResourceAppService` (implements `IApiResourceAppService`): Implements the use cases of the API resource management UI. -* `IdentityResourceAppService` (implements `IIdentityResourceAppService`): Implements the use cases of the Identity resource management UI. - -### Database Providers - -#### Common - -##### Table/Collection Prefix & Schema - -All tables/collections use the `IdentityServer` prefix by default. Set static properties on the `AbpIdentityServerDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection String - -This module uses `AbpIdentityServer` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -* **IdentityServerApiResources** - * IdentityServerApiSecrets - * IdentityServerApiScopes - * IdentityServerApiScopeClaims - * IdentityServerApiClaims -* **IdentityServerClients** - * IdentityServerClientScopes - * IdentityServerClientSecrets - * IdentityServerClientGrantTypes - * IdentityServerClientCorsOrigins - * IdentityServerClientRedirectUris - * IdentityServerClientPostLogoutRedirectUris - * IdentityServerClientIdPRestrictions - * IdentityServerClientClaims - * IdentityServerClientProperties -* **IdentityServerPersistedGrants** -* **IdentityServerIdentityResources** - * IdentityServerIdentityClaims - -#### MongoDB - -##### Collections - -* **IdentityServerApiResources** -* **IdentityServerClients** -* **IdentityServerPersistedGrants** -* **IdentityServerIdentityResources** \ No newline at end of file diff --git a/docs/en/Modules/Index.md b/docs/en/Modules/Index.md deleted file mode 100644 index 5e280268e7..0000000000 --- a/docs/en/Modules/Index.md +++ /dev/null @@ -1,32 +0,0 @@ -# Application Modules - -ABP is a **modular application framework** which consists of dozens of **NuGet & NPM packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on. - -There are **two types of modules.** They don't have any structural difference but are categorized by functionality and purpose: - -* [**Framework modules**](https://github.com/abpframework/abp/tree/dev/framework/src): These are **core modules of the framework** like caching, emailing, theming, security, serialization, validation, EF Core integration, MongoDB integration... etc. They do not have application/business functionalities but makes your daily development easier by providing common infrastructure, integration and abstractions. -* [**Application modules**](https://github.com/abpframework/abp/tree/dev/modules): These modules implement specific application/business functionalities like blogging, document management, identity management, tenant management... etc. They generally have their own entities, services, APIs and UI components. - -## Open Source Application Modules - -There are some **free and open source** application modules developed and maintained as a part of the ABP Framework. - -* [**Account**](Account.md): Provides UI for the account management and allows user to login/register to the application. -* [**Audit Logging**](Audit-Logging.md): Persists audit logs to a database. -* [**Background Jobs**](Background-Jobs.md): Persist background jobs when using the default background job manager. -* [**CMS Kit**](Cms-Kit/Index.md): A set of reusable *Content Management System* features. -* [**Docs**](Docs.md): Used to create technical documentation website. ABP's [own documentation](https://docs.abp.io) already using this module. -* [**Feature Management**](Feature-Management.md): Used to persist and manage the [features](../Features.md). -* **[Identity](Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. -* [**IdentityServer**](IdentityServer.md): Integrates to IdentityServer4. -* [**OpenIddict**](OpenIddict.md): Integrates to OpenIddict. -* [**Permission Management**](Permission-Management.md): Used to persist permissions. -* **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md). -* [**Tenant Management**](Tenant-Management.md): Manages tenants for a [multi-tenant](../Multi-Tenancy.md) application. -* [**Virtual File Explorer**](Virtual-File-Explorer.md): Provided a simple UI to view files in [virtual file system](../Virtual-File-System.md). - -See [the GitHub repository](https://github.com/abpframework/abp/tree/dev/modules) for source code of all modules. - -## Commercial Application Modules - -[ABP Commercial](https://commercial.abp.io/) license provides **additional pre-built application modules** on top of the ABP framework. See the [module list](https://commercial.abp.io/modules) provided by the ABP Commercial. diff --git a/docs/en/Modules/OpenIddict.md b/docs/en/Modules/OpenIddict.md deleted file mode 100644 index fe0b8270d3..0000000000 --- a/docs/en/Modules/OpenIddict.md +++ /dev/null @@ -1,515 +0,0 @@ -## ABP OpenIddict Module - -OpenIddict module provides an integration with the [OpenIddict](https://github.com/openiddict/openiddict-core) which provides advanced authentication features like single sign-on, single log-out, and API access control. This module persists applications, scopes, and other OpenIddict-related objects to the database. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as a package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/openiddict). The source code is licensed by [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -This module implements the domain logic and database integrations but does not provide any UI. Management UI is useful if you need to add applications and scopes on the fly. In this case, you may build the management UI yourself or consider purchasing the [ABP Commercial](https://commercial.abp.io/) which provides the management UI for this module. - -## Relations to Other Modules - -This module is based on the [Identity Module](Identity.md) and has an [integration package](https://www.nuget.org/packages/Volo.Abp.Account.Web.OpenIddict) with the [Account Module](Account.md). - -## Options - -### OpenIddictBuilder - -`OpenIddictBuilder` can be configured in the `PreConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). - -Example: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -``` - -`OpenIddictBuilder` contains various extension methods to configure the OpenIddict services: - -- `AddServer()` registers the OpenIddict token server services in the DI container. Contains `OpenIddictServerBuilder` configurations. -- `AddCore()` registers the OpenIddict core services in the DI container. Contains `OpenIddictCoreBuilder` configurations. -- `AddValidation()` registers the OpenIddict token validation services in the DI container. Contains `OpenIddictValidationBuilder` configurations. - -### OpenIddictCoreBuilder - -`OpenIddictCoreBuilder` contains extension methods to configure the OpenIddict core services. - -Example: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -``` - -These services contain: - -- Adding `ApplicationStore`, `AuthorizationStore`, `ScopeStore`, `TokenStore`. -- Replacing `ApplicationManager`, `AuthorizationManager`, `ScopeManager`, `TokenManager`. -- Replacing `ApplicationStoreResolver`, `AuthorizationStoreResolver`, `ScopeStoreResolver`, `TokenStoreResolver`. -- Setting `DefaultApplicationEntity`, `DefaultAuthorizationEntity`, `DefaultScopeEntity`, `DefaultTokenEntity`. - -### OpenIddictServerBuilder - -`OpenIddictServerBuilder` contains extension methods to configure OpenIddict server services. - -Example: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -``` - -These services contain: - -- Registering claims, scopes. -- Setting the `Issuer` URI that is used as the base address for the endpoint URIs returned from the discovery endpoint. -- Adding development signing keys, encryption/signing keys, credentials, and certificates. -- Adding/removing event handlers. -- Enabling/disabling grant types. -- Setting authentication server endpoint URIs. - -### OpenIddictValidationBuilder - -`OpenIddictValidationBuilder` contains extension methods to configure OpenIddict validation services. - -Example: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -``` - -These services contain: - -- `AddAudiences()` for resource servers. -- `SetIssuer()` URI that is used to determine the actual location of the OAuth 2.0/OpenID Connect configuration document when using provider discovery. -- `SetConfiguration()` to configure `OpenIdConnectConfiguration`. -- `UseIntrospection()` to use introspection instead of local/direct validation. -- Adding encryption key, credentials, and certificates. -- Adding/removing event handlers. -- `SetClientId() ` to set the client identifier `client_id ` when communicating with the remote authorization server (e.g for introspection). -- `SetClientSecret()` to set the identifier `client_secret` when communicating with the remote authorization server (e.g for introspection). -- `EnableAuthorizationEntryValidation()` to enable authorization validation to ensure the `access token` is still valid by making a database call for each API request. *Note:* This may have a negative impact on performance and can only be used with an OpenIddict-based authorization server. -- `EnableTokenEntryValidation()` to enable authorization validation to ensure the `access token` is still valid by making a database call for each API request. *Note:* This may have a negative impact on performance and it is required when the OpenIddict server is configured to use reference tokens. -- `UseLocalServer()` to register the OpenIddict validation/server integration services. -- `UseAspNetCore()` to register the OpenIddict validation services for ASP.NET Core in the DI container. - -## Internals - -### Domain Layer - -#### Aggregates - -##### OpenIddictApplication - -OpenIddictApplications represent the applications that can request tokens from your OpenIddict Server. - -- `OpenIddictApplications` (aggregate root): Represents an OpenIddict application. - - `ClientId` (string): The client identifier associated with the current application. - - `ClientSecret` (string): The client secret associated with the current application. Maybe hashed or encrypted for security reasons. - - `ConsentType` (string): The consent type associated with the current application. - - `DisplayName` (string): The display name associated with the current application. - - `DisplayNames` (string): The localized display names associated with the current application serialized as a JSON object. - - `Permissions` (string): The permissions associated with the current application, serialized as a JSON array. - - `PostLogoutRedirectUris` (string): The logout callback URLs associated with the current application, serialized as a JSON array. - - `Properties` (string): The additional properties associated with the current application serialized as a JSON object or null. - - `RedirectUris` (string): The callback URLs associated with the current application, serialized as a JSON array. - - `Requirements` (string): The requirements associated with the current application - - `Type` (string): The application type associated with the current application. - - `ClientUri` (string): URI to further information about client. - - `LogoUri` (string): URI to client logo. - -##### OpenIddictAuthorization - -OpenIddictAuthorizations are used to keep the allowed scopes, authorization flow types. - -- `OpenIddictAuthorization` (aggregate root): Represents an OpenIddict authorization. - - - `ApplicationId` (Guid?): The application associated with the current authorization. - - - `Properties` (string): The additional properties associated with the current authorization serialized as a JSON object or null. - - - `Scopes` (string): The scopes associated with the current authorization, serialized as a JSON array. - - - `Status` (string): The status of the current authorization. - - - `Subject` (string): The subject associated with the current authorization. - - - `Type` (string): The type of the current authorization. - -##### OpenIddictScope - -OpenIddictScopes are used to keep the scopes of resources. - -- `OpenIddictScope` (aggregate root): Represents an OpenIddict scope. - - - `Description` (string): The public description associated with the current scope. - - - `Descriptions` (string): The localized public descriptions associated with the current scope, serialized as a JSON object. - - - `DisplayName` (string): The display name associated with the current scope. - - - `DisplayNames` (string): The localized display names associated with the current scope serialized as a JSON object. - - - `Name` (string): The unique name associated with the current scope. - - `Properties` (string): The additional properties associated with the current scope serialized as a JSON object or null. - - `Resources` (string): The resources associated with the current scope, serialized as a JSON array. - -##### OpenIddictToken - -OpenIddictTokens are used to persist the application tokens. - -- `OpenIddictToken` (aggregate root): Represents an OpenIddict token. - - - `ApplicationId` (Guid?): The application associated with the current token. - - `AuthorizationId` (Guid?): The application associated with the current token. - - `CreationDate` (DateTime?): The UTC creation date of the current token. - - `ExpirationDate` (DateTime?): The UTC expiration date of the current token. - - `Payload` (string): The payload of the current token, if applicable. Only used for reference tokens and may be encrypted for security reasons. - - - `Properties` (string): The additional properties associated with the current token serialized as a JSON object or null. - - `RedemptionDate` (DateTime?): The UTC redemption date of the current token. - - `Status` (string): The status of the current authorization. - - - `ReferenceId` (string): The reference identifier associated with the current token, if applicable. Only used for reference tokens and may be hashed or encrypted for security reasons. - - - `Status` (string): The status of the current token. - - - `Subject` (string): The subject associated with the current token. - - - `Type` (string): The type of the current token. - -#### Stores - -This module implements OpenIddict stores: - -- `IAbpOpenIdApplicationStore` -- `IOpenIddictAuthorizationStore` -- `IOpenIddictScopeStore` -- `IOpenIddictTokenStore` - -#### AbpOpenIddictStoreOptions - -You can configure the `PruneIsolationLevel/DeleteIsolationLevel` of `AbpOpenIddictStoreOptions` to set the isolation level for the store operations becasue different databases have different isolation levels. - -##### Repositories - -The following custom repositories are defined in this module: - -- `IOpenIddictApplicationRepository` -- `IOpenIddictAuthorizationRepository` -- `IOpenIddictScopeRepository` -- `IOpenIddictTokenRepository` - -##### Domain Services - -This module doesn't contain any domain service but overrides the service below: - -- `AbpApplicationManager` used to populate/get `AbpApplicationDescriptor` information that contains `ClientUri` and `LogoUri`. - -### Database Providers - -#### Common - -##### Table/Collection Prefix & Schema - -All tables/collections use the `OpenIddict` prefix by default. Set static properties on the `AbpOpenIddictDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). - -##### Connection String - -This module uses `AbpOpenIddict` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string. - -See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details. - -#### Entity Framework Core - -##### Tables - -- **OpenIddictApplications** -- **OpenIddictAuthorizations** -- **OpenIddictScopes** -- **OpenIddictTokens** - -#### MongoDB - -##### Collections - -- **OpenIddictApplications** -- **OpenIddictAuthorizations** -- **OpenIddictScopes** -- **OpenIddictTokens** - -## ASP.NET Core Module - -This module integrates ASP NET Core, with built-in MVC controllers for four protocols. It uses OpenIddict's [Pass-through mode](https://documentation.openiddict.com/guides/index.html#pass-through-mode). - -```cs -AuthorizeController -> connect/authorize -TokenController -> connect/token -LogoutController -> connect/logout -UserInfoController -> connect/userinfo -``` - -> **Device flow** implementation will be done in the commercial module. - -#### AbpOpenIddictAspNetCoreOptions - -`AbpOpenIddictAspNetCoreOptions` can be configured in the `PreConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). - -Example: - -```csharp -PreConfigure(options => -{ - //Set options here... -}); -``` - -`AbpOpenIddictAspNetCoreOptions` properties: - -- `UpdateAbpClaimTypes(default: true)`: Updates `AbpClaimTypes` to be compatible with the Openiddict claims. -- `AddDevelopmentEncryptionAndSigningCertificate(default: true)`: Registers (and generates if necessary) a user-specific development encryption/development signing certificate. This is a certificate used for signing and encrypting the tokens and for **development environment only**. You must set it to **false** for non-development environments. - -> `AddDevelopmentEncryptionAndSigningCertificate` cannot be used in applications deployed on IIS or Azure App Service: trying to use them on IIS or Azure App Service will result in an exception being thrown at runtime (unless the application pool is configured to load a user profile). To avoid that, consider creating self-signed certificates and storing them in the X.509 certificates store of the host machine(s). Please refer to: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html#registering-a-development-certificate - -#### Automatically Removing Orphaned Tokens/Authorizations - -The background task that automatically removes orphaned tokens/authorizations. This can be configured by `TokenCleanupOptions` to manage it. - -`TokenCleanupOptions` can be configured in the `ConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). - -Example: - -```csharp -Configure(options => -{ - //Set options here... -}); -``` - -`TokenCleanupOptions` properties: - -- `IsCleanupEnabled` (default: true): Enable/disable token clean up. -- `CleanupPeriod` (default: 3,600,000 ms): Setting clean up period. -- `DisableAuthorizationPruning`: Setting a boolean indicating whether authorizations pruning should be disabled. -- `DisableTokenPruning`: Setting a boolean indicating whether token pruning should be disabled. -- `MinimumAuthorizationLifespan` (default: 14 days): Setting the minimum lifespan authorizations must have to be pruned. Cannot be less than 10 minutes. -- `MinimumTokenLifespan` (default: 14 days): Setting the minimum lifespan tokens must have to be pruned. Cannot be less than 10 minutes. - -#### Updating Claims In Access_token and Id_token - -[Claims Principal Factory](https://docs.abp.io/en/abp/latest/Authorization#claims-principal-factory) can be used to add/remove claims to the `ClaimsPrincipal`. - -The `AbpDefaultOpenIddictClaimsPrincipalHandler` service will add `Name`, `Email,` and `Role` types of Claims to `access_token` and `id_token`, other claims are only added to `access_token` by default, and remove the `SecurityStampClaimType` secret claim of `Identity`. - -Create a service that inherits from `IAbpOpenIddictClaimsPrincipalHandler` and add it to DI to fully control the destinations of claims. - -```cs -public class MyClaimDestinationsHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency -{ - public virtual Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context) - { - foreach (var claim in context.Principal.Claims) - { - if (claim.Type == MyClaims.MyClaimsType) - { - claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken); - } - - if (claim.Type == MyClaims.MyClaimsType2) - { - claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); - } - } - - return Task.CompletedTask; - } -} - -Configure(options => -{ - options.ClaimsPrincipalHandlers.Add(); -}); -``` - -For detailed information, please refer to: [OpenIddict claim destinations](https://documentation.openiddict.com/configuration/claim-destinations.html) - -#### Disable AccessToken Encryption - -ABP disables the `access token encryption` by default for compatibility, it can be enabled manually if needed. - -```cs -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - builder.Configure(options => options.DisableAccessTokenEncryption = false); - }); -} -``` - -https://documentation.openiddict.com/configuration/token-formats.html#disabling-jwt-access-token-encryption - -### Request/Response Process - -The `OpenIddict.Server.AspNetCore` adds an authentication scheme(`Name: OpenIddict.Server.AspNetCore, handler: OpenIddictServerAspNetCoreHandler`) and implements the `IAuthenticationRequestHandler` interface. - -It will be executed first in `AuthenticationMiddleware` and can short-circuit the current request. Otherwise, `DefaultAuthenticateScheme` will be called and continue to execute the pipeline. - -`OpenIddictServerAspNetCoreHandler` will call various built-in handlers (handling requests and responses), And the handler will process according to the context or skip logic that has nothing to do with it. - -Example of a token request: - -``` -POST /connect/token HTTP/1.1 -Content-Type: application/x-www-form-urlencoded - - grant_type=password& - client_id=AbpApp& - client_secret=1q2w3e*& - username=admin& - password=1q2w3E*& - scope=AbpAPI offline_access -``` - -This request will be processed by various handlers. They will confirm the endpoint type of the request, check `HTTP/HTTPS`, verify that the request parameters (`client. scope, etc`) are valid and exist in the database, etc. Various protocol checks. And build a `OpenIddictRequest` object, If there are any errors, the response content may be set and directly short-circuit the current request. - -If everything is ok, the request will go to our processing controller(eg `TokenController`), we can get an `OpenIddictRequest` from the HTTP request at this time. The rest will be based on this object. - -Check the `username` and `password` in the request. If it is correct create a `ClaimsPrincipal` object and return a `SignInResult`, which uses the `OpenIddict.Validation.AspNetCore` authentication scheme name, will calls `OpenIddictServerAspNetCoreHandler` for processing. - -`OpenIddictServerAspNetCoreHandler` do some checks to generate json and replace the http response content. - -The `ForbidResult` `ChallengeResult` are all the above types of processing. - -If you need to customize OpenIddict, you need to replace/delete/add new handlers and make it execute in the correct order. - -Please refer to: -https://documentation.openiddict.com/guides/index.html#events-model - -### PKCE - -https://documentation.openiddict.com/configuration/proof-key-for-code-exchange.html - -### Setting Tokens Lifetime - -Update `PreConfigureServices` method of AuthServerModule (or HttpApiHostModule if you don't have tiered/separate-authserver) file: - -```csharp -PreConfigure(builder => -{ - builder.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(30)); - builder.SetAccessTokenLifetime(TimeSpan.FromMinutes(30)); - builder.SetIdentityTokenLifetime(TimeSpan.FromMinutes(30)); - builder.SetRefreshTokenLifetime(TimeSpan.FromDays(14)); -}); -``` - -### Refresh Token - -To use refresh token, it must be supported by OpenIddictServer and the `refresh_token` must be requested by the application. - -> **Note:** Angular application is already configured to use `refresh_token`. - -#### Configuring OpenIddictServer - -Update the **OpenIddictDataSeedContributor**, add `OpenIddictConstants.GrantTypes.RefreshToken` to grant types in `CreateApplicationAsync` method: - -```csharp -await CreateApplicationAsync( - ... - grantTypes: new List //Hybrid flow - { - OpenIddictConstants.GrantTypes.AuthorizationCode, - OpenIddictConstants.GrantTypes.Implicit, - OpenIddictConstants.GrantTypes.RefreshToken, - }, - ... -``` - -> **Note:** You need to re-create this client if you have generated the database already. - -#### Configuring Application: - -You need to request the **offline_access scope** to be able to receive `refresh_token`. - -In **Razor/MVC, Blazor-Server applications**, add `options.Scope.Add("offline_access");` to **OpenIdConnect** options. These application templates are using cookie authentication by default and has default cookie expire options set as: - -```csharp -.AddCookie("Cookies", options => -{ - options.ExpireTimeSpan = TimeSpan.FromDays(365); -}) -``` - -[Cookie ExpireTimeSpan will ignore access_token expiration](https://learn.microsoft.com/en-us/dotnet/api/Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions.ExpireTimeSpan?view=aspnetcore-7.0&viewFallbackFrom=net-7.0) and expired access_token will still be valid if it is set to higher value than the `refresh_token lifetime`. It is recommended to keep **Cookie ExpireTimeSpan** and the **Refresh Token lifetime** same, hence the new token will be persisted in the cookie. - -In **Blazor wasm** applications, add `options.ProviderOptions.DefaultScopes.Add("offline_access");` to **AddOidcAuthentication** options. - -In **Angular** applications, add `offline_access` to **oAuthConfig** scopes in *environment.ts* file. (Angular applications already have this configuration). - -## About localization - -We don't localize any error messages in the OpenIddict module, Because the OAuth 2.0 specification restricts the charset you're allowed to use for the error and error_description parameters: - -> A.7. "error" Syntax -> The "error" element is defined in Sections 4.1.2.1, 4.2.2.1, 5.2, 7.2, and 8.5: - -``` -error = 1*NQSCHAR -``` - -> A.8. "error_description" Syntax ->T he "error_description" element is defined in Sections 4.1.2.1, 4.2.2.1, 5.2, and 7.2: - -``` -error-description = 1*NQSCHAR -NQSCHAR = %x20-21 / %x23-5B / %x5D-7E -``` - -## Demo projects - -In the module's `app` directory there are six projects(including `angular`) - -* `OpenIddict.Demo.Server`: An abp application with integrated modules (has two `clients` and a `scope`). -* `OpenIddict.Demo.API`: ASP NET Core API application using JwtBearer authentication. -* `OpenIddict.Demo.Client.Mvc`: ASP NET Core MVC application using `OpenIdConnect` for authentication. -* `OpenIddict.Demo.Client.Console`: Use `IdentityModel` to test OpenIddict's various endpoints, and call the api of `OpenIddict.Demo.API`. -* `OpenIddict.Demo.Client.BlazorWASM:` ASP NET Core Blazor application using `OidcAuthentication` for authentication. -* `angular`: An angular application that integrates the abp ng modules and uses oauth for authentication. - -#### How to run? - -Confirm the connection string of `appsettings.json` in the `OpenIddict.Demo.Server` project. Running the project will automatically create the database and initialize the data. -After running the `OpenIddict.Demo.API` project, then you can run the rest of the projects to test. - -## Migrating Guide - -[Migrating from IdentityServer to OpenIddict Step by Step Guide ](../Migration-Guides/OpenIddict-Step-by-Step.md) diff --git a/docs/en/Modules/Permission-Management.md b/docs/en/Modules/Permission-Management.md deleted file mode 100644 index e77a284a42..0000000000 --- a/docs/en/Modules/Permission-Management.md +++ /dev/null @@ -1,109 +0,0 @@ -# Permission Management Module - -This module implements the `IPermissionStore` to store and manage permissions values in a database. - -> This document covers only the permission management module which persists permission values to a database. See the [Authorization document](../Authorization.md) to understand the authorization and permission systems. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/permission-management). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -### Permission Management Dialog - -Permission management module provides a reusable dialog to manage permissions related to an object. For example, the [Identity Module](Identity.md) uses it to manage permissions of users and roles. The following image shows Identity Module's Role Management page: - -![permissions-module-open-dialog](../images/permissions-module-open-dialog.png) - -When you click *Actions* -> *Permissions* for a role, the permission management dialog is opened. An example screenshot from this dialog: - -![permissions-module-dialog](../images/permissions-module-dialog.png) - -In this dialog, you can grant permissions for the selected role. The tabs in the left side represents main permission groups and the right side contains the permissions defined in the selected group. - -## IPermissionManager - -`IPermissionManager` is the main service provided by this module. It is used to read and change the permission values. `IPermissionManager` is typically used by the *Permission Management Dialog*. However, you can inject it if you need to set a permission value. - -> If you just want to read/check permission values for the current user, use the `IAuthorizationService` or the `[Authorize]` attribute as explained in the [Authorization document](../Authorization.md). - -**Example: Grant permissions to roles and users using the `IPermissionManager` service** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IPermissionManager _permissionManager; - - public MyService(IPermissionManager permissionManager) - { - _permissionManager = permissionManager; - } - - public async Task GrantRolePermissionDemoAsync( - string roleName, string permission) - { - await _permissionManager - .SetForRoleAsync(roleName, permission, true); - } - - public async Task GrantUserPermissionDemoAsync( - Guid userId, string roleName, string permission) - { - await _permissionManager - .SetForUserAsync(userId, permission, true); - } -} -```` - -## Permission Management Providers - -Permission Management Module is extensible, just like the [permission system](../Authorization.md). You can extend it by defining permission management providers. - -[Identity Module](Identity.md) defines the following permission management providers: - -* `UserPermissionManagementProvider`: Manages user-based permissions. -* `RolePermissionManagementProvider`: Manages role-based permissions. - -`IPermissionManager` uses these providers when you get/set permissions. You can define your own provider by implementing the `IPermissionManagementProvider` or inheriting from the `PermissionManagementProvider` base class. - -**Example:** - -````csharp -public class CustomPermissionManagementProvider : PermissionManagementProvider -{ - public override string Name => "Custom"; - - public CustomPermissionManagementProvider( - IPermissionGrantRepository permissionGrantRepository, - IGuidGenerator guidGenerator, - ICurrentTenant currentTenant) - : base( - permissionGrantRepository, - guidGenerator, - currentTenant) - { - } -} -```` - -`PermissionManagementProvider` base class makes the default implementation (using the `IPermissionGrantRepository`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each permission value record). - -Once you create your provider class, you should register it using the `PermissionManagementOptions` [options class](../Options.md): - -````csharp -Configure(options => -{ - options.ManagementProviders.Add(); -}); -```` - -The order of the providers are important. Providers are executed in the reverse order. That means the `CustomPermissionManagementProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. - -## See Also - -* [Authorization](../Authorization.md) \ No newline at end of file diff --git a/docs/en/Modules/Setting-Management.md b/docs/en/Modules/Setting-Management.md deleted file mode 100644 index e26c7c806e..0000000000 --- a/docs/en/Modules/Setting-Management.md +++ /dev/null @@ -1,313 +0,0 @@ -# Setting Management Module - -Setting Management Module implements the `ISettingStore` (see [the setting system](../Settings.md)) to store the setting values in a database and provides the `ISettingManager` to manage (change) the setting values in the database. - -> Setting Management module is already installed and configured for [the startup templates](../Startup-Templates/Index.md). So, most of the times you don't need to manually add this module to your application. - -## ISettingManager - -`ISettingManager` is used to get and set the values for the settings. Examples: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.SettingManagement; - -namespace Demo -{ - public class MyService : ITransientDependency - { - private readonly ISettingManager _settingManager; - - //Inject ISettingManager service - public MyService(ISettingManager settingManager) - { - _settingManager = settingManager; - } - - public async Task FooAsync() - { - Guid user1Id = ...; - Guid tenant1Id = ...; - - //Get/set a setting value for the current user or the specified user - - string layoutType1 = - await _settingManager.GetOrNullForCurrentUserAsync("App.UI.LayoutType"); - string layoutType2 = - await _settingManager.GetOrNullForUserAsync("App.UI.LayoutType", user1Id); - - await _settingManager.SetForCurrentUserAsync("App.UI.LayoutType", "LeftMenu"); - await _settingManager.SetForUserAsync(user1Id, "App.UI.LayoutType", "LeftMenu"); - - //Get/set a setting value for the current tenant or the specified tenant - - string layoutType3 = - await _settingManager.GetOrNullForCurrentTenantAsync("App.UI.LayoutType"); - string layoutType4 = - await _settingManager.GetOrNullForTenantAsync("App.UI.LayoutType", tenant1Id); - - await _settingManager.SetForCurrentTenantAsync("App.UI.LayoutType", "LeftMenu"); - await _settingManager.SetForTenantAsync(tenant1Id, "App.UI.LayoutType", "LeftMenu"); - - //Get/set a global and default setting value - - string layoutType5 = - await _settingManager.GetOrNullGlobalAsync("App.UI.LayoutType"); - string layoutType6 = - await _settingManager.GetOrNullDefaultAsync("App.UI.LayoutType"); - - await _settingManager.SetGlobalAsync("App.UI.LayoutType", "TopMenu"); - } - } -} - -```` - -So, you can get or set a setting value for different setting value providers (Default, Global, User, Tenant... etc). - -> Use the `ISettingProvider` instead of the `ISettingManager` if you only need to read the setting values, because it implements caching and supports all deployment scenarios. You can use the `ISettingManager` if you are creating a setting management UI. - -### Setting Cache - -Setting values are cached using the [distributed cache](../Caching.md) system. Always use the `ISettingManager` to change the setting values which manages the cache for you. - -## Setting Management Providers - -Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered it the following order: - -* `DefaultValueSettingManagementProvider`: Gets the value from the default value of the setting definition. It can not set the default value since default values are hard-coded on the setting definition. -* `ConfigurationSettingManagementProvider`: Gets the value from the [IConfiguration service](../Configuration.md). It can not set the configuration value because it is not possible to change the configuration values on runtime. -* `GlobalSettingManagementProvider`: Gets or sets the global (system-wide) value for a setting. -* `TenantSettingManagementProvider`: Gets or sets the setting value for a tenant. -* `UserSettingManagementProvider`: Gets the setting value for a user. - -`ISettingManager` uses the setting management providers on get/set methods. Typically, every setting management provider defines extension methods on the `ISettingManagement` service (like `SetForUserAsync` defined by the user setting management provider). - -If you want to create your own provider, implement the `ISettingManagementProvider` interface or inherit from the `SettingManagementProvider` base class: - -````csharp -public class CustomSettingProvider : SettingManagementProvider, ITransientDependency -{ - public override string Name => "Custom"; - - public CustomSettingProvider(ISettingManagementStore store) - : base(store) - { - } -} -```` - -`SettingManagementProvider` base class makes the default implementation (using the `ISettingManagementStore`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each setting value record). - -Once you create your provider class, you should register it using the `SettingManagementOptions` [options class](../Options.md): - -````csharp -Configure(options => -{ - options.Providers.Add(); -}); -```` - -The order of the providers are important. Providers are executed in the reverse order. That means the `CustomSettingProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. - -## See Also - -* [Settings](../Settings.md) - -## Setting Management UI - -Setting Mangement module provided the email setting UI by default. - -![EmailSettingUi](../images/setting-management-email-ui.png) - -> You can click the Send test email button to send a test email to check your email settings. - -Setting it is extensible; You can add your tabs to this page for your application settings. - -### MVC UI - -#### Create a setting View Component - -Create `MySettingGroup` folder under the `Components` folder. Add a new view component. Name it as `MySettingGroupViewComponent`: - -![MySettingGroupViewComponent](../images/my-setting-group-view-component.png) - -Open the `MySettingGroupViewComponent.cs` and change the whole content as shown below: - -```csharp -public class MySettingGroupViewComponent : AbpViewComponent -{ - public virtual IViewComponentResult Invoke() - { - return View("~/Components/MySettingGroup/Default.cshtml"); - } -} -``` - -> You can also use the `InvokeAsync` method, In this example, we use the `Invoke` method. - -#### Default.cshtml - -Create a `Default.cshtml` file under the `MySettingGroup` folder. - -Open the `Default.cshtml` and change the whole content as shown below: - -```html -
-

My setting group page

-
-``` - -#### BookStoreSettingPageContributor - -Create a `BookStoreSettingPageContributor.cs` file under the `Settings` folder: - -![BookStoreSettingPageContributor](../images/my-setting-group-page-contributor.png) - -The content of the file is shown below: - -```csharp -public class BookStoreSettingPageContributor : ISettingPageContributor -{ - public Task ConfigureAsync(SettingPageCreationContext context) - { - context.Groups.Add( - new SettingPageGroup( - "Volo.Abp.MySettingGroup", - "MySettingGroup", - typeof(MySettingGroupViewComponent), - order : 1 - ) - ); - - return Task.CompletedTask; - } - - public Task CheckPermissionsAsync(SettingPageCreationContext context) - { - // You can check the permissions here - return Task.FromResult(true); - } -} -``` - -Open the `BookStoreWebModule.cs` file and add the following code: - -```csharp -Configure(options => -{ - options.Contributors.Add(new BookStoreSettingPageContributor()); -}); -``` - -#### Run the Application - -Navigate to `/SettingManagement` route to see the changes: - -![Custom Settings Tab](../images/my-setting-group-ui.png) - -### Blazor UI - -#### Create a Razor Component - -Create `MySettingGroup` folder under the `Pages` folder. Add a new razor component. Name it as `MySettingGroupComponent`: - -![MySettingGroupComponent](../images/my-setting-group-component.png) - -Open the `MySettingGroupComponent.razor` and change the whole content as shown below: - -```csharp - -

my setting group

-
-``` - -#### BookStoreSettingComponentContributor - -Create a `BookStoreSettingComponentContributor.cs` file under the `Settings` folder: - -![BookStoreSettingComponentContributor](../images/my-setting-group-component-contributor.png) - -The content of the file is shown below: - -```csharp -public class BookStoreSettingComponentContributor : ISettingComponentContributor -{ - public Task ConfigureAsync(SettingComponentCreationContext context) - { - context.Groups.Add( - new SettingComponentGroup( - "Volo.Abp.MySettingGroup", - "MySettingGroup", - typeof(MySettingGroupComponent), - order : 1 - ) - ); - - return Task.CompletedTask; - } - - public Task CheckPermissionsAsync(SettingComponentCreationContext context) - { - // You can check the permissions here - return Task.FromResult(true); - } -} -``` - -Open the `BookStoreBlazorModule.cs` file and add the following code: - -```csharp -Configure(options => -{ - options.Contributors.Add(new BookStoreSettingComponentContributor()); -}); -``` - -#### Run the Application - -Navigate to `/setting-management` route to see the changes: - -![Custom Settings Tab](../images/my-setting-group-blazor.png) - -### Angular UI - -#### Create a Component - -Create a component with the following command: - -```bash -yarn ng generate component my-settings -``` - -Open the `app.component.ts` and modify the file as shown below: - -```js -import { Component } from '@angular/core'; -import { SettingTabsService } from '@abp/ng.setting-management/config'; // imported SettingTabsService -import { MySettingsComponent } from './my-settings/my-settings.component'; // imported MySettingsComponent - -@Component(/* component metadata */) -export class AppComponent { - constructor(private settingTabs: SettingTabsService) // injected MySettingsComponent - { - // added below - settingTabs.add([ - { - name: 'MySettings', - order: 1, - requiredPolicy: 'policy key here', - component: MySettingsComponent, - }, - ]); - } -} -``` - -#### Run the Application - -Navigate to `/setting-management` route to see the changes: - -![Custom Settings Tab](../images/custom-settings.png) \ No newline at end of file diff --git a/docs/en/Modules/Tenant-Management.md b/docs/en/Modules/Tenant-Management.md deleted file mode 100644 index 6245db82c8..0000000000 --- a/docs/en/Modules/Tenant-Management.md +++ /dev/null @@ -1,134 +0,0 @@ -# Tenant Management Module - -[Multi-Tenancy](../Multi-Tenancy.md) is one of the core features of ABP Framework. It provides the fundamental infrastructure to build your own SaaS (Software-as-a-Service) solution. ABP's multi-tenancy system abstracts where your tenants are stored, by providing the `ITenantStore` interface. All you need to do is to implement that interface. - -**The Tenant Management module is an implementation of the the `ITenantStore` interface. It stores tenants in a database. It also provides UI to manage your tenants and their [features](../Features.md).** - -> Please **refer to the [Multi-Tenancy](../Multi-Tenancy.md) documentation** to understand the multi-tenancy system of the ABP Framework. This document focuses on the Tenant Management module. - -### About the ABP Commercial SaaS Module - -The [SaaS Module](https://commercial.abp.io/modules/Volo.Saas) is an alternative implementation of this module with more features and possibilities. It is distributed as a part of the [ABP Commercial](https://commercial.abp.io/) subscription. - -## How to Install - -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. - -### The Source Code - -The source code of this module can be accessed [here](https://github.com/abpframework/abp/tree/dev/modules/tenant-management). The source code is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can freely use and customize it. - -## User Interface - -This module adds "*Administration -> Tenant Management -> Tenants*" menu item to the main menu of the application, which opens the page shown below: - -![module-tenant-management-page](../images/module-tenant-management-page.png) - -In this page, you see the all the tenants. You can create a new tenant as shown below: - -![module-tenant-management-new-tenant](../images/module-tenant-management-new-tenant.png) - -In this modal; - -* **Name**: The unique name of the tenant. If you use subdomains for your tenants (like https://some-tenant.your-domain.com), this will be the subdomain name. -* **Admin Email Address**: Email address of the admin user for this tenant. -* **Admin Password**: The password of the admin user for this tenant. - -When you click to *Actions* button near to a tenant, you will see the actions you can take: - -![module-tenant-management-actions](../images/module-tenant-management-actions.png) - -### Managing the Tenant Features - -The Features action opens a modal to enable/disable/set [features](../Features.md) for the related tenant. Here, an example modal: - -![features-modal](../images/features-modal.png) - -### Managing the Host Features - -*Manage Host features* button is used to set features for the host side, if you use the features of your application also in the host side. - -## Distributed Events - -This module defines the following ETOs (Event Transfer Objects) to allow you to subscribe to changes on the entities of the module; - -- `TenantEto` is published on changes done on an `Tenant` entity. - -**Example: Get notified when a new tenant has been created** - -```cs -public class MyHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - TenantEto tenant = eventData.Entity; - // TODO: ... - } -} -``` - - - -`TenantEto` is configured to automatically publish the events. You should configure yourself for the others. See the [Distributed Event Bus document](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Distributed-Event-Bus.md) to learn details of the pre-defined events. - -> Subscribing to the distributed events is especially useful for distributed scenarios (like microservice architecture). If you are building a monolithic application, or listening events in the same process that runs the Tenant Management Module, then subscribing to the [local events](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Local-Event-Bus.md) can be more efficient and easier. - -## Internals - -This section can be used as a reference if you want to [customize](../Customizing-Application-Modules-Guide.md) this module without changing [its source code](https://github.com/abpframework/abp/tree/dev/modules/tenant-management). - -### Domain Layer - -#### Aggregates - -* `Tenant` - -#### Repositories - -* `ITenantRepository` - -#### Domain Services - -* `TenantManager` - -### Application Layer - -#### Application Services - -* `TenantAppService` - -#### Permissions - -- `AbpTenantManagement.Tenants`: Tenant management. -- `AbpTenantManagement.Tenants.Create`: Creating a new tenant. -- `AbpTenantManagement.Tenants.Update`: Editing an existing tenant. -- `AbpTenantManagement.Tenants.Delete`: Deleting an existing tenant. -- `AbpTenantManagement.Tenants.ManageFeatures`: Manage features of the tenants. - -### Entity Framework Core Integration - -* `TenantManagementDbContext` (implements `ITenantManagementDbContext`) - -**Database Tables:** - -* `AbpTenants` -* `AbpTenantConnectionStrings` - -### MongoDB Integration - -* `TenantManagementMongoDbContext` (implements `ITenantManagementMongoDbContext`) - -**Database Collections:** - -* `AbpTenants` (also includes the connection string) - -## Notices - -ABP Framework allows to use *database per tenant* approach that allows a tenant can have a dedicated database. This module has the fundamental infrastructure to make that implementation possible (see its source code), however it doesn't implement the application layer and UI functionalities to provide it as an out of the box implementation. You can implement these features yourself, or consider to use the [ABP Commercial Saas Module](https://docs.abp.io/en/commercial/latest/modules/saas) that fully implements it and provides much more business features. - -## See Also - -* [Multi-Tenancy](../Multi-Tenancy.md) -* [ABP Commercial SaaS Module](https://docs.abp.io/en/commercial/latest/modules/saas) diff --git a/docs/en/Modules/Virtual-File-Explorer.md b/docs/en/Modules/Virtual-File-Explorer.md deleted file mode 100644 index a952b2c1db..0000000000 --- a/docs/en/Modules/Virtual-File-Explorer.md +++ /dev/null @@ -1,87 +0,0 @@ -# Virtual File Explorer Module - -## What is Virtual File Explorer Module? - -Virtual File Explorer Module provided a simple UI to view all files in [virtual file system](../Virtual-File-System.md). - -> Virtual File Explorer Module is not installed for [the startup templates](../Startup-Templates/Index.md). So, you need to manually add this module to your application. - -### Installation - -#### 1- Use ABP CLI - -It is recommended to use the [ABP CLI](../CLI.md) to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command: - -``` -abp add-module Volo.VirtualFileExplorer -``` - -> If you haven't done it yet, you first need to install the [ABP CLI](../CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.VirtualFileExplorer.Web). - -#### 2- Manually install - -Or you can also manually install nuget package to `Acme.MyProject.Web` project: - -* Install [Volo.Abp.VirtualFileExplorer.Web](https://www.nuget.org/packages/Volo.Abp.VirtualFileExplorer.Web/) nuget package to `Acme.MyProject.Web` project. - - `Install-Package Volo.Abp.VirtualFileExplorer.Web` - -##### 2.1- Adding Module Dependencies - - * Open `MyProjectWebModule.cs`and add `typeof(AbpVirtualFileExplorerWebModule)` as shown below; - - ```csharp - [DependsOn( - typeof(AbpVirtualFileExplorerWebModule), - typeof(MyProjectApplicationModule), - typeof(MyProjectEntityFrameworkCoreModule), - typeof(AbpAutofacModule), - typeof(AbpIdentityWebModule), - typeof(AbpAccountWebModule), - typeof(AbpAspNetCoreMvcUiBasicThemeModule) - )] - public class MyProjectWebModule : AbpModule - { - //... - } - ``` - -##### 2.2- Adding NPM Package - - * Open `package.json` and add `@abp/virtual-file-explorer": "^2.9.0` as shown below: - - ```json - { - "version": "1.0.0", - "name": "my-app", - "private": true, - "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^2.9.0", - "@abp/virtual-file-explorer": "^2.9.0" - } - } - ``` - - Then open the command line terminal in the `Acme.MyProject.Web` project folder and run the following command: - -````bash -abp install-libs -```` - -That's all,Now run the application and Navigate to `/VirtualFileExplorer`. You will see virtual file explorer page: - -![Virtual-File-Explorer](../images/virtual-file-explorer.png) - -### Options - -You can disabled virtual file explorer module via `AbpVirtualFileExplorerOptions` options: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.IsEnabled = false; - }); -} -``` \ No newline at end of file diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md deleted file mode 100644 index 101712da35..0000000000 --- a/docs/en/MongoDB.md +++ /dev/null @@ -1,513 +0,0 @@ -# MongoDB Integration - -This document explains how to integrate MongoDB as a database provider to ABP based applications and how to configure it. - -## Installation - -`Volo.Abp.MongoDB` is the main NuGet package for the MongoDB integration. Install it to your project (for a layered application, to your data/infrastructure layer), You can use the [ABP CLI](CLI.md) to install it to your project. Execute the following command in the folder of the .csproj file of the layer: - -``` -abp add-package Volo.Abp.MongoDB -``` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.MongoDB). - -Then add `AbpMongoDbModule` module dependency to your [module](Module-Development-Basics.md): - -```c# -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -## Creating a Mongo Db Context - -ABP introduces **Mongo Db Context** concept (which is similar to Entity Framework Core's DbContext) to make it easier to use collections and configure them. An example is shown below: - -```c# -public class MyDbContext : AbpMongoDbContext -{ - public IMongoCollection Questions => Collection(); - - public IMongoCollection Categories => Collection(); - - protected override void CreateModel(IMongoModelBuilder modelBuilder) - { - base.CreateModel(modelBuilder); - - //Customize the configuration for your collections. - } -} -``` - -* It's derived from `AbpMongoDbContext` class. -* Adds a public `IMongoCollection` property for each mongo collection. ABP uses these properties to create default repositories by default. -* Overriding `CreateModel` method allows to configure collection configuration. - -### Configure Mapping for a Collection - -ABP automatically register entities to MongoDB client library for all `IMongoCollection` properties in your DbContext. For the example above, `Question` and `Category` entities are automatically registered. - -For each registered entity, it calls `AutoMap()` and configures known properties of your entity. For instance, if your entity implements `IHasExtraProperties` interface (which is already implemented for every aggregate root by default), it automatically configures `ExtraProperties`. - -So, most of times you don't need to explicitly configure registration for your entities. However, if you need it you can do it by overriding the `CreateModel` method in your DbContext. Example: - -````csharp -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.Entity(b => - { - b.CollectionName = "MyQuestions"; //Sets the collection name - b.BsonMap.UnmapProperty(x => x.MyProperty); //Ignores 'MyProperty' - }); -} -```` - -This example changes the mapped collection name to 'MyQuestions' in the database and ignores a property in the `Question` class. - -If you only need to configure the collection name, you can also use `[MongoCollection]` attribute for the collection in your DbContext. Example: - -````csharp -[MongoCollection("MyQuestions")] //Sets the collection name -public IMongoCollection Questions => Collection(); -```` - -### Configure Indexes and CreateCollectionOptions for a Collection - -You can configure indexes and `CreateCollectionOptions` for your collections by overriding the `CreateModel` method. Example: - -````csharp -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.Entity(b => - { - b.CreateCollectionOptions.Collation = new Collation(locale:"en_US", strength: CollationStrength.Secondary); - b.ConfigureIndexes(indexes => - { - indexes.CreateOne( - new CreateIndexModel( - Builders.IndexKeys.Ascending("MyProperty"), - new CreateIndexOptions { Unique = true } - ) - ); - } - ); - }); -} -```` - -This example sets a collation for the collection and creates a unique index for the `MyProperty` property. - -### Configure the Connection String Selection - -If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example: - -````csharp -[ConnectionStringName("MySecondConnString")] -public class MyDbContext : AbpMongoDbContext -{ - -} -```` - -If you don't configure, the `Default` connection string is used. If you configure a specific connection string name, but not define this connection string name in the application configuration then it fallbacks to the `Default` connection string. - -## Registering DbContext To Dependency Injection - -Use `AddAbpDbContext` method in your module to register your DbContext class for [dependency injection](Dependency-Injection.md) system. - -```c# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddMongoDbContext(); - - //... - } - } -} -``` - -### Add Default Repositories - -ABP can automatically create default [generic repositories](Repositories.md) for the entities in your DbContext. Just use `AddDefaultRepositories()` option on the registration: - -````C# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); -}); -```` - -This will create a repository for each [aggregate root entity](Entities.md) (classes derived from `AggregateRoot`) by default. If you want to create repositories for other entities too, then set `includeAllEntities` to `true`: - -```c# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(includeAllEntities: true); -}); -``` - -Then you can inject and use `IRepository` in your services. Assume that you have a `Book` entity with `Guid` primary key: - -```csharp -public class Book : AggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } -} -``` - -(`BookType` is a simple `enum` here) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): - -```csharp -public class BookManager : DomainService -{ - private readonly IRepository _bookRepository; - - public BookManager(IRepository bookRepository) //inject default repository - { - _bookRepository = bookRepository; - } - - public async Task CreateBook(string name, BookType type) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var book = new Book - { - Id = GuidGenerator.Create(), - Name = name, - Type = type - }; - - await _bookRepository.InsertAsync(book); //Use a standard repository method - - return book; - } -} -``` - -This sample uses `InsertAsync` method to insert a new entity to the database. - -### Add Custom Repositories - -Default generic repositories are powerful enough in most cases (since they implement `IQueryable`). However, you may need to create a custom repository to add your own repository methods. - -Assume that you want to delete all books by type. It's suggested to define an interface for your custom repository: - -```csharp -public interface IBookRepository : IRepository -{ - Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken) - ); -} -``` - -You generally want to derive from the `IRepository` to inherit standard repository methods. However, you don't have to. Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`MongoDB` project in a [startup template](https://abp.io/Templates)). - -Example implementation of the `IBookRepository` interface: - -```csharp -public class BookRepository : - MongoDbRepository, - IBookRepository -{ - public BookRepository(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken)) - { - var collection = await GetCollectionAsync(cancellationToken); - await collection.DeleteManyAsync( - Builders.Filter.Eq(b => b.Type, type), - cancellationToken - ); - } -} -``` - -Now, it's possible to [inject](Dependency-Injection.md) the `IBookRepository` and use the `DeleteBooksByType` method when needed. - -#### Override Default Generic Repository - -Even if you create a custom repository, you can still inject the default generic repository (`IRepository` for this example). Default repository implementation will not use the class you have created. - -If you want to replace default repository implementation with your custom repository, do it inside `AddMongoDbContext` options: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - options.AddRepository(); //Replaces IRepository -}); -``` - -This is especially important when you want to **override a base repository method** to customize it. For instance, you may want to override `DeleteAsync` method to delete an entity in a more efficient way: - -```csharp -public async override Task DeleteAsync( - Guid id, - bool autoSave = false, - CancellationToken cancellationToken = default) -{ - //TODO: Custom implementation of the delete method -} -``` - -### Access to the MongoDB API - -In most cases, you want to hide MongoDB APIs behind a repository (this is the main purpose of the repository). However, if you want to access the MongoDB API over the repository, you can use `GetDatabaseAsync()`, `GetCollectionAsync()` or `GetAggregateAsync()` extension methods. Example: - -```csharp -public class BookService -{ - private readonly IRepository _bookRepository; - - public BookService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task FooAsync() - { - IMongoDatabase database = await _bookRepository.GetDatabaseAsync(); - IMongoCollection books = await _bookRepository.GetCollectionAsync(); - IAggregateFluent bookAggregate = await _bookRepository.GetAggregateAsync(); - } -} -``` - -> Important: You must reference to the `Volo.Abp.MongoDB` package from the project you want to access to the MongoDB API. This breaks encapsulation, but this is what you want in that case. - -### Transactions - -MongoDB supports multi-document transactions starting from the version 4.0 and the ABP Framework supports it. However, the [startup template](Startup-Templates/Index.md) **disables** transactions by default. If your MongoDB **server** supports transactions, you can enable the it in the *YourProjectMongoDbModule* class: - -```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; -}); -``` - -> Or you can delete this code since this is already the default behavior. - -### Advanced Topics - -### Controlling the Multi-Tenancy - -If your solution is [multi-tenant](Multi-Tenancy.md), tenants may have **separate databases**, you have **multiple** `DbContext` classes in your solution and some of your `DbContext` classes should be usable **only from the host side**, it is suggested to add `[IgnoreMultiTenancy]` attribute on your `DbContext` class. In this case, ABP guarantees that the related `DbContext` always uses the host [connection string](Connection-Strings.md), even if you are in a tenant context. - -**Example:** - -````csharp -[IgnoreMultiTenancy] -public class MyDbContext : AbpMongoDbContext -{ - ... -} -```` - -Do not use the `[IgnoreMultiTenancy]` attribute if any one of your entities in your `DbContext` can be persisted in a tenant database. - -> When you use repositories, ABP already uses the host database for the entities don't implement the `IMultiTenant` interface. So, most of time you don't need to `[IgnoreMultiTenancy]` attribute if you are using the repositories to work with the database. - -#### Set Default Repository Classes - -Default generic repositories are implemented by `MongoDbRepository` class by default. You can create your own implementation and use it for default repository implementation. - -First, define your repository classes like that: - -```csharp -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} - -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -First one is for [entities with composite keys](Entities.md), second one is for entities with single primary key. - -It's suggested to inherit from the `MongoDbRepository` class and override methods if needed. Otherwise, you will have to implement all standard repository methods manually. - -Now, you can use `SetDefaultRepositoryClasses` option: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.SetDefaultRepositoryClasses( - typeof(MyRepositoryBase<,>), - typeof(MyRepositoryBase<>) - ); - //... -}); -``` - -#### Set Base MongoDbContext Class or Interface for Default Repositories - -If your MongoDbContext inherits from another MongoDbContext or implements an interface, you can use that base class or interface as the MongoDbContext for default repositories. Example: - -```csharp -public interface IBookStoreMongoDbContext : IAbpMongoDbContext -{ - Collection Books { get; } -} -``` - -`IBookStoreMongoDbContext` is implemented by the `BookStoreMongoDbContext` class. Then you can use generic overload of the `AddDefaultRepositories`: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - //... -}); -``` - -Now, your custom `BookRepository` can also use the `IBookStoreMongoDbContext` interface: - -```csharp -public class BookRepository - : MongoDbRepository, - IBookRepository -{ - //... -} -``` - -One advantage of using interface for a MongoDbContext is then it becomes replaceable by another implementation. - -#### Replace Other DbContextes - -Once you properly define and use an interface for a MongoDbContext , then any other implementation can use the following ways to replace it: - -#### ReplaceDbContext Attribute - -```csharp -[ReplaceDbContext(typeof(IBookStoreMongoDbContext))] -public class OtherMongoDbContext : AbpMongoDbContext, IBookStoreMongoDbContext -{ - //... -} -``` - -#### ReplaceDbContext Option - -```csharp -context.Services.AddMongoDbContext(options => -{ - //... - options.ReplaceDbContext(); -}); -``` - -In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime. - -#### Replacing with Multi-Tenancy - -It is also possible to replace a DbContext based on the [multi-tenancy](Multi-Tenancy.md) side. `ReplaceDbContext` attribute and `ReplaceDbContext` method can get a `MultiTenancySides` option with a default value of `MultiTenancySides.Both`. - -**Example:** Replace DbContext only for tenants, using the `ReplaceDbContext` attribute - -````csharp -[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)] -```` - -**Example:** Replace DbContext only for the host side, using the `ReplaceDbContext` method - -````csharp -options.ReplaceDbContext(MultiTenancySides.Host); -```` - -### Customize Bulk Operations - -If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IMongoDbBulkOperationProvider`. - -- You may use example template below: - -```csharp -public class MyCustomMongoDbBulkOperationProvider - : IMongoDbBulkOperationProvider, ITransientDependency -{ - public async Task DeleteManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task InsertManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task UpdateManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Your logic here. - } -} -``` - -## See Also - -* [Entities](Entities.md) -* [Repositories](Repositories.md) -* [Video tutorial](https://abp.io/video-courses/essentials/abp-mongodb) \ No newline at end of file diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md deleted file mode 100644 index 922976767d..0000000000 --- a/docs/en/Multi-Tenancy.md +++ /dev/null @@ -1,443 +0,0 @@ -# Multi-Tenancy - -Multi-Tenancy is a widely used architecture to create **SaaS applications** where the hardware and software **resources are shared by the customers** (tenants). ABP Framework provides all the base functionalities to create **multi tenant applications**. - -Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) the multi-tenancy as like that: - -> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants. - -## Terminology: Host vs Tenant - -There are two main side of a typical SaaS / Multi-tenant application: - -* A **Tenant** is a customer of the SaaS application that pays money to use the service. -* **Host** is the company that owns the SaaS application and manages the system. - -The Host and the Tenant terms will be used for that purpose in the rest of the document. - -## Configuration - -### AbpMultiTenancyOptions: Enable/Disable Multi-Tenancy - -`AbpMultiTenancyOptions` is the main options class to **enable/disable the multi-tenancy** for your application. - -**Example: Enable multi-tenancy** - -```csharp -Configure(options => -{ - options.IsEnabled = true; -}); -``` - -> Multi-Tenancy is disabled in the ABP Framework by default. However, it is **enabled by default** when you create a new solution using the [startup template](Startup-Templates/Application.md). `MultiTenancyConsts` class in the solution has a constant to control it in a single place. - -### Database Architecture - -ABP Framework supports all the following approaches to store the tenant data in the database; - -* **Single Database**: All tenants are stored in a single database. -* **Database per Tenant**: Every tenant has a separate, dedicated database to store the data related to that tenant. -* **Hybrid**: Some tenants share a single databases while some tenants may have their own databases. - -[Tenant management module](Modules/Tenant-Management.md) (which comes pre-installed with the startup projects) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches. - -## Usage - -Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible. - -### IMultiTenant - -You should implement the `IMultiTenant` interface for your [entities](Entities.md) to make them **multi-tenancy ready**. - -**Example: A multi-tenant *Product* entity** - -````csharp -using System; -using Volo.Abp.Domain.Entities; -using Volo.Abp.MultiTenancy; - -namespace MultiTenancyDemo.Products -{ - public class Product : AggregateRoot, IMultiTenant - { - public Guid? TenantId { get; set; } //Defined by the IMultiTenant interface - - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -`IMultiTenant` interface just defines a `TenantId` property. When you implement this interface, ABP Framework **automatically** [filters](Data-Filtering.md) entities for the current tenant when you query from database. So, you don't need to manually add `TenantId` condition while performing queries. A tenant can not access to data of another tenant by default. - -#### Why the TenantId Property is Nullable? - -`IMultiTenant.TenantId` is **nullable**. When it is null that means the entity is owned by the **Host** side and not owned by a tenant. It is useful when you create a functionality in your system that is both used by the tenant and the host sides. - -For example, `IdentityUser` is an entity defined by the [Identity Module](Modules/Identity.md). The host and all the tenants have their own users. So, for the host side, users will have a `null` `TenantId` while tenant users will have their related `TenantId`. - -> **Tip**: If your entity is tenant-specific and has no meaning in the host side, you can force to not set `null` for the `TenantId` in the constructor of your entity. - -#### When to set the TenantId? - -ABP automatically sets the `TenantId` for you when you create a new entity object. It is done in the constructor of the base `Entity` class (all other base entity and aggregate root classes are derived from the `Entity` class). The `TenantId` is set from the current value of the `ICurrentTenant.Id` property (see the next section). - -If you set the `TenantId` value for a specific entity object, it will override the value set by the base class. If you want to set the `TenantId` property yourself, we recommend to do it in the constructor of your entity class and do not change (update) it again (Actually, changing it means that you are moving the entity from a tenant to another tenant. If you want that, you need an extra care about the related entities in the database). - -### ICurrentTenant - -`ICurrentTenant` is the main service to interact with the multi-tenancy infrastructure. - -`ApplicationService`, `DomainService`, `AbpController` and some other base classes already has pre-injected `CurrentTenant` properties. For other type of classes, you can inject the `ICurrentTenant` into your service. - -#### Tenant Properties - -`ICurrentTenant` defines the following properties; - -* `Id` (`Guid`): Id of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request. -* `Name` (`string`): Name of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request. -* `IsAvailable` (`bool`): Returns `true` if the `Id` is not `null`. - -#### Change the Current Tenant - -ABP Framework automatically filters the resources (database, cache...) based on the `ICurrentTenant.Id`. However, in some cases you may want to perform an operation on behalf of a specific tenant, generally when you are in the host context. - -`ICurrentTenant.Change` method changes the current tenant for a limited scope, so you can safely perform operations for the tenant. - -**Example: Get product count of a specific tenant** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace MultiTenancyDemo.Products -{ - public class ProductManager : DomainService - { - private readonly IRepository _productRepository; - - public ProductManager(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task GetProductCountAsync(Guid? tenantId) - { - using (CurrentTenant.Change(tenantId)) - { - return await _productRepository.GetCountAsync(); - } - } - } -} -```` - -* `Change` method can be used in a **nested way**. It restores the `CurrentTenant.Id` to the previous value after the `using` statement. -* When you use `CurrentTenant.Id` inside the `Change` scope, you get the `tenantId` provided to the `Change` method. So, the repository also get this `tenantId` and can filter the database query accordingly. -* Use `CurrentTenant.Change(null)` to change scope to the host context. - -> Always use the `Change` method with a `using` statement like done in this example. - -### Data Filtering: Disable the Multi-Tenancy Filter - -As mentioned before, ABP Framework handles data isolation between tenants using the [Data Filtering](Data-Filtering.md) system. In some cases, you may want to disable it and perform a query on all the data, without filtering for the current tenant. - -**Example: Get count of products in the database, including all the products of all the tenants.** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; -using Volo.Abp.MultiTenancy; - -namespace MultiTenancyDemo.Products -{ - public class ProductManager : DomainService - { - private readonly IRepository _productRepository; - private readonly IDataFilter _dataFilter; - - public ProductManager( - IRepository productRepository, - IDataFilter dataFilter) - { - _productRepository = productRepository; - _dataFilter = dataFilter; - } - - public async Task GetProductCountAsync() - { - using (_dataFilter.Disable()) - { - return await _productRepository.GetCountAsync(); - } - } - } -} - -```` - -See the [Data Filtering document](Data-Filtering.md) for more. - -> Note that this approach won't work if your tenants have **separate databases** since there is no built-in way to query from multiple database in a single database query. You should handle it yourself if you need it. - -## Infrastructure - -### Determining the Current Tenant - -The first thing for a multi-tenant application is to determine the current tenant on the runtime. - -ABP Framework provides an extensible **Tenant Resolving** system for that purpose. Tenant Resolving system then used in the **Multi-Tenancy Middleware** to determine the current tenant for the current HTTP request. - -#### Tenant Resolvers - -##### Default Tenant Resolvers - -The following resolvers are provided and configured by default; - -* `CurrentUserTenantResolveContributor`: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for the security**. -* `QueryStringTenantResolveContributor`: Tries to find current tenant id from query string parameters. The parameter name is `__tenant` by default. -* `RouteTenantResolveContributor`: Tries to find current tenant id from route (URL path). The variable name is `__tenant` by default. If you defined a route with this variable, then it can determine the current tenant from the route. -* `HeaderTenantResolveContributor`: Tries to find current tenant id from HTTP headers. The header name is `__tenant` by default. -* `CookieTenantResolveContributor`: Tries to find current tenant id from cookie values. The cookie name is `__tenant` by default. - -###### Problems with the NGINX - -You may have problems with the `__tenant` in the HTTP Headers if you're using the [nginx](https://www.nginx.com/) as the reverse proxy server. Because it doesn't allow to use underscore and some other special characters in the HTTP headers and you may need to manually configure it. See the following documents please: -http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers -http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers - -###### AbpAspNetCoreMultiTenancyOptions - -`__tenant` parameter name can be changed using `AbpAspNetCoreMultiTenancyOptions`. - -**Example:** - -````csharp -services.Configure(options => -{ - options.TenantKey = "MyTenantKey"; -}); -```` - -If you change the `TenantKey`, make sure to pass it to `CoreModule` in the Angular client as follows: - -```js -@NgModule({ - imports: [ - CoreModule.forRoot({ - // ... - tenantKey: 'MyTenantKey' - }), - ], - // ... -}) -export class AppModule {} -``` - -If you need to access it, you can inject it as follows: - -```js -import { Inject } from '@angular/core'; -import { TENANT_KEY } from '@abp/ng.core'; - -class SomeComponent { - constructor(@Inject(TENANT_KEY) private tenantKey: string) {} -} -``` - -> However, we don't suggest to change this value since some clients may assume the the `__tenant` as the parameter name and they might need to manually configure then. - -The `MultiTenancyMiddlewareErrorPageBuilder` is used to handle inactive and non-existent tenants. - -It will respond to an error page by default, you can change it if you want, eg: only output the error log and continue ASP NET Core's request pipeline. - -```csharp -Configure(options => -{ - options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) => - { - // Handle the exception. - - // Return true to stop the pipeline, false to continue. - return true; - }; -}); -``` - -##### Domain/Subdomain Tenant Resolver - -In a real application, most of times you will want to determine the current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure the `AbpTenantResolveOptions` to add the domain tenant resolver. - -**Example: Add a subdomain resolver** - -````csharp -Configure(options => -{ - options.AddDomainTenantResolver("{0}.mydomain.com"); -}); -```` - -* `{0}` is the placeholder to determine the current tenant's unique name. -* Add this code to the `ConfigureServices` method of your [module](Module-Development-Basics.md). -* This should be done in the *Web/API Layer* since the URL is a web related stuff. - -Openiddict is the default Auth Server in ABP (since v6.0). When you use OpenIddict, you must add this code to the `PreConfigure` method as well. - -```csharp -// using Volo.Abp.OpenIddict.WildcardDomains - -PreConfigure(options => -{ - options.EnableWildcardDomainSupport = true; - options.WildcardDomainsFormat.Add("https://{0}.mydomain.com"); -}); -``` - -You must add this code to the `Configure` method as well. - -```csharp -// using Volo.Abp.MultiTenancy; - -Configure(options => -{ - options.AddDomainTenantResolver("{0}.mydomain.com"); -}); - -``` - -> There is an [example](https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver) that uses the subdomain to determine the current tenant. - -If you use a sepereted Auth server, you must install `[Owl.TokenWildcardIssuerValidator](https://www.nuget.org/packages/Owl.TokenWildcardIssuerValidator)` on the `HTTPApi.Host` project - -```bash -dotnet add package Owl.TokenWildcardIssuerValidator -``` - -Then fix the options of the `.AddJwtBearer` block - -```csharp -// using using Owl.TokenWildcardIssuerValidator; - -context.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); - options.Audience = "ExampleProjectName"; - - // start of added block - options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; - options.TokenValidationParameters.ValidIssuers = new[] - { - "https://{0}.mydomain.com:44349/" //the port may different - }; - // end of added block - }); - -``` - -##### Custom Tenant Resolvers - -You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: - -````csharp -Configure(options => -{ - options.TenantResolvers.Add(new MyCustomTenantResolveContributor()); -}); -```` - -`MyCustomTenantResolveContributor` must inherit from the `TenantResolveContributorBase` (or implement the `ITenantResolveContributor`) as shown below: - -````csharp -using System.Threading.Tasks; -using Volo.Abp.MultiTenancy; - -namespace MultiTenancyDemo.Web -{ - public class MyCustomTenantResolveContributor : TenantResolveContributorBase - { - public override string Name => "Custom"; - - public override Task ResolveAsync(ITenantResolveContext context) - { - //TODO... - } - } -} -```` - -* A tenant resolver should set `context.TenantIdOrName` if it can determine it. If not, just leave it as is to allow the next resolver to determine it. -* `context.ServiceProvider` can be used if you need to additional services to resolve from the [dependency injection](Dependency-Injection.md) system. - -#### Multi-Tenancy Middleware - -Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware) that determines the current tenant from the HTTP request and sets the `ICurrentTenant` properties. - -Multi-Tenancy middleware is typically placed just under the [authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication) middleware (`app.UseAuthentication()`): - -````csharp -app.UseMultiTenancy(); -```` - -> This middleware is already configured in the startup templates, so you normally don't need to manually add it. - -### Tenant Store - -`ITenantStore` is used to get the tenant configuration from a data source. - -> Tenant names are not case-sensitive. `ITenantStore` will use the `NormalizedName` parameter to get tenants, You need to use `ITenantNormalizer` to normalize tenant names. - -#### Tenant Management Module - -The [tenant management module](Modules/Tenant-Management) is **included in the startup templates** and implements the `ITenantStore` interface to get the tenants and their configuration from a database. It also provides the necessary functionality and UI to manage the tenants and their connection strings. - -#### Configuration Data Store - -**If you don't want to use the tenant management module**, the `DefaultTenantStore` is used as the `ITenantStore` implementation. It gets the tenant configurations from the [configuration system](Configuration.md) (`IConfiguration`). You can either configure the `AbpDefaultTenantStoreOptions` [options](Options.md) or set it in your `appsettings.json` file: - -**Example: Define tenants in appsettings.json** - -````json -"Tenants": [ - { - "Id": "446a5211-3d72-4339-9adc-845151f8ada0", - "Name": "tenant1", - "NormalizedName": "TENANT1" - }, - { - "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", - "Name": "tenant2", - "NormalizedName": "TENANT2", - "ConnectionStrings": { - "Default": "...tenant2's db connection string here..." - } - } - ] -```` - -> It is recommended to **use the Tenant Management module**, which is already pre-configured when you create a new application with the ABP startup templates. - -### Other Multi-Tenancy Infrastructure - -ABP Framework was designed to respect to the multi-tenancy in every aspect and most of the times everything will work as expected. - -BLOB Storing, Caching, Data Filtering, Data Seeding, Authorization and all the other services are designed to properly work in a multi-tenant system. - -## The Tenant Management Module - -ABP Framework provides all the the infrastructure to create a multi-tenant application, but doesn't make any assumption about how you manage (create, delete...) your tenants. - -The [Tenant Management module](Modules/Tenant-Management.md) provides a basic UI to manage your tenants and set their connection strings. It is pre-configured for the [application startup template](Startup-Templates/Application.md). - -## See Also - -* [Features](Features.md) diff --git a/docs/en/Nightly-Builds.md b/docs/en/Nightly-Builds.md deleted file mode 100644 index 5b49cb7b38..0000000000 --- a/docs/en/Nightly-Builds.md +++ /dev/null @@ -1,29 +0,0 @@ -# Nightly Builds - -All framework & module packages (both open-source and commercial) are deployed to MyGet every night on weekdays. So, you can install the latest dev-branch builds to try out functionality prior to release. - -## Install & Uninstall Nightly Preview Packages - -The latest version of nightly preview packages can be installed by running the below command in the root folder of the application: - -```bash -abp switch-to-nightly -``` - -> Note that this command doesn't create a project with nightly preview packages. Instead, it switches package versions of a project with the nightly preview packages. - -After this command, a new NuGet feed will be added to the `NuGet.Config` file of your project. Then, you can get the latest code of the ABP Framework without waiting for the next release. - -> ABP nightly NuGet feed is [https://www.myget.org/F/abp-nightly/api/v3/index.json](https://www.myget.org/F/abp-nightly/api/v3/index.json). - -Also, this command creates `.npmrc` files containing two NPM registries in the directory where the `package.json` files are located in your solution. - -> You can check the [abp-nightly gallery](https://www.myget.org/gallery/abp-nightly) (for NPM & NuGet / open-source) and [abp-commercial-npm-nightly gallery](https://www.myget.org/gallery/abp-commercial-npm-nightly) (for NPM / commercial) to see the all nightly preview packages. - -If you're using the ABP Framework nightly preview packages, you can switch back to the stable version by using this command: - -```bash -abp switch-to-stable -``` - -See the [ABP CLI documentation](./CLI.md) for more information. diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md deleted file mode 100644 index bc4fc911c4..0000000000 --- a/docs/en/Object-Extensions.md +++ /dev/null @@ -1,416 +0,0 @@ -# Object Extensions - -ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module. - -> Object extension system normally is not needed for your own objects since you can easily add regular properties to your own classes. - -## IHasExtraProperties Interface - -This is the interface to make a class extensible. It simply defines a `Dictionary` property: - -````csharp -ExtraPropertyDictionary ExtraProperties { get; } -```` - -`ExtraPropertyDictionary` class is inherited from the `Dictionary` class. You can add or get extra properties using this dictionary. - -### Base Classes - -`IHasExtraProperties` interface is implemented by several base classes by default: - -* Implemented by the `AggregateRoot` class (see [entities](Entities.md)). -* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes. -* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object. - -So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually. - -### Fundamental Extension Methods - -While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties. - -#### SetProperty - -Used to set the value of an extra property: - -````csharp -user.SetProperty("Title", "My Title"); -user.SetProperty("IsSuperUser", true); -```` - -`SetProperty` returns the same object, so you can chain it: - -````csharp -user.SetProperty("Title", "My Title") - .SetProperty("IsSuperUser", true); -```` - -#### GetProperty - -Used to read the value of an extra property: - -````csharp -var title = user.GetProperty("Title"); - -if (user.GetProperty("IsSuperUser")) -{ - //... -} -```` - -* `GetProperty` is a generic method and takes the object type as the generic parameter. -* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc). - -##### Non Primitive Property Types - -If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`. - -#### HasProperty - -Used to check if the object has a property set before. - -#### RemoveProperty - -Used to remove a property from the object. Use this methods instead of setting a `null` value for the property. - -### Some Best Practices - -Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead; - -* Define a constant for your extra property names -* Create extension methods to easily set your extra properties. - -Example: - -````csharp -public static class IdentityUserExtensions -{ - private const string TitlePropertyName = "Title"; - - public static void SetTitle(this IdentityUser user, string title) - { - user.SetProperty(TitlePropertyName, title); - } - - public static string GetTitle(this IdentityUser user) - { - return user.GetProperty(TitlePropertyName); - } -} -```` - -Then you can easily set or get the `Title` property: - -````csharp -user.SetTitle("My Title"); -var title = user.GetTitle(); -```` - -## Object Extension Manager - -While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes. - -Explicitly defining an extra property has some use cases: - -* Allows to control how the extra property is handled on object to object mapping (see the section below). -* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md). - -> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside. - -### AddOrUpdate - -`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object. - -Example: Define extra properties for the `IdentityUser` entity: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdate(options => - { - options.AddOrUpdateProperty("SocialSecurityNumber"); - options.AddOrUpdateProperty("IsSuperUser"); - } - ); -```` - -### AddOrUpdateProperty - -While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty("SocialSecurityNumber"); -```` - -Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - new[] - { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), - typeof(IdentityUserUpdateDto) - }, - "SocialSecurityNumber" - ); -```` - -### Property Configuration - -`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - //Configure options... - }); -```` - -> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document. - -The following sections explain the fundamental property configuration options. - -#### Default Value - -A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. - -There are two ways to override the default value: - -##### DefaultValue Option - -`DefaultValue` option can be set to any value: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "MyIntProperty", - options => - { - options.DefaultValue = 42; - }); -```` - -##### DefaultValueFactory Options - -`DefaultValueFactory` can be set to a function that returns the default value: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "MyDateTimeProperty", - options => - { - options.DefaultValueFactory = () => DateTime.Now; - }); -```` - -`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . - -> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. - -#### CheckPairDefinitionOnMapping - -Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. - -## Validation - -You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation: - -1. You can add **data annotation attributes** for a property. -2. You can write an action (code block) to perform a **custom validation**. - -Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated. - -### Data Annotation Attributes - -All of the standard data annotation attributes are valid for extra properties. Example: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.Attributes.Add(new RequiredAttribute()); - options.Attributes.Add( - new StringLengthAttribute(32) { - MinimumLength = 6 - } - ); - }); -```` - -With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. - -#### Default Validation Attributes - -There are some attributes **automatically added** when you create certain type of properties; - -* `RequiredAttribute` is added for non nullable primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. -* `EnumDataTypeAttribute` is added for enum types, to prevent to set invalid enum values. - -Use `options.Attributes.Clear();` if you don't want these attributes. - -### Custom Validation - -If you need, you can add a custom action that is executed to validate the extra properties. Example: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.Validators.Add(context => - { - var socialSecurityNumber = context.Value as string; - - if (socialSecurityNumber == null || - socialSecurityNumber.StartsWith("X")) - { - context.ValidationErrors.Add( - new ValidationResult( - "Invalid social security number: " + socialSecurityNumber, - new[] { "SocialSecurityNumber" } - ) - ); - } - }); - }); -```` - -`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios. - -In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example: - -````csharp -ObjectExtensionManager.Instance -.AddOrUpdate(objConfig => -{ - //Define two properties with their own validation rules - - objConfig.AddOrUpdateProperty("Password", propertyConfig => - { - propertyConfig.Attributes.Add(new RequiredAttribute()); - }); - - objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig => - { - propertyConfig.Attributes.Add(new RequiredAttribute()); - }); - - //Write a common validation logic works on multiple properties - - objConfig.Validators.Add(context => - { - if (context.ValidatingObject.GetProperty("Password") != - context.ValidatingObject.GetProperty("PasswordRepeat")) - { - context.ValidationErrors.Add( - new ValidationResult( - "Please repeat the same password!", - new[] { "Password", "PasswordRepeat" } - ) - ); - } - }); -}); -```` - -## Object to Object Mapping - -Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. - -This section offers some **good practices** to control your extra properties on object mapping. - -### MapExtraPropertiesTo - -`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: - -````csharp -identityUser.MapExtraPropertiesTo(identityUserDto); -```` - -`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. - -#### MappingPropertyDefinitionChecks - -`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: - -````csharp -identityUser.MapExtraPropertiesTo( - identityUserDto, - MappingPropertyDefinitionChecks.None -); -```` - -> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. - -If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.CheckPairDefinitionOnMapping = false; - }); -```` - -#### Ignored Properties - -You may want to ignore some properties on a specific mapping operation: - -````csharp -identityUser.MapExtraPropertiesTo( - identityUserDto, - ignoredProperties: new[] {"MySensitiveProp"} -); -```` - -Ignored properties are not copied to the target object. - -#### AutoMapper Integration - -If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. - -You can use the `MapExtraProperties()` method inside your mapping profile. - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .MapExtraProperties(); - } -} -```` - -It has the same parameters with the `MapExtraPropertiesTo` method. - -## Entity Framework Core Database Mapping - -If you're using the EF Core, you can map an extra property to a table field in the database. Example: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.MapEfCore(b => b.HasMaxLength(32)); - } - ); -```` - -See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. - -## See Also - -* [Module Entity Extensions](Module-Entity-Extensions.md) diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md deleted file mode 100644 index 40efdd7ee6..0000000000 --- a/docs/en/Object-To-Object-Mapping.md +++ /dev/null @@ -1,341 +0,0 @@ -# Object To Object Mapping - -It's common to map an object to another similar object. It's also tedious and repetitive since generally both classes have the same or similar properties mapped to each other. Imagine a typical [application service](Application-Services.md) method below: - -```csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task CreateUser(CreateUserInput input) - { - //Manually creating a User object from the CreateUserInput object - var user = new User - { - Name = input.Name, - Surname = input.Surname, - EmailAddress = input.EmailAddress, - Password = input.Password - }; - - await _userRepository.InsertAsync(user); - } -} -``` - -`CreateUserInput` is a simple [DTO](Data-Transfer-Objects.md) class and the `User` is a simple [entity](Entities.md). The code above creates a `User` entity from the given input object. The `User` entity will have more properties in a real-world application and manually creating it will become tedious and error-prone. You also have to change the mapping code when you add new properties to `User` and `CreateUserInput` classes. - -We can use a library to automatically handle these kind of mappings. ABP provides abstractions for object to object mapping and has an integration package to use [AutoMapper](http://automapper.org/) as the object mapper. - -## IObjectMapper - -`IObjectMapper` interface (in the [Volo.Abp.ObjectMapping](https://www.nuget.org/packages/Volo.Abp.ObjectMapping) package) defines a simple `Map` method. The example code introduced before can be re-written as shown below: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task CreateUser(CreateUserInput input) - { - //Automatically creating a new User object using the CreateUserInput object - var user = ObjectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -> `ObjectMapper` is defined in the `ApplicationService` base class in this example. You can directly inject the `IObjectMapper` interface when you need it somewhere else. - -Map method has two generic argument: First one is the source object type while the second one is the destination object type. - -If you need to set properties of an existing object, you can use the second overload of the `Map` method: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task UpdateUserAsync(Guid id, UpdateUserInput input) - { - var user = await _userRepository.GetAsync(id); - - //Automatically set properties of the user object using the UpdateUserInput - ObjectMapper.Map(input, user); - - await _userRepository.UpdateAsync(user); - } -} -```` - -You should have defined the mappings before to be able to map objects. See the AutoMapper integration section to learn how to define mappings. - -## AutoMapper Integration - -[AutoMapper](http://automapper.org/) is one of the most popular object to object mapping libraries. [Volo.Abp.AutoMapper](https://www.nuget.org/packages/Volo.Abp.AutoMapper) package defines the AutoMapper integration for the `IObjectMapper`. - -Once you define mappings described as below, you can use the `IObjectMapper` interface just like explained before. - -### Define Mappings - -AutoMapper provides multiple ways of defining mapping between classes. Refer to [its own documentation](https://docs.automapper.org) for all details. - -One way to define object mappings is creating a [Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class. Example: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - } -} -```` - -You should then register profiles using the `AbpAutoMapperOptions`: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Add all mappings defined in the assembly of the MyModule class - options.AddMaps(); - }); - } -} -```` - -`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html). - -### Configuration Validation - -`AddMaps` optionally takes a `bool` parameter to control the [configuration validation](https://docs.automapper.org/en/stable/Configuration-validation.html) for your [module](Module-Development-Basics.md): - -````csharp -options.AddMaps(validate: true); -```` - -While this option is `false` by default, it is suggested to enable configuration validation as a best practice. - -Configuration validation can be controlled per profile class using `AddProfile` instead of `AddMaps`: - -````csharp -options.AddProfile(validate: true); -```` - -> If you have multiple profiles and need to enable validation only for a few of them, first use `AddMaps` without validation, then use `AddProfile` for each profile you want to validate. - -### Mapping the Object Extensions - -[Object extension system](Object-Extensions.md) allows to define extra properties for existing classes. ABP Framework provides a mapping definition extension to properly map extra properties of two objects. - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .MapExtraProperties(); - } -} -```` - -It is suggested to use the `MapExtraProperties()` method if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](Object-Extensions.md) for more. - -### Other Useful Extension Methods - -There are some more extension methods those can simplify your mapping code. - -#### Ignoring Audit Properties - -It is common to ignore audit properties when you map an object to another. - -Assume that you need to map a `ProductDto` ([DTO](Data-Transfer-Objects.md)) to a `Product` [entity](Entities.md) and the entity is inheriting from the `AuditedEntity` class (which provides properties like `CreationTime`, `CreatorId`, `IHasModificationTime`... etc). - -You probably want to ignore these base properties while mapping from the DTO. You can use `IgnoreAuditedObjectProperties()` method to ignore all audit properties (instead of manually ignoring them one by one): - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .IgnoreAuditedObjectProperties(); - } -} -```` - -There are more extension methods like `IgnoreFullAuditedObjectProperties()` and `IgnoreCreationAuditedObjectProperties()` those can be used based on your entity type. - -> See the "*Base Classes & Interfaces for Audit Properties*" section in the [entities document](Entities.md) to know more about auditing properties. - -#### Ignoring Other Properties - -In AutoMapper, you typically write such a mapping code to ignore a property: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .ForMember(x => x.CreationTime, map => map.Ignore()); - } -} -```` - -We found it unnecessarily long and created the `Ignore()` extension method: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .Ignore(x => x.CreationTime); - } -} -```` - -## Advanced Topics - -### IObjectMapper Interface - -Assume that you have created a **reusable module** which defines AutoMapper profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](Module-Development-Basics.md). - -`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper even if the final application uses another default object mapping library. - -`IObjectMapper` is used to contextualize the object mapper, so you can use different libraries for different modules/contexts. - -Example usage: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - private readonly IObjectMapper _objectMapper; - - public UserAppService( - IRepository userRepository, - IObjectMapper objectMapper) //Inject module specific mapper - { - _userRepository = userRepository; - _objectMapper = objectMapper; - } - - public async Task CreateUserAsync(CreateUserInput input) - { - //Use the module specific mapper - var user = _objectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -`UserAppService` injects the `IObjectMapper`, the specific object mapper for this module. It's usage is exactly same of the `IObjectMapper`. - -The example code above don't use the `ObjectMapper` property defined in the `ApplicationService`, but injects the `IObjectMapper`. However, it is still possible to use the base property since the `ApplicationService` defines an `ObjectMapperContext` property that can be set in the class constructor. So, the example about can be re-written as like below: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - //Set the object mapper context - ObjectMapperContext = typeof(MyModule); - } - - public async Task CreateUserAsync(CreateUserInput input) - { - var user = ObjectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -While using the contextualized object mapper is same as the normal object mapper, you should register the contextualized mapper in your module's `ConfigureServices` method: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Use AutoMapper for MyModule - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddMaps(validate: true); - }); - } -} -```` - -`IObjectMapper` is an essential feature for a reusable module where it can be used in multiple applications each may use a different library for object to object mapping. All pre-built ABP modules are using it. But, for the final application, you can ignore this interface and always use the default `IObjectMapper` interface. - -### IObjectMapper Interface - -ABP allows you to customize the mapping code for specific classes. Assume that you want to create a custom class to map from `User` to `UserDto`. In this case, you can create a class that implements the `IObjectMapper`: - -````csharp -public class MyCustomUserMapper : IObjectMapper, ITransientDependency -{ - public UserDto Map(User source) - { - //TODO: Create a new UserDto - } - - public UserDto Map(User source, UserDto destination) - { - //TODO: Set properties of an existing UserDto - return destination; - } -} -```` - -ABP automatically discovers and registers the `MyCustomUserMapper` and it is automatically used whenever you use the `IObjectMapper` to map `User` to `UserDto`. A single class may implement more than one `IObjectMapper` each for a different object pairs. - -> This approach is powerful since `MyCustomUserMapper` can inject any other service and use in the `Map` methods. - -Once you implement `IObjectMapper`, ABP can automatically convert a collection of `User` objects to a collection of `UserDto` objects. The following generic collection types are supported: - -* `IEnumerable` -* `ICollection` -* `Collection` -* `IList` -* `List` -* `T[]` (array) - -**Example:** - -````csharp -var users = await _userRepository.GetListAsync(); // returns List -var dtos = ObjectMapper.Map, List>(users); // creates List -```` diff --git a/docs/en/Options.md b/docs/en/Options.md deleted file mode 100644 index 76616aca28..0000000000 --- a/docs/en/Options.md +++ /dev/null @@ -1,118 +0,0 @@ -# Options - -Microsoft has introduced [the options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) that is used to configure a group of settings used by the framework services. This pattern is implemented by the [Microsoft.Extensions.Options](https://www.nuget.org/packages/Microsoft.Extensions.Options) NuGet package, so it is usable by any type of applications in addition to ASP.NET Core based applications. - -ABP framework follows this option pattern and defines options classes to configure the framework and the modules (they are explained in the documents of the related feature). - -Since [the Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) explains the pattern in detail, no reason to repeat all. However, ABP adds a few more features and they will be explained here. - -## Configure Options - -You typically configure options in the `ConfigureServices` of the `Startup` class. However, since ABP framework provides a modular infrastructure, you configure options in the `ConfigureServices` of your [module](Module-Development-Basics.md). Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.IsEnabled = false; - }); -} -```` - -* `AbpAuditingOptions` is a simple class defines some properties like `IsEnabled` used here. -* `AbpModule` base class defines `Configure` method to make the code simpler. So, instead of `context.Services.Configure<...>`, you can directly use the `Configure<...>` shortcut method. - -If you are developing a reusable module, you may need to define an options class to allow developers to configure your module. In this case, define a plain options class as shown below: - -````csharp -public class MyOptions -{ - public int Value1 { get; set; } - public bool Value2 { get; set; } -} -```` - -Then developers can configure your options just like the `AbpAuditingOptions` example above: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.Value1 = 42; - options.Value2 = true; - }); -} -```` - -* In this example, used the shortcut `Configure<...>` method. - -### Get the Option Value - -Whenever you need to get the value of an option, [inject](Dependency-Injection.md) the `IOptions` service into your class and use its `.Value` property. Example: - -````csharp -public class MyService : ITransientDependency -{ - private readonly MyOptions _options; - - public MyService(IOptions options) - { - _options = options.Value; //Notice the options.Value usage! - } - - public void DoIt() - { - var v1 = _options.Value1; - var v2 = _options.Value2; - } -} -```` - -Read [the Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) for all details of the options pattern. - -## Pre Configure - -One restriction of the options pattern is that you can only resolve (inject) the `IOptions` and get the option values when the dependency injection configuration completes (that means the `ConfigureServices` methods of all modules complete). - -If you are developing a module, you may need to allow developers to set some options and use these options in the dependency injection registration phase. You may need to configure other services or change the dependency injection registration code based on these option values. - -For such cases, ABP introduces the `PreConfigure` and the `ExecutePreConfiguredActions` extension methods for the `IServiceCollection`. The pattern works as explained below. - -Define a pre option class in your module. Example: - -````csharp -public class MyPreOptions -{ - public bool MyValue { get; set; } -} -```` - -Then any [module class](Module-Development-Basics.md) depends on your module can use the `PreConfigure` method in its `PreConfigureServices` method. Example: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.MyValue = true; - }); -} -```` - -> Multiple modules can pre-configure the options and override the option values based on their dependency order. - -Finally, your module can execute the `ExecutePreConfiguredActions` method in its `ConfigureServices` method to get the configured option values. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var options = context.Services.ExecutePreConfiguredActions(); - if (options.MyValue) - { - //... - } -} -```` - diff --git a/docs/en/PlugIn-Modules.md b/docs/en/PlugIn-Modules.md deleted file mode 100644 index 069bfd40ab..0000000000 --- a/docs/en/PlugIn-Modules.md +++ /dev/null @@ -1,220 +0,0 @@ -# Plug-In Modules - -It is possible to load [modules](Module-Development-Basics.md) as plug-ins. That means you may not reference to a module's assembly in your solution, but you can load that module in the application startup just like any other module. - -## Basic Usage - -The `WebApplicationBuilder.AddApplicationAsync()` extension method can get options to configure the plug-in sources. - -**Example: Load plugins from a folder** - -````csharp -await builder.AddApplicationAsync(options => -{ - options.PlugInSources.AddFolder(@"D:\Temp\MyPlugIns"); -}); -```` - -* This is the `Startup` class of a typical ASP.NET Core application. -* `PlugInSources.AddFolder` gets a folder path and to load assemblies (typically `dll`s) in that folder. - -That's all. ABP will discover the modules in the given folder, configure and initialize them just like regular modules. - -### Plug-In Sources - -`options.PlugInSources` is actually a list of `IPlugInSource` implementations and `AddFolder` is just a shortcut for the following expression: - -````csharp -options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); -```` - -> `AddFolder()` only looks for the assembly file in the given folder, but not looks for the sub-folders. You can pass `SearchOption.AllDirectories` as a second parameter to explore plug-ins also from the sub-folders, recursively. - -There are two more built-in Plug-In Source implementations: - -* `PlugInSources.AddFiles()` gets a list of assembly (typically `dll`) files. This is a shortcut of using `FilePlugInSource` class. -* `PlugInSources.AddTypes()` gets a list of module class types. If you use this, you need to load the assemblies of the modules yourself, but it provides flexibility when needed. This is a shortcut of using `TypePlugInSource` class. - -If you need, you can create your own `IPlugInSource` implementation and add to the `options.PlugInSources` just like the others. - -## Example: Creating a Simple Plug-In - -Create a simple **Class Library Project** in a solution: - -![simple-plugin-library](images/simple-plugin-library.png) - -You can add the ABP Framework packages that you need to use in the module. At least, you should add the `Volo.Abp.Core` package to the project, Execute the following command in the folder of the .csproj file that you want to install the package on: - -````bash -abp add-package Volo.Abp.Core -```` - - If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Core). - -Every [module](Module-Development-Basics.md) must declare a class derived from the `AbpModule`. Here, a simple module class that resolves a service and initializes it on the application startup: - -````csharp -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; -using Volo.Abp.Modularity; - -namespace MyPlugIn -{ - public class MyPlungInModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var myService = context.ServiceProvider - .GetRequiredService(); - - myService.Initialize(); - } - } -} -```` - -`MyService` can be any class registered to [Dependency Injection](Dependency-Injection.md) system, as show below: - -````csharp -using Microsoft.Extensions.Logging; -using Volo.Abp.DependencyInjection; - -namespace MyPlugIn -{ - public class MyService : ITransientDependency - { - private readonly ILogger _logger; - - public MyService(ILogger logger) - { - _logger = logger; - } - - public void Initialize() - { - _logger.LogInformation("MyService has been initialized"); - } - } -} -```` - -Build the project, open the build folder, find the `MyPlugIn.dll`: - -![simple-plug-in-dll-file](images/simple-plug-in-dll-file.png) - -Copy `MyPlugIn.dll` into the plug-in folder (`D:\Temp\MyPlugIns` for this example). - -> Please delete the `MyPlugIn.deps.json` file if you use `build folder` folder as `PlugInSources`. - -If you have configured the main application like described above (see Basic Usage section), you should see the `MyService has been initialized` log in the application startup. - -## Example: Creating a Plug-In With Razor Pages - -Creating plug-ins with views inside requires a bit more attention. - -> This example assumes you've [created a new web application](https://abp.io/get-started) using the application startup template and MVC / Razor Pages UI. - -Create a new **Class Library** project in a solution: - -![simple-razor-plugin](images/simple-razor-plugin.png) - -Edit the `.csproj` file content: - -````xml - - - - net5.0 - Library - true - - - - - - - -```` - -* Changed `Sdk` to `Microsoft.NET.Sdk.Web`. -* Added `OutputType` and `IsPackable` properties. -* Added `Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared` NuGet package. - -> Depending on [Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) package is not required. You can reference to a more base package like [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc/). However, if you will build a UI page/component, it is suggested to reference to the [Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) package since it is the most high-level package without depending on a particular [theme](UI/AspNetCore/Theming.md). If there is no problem to depend on a particular theme, you can directly reference to the theme's package to be able to use the theme-specific features in your plug-in. - -Then create your module class in the plug-in: - -````csharp -using System.IO; -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.Modularity; - -namespace MyMvcUIPlugIn -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] - public class MyMvcUIPlugInModule : AbpModule - { - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(mvcBuilder => - { - //Add plugin assembly - mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); - - //Add CompiledRazorAssemblyPart if the PlugIn module contains razor views. - mvcBuilder.PartManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); - }); - } - } -} -```` - -* Depending on the `AbpAspNetCoreMvcUiThemeSharedModule` since we added the related NuGet package. -* Adding the plug-in's assembly as `AssemblyPart` and `CompiledRazorAssemblyPart` to the `PartManager` of ASP.NET Core MVC. This is required by ASP.NET Core. Otherwise, your controllers or views inside the plug-in doesn't work. - -You can now add a razor page, like `MyPlugInPage.cshtml` inside the `Pages` folder: - -````html -@page -@model MyMvcUIPlugIn.Pages.MyPlugInPage -

Welcome to my plug-in page

-

This page is located inside a plug-in module! :)

-```` - -Now, you can build the plug-in project. It will produce the following output: - -![simple-razor-plug-in-dll-file](images/simple-razor-plug-in-dll-file.png) - -Copy the `MyMvcUIPlugIn.dll` into the plug-in folder (`D:\Temp\MyPlugIns` for this example). - -If you have configured the main application like described above (see Basic Usage section), you should be able to visit the `/MyPlugInPage` URL when your application: - -![simple-plugin-output](images/simple-plugin-output.png) - -## Discussions - -In real world, your plug-in may have some external dependencies. Also, your application might be designed to support plug-ins. All these are your own system requirements. What ABP does is just loading modules on the application startup. What you do inside that modules is up to you. - -However, we can provide a few suggestions for some common cases. - -### Library Dependencies - -For package/dll dependencies, you can copy the related dlls to the plug-in folder. ABP automatically loads all assemblies in the folder and your plug-in will work as expected. - -> See [Microsoft's documentation](https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies) for some additional explanations for that case. - -### Database Schema - -If your module uses a relational database and [Entity Framework Core](Entity-Framework-Core.md), it will need to have its tables available in the database. There are different ways to ensure the tables have been created when an application uses the plug-in. Some examples; - -1. The Plugin may check if the database tables does exists and create the tables on the application startup or migrate them if the plug-in has been updated and requires some schema changes. You can use EF Core's migration API to do that. -2. You can improve the `DbMigrator` application to find migrations of the plug-ins and execute them. - -There may be other solutions. For example, if your DB admin doesn't allow you to change the database schema in the application code, you may need to manually send a SQL file to the database admin to apply it to the database. - -### Plug In Sample - -We have a sample for this, You can download it from [abp-sample repository](https://github.com/abpframework/abp-samples/tree/master/DocumentationSamples/Plug-In) diff --git a/docs/en/Previews.md b/docs/en/Previews.md deleted file mode 100644 index 70ddc4fe2b..0000000000 --- a/docs/en/Previews.md +++ /dev/null @@ -1,48 +0,0 @@ -# Preview Releases - -The preview versions are released **~4 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions. - -Versioning of a preview release is like that: - -* 3.1.0-rc.1 -* 4.0.0-rc.1 - -More than one preview releases (like 3.1.0-rc.2 and 3.1.0-rc.3) might be published until the stable version (like 3.1.0). - -## Using the Preview Versions - -### Update the CLI - -Before creating or updating an existing solution make sure to update the CLI to the latest preview version, for example: - -````bash -dotnet tool update --global Volo.Abp.Cli --version 6.0.0-rc.2 -```` - -### New Solutions - -To create a project for testing the preview version, you can select the "**preview**" option on the [download page](https://abp.io/get-started) or use the "**--preview**" parameter with the [ABP CLI](CLI.md) new command: - -````bash -abp new Acme.BookStore --preview -```` - -This command will create a new project using the latest preview NuGet packages, NPM packages and the solution template. Whenever the stable version is released, you can switch to the stable version for your solution using the `abp switch-to-stable` command in the root folder of your solution. - -### Existing Solutions - -If you already have a solution and want to use/test the latest preview version, use the following [ABP CLI](CLI.md) command in the root folder of your solution. - -````bash -abp switch-to-preview -```` - -You can return back to the latest stable using the `abp switch-to-stable ` command later. - -````bash -abp switch-to-stable -```` - -## Providing Feedback - -You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback. diff --git a/docs/en/RabbitMq.md b/docs/en/RabbitMq.md deleted file mode 100644 index 9fc04aa5d6..0000000000 --- a/docs/en/RabbitMq.md +++ /dev/null @@ -1,3 +0,0 @@ -# RabbitMQ - -TODO! \ No newline at end of file diff --git a/docs/en/Redis-Cache.md b/docs/en/Redis-Cache.md deleted file mode 100644 index aa756e1a2e..0000000000 --- a/docs/en/Redis-Cache.md +++ /dev/null @@ -1,45 +0,0 @@ -# Redis Cache - -ABP Framework [Caching System](Caching.md) extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). So, **any provider** supported by the standard ASP.NET Core distributed cache can be usable in your application and can be configured just like **documented by Microsoft**. - -However, ABP provides an **integration package** for Redis Cache: [Volo.Abp.Caching.StackExchangeRedis](https://www.nuget.org/packages/Volo.Abp.Caching.StackExchangeRedis). There are two reasons for using this package, instead of the standard [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis/) package. - -1. It implements `SetManyAsync` and `GetManyAsync` methods. These are not standard methods of the Microsoft Caching library, but added by the ABP Framework [Caching](Caching.md) system. They **significantly increases the performance** when you need to set/get multiple cache items with a single method call. -2. It **simplifies** the Redis cache **configuration** (will be explained below). - -> Volo.Abp.Caching.StackExchangeRedis is already uses the Microsoft.Extensions.Caching.StackExchangeRedis package, but extends and improves it. - -## Installation - -> This package is already installed in the application startup template if it is using Redis. - -Open a command line in the folder of your `.csproj` file and type the following ABP CLI command: - -````bash -abp add-package Volo.Abp.Caching.StackExchangeRedis -```` - -## Configuration - -Volo.Abp.Caching.StackExchangeRedis package automatically gets the Redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: - -````js -"Redis": { - "IsEnabled": "true", - "Configuration": "127.0.0.1" -} -```` -The setting `IsEnabled` is optional and will be considered `true` if it is not set. - -Alternatively you can configure the standard [RedisCacheOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions) [options](Options.md) class in the `ConfigureServices` method of your [module](Module-Development-Basics.md): - -````csharp -Configure(options => -{ - //... -}); -```` - -## See Also - -* [Caching](Caching.md) \ No newline at end of file diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md deleted file mode 100644 index d99e0cb83e..0000000000 --- a/docs/en/Repositories.md +++ /dev/null @@ -1,511 +0,0 @@ -# Repositories - -"*Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects*" (Martin Fowler). - -Repositories, in practice, are used to perform database operations for domain objects (see [Entities](Entities.md)). Generally, a separate repository is used for each **aggregate root** or entity. - -## Generic Repositories - -ABP can provide a **default generic repository** for each aggregate root or entity. You can [inject](Dependency-Injection.md) `IRepository` into your service and perform standard **CRUD** operations. - -> The database provider layer should be properly configured to be able to use the default generic repositories. It is **already done** if you've created your project using the startup templates. If not, refer to the database provider documents ([EF Core](Entity-Framework-Core.md) / [MongoDB](MongoDB.md)) to configure it. - -**Example usage of a default generic repository:** - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Demo -{ - public class PersonAppService : ApplicationService - { - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task CreateAsync(CreatePersonDto input) - { - var person = new Person(input.Name); - - await _personRepository.InsertAsync(person); - } - - public async Task GetCountAsync(string filter) - { - return await _personRepository.CountAsync(p => p.Name.Contains(filter)); - } - } -} -```` - -In this example; - -* `PersonAppService` simply injects `IRepository` in it's constructor. -* `CreateAsync` method uses `InsertAsync` to save the new entity. -* `GetCountAsync` method gets a filtered count of all people in the database. - -### Standard Repository Methods - -Generic Repositories provide some standard CRUD features out of the box: - -* `GetAsync`: Returns a single entity by its `Id` or a predicate (lambda expression). - * Throws `EntityNotFoundException` if the requested entity was not found. - * Throws `InvalidOperationException` if there are multiple entities with the given predicate. -* `FindAsync`: Returns a single entity by its `Id` or a predicate (lambda expression). - * Returns `null` if the requested entity was not found. - * Throws `InvalidOperationException` if there are multiple entities with the given predicate. -* `InsertAsync`: Inserts a new entity into the database. -* `UpdateAsync`: Updates an existing entity in the database. -* `DeleteAsync`: Deletes the given entity from the database. - * This method has an overload that takes a predicate (lambda expression) to delete multiple entities to satisfy the given condition. -* `GetListAsync`: Returns the list of all entities in the database. -* `GetPagedListAsync`: Returns a limited list of entities. Gets `skipCount`, `maxResultCount`, and `sorting` parameters. -* `GetCountAsync`: Gets the count of all entities in the database. - -There are overloads of these methods. - -* Provides `UpdateAsync` and `DeleteAsync` methods to update or delete an entity by entity object or its id. -* Provides `DeleteAsync` method to delete multiple entities by a filter. - -### Querying / LINQ over the Repositories - -Repositories provide the `GetQueryableAsync()` method that returns an `IQueryable` object. You can use this object to perform LINQ queries on the entities in the database. - -**Example: Use LINQ with the repositories** - -````csharp -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Demo -{ - public class PersonAppService : ApplicationService - { - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task> GetListAsync(string filter) - { - //Obtain the IQueryable - IQueryable queryable = await _personRepository.GetQueryableAsync(); - - //Create a query - var query = from person in queryable - where person.Name == filter - orderby person.Name - select person; - - //Execute the query to get list of people - var people = query.ToList(); - - //Convert to DTO and return to the client - return people.Select(p => new PersonDto {Name = p.Name}).ToList(); - } - } -} -```` - -You could also use the LINQ extension methods: - -````csharp -public async Task> GetListAsync(string filter) -{ - //Obtain the IQueryable - IQueryable queryable = await _personRepository.GetQueryableAsync(); - - //Execute a query - var people = queryable - .Where(p => p.Name.Contains(filter)) - .OrderBy(p => p.Name) - .ToList(); - - //Convert to DTO and return to the client - return people.Select(p => new PersonDto {Name = p.Name}).ToList(); -} -```` - -Any standard LINQ method can be used over the `IQueryable` returned from the repository. - -> This sample uses `ToList()` method, but it is **strongly suggested to use the asynchronous methods** to perform database queries, like `ToListAsync()` for this example. See the **`IQueryable` & Async Operations** section to learn how you can do it. - -> **Exposing `IQueryable` outside of a repository** class may leak your data access logic to the application layer. If you want to strictly follow the **layered architecture** principles, you can consider to implement a custom repository class and wrap your data access logic inside your repository class. You can see the ***Custom Repositories*** section to learn how to create custom repository classes for your application. - -### Bulk Operations - -There are some methods to perform bulk operations in the database; - -* `InsertManyAsync` -* `UpdateManyAsync` -* `DeleteManyAsync` - -These methods work with multiple entities and can take advantage of bulk operations if supported by the underlying database provider. - -> Optimistic concurrency control may not be possible when you use `UpdateManyAsync` and `DeleteManyAsync` methods. - -### Soft / Hard Delete - -`DeleteAsync` method of the repository doesn't delete the entity if the entity is a **soft-delete** entity (that implements `ISoftDelete`). Soft-delete entities are marked as "deleted" in the database. The Data Filter system ensures that the soft deleted entities are not retrieved from the database normally. - -If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method to physically delete the entity from the database in case you need it. - -> See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. - -### Delete Direct - -`DeleteDirectAsync` method of the repository deletes all entities that fit to the given predicate. It directly deletes entities from the database, without fetching them. - -Some features (like soft-delete, multi-tenancy, and audit logging) won't work, so use this method carefully when you need it. Use the `DeleteAsync` method if you need these features. - -> Currently only [EF Core supports it](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#basic-executedelete-examples), For the ORMs that don't support direct delete, we will fall back to the existing `DeleteAsync` method. - -### Ensure Entities Exist - -The `EnsureExistsAsync` extension method accepts entity id or entities query expression to ensure entities exist, otherwise, it will throw `EntityNotFoundException`. - -### Enabling / Disabling the Change Tracking - -ABP provides repository extension methods and attributes that can be used to control the change-tracking behavior for queried entities in the underlying database provider. - -Disabling change tracking can gain performance if you query many entities from the database for read-only purposes. Querying single or a few entities won't make much performance difference, but you are free to use it whenever you like. - -> If the underlying database provider doesn't support change tracking, then this system won't have any effect. [Entity Framework Core](Entity-Framework-Core.md) supports change tracking, for example, while the [MongoDB](MongoDB.md) provider doesn't support it. - -#### Repository Extension Methods for Change Tracking - -Change tracking is enabled unless you explicitly disable it. - -**Example: Using the `DisableTracking` extension method** - -````csharp -public class MyDemoService : ApplicationService -{ - private readonly IRepository _personRepository; - - public MyDemoService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task DoItAsync() - { - // Change tracking is enabled in that point (by default) - - using (_personRepository.DisableTracking()) - { - // Change tracking is disabled in that point - var list = await _personRepository.GetPagedListAsync(0, 100, "Name ASC"); - } - - // Change tracking is enabled in that point (by default) - } -} -```` - -> `DisableTracking` extension method returns an `IDisposable` object, so you can safely **restore** the change tracking behavior to the **previous state** once the `using` block ends. Basically, `DisableTracking` method ensures that the change tracking is disabled inside the `using` block, but doesn't affect outside of the `using` block. That means, if change tracking was already disabled, `DisableTracking` and the disposable return value do nothing. - -`EnableTracking()` method works exactly opposite to the `DisableTracking()` method. You typically won't use it (because the change tracking is already enabled by default), but it is there in case you need that. - -#### Attributes for Change Tracking - -You typically use the `DisableTracking()` method for the application service methods that only return data, but don't make any change to entities. For such cases, you can use the `DisableEntityChangeTracking` attribute on your method/class as a shortcut to disable the change tracking for the whole method body. - -**Example: Using the `DisableEntityChangeTracking` attribute on a method** - -````csharp -[DisableEntityChangeTracking] -public virtual async Task> GetListAsync() -{ - /* We disabled the change tracking in this method - because we won't change the people objects */ - var people = await _personRepository.GetListAsync(); - return ObjectMapper.Map, List(people); -} -```` - -`EnableEntityChangeTracking` can be used for the opposite purpose, and it ensures that the change tracking is enabled for a given method. Since the change tracking is enabled by default, `EnableEntityChangeTracking` may be needed only if you know that your method is called from a context that disables the change tracking. - -`DisableEntityChangeTracking` and `EnableEntityChangeTracking` attributes can be used on a **method** or on a **class** (which affects all of the class methods). - -ABP uses dynamic proxying to make these attributes work. There are some rules here: - -* If you are **not injecting** the service over an interface (like `IPersonAppService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work. -* Only `async` methods (methods returning a `Task` or `Task`) are intercepted. - -> Change tracking behavior doesn't affect tracking entity objects returned from `InsertAsync` and `UpdateAsync` methods. The objects returned from these methods are always tracked (if the underlying provider has the change tracking feature) and any change you make to these objects are saved into the database. - -## Other Generic Repository Types - -Standard `IRepository` interface exposes the standard `IQueryable` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`. - -### Basic Repositories - -ABP provides `IBasicRepository` and `IBasicRepository` interfaces to support such scenarios. You can extend these interfaces (and optionally derive from `BasicRepositoryBase`) to create custom repositories for your entities. - -Depending on `IBasicRepository` but not depending on `IRepository` has the advantage of making it possible to work with all data sources even if they don't support `IQueryable`. - -Major vendors, like Entity Framework, NHibernate, or MongoDB already support `IQueryable`. So, working with `IRepository` is the **suggested** way for typical applications. However reusable module developers may consider `IBasicRepository` to support a wider range of data sources. - -### Read Only Repositories - -There are also `IReadOnlyRepository` and `IReadOnlyBasicRepository` interfaces for those who only want to depend on the querying capabilities of the repositories. - -The `IReadOnlyRepository` derives the `IReadOnlyBasicRepository` and provides the following properties and methods as well: - -Properties: - -`AsyncExecuter`: a service that is used to execute an `IQueryable` object asynchronously **without depending on the actual database provider**. - -Methods: - -- `GetListAsync()` -- `GetQueryableAsync()` -- `WithDetails()` 1 overload -- `WithDetailsAsync()` 1 overload - -Whereas the `IReadOnlyBasicRepository` provides the following methods: - -- `GetCountAsync()` -- `GetListAsync()` -- `GetPagedListAsync()` - -They can all be seen below: - -![generic-repositories](images/generic-repositories.png) - -#### Read Only Repositories behavior in Entity Framework Core - -Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/) because it is expected that you won't update entities queried from a read-only repository. If you need to track the entities, you can still use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document). - -> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) and then cast it to a read-only repository interface. - -### Generic Repository without a Primary Key - -If your entity does not have an Id primary key (it may have a composite primary key for instance) then you cannot use the `IRepository` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository` for your entity. - -> `IRepository` has a few missing methods that normally work with the `Id` property of an entity. Because the entity has no `Id` property in that case, these methods are not available. One example is the `Get` method which gets an id and returns the entity with the given id. However, you can still use `IQueryable` features to query entities by standard LINQ methods. - -## Custom Repositories - -Default generic repositories will be sufficient for most cases. However, you may need to create a custom repository class for your entity. - -### Custom Repository Example - -ABP does not force you to implement any interface or inherit from any base class for a repository. It can be just a simple POCO class. However, it's suggested to inherit existing repository interfaces and classes to make your work easier and get the standard methods out of the box. - -#### Custom Repository Interface - -First, define an interface in your domain layer: - -```c# -public interface IPersonRepository : IRepository -{ - Task FindByNameAsync(string name); -} -``` - -This interface extends `IRepository` to take advantage of pre-built repository functionality. - -#### Custom Repository Implementation - -A custom repository is tightly coupled to the data access tool type you are using. In this example, we will use Entity Framework Core: - -````C# -public class PersonRepository : EfCoreRepository, IPersonRepository -{ - public PersonRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - public async Task FindByNameAsync(string name) - { - var dbContext = await GetDbContextAsync(); - return await dbContext.Set() - .Where(p => p.Name == name) - .FirstOrDefaultAsync(); - } -} -```` - -You can directly access the data access provider (`DbContext` in this case) to perform operations. - -> See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. - -## IQueryable & Async Operations - -`IRepository` provides `GetQueryableAsync()` to obtain an `IQueryable`, which means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Querying / LINQ over the Repositories*" section above. - -**Example: Using the `Where(...)` and the `ToList()` extension methods** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = queryable - .Where(p => p.Name.Contains(nameFilter)) - .ToList(); -```` - -`.ToList`, `Count()`... are standard extension methods defined in the `System.Linq` namespace ([see all](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)). - -You normally want to use `.ToListAsync()`, `.CountAsync()`... instead, to be able to write a **truly async code**. - -However, you see that you can't use all the async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; - -* These async methods **are not standard LINQ methods** and they are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package. -* The standard template **doesn't have a reference** to the EF Core package from the domain and application layers, to be independent from the database provider. - -Based on your requirements and development model, you have the following options to be able to use the async methods. - -> Using async methods is strongly suggested! Don't use sync LINQ methods while executing database queries to be able to develop a scalable application. - -### Option-1: Reference to the Database Provider Package - -**The easiest solution** is to directly add the EF Core package from the project you want to use these async methods. - -> Add the [Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore) NuGet package to your project, which indirectly references the EF Core package. This ensures that you use the correct version of the EF Core compatible with the rest of your application. - -When you add the NuGet package to your project, you can take full power of the EF Core extension methods. - -**Example: Directly using the `ToListAsync()` after adding the EF Core package** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = queryable - .Where(p => p.Name.Contains(nameFilter)) - .ToListAsync(); -```` - -This method is suggested; - -* If you are developing an application and you **don't plan to change** EF Core in the future, or you can **tolerate** it if you need to change it later. We believe that's reasonable if you are developing a final application. - -#### MongoDB Case - -If you are using [MongoDB](MongoDB.md), you need to add the [Volo.Abp.MongoDB](https://www.nuget.org/packages/Volo.Abp.MongoDB) NuGet package to your project. Even in this case, you can't directly use async LINQ extensions (like `ToListAsync`) because MongoDB doesn't provide async extension methods for `IQueryable`, but provides for `IMongoQueryable`. You need to cast the query to `IMongoQueryable` first to be able to use the async extension methods. - -**Example: Cast `IQueryable` to `IMongoQueryable` and use `ToListAsync()`** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = ((IMongoQueryable) queryable - .Where(p => p.Name.Contains(nameFilter))) - .ToListAsync(); -```` - -### Option-2: Use the IRepository Async Extension Methods - -ABP Framework provides async extension methods for the repositories, just similar to async LINQ extension methods. - -**Example: Use `CountAsync` and `FirstOrDefaultAsync` methods on the repositories** - -````csharp -var countAll = await _personRepository - .CountAsync(); - -var count = await _personRepository - .CountAsync(x => x.Name.StartsWith("A")); - -var book1984 = await _bookRepository - .FirstOrDefaultAsync(x => x.Name == "John"); -```` - -The standard LINQ extension methods are supported: *AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync*. - -This approach still **has a limitation**. You need to call the extension method directly on the repository object. For example, the below usage is **not supported**: - -```csharp -var queryable = await _bookRepository.GetQueryableAsync(); -var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync(); -``` - -This is because the `CountAsync()` method in this example is called on an `IQueryable` interface, not on the repository object. See the other options for such cases. - -This method is suggested **wherever possible**. - -### Option-3: IAsyncQueryableExecuter - -`IAsyncQueryableExecuter` is a service that is used to execute an `IQueryable` object asynchronously **without depending on the actual database provider**. - -**Example: Inject & use the `IAsyncQueryableExecuter.ToListAsync()` method** - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Linq; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - private readonly IAsyncQueryableExecuter _asyncExecuter; - - public ProductAppService( - IRepository productRepository, - IAsyncQueryableExecuter asyncExecuter) - { - _productRepository = productRepository; - _asyncExecuter = asyncExecuter; - } - - public async Task> GetListAsync(string name) - { - //Obtain the IQueryable - var queryable = await _productRepository.GetQueryableAsync(); - - //Create the query - var query = queryable - .Where(p => p.Name.Contains(name)) - .OrderBy(p => p.Name); - - //Run the query asynchronously - List products = await _asyncExecuter.ToListAsync(query); - - //... - } - } -} -```` - -> `ApplicationService` and `DomainService` base classes already have `AsyncExecuter` properties pre-injected and usable without needing an explicit constructor injection. - -ABP Framework executes the query asynchronously using the actual database provider's API. While that is not the usual way to execute a query, it is the best way to use the async API without depending on the database provider. - -This method is suggested; - -* If you want to develop your application code **without depending** on the database provider. -* If you are building a **reusable library** that doesn't have a database provider integration package, but needs to execute an `IQueryable` object in some case. - -For example, ABP Framework uses the `IAsyncQueryableExecuter` in the `CrudAppService` base class (see the [application services](Application-Services.md) document). - -### Option-4: Custom Repository Methods - -You can always create custom repository methods and use the database provider-specific APIs, like async extension methods here. See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. - -This method is suggested; - -* If you want to **completely isolate** your domain & application layers from the database provider. -* If you develop a **reusable [application module](Modules/Index.md)** and don't want to force to a specific database provider, which should be done as a [best practice](Best-Practices/Index.md). - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/generic-repositories) diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md deleted file mode 100644 index 667e256fbf..0000000000 --- a/docs/en/Road-Map.md +++ /dev/null @@ -1,43 +0,0 @@ -# ABP Framework Road Map - -This document provides a road map, release schedule, and planned features for the ABP Framework. - -## Next Versions - -### v8.3 - -The next version will be 8.3 and planned to release the stable 8.3 version in August 2024. We will be mostly working on the following topics: - -* Angular Universal ([#15782](https://github.com/abpframework/abp/issues/15782)) -* Angular generate-proxy root namespace options ([#18932](https://github.com/abpframework/abp/issues/18932)) -* Blazor global JS & CSS at runtime ([#19963](https://github.com/abpframework/abp/issues/19963)) -* CMS Kit - Improvement in editing approval system for comments ([#19976](https://github.com/abpframework/abp/issues/19976)) -* Improvements on the existing features and provide more guides. - -See the [8.3 milestone](https://github.com/abpframework/abp/milestone/101) for all the issues we've planned to work on. - -## Backlog Items - -The *Next Versions* section above shows the main focus of the planned versions. However, in each release, we add new features to the core framework and the [application modules](Modules/Index.md). - -Here is a list of major items in the backlog we are considering working on in the next versions. - -* [#86](https://github.com/abpframework/abp/issues/86) / GrapQL Integration -* [#236](https://github.com/abpframework/abp/issues/236) / Resource-based authorization system -* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](https://docs.abp.io/en/abp/latest/Modules/Index)) -* [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure -* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook system -* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB Integration for multitenancy -* [#2296](https://github.com/abpframework/abp/issues/2296) / Feature toggling infrastructure -* [#16342](https://github.com/abpframework/abp/issues/16342) / CmsKit: Meta information for SEO -* [#16260](https://github.com/abpframework/abp/issues/16260) / GCP Blob Storage Provider -* [#15932](https://github.com/abpframework/abp/issues/15932) / Introduce ABP Diagnostics Module -* [#16756](https://github.com/abpframework/abp/issues/16756) / Blob Storing - Provider configuration UI -* [#16744](https://github.com/abpframework/abp/issues/16744) / State Management API - -You can always check the milestone planning and the prioritized backlog issues on [the GitHub repository](https://github.com/abpframework/abp/milestones) for a detailed road map. The backlog items are subject to change. We are adding new items and changing priorities based on the community feedback and goals of the project. - -## Feature Requests - -Vote for your favorite feature on the related GitHub issues (and write your thoughts). You can create an issue on [the GitHub repository](https://github.com/abpframework/abp) for your feature requests, but first search in the existing issues. - diff --git a/docs/en/SMS-Sending.md b/docs/en/SMS-Sending.md deleted file mode 100644 index 3043ce643a..0000000000 --- a/docs/en/SMS-Sending.md +++ /dev/null @@ -1,113 +0,0 @@ -# SMS Sending - -The ABP Framework provides an abstraction to sending SMS. Having such an abstraction has some benefits; - -- You can then **easily change** your SMS sender without changing your application code. -- If you want to create **reusable application modules**, you don't need to make assumption about how the SMS are sent. - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Sms -``` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Sms). - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Sms](https://www.nuget.org/packages/Volo.Abp.Sms) NuGet package to your project: - -``` -Install-Package Volo.Abp.Sms -``` - -2. Add the `AbpSmsModule` to the dependency list of your module: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpSmsModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -``` - -## Sending SMS - -[Inject](Dependency-Injection.md) the `ISmsSender` into any service and use the `SendAsync` method to send a SMS. - -**Example:** - -```csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Sms; - -namespace MyProject -{ - public class MyService : ITransientDependency - { - private readonly ISmsSender _smsSender; - - public MyService(ISmsSender smsSender) - { - _smsSender = smsSender; - } - - public async Task DoItAsync() - { - await _smsSender.SendAsync( - "+012345678901", // target phone number - "This is test sms..." // message text - ); - } - } -} -``` - -The given `SendAsync` method in the example is an extension method to send an SMS with primitive parameters. In addition, you can pass an `SmsMessage` object which has the following properties: - -- `PhoneNumber` (`string`): Target phone number -- `Text` (`string`): Message text -- `Properties` (`Dictionary`): Key-value pairs to pass custom arguments - -## NullSmsSender - -`NullSmsSender` is a the default implementation of the `ISmsSender`. It writes SMS content to the [standard logger](Logging.md), rather than actually sending the SMS. - -This class can be useful especially in development time where you generally don't want to send real SMS. **However, if you want to actually send SMS, you should implement the `ISmsSender` in your application code.** - -## Implementing the ISmsSender - -You can easily create your SMS sending implementation by creating a class that implements the `ISmsSender` interface, as shown below: - -```csharp -using System.IO; -using System.Threading.Tasks; -using Volo.Abp.Sms; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyCustomSmsSender : ISmsSender, ITransientDependency - { - public async Task SendAsync(SmsMessage smsMessage) - { - // Send sms - } - } -} -``` - -## More - -[ABP Commercial](https://commercial.abp.io/) provides Twilio integration package to send SMS over [Twilio service](https://docs.abp.io/en/commercial/latest/modules/twilio-sms). diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md deleted file mode 100644 index 088fb78f67..0000000000 --- a/docs/en/Samples/Index.md +++ /dev/null @@ -1,99 +0,0 @@ -# Sample Applications - -Here, a list of official samples built with the ABP Framework. Most of these samples are located under the [abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub repository. - -## eShopOnAbp - -Reference microservice solution built with the ABP Framework and .NET. - -* [Source code](https://github.com/abpframework/eShopOnAbp) - -## EventHub - -This is a reference application built with the ABP Framework. It implements the Domain Driven Design with multiple application layers. - -* [Live](https://openeventhub.com/) -* [Source code](https://github.com/abpframework/eventhub) - -## CMS Kit Demo - -This reference application built with the ABP Framework and demonstrates the [CMS Kit Module's](../Modules/Cms-Kit/Index.md) capabilities. - -* [Live](https://cms-kit-demo.abpdemo.com/) -* [Source code](https://github.com/abpframework/cms-kit-demo) - -## Book Store - -A simple CRUD application to show basic principles of developing an application with the ABP Framework. The same sample was implemented with different technologies: - -* **Book Store: Razor Pages UI & Entity Framework Core** - * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF) - * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* **Book Store: Blazor UI & Entity Framework Core** - * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor&DB=EF) - * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* **Book Store: Angular UI & MongoDB** - * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG&DB=Mongo) - * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -* **Book Store: Modular application (Razor Pages UI & EF Core)** - * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) - -While there is no Razor Pages & MongoDB combination, you can check both documents to understand it since DB & UI selection don't effect each other. - -## Other Samples - -* **Event Organizer**: A sample application to create events (meetups) and allow others to register the events. Developed using EF Core and Blazor UI. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer) - * [Article](https://community.abp.io/articles/creating-an-event-organizer-application-with-the-blazor-ui-wbe0sf2z) -* **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo) - * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) -* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) - * [SignalR Integration document](../SignalR-Integration.md) -* **Real Time Messaging In A Distributed Architecture** (using SingalR & RabbitMQ) - * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo) - * [Article](https://community.abp.io/articles/real-time-messaging-in-a-distributed-architecture-using-abp-framework-singalr-rabbitmq-daf47e17) -* **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) - * [Widget documentation](../UI/AspNetCore/Widgets.md) -* **RabbitMQ Event Bus Demo**: A solution consists of two applications communicating to each other via distributed events with RabbitMQ integration. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) - * [Distributed event bus document](../Distributed-Event-Bus.md) - * [RabbitMQ distributed event bus integration document](../Distributed-Event-Bus-RabbitMQ-Integration.md) -* **Text Templates Demo**: Shows different use cases of the text templating system. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) - * [Text templating documentation](../Text-Templating.md) -* **Stored Procedure Demo**: Demonstrates how to use stored procedures, database views and functions with best practices. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo) -* **Passwordless Authentication**: Shows how to add a custom token provider to authenticate a user with a link, instead of entering a password. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication) - * [Article](https://community.abp.io/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj) -* **Authentication Customization**: A solution to show how to customize the authentication for ASP.NET Core MVC / Razor Pages applications. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization) - * Related articles: - * [Azure Active Directory Authentication](https://community.abp.io/articles/how-to-use-the-azure-active-directory-authentication-for-mvc-razor-page-applications-4603b9cf) - * [Customize the Login Page](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd) - * [Customize the SignIn Manager](https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753) -* **GRPC Demo**: Shows how to add a gRPC service to an ABP Framework based web application and consume it from a console application. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) -* **Telerik Blazor Integration**: Shows how to install and use Telerik Blazor components with the ABP Framework. - * [Article](https://community.abp.io/articles/how-to-integrate-the-telerik-blazor-components-to-the-abp-blazor-ui-q8g31abb) -* **Angular Material Integration**: Implemented the web application tutorial using the Angular Material library. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial) - * [Article](https://community.abp.io/articles/using-angular-material-components-with-the-abp-framework-af8ft6t9) -* **DevExtreme Angular Component Integration**: How to install and use DevExtreme components in the ABP Framework Angular UI. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Angular) - * [Article](https://community.abp.io/articles/using-devextreme-angular-components-with-the-abp-framework-x5nyvj3i) -* **DevExtreme MVC / Razor Pages Component Integration**: How to install and use DevExtreme components in the ABP Framework MVC / Razor Pages UI. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Mvc) - * [Article](https://community.abp.io/articles/using-devextreme-components-with-the-abp-framework-zb8z7yqv) -* **Syncfusion Blazor Integration**: Shows how to install and integrate Syncfusion UI with ABP Framework Blazor UI. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/SyncfusionSample) - * [Article](https://community.abp.io/articles/using-syncfusion-components-with-the-abp-framework-5ccvi8kc) -* **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) - * [Documentation](../Getting-Started-AspNetCore-Application.md) -* **Using Elsa Workflow with ABP Framework**: Shows how to use the Elsa Core workflow library within an ABP-based application. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/ElsaDemo) - * [Article](https://community.abp.io/articles/using-elsa-workflow-with-the-abp-framework-773siqi9) diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md deleted file mode 100644 index 1e2549338f..0000000000 --- a/docs/en/Samples/Microservice-Demo.md +++ /dev/null @@ -1,1421 +0,0 @@ -# Microservice Demo Solution - -> This solution is no longer maintained. See [the eShopOnAbp project](https://github.com/abpframework/eShopOnAbp) for the replacement solution. - -*"Microservices are a software development technique—a variant of the **service-oriented architecture** (SOA) architectural style that structures an application as a collection of **loosely coupled services**. In a microservices architecture, services are **fine-grained** and the protocols are **lightweight**. The benefit of decomposing an application into different smaller services is that it improves **modularity**. This makes the application easier to understand, develop, test, and become more resilient to architecture erosion. It **parallelizes development** by enabling small autonomous teams to **develop, deploy and scale** their respective services independently. It also allows the architecture of an individual service to emerge through **continuous refactoring**. Microservices-based architectures enable **continuous delivery and deployment**."* - -— [Wikipedia](https://en.wikipedia.org/wiki/Microservices) - -## Introduction - -One of the major goals of the ABP framework is to provide a [convenient infrastructure to create microservice solutions](../Microservice-Architecture.md). - -This sample aims to demonstrate a simple yet complete microservice solution; - -* Has multiple, independent, self-deployable **microservices**. -* Multiple **web applications**, each uses a different API gateway. -* Has multiple **gateways** / BFFs (Backend for Frontends) developed using the [Ocelot](https://github.com/ThreeMammals/Ocelot) library. -* Has an **authentication service** developed using the [IdentityServer](https://identityserver.io/) framework. It's also a SSO (Single Sign On) application with necessary UIs. -* Has **multiple databases**. Some microservices has their own database while some services/applications shares a database (to demonstrate different use cases). -* Has different types of databases: **SQL Server** (with **Entity Framework Core** ORM) and **MongoDB**. -* Has a **console application** to show the simplest way of using a service by authenticating. -* Uses [Redis](https://redis.io/) for **distributed caching**. -* Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**. -* Uses [Docker](https://www.docker.com/) & [Kubernetes](https://kubernetes.io/) to **deploy** & run all services and applications. -* Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)). - -The diagram below shows the system: - -![microservice-sample-diagram-2](../images/microservice-sample-diagram-3.png) - -### Source Code - -You can get the source code from [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/MicroserviceDemo). - -## Running the Solution - -### Pre Requirements - -To be able to run the solution from source code, following tools should be installed and running on your computer: - -* [SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 2015+ (can be [express edition](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express)) -* [Redis](https://redis.io/download) 5.0+ -* [RabbitMQ](https://www.rabbitmq.com/install-windows.html) 3.7.11+ -* [MongoDB](https://www.mongodb.com/download-center) 4.0+ -* [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ -* [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to show logs) - -### Running Infrastructure - -* Docker-compose is used to run the pre requirements with ease as default. If you don't have it, you can download and start using [Docker for Windows](https://docs.docker.com/docker-for-windows/) from [here](https://docs.docker.com/docker-for-windows/install/) on windows environment. -* Run the command `docker-compose -f docker-compose.infrastructure.yml -f docker-compose.infrastructure.override.yml up -d` at `MicroserviceDemo` directory or run the powershell script `__Run_Infrastructure.ps1` located at `MicroserviceDemo/_run` directory. -* If you don't want to use docker for pre required services and install them on your local development, you need to update `appsettings.json` files of the projects in the MicroserviceDemo solution accordingly. - -### Open & Build the Visual Studio Solution - -* Open the `samples\MicroserviceDemo\MicroserviceDemo.sln` in Visual Studio 2017 (15.9.0+). -* Run `dotnet restore` from the command line inside the `samples\MicroserviceDemo` folder. -* Build the solution in Visual Studio. - -### Create Databases - -MongoDB database is created dynamically, however you need to create database schemas for SQL server databases. The solution is configured to use Entity Core Code First migrations, so you can easily create databases. - -There are two SQL server databases in this solution. - -#### MsDemo_Identity Database - -* Right click to the `AuthServer.Host` project and click to the `Set as startup project`. -* Open the **Package Manager Console** (Tools -> Nuget Package Manager -> Package Manager Console) -* Select `AuthServer.Host` as the **Default project**. -* Run `Update-Database` command. - -![microservice-sample-update-database-authserver](../images/microservice-sample-update-database-authserver.png) - -#### MsDemo_ProductManagement - -- Right click to the `ProductService.Host` project and click to the `Set as startup project`. -- Open the **Package Manager Console** (Tools -> Nuget Package Manager -> Package Manager Console) -- Select `ProductService.Host` as the **Default project**. -- Run `Update-Database` command. - -![microservice-sample-update-database-products](../images/microservice-sample-update-database-products.png) - -### Run Projects - -Run the projects with the following order (right click to each project, set as startup project an press Ctrl+F5 to run without debug): - -* AuthServer.Host -* IdentityService.Host -* TenantManagementService.Host -* BloggingService.Host -* ProductService.Host -* InternalGateway.Host -* BackendAdminAppGateway.Host -* PublicWebSiteGateway.Host -* BackendAdminApp.Host -* PublicWebSite.Host - -When you run projects, they will add some initial demo data to their databases. - -## A Brief Overview of the Solution - -The Visual Studio solution consists of multiple projects each have different roles in the system: - -![microservice-sample-solution](../images/microservice-sample-solution-2.png) - -### Applications - -These are the actual applications those have user interfaces to interact to the users and use the system. - -- **AuthServer.Host**: Host the IdentityServer4 to provide an authentication service to other services and applications. It is a single-sign server and contains the login page. -- **BackendAdminApp.Host**: This is a backend admin application that host UI for Identity and Product management modules. -- **PublicWebSite.Host**: As public web site that contains a simple product list page and blog module UI. -- **ConsoleClientDemo**: A simple console application to demonstrate the usage of services from a C# application. - -### Gateways / BFFs (Backend for Frontend) - -Gateways are used to provide a single entry point to the applications. It can also used for rate limiting, load balancing... etc. Used the [Ocelot](https://github.com/ThreeMammals/Ocelot) library. - -* **BackendAdminAppGateway.Host**: Used by the BackendAdminApp.Host application as backend. -* **PublicWebSiteGateway.Host**: Used by the PublicWebSite.Host application as backend. -* **InternalGateway.Host**: Used for inter-service communication (the communication between microservices). - -### Microservices - -Microservices have no UI, but exposes some REST APIs. - -- **IdentityService.Host**: Hosts the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module's API. -- **TenantManagementService.Host**: Hosts the ABP Tenant Management module which is used to manage roles. It has no additional service, but only hosts the Tenant Management module's API. -- **BloggingService.Host**: Hosts the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module's API. -- **ProductService.Host**: Hosts the Product module (that is inside the solution) which is used to manage products. It also contains the EF Core migrations to create/update the Product Management database schema. - -### Modules - -* **Product**: A layered module that is developed with the [module development best practices](../Best-Practices/Index.md). It can be embedded into a monolithic application or can be hosted as a microservice by separately deploying API and UI (as done in this demo solution). - -### Databases - -This solution is using multiple databases: - -* **MsDemo_Identity**: An SQL database. Used **SQL Server** by default, but can be any DBMS supported by the EF Core. Shared by AuthServer, IdentityService and the TenantManagementService. Also audit logs, permissions and settings are stored in this database (while they could have their own databases, shared the same database to keep it simple). -* **MsDemo_ProductManagement**: An SQL database. Again, used **SQL Server** by default, but can be any DBMS supported by the EF Core. Used by the ProductService as a dedicated database. -* **MsDemo_Blogging**: A **MongoDB** database. Used by the BloggingService. -* **Elasticsearch**: Used to write logs over Serilog. - -## Applications - -### Authentication Server (AuthServer.Host) - -This project is used by all other services and applications for authentication & single sign on. Mainly, uses **IdentityServer4** to provide these services. It uses some of the [pre-build ABP modules](../Modules/Index) like *Identity*, *Audit Logging* and *Permission Management*. - -#### Database & EF Core Configuration - -This application uses a SQL database (named it as **MsDemo_Identity**) and maintains its schema via **Entity Framework Core migrations.** - -It has a DbContext named **AuthServerDbContext** and defined as shown below: - -````csharp -public class AuthServerDbContext : AbpDbContext -{ - public AuthServerDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureIdentity(); - modelBuilder.ConfigureIdentityServer(); - modelBuilder.ConfigureAuditLogging(); - modelBuilder.ConfigurePermissionManagement(); - modelBuilder.ConfigureSettingManagement(); - } -} -```` - -In the **OnModelCreating**, you see **ConfigureX()** method calls. A module with a database schema generally declares such an extension method to configure EF Core mappings for its own entities. This is a flexible approach where you can arrange your databases and modules inside them; You can use a different database for each module, or combine some of them in a shared database. In the AuthServer project, we decided to combine multiple module schemas in a single EF Core DbContext, in a single physical database. These modules are Identity, IdentityServer, AuditLogging, PermissionManagement and SettingManagement modules. - -Notice that this DbContext is only for database migrations. All modules have their own `DbContext` classes those are used in the runtime by the modules. - -#### User Interface - -AuthServer has a simple home page that shows the current user info if the current user has logged in: - -![microservice-sample-authserver-home](../images/microservice-sample-authserver-home.png) - -It also provides Login & Register pages: - -![microservice-sample-authserver-login](../images/microservice-sample-authserver-login.png) - -These pages are not included in the project itself. Instead, AuthServer project uses the prebuilt ABP [account module](https://github.com/abpframework/abp/tree/master/modules/account) with IdentityServer extension. That means it can also act as an OpenId Connect server with necessary UI and logic. - -#### Dependencies - -* **RabbitMQ** for messaging to other services. -* **Redis** for distributed/shared caching. -* **Elasticsearch** for storing logs. - -### Backend Admin Application (BackendAdminApp.Host) - -This is a web application that is used to manage users, roles, permissions and products in the system. - -#### Authentication - -BackendAdminApp redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the backend application again. Authentication configuration is setup in the `BackendAdminAppHostModule` class: - -````charp -context.Services.AddAuthentication(options => -{ - options.DefaultScheme = "Cookies"; - options.DefaultChallengeScheme = "oidc"; -}) -.AddCookie("Cookies", options => -{ - options.Cookie.Expiration = TimeSpan.FromDays(365); - options.ExpireTimeSpan = TimeSpan.FromDays(365); -}) -.AddOpenIdConnect("oidc", options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - options.RequireHttpsMetadata = false; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.Scope.Add("role"); - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("BackendAdminAppGateway"); - options.Scope.Add("IdentityService"); - options.Scope.Add("ProductService"); - options.ClaimActions.MapAbpClaimTypes(); -}); -```` - -* It adds "Cookies" authentication as the primary authentication type. -* "oidc" authentication is configured to use the AuthServer application as the authentication server. -* It requires the additional identity scopes *role*, *email* and *phone*. -* It requires the API resource scopes *BackendAdminAppGateway*, *IdentityService* and *ProductService* because it will use these services as APIs. - -IdentityServer client settings are stored inside the `appsettings.json` file: - -````json -"AuthServer": { - "Authority": "http://localhost:64999", - "ClientId": "backend-admin-app-client", - "ClientSecret": "1q2w3e*" -} -```` - -#### User Interface - -The BackendAdminApp.Host project itself has not a single UI element/page. It is only used to serve UI pages of the Identity and Product Management modules. `BackendAdminAppHostModule` adds dependencies to `AbpIdentityWebModule` (*[Volo.Abp.Identity.Web](https://www.nuget.org/packages/Volo.Abp.Identity.Web)* package) and `ProductManagementWebModule` (*ProductManagement.Web* project) for that purpose. - -A screenshot from the user management page: - -![microservice-sample-backend-ui](../images/microservice-sample-backend-ui.png) - -A screenshot from the permission management modal for a role: - -![microservice-sample-backend-ui-permissions](../images/microservice-sample-backend-ui-permissions.png) - -#### Using Microservices - -Backend admin application uses the Identity and Product microservices for all operations, over the Backend Admin Gateway (BackendAdminAppGateway.Host). - -##### Remote End Point - -`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65115/" - } -} -```` - -`http://localhost:65115/` is the URL of the *BackendAdminAppGateway.Host* project. It knows where are Identity and Product services are located. - -##### HTTP Clients - -ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. - -For that purpose, `BackendAdminAppHostModule` class declares dependencies for `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule`. - -Once you refer these client packages, you can directly inject an application service interface (e.g. `IIdentityUserAppService`) and use its methods like a local method call. It actually invokes remote service calls over HTTP to the related service endpoint. - -##### Passing the Access Token - -Since microservices requires authentication & authorization, each remote service call should contain an Authentication header. This header is obtained from the `access_token` inside the current `HttpContext` for the current user. This is automatically done when you use the `Volo.Abp.Http.Client.IdentityModel` package. `BackendAdminAppHostModule` declares dependencies to this package and to the related `AbpHttpClientIdentityModelModule` class. It is integrated to the HTTP Clients explained above. - -#### Dependencies - -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Public Web Site (PublicWebSite.Host) - -This is a public web site project that has a web blog and product list page. - -#### Authentication - -PublicWebSite can show blog posts and product list without login. If you login, you can also manage blogs. It redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the public web site application again. Authentication configuration is setup in the `PublicWebSiteHostModule` class: - -```charp -context.Services.AddAuthentication(options => -{ - options.DefaultScheme = "Cookies"; - options.DefaultChallengeScheme = "oidc"; -}) -.AddCookie("Cookies", options => -{ - options.Cookie.Expiration = TimeSpan.FromDays(365); - options.ExpireTimeSpan = TimeSpan.FromDays(365); -}) -.AddOpenIdConnect("oidc", options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - options.RequireHttpsMetadata = false; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.Scope.Add("role"); - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("PublicWebSiteGateway"); - options.Scope.Add("ProductService"); - options.Scope.Add("BloggingService"); - options.ClaimActions.MapAbpClaimTypes(); -}); -``` - -- It adds "Cookies" authentication as the primary authentication type. -- "oidc" authentication is configured to use the AuthServer application as the authentication server. -- It requires the additional identity scopes *role*, *email* and *phone*. -- It requires the API resource scopes *PublicWebSiteGateway*, *BloggingService* and *ProductService* because it will use these services as APIs. - -IdentityServer client settings are stored inside the `appsettings.json` file: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ClientId": "public-website-client", - "ClientSecret": "1q2w3e*" -} -``` - -#### User Interface - -The PublicWebSite.Host project has a page to list products (`Pages/Products.cshtml`). It also uses the UI from the blogging module. `PublicWebSiteHostModule` adds dependencies to `BloggingWebModule` (*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* package) for that purpose. - -A screenshot from the Products page: - -![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) - -#### Using Microservices - -Public web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host). - -##### Remote End Point - -`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: - -```json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:64897/" - } -} -``` - -`http://localhost:64897/` is the URL of the *PublicWebSiteGateway.Host* project. It knows where are Blogging and Product services are located. - -##### HTTP Clients - -`PublicWebSiteHostModule` class declares dependencies for `BloggingHttpApiClientModule` and `ProductManagementHttpApiClientModule` to be able to use remote HTTP APIs for these services. - -##### Passing the Access Token - -Just like explained in the Backend Admin Application section, Public Web Site project also uses the `AbpHttpClientIdentityModelModule` to pass `access_token` to the calling services for authentication. - -#### Dependencies - -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Console Client Demo - -Finally, the solution includes a very simple console application, named ConsoleClientDemo, that uses Identity and Product services by authenticating through the AuthServer. It uses the Internal Gateway (InternalGateway.Host) to perform HTTP API calls. - -#### Remote Service Configuration - -`RemoteService` configuration in the `appsettings.json` file is simple: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65129/" - } -} -```` - -`http://localhost:65129/` is the URL of the Internal Gateway. All API calls to the services are performed over this URL. - -#### Authentication (IdentityServer Client) Configuration - -`appsettings.json` also has a configuration for the IdentityServer authentication: - -````json -"IdentityClients": { - "Default": { - "GrantType": "client_credentials", - "ClientId": "console-client-demo", - "ClientSecret": "1q2w3e*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService ProductService" - } -} -```` - -This sample uses the `client_credentials` grant type which requires a `ClientId` and `ClientSecret` for the authentication process. There are also [other grant types](http://docs.identityserver.io/en/latest/topics/grant_types.html). For example, you can use the following configuration to swith to the `password` (Resource Owner Password) grant type: - -````json -"IdentityClients": { - "Default": { - "GrantType": "password", - "ClientId": "console-client-demo", - "ClientSecret": "1q2w3e*", - "UserName": "admin", - "UserPassword": "1q2w3E*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService ProductService" - } -} -```` - -Resource Owner Password requires a `UserName` & `UserPassword` in addition to client credentials. This grant type is useful to call remote services on behalf of a user. - -`Scope` declares the APIs (and the gateway) to grant access. This application uses the Internal Gateway. - -#### HTTP Client Dependencies - -`ConsoleClientDemoModule` has dependencies to `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule` in order to use Identity and Product APIs. It also has `AbpHttpClientIdentityModelModule` dependency to authenticate via IdentityServer. - -#### Using the Services - -Using the services is straightforward. See the `ClientDemoService` class which simply injects `IIdentityUserAppService` and `IProductAppService` and uses them. This class also shows a manual HTTP call using an `HttpClient` object. See source code of the `ClientDemoService` for details. - -## API Gateways / BFFs (Backend for Frontend) - -Gateways are used to provide a **single entry point** to the applications. In this way, an application only deal with a single service address (API endpoint) instead of a different addresses for each services. Gateways are also used for rate limiting, security, authentication, load balancing and many more requirements. - -"**Backend for Frontend**" (BFF) is a common architectural pattern which offers to build a **dedicated and specialized** gateway for each different application / client type. This solution uses this pattern and has multiple gateways. - -This solution uses the [Ocelot](https://github.com/ThreeMammals/Ocelot) library to build API Gateways. It's a widely accepted API Gateway library for ASP.NET Core. - -### Backend Admin Application Gateway (BackendAdminAppGateway.Host) - -This is backend (server side API) for the "Backend Admin Application" (don't confuse about the naming; Backend Admin Application is a frontend web application actually, but used by system admins rather than regular users). - -#### Authentication - -This gateway uses IdentityServer `Bearer` authentication and configured like that: - -````csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -```` - -`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName` is the API which is being protected, `BackendAdminAppGateway` in this case. So, this solution defines gateways as API resources. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -````json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "BackendAdminAppGateway" -} -```` - -#### Ocelot Configuration - -Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: - -````json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/identity/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 63568 - } - ], - "UpstreamPathTemplate": "/api/identity/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:65115" -} -```` - -`ReRoutes` is an array of URL mappings. `BaseUrl` in the `GlobalConfiguration` section is the URL of this gateway (Ocelot needs to know its own URL). See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the configuration. - -Ocelot is a finalizer ASP.NET Core middleware and should be written as the last item in the pipeline: - -````csharp -app.UseOcelot().Wait(); -```` - -It handles and redirects requests based on the configuration above. - -#### ABP Configuration Endpoints - -ABP provides some built-in APIs to get some configuration and information from the server. Examples: - -* `/api/abp/application-configuration` returns localization texts, permission and setting values (try http://localhost:65115/api/abp/application-configuration for this gateway). -* `/Abp/ServiceProxyScript` returns dynamic javascript proxies to call services from a javascript client (try http://localhost:65115/Abp/ServiceProxyScript for this gateway). - -These endpoints should be served by the gateway service, not by microservices. A microservice can only know permissions related to that microservice. But, once properly configured, gateway can aggregate permission values for multiple services as a single list which is more suitable for clients. - -For this purpose, the ASP.NET Core pipeline was configured to handle some specific routes via MVC, instead of Ocelot. To make this possible, MapWhen extension method is used like that: - -````csharp -app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") || - ctx.Request.Path.ToString().StartsWith("/Abp/"), - app2 => - { - app2.UseConfiguredEndpoints(); - }); - -app.UseOcelot().Wait(); -```` - -This configuration uses standard MVC middleware when request path starts with `/api/abp/` or `/Abp/`. - -#### Swagger - -This gateway is configured to use the [swagger UI](https://swagger.io/tools/swagger-ui/), a popular tool to discover & test HTTP APIs. Normally, Ocelot does not support to show APIs on the swagger, because it can not know details of each microservice API. But it is possible when you follow ABP layered module architecture [best practices](../Best-Practices/Index.md). - -`BackendAdminAppGatewayHostModule` adds dependency to `AbpIdentityHttpApiModule` (*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* package) and `ProductManagementHttpApiModule` (*ProductManagement.HttpApi* project) to include their HTTP API Controllers. In this way, swagger can discover them. While it references to the API layer, it does not reference to the implementation of application services, because they will be running in the related microservice endpoints and redirected by the Ocelot based on the request URL. - -Anyway, when you open the URL `http://localhost:65115/swagger/index.html`, you will see APIs of all configured microservices. - -#### Permission Management - -Backend Admin Application provides a permission management UI (seen before) and uses this gateway to get/set permissions. Permission management API is hosted inside the gateway, instead of a separate service. This is a design decision, but it could be hosted as another microservice if you would like. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Public Web Site Gateway (PublicWebSiteGateway.Host) - -This is backend (server side API gateway) for the "Public Web Site" application. - -#### Authentication - -This gateway uses IdentityServer `Bearer` authentication and configured like that: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName` is the API which is being protected, `PublicWebSiteGateway` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "PublicWebSiteGateway" -} -``` - -#### Ocelot Configuration - -Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: - -```json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/blogging/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 62157 - } - ], - "UpstreamPathTemplate": "/api/blogging/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:64897" -} -``` - -See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the Ocelot configuration. - -#### Other - -See the "ABP Configuration Endpoints" and "Swagger" topics inside the "Backend Admin Application Gateway" section which are very similar for this gateway. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Internal Gateway (InternalGateway.Host) - -This gateway is not a BFF. It is designed for inter-microservice communication and is not exposed publicly. - -#### Authentication - -This gateway uses IdentityServer `Bearer` authentication and configured like that: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName` is the API which is being protected, `InternalGateway` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "InternalGateway" -} -``` - -#### Ocelot Configuration - -Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: - -```json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/identity/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 63568 - } - ], - "UpstreamPathTemplate": "/api/identity/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/blogging/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 62157 - } - ], - "UpstreamPathTemplate": "/api/blogging/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:65129" -} -``` - -`ReRoutes` configuration covers all microservices in the system. See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the Ocelot configuration. - -#### Other - -See the "ABP Configuration Endpoints" and "Swagger" topics inside the "Backend Admin Application Gateway" section which are very similar for this gateway. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -## Microservices - -Microservices are standalone HTTP APIs those implement the business of the system in a distributed manner. - -* They are used by applications and other microservices through the gateways and HTTP APIs. -* They can raise or register to events in the system. -* They can communicate to each other via asynchronous messaging. - -### Identity Service (IdentityService.Host) - -This service provides user and role management APIs. - -#### Database - -Shares the same database (MsDemo_Identity) with the AuthServer application. - -#### Identity Module - -This service actually just hosts the ABP Identity package/module. Does not include any API itself. In order to host it, adds the following dependencies: - -* `AbpIdentityHttpApiModule` (*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* package) to provide Identity APIs. -* `AbpIdentityApplicationModule` (*[Volo.Abp.Identity.Application](https://www.nuget.org/packages/Volo.Abp.Identity.Application)* package) to host the implementation of the application and domain layers of the module. -* `AbpIdentityEntityFrameworkCoreModule` (*[Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* package) to use EF Core as database API. - -See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. - -#### Authentication - -This microservice uses IdentityServer `Bearer` authentication and configured like that: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName` is the API which is being protected, `IdentityService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "IdentityService" -} -``` - -#### Swagger - -Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:63568/`, you are redirected to the swagger page to see and test the API. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Blogging Service (BloggingService.Host) - -This service hosts the blogging API. - -#### Database - -It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. It also uses the MsDemo_Identity SQL database for audit logs, permissions and settings. So, there are two connection strings in the `appsettings.json` file: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", - "Blogging": "mongodb://localhost/MsDemo_Blogging" -} -```` - -#### Blogging Module - -This service actually just hosts the ABP Blogging package/module. Does not include any API itself. In order to host it, adds the following dependencies: - -- `BloggingHttpApiModule` (*[Volo.Blogging.HttpApi](https://www.nuget.org/packages/Volo.Blogging.HttpApi)* package) to provide Blogging APIs. -- `BloggingApplicationModule` (*[Volo.Blogging.Application](https://www.nuget.org/packages/Volo.Blogging.Application)* package) to host the implementation of the application and domain layers of the module. -- `BloggingMongoDbModule` (*[Volo.Blogging.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* package) to use MongoDB as the database. - -See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. - -#### Authentication - -This microservice uses IdentityServer `Bearer` authentication and configured like that: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName` is the API which is being protected, `BloggingService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "BloggingService" -} -``` - -#### IdentityServer Client - -This microservice also uses the Identity microservice API through the Internal Gateway, because it needs to query user details (username, email, phone, name and surname) in some cases. So, it is also a client for the IdentityServer and defines a section in the `appsettings.json` file for that: - -````json -"IdentityClients": { - "Default": { - "GrantType": "client_credentials", - "ClientId": "blogging-service-client", - "ClientSecret": "1q2w3e*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService" - } -} -```` - -Since it uses the Internal Gateway, it should also configure the remote endpoint of the gateway: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65129/", - "UseCurrentAccessToken": "false" - } -} -```` - -When you set `UseCurrentAccessToken` to `false`, ABP ignores the current `access_token` in the current `HttpContext` and authenticates to the AuthServer with the credentials defined above. - -Why not using the token of the current user in the current request? Because, the user may not have required permissions on the Identity module, so it can not just pass the current authentication token directly to the Identity service. In addition, some of the blog service APIs are anonymous (not requires authenticated user), so in some cases there is no "current user" in the HTTP request. For these reasons, Blogging service should be defined as a client for the Identity service with its own credentials and permissions. - -If you check the `AbpPermissionGrants` table in the `MsDemo_Identity` database, you can see the related permission for the `blogging-service-client`. - -![microservice-sample-blogservice-permission-in-database](../images/microservice-sample-blogservice-permission-in-database.png) - -#### Swagger - -Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:62157/`, you are redirected to the swagger page to see and test the API. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -### Product Service (ProductService.Host) - -This service hosts the Product Management API. - -#### Database & EF Core Migrations - -It has a separated SQL database, named **MsDemo_ProductManagement**, for the product management module. It uses EF Core as the database provider and has a DbContext named `ProductServiceMigrationDbContext`: - -````csharp -public class ProductServiceMigrationDbContext : AbpDbContext -{ - public ProductServiceMigrationDbContext( - DbContextOptions options - ) : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureProductManagement(); - } -} -```` - -Actual model configuration is done inside the `modelBuilder.ConfigureProductManagement()` extension method. This project maintains the database schema using EF Core migrations. - -Notice that this DbContext is only for database migrations. Product Management module has its own `DbContext` class that is used in the runtime (See `ProductManagementDbContext` class in the ProductManagement.EntityFrameworkCore project). - -There are two connection strings in the `appsettings.json` file: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", - "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True" -} -```` - -`Default` connection strings points to the MsDemo_Identity database that is used for audit logging, permission and setting stores. `ProductManagement` connection string is used by the product module. - -#### Product Module - -This service actually just hosts the Product Management module. Does not include any API itself. In order to host it, adds the following dependencies: - -- `ProductManagementHttpApiModule` to provide product management APIs. -- `ProductManagementApplicationModule` to host the implementation of the application and domain layers of the module. -- `ProductManagementEntityFrameworkCoreModule` to use EF Core as database API. - -See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. See the Product Management module section below for more information about this module. - -#### Authentication - -This microservice uses IdentityServer `Bearer` authentication and configured like that: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName` is the API which is being protected, `ProductService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "ProductService" -} -``` - -#### Swagger - -Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:60244/`, you are redirected to the swagger page to see and test the API. - -#### Dependencies - -- **RabbitMQ** for messaging to other services. -- **Redis** for distributed/shared caching. -- **Elasticsearch** for storing logs. - -## Modules - -ABP provides a strong infrastructure to make modular application development easier by providing services and architecture (see the [module development best practices guide](../Best-Practices/Index.md)). - -This solution demonstrate how to use [prebuilt application modules](../Modules/Index.md) in a distributed architecture. The solution also includes a simple "Product Management" module to show the implementation of a well layered module example. - -### Product Management - -Product Management is a module that consists of several layers and packages/projects: - -![microservice-sample-product-module-in-solution](../images/microservice-sample-product-module-in-solution.png) - -* `ProductManagement.Domain.Shared` contains constants and types shared among all layers. -* `ProductManagement.Domain` contains the domain logic and defines entities, domain services, domain events, business/domain exceptions. -* `ProductManagement.Application.Contracts` contains application service interfaces and DTOs. -* `ProductManagement.Application` contains the implementation of application services. -* `ProductManagement.EntityFrameworkCore` contains DbConext and other EF Core related classes and configuration. -* `ProductManagement.HttpApi` contains API Controllers. -* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. -* `ProductManagement.Web` contains the UI elements (pages, scripts, styles... etc). - -By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers. - -In this solution, Web layer runs in the Backend Admin Application while API layer is hosted by the Product microservice. - -This tutorial will highlight some important aspects of the module. But, it's suggested to see the source code for a better understanding. - -#### Domain Layer - -`Product` is the main [Aggregate Root](../Entities.md) of this module: - -````csharp -public class Product : AuditedAggregateRoot -{ - /// - /// A unique value for this product. - /// ProductManager ensures the uniqueness of it. - /// It can not be changed after creation of the product. - /// - [NotNull] - public string Code { get; private set; } - - [NotNull] - public string Name { get; private set; } - - public float Price { get; private set; } - - public int StockCount { get; private set; } - - //... -} -```` - -All of its properties have private setters which prevents any direct change of the properties from out of the class. Product class ensures its own integrity and validity by its own constructors and methods. - -It has two constructors: - -````csharp -private Product() -{ - //Default constructor is needed for ORMs. -} - -internal Product( - Guid id, - [NotNull] string code, - [NotNull] string name, - float price = 0.0f, - int stockCount = 0) -{ - Check.NotNullOrWhiteSpace(code, nameof(code)); - - if (code.Length >= ProductConsts.MaxCodeLength) - { - throw new ArgumentException( - $"Product code can not be longer than {ProductConsts.MaxCodeLength}" - ); - } - - Id = id; - Code = code; - SetName(Check.NotNullOrWhiteSpace(name, nameof(name))); - SetPrice(price); - SetStockCountInternal(stockCount, triggerEvent: false); -} - -```` - -Default (**parameterless**) constructor is private and is not used in the application code. It is needed because most ORMs requires a parameterless constructor on deserializing entities while getting from the database. - -Second constructor is **internal** that means it can only be used inside the domain layer. This enforces to use the `ProductManager` while creating a new `Product`. Because, `ProductManager` should implement a business rule on a new product creation. This constructor only requires the minimal required arguments to create a new product with some optional arguments. It checks some simple business rules to ensure that the entity is created as a valid product. - -Rest of the class has methods to manipulate properties of the entity. Example: - -````csharp -public Product SetPrice(float price) -{ - if (price < 0.0f) - { - throw new ArgumentException($"{nameof(price)} can not be less than 0.0!"); - } - - Price = price; - return this; -} - -```` - -`SetPrice` method is used to change the price of the product in a safe manner (by checking a validation rule). - -`SetStockCount` is another method that is used to change stock count of a product: - -````csharp -public Product SetStockCount(int stockCount) -{ - return SetStockCountInternal(stockCount); -} - -private Product SetStockCountInternal(int stockCount, bool triggerEvent = true) -{ - if (StockCount < 0) - { - throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!"); - } - - if (StockCount == stockCount) - { - return this; - } - - if (triggerEvent) - { - AddDistributedEvent( - new ProductStockCountChangedEto( - Id, StockCount, stockCount - ) - ); - } - - StockCount = stockCount; - return this; -} - -```` - -This method also triggers a **distributed event** with the `ProductStockCountChangedEto` parameter (Eto is a conventional postfix stands for **E**vent **T**ransfer **O**bject, but not required) to notify listeners that stock count of a product has changed. Any subscriber can receive this event and perform an action based on that knowledge. - -Events are distributed by RabbitMQ for this solution. But ABP is message broker independent by providing necessary abstractions (see the [Event Bus](../Event-Bus.md) document). - -As said before, this module forces to always use the `ProductManager` to create a new `Product`. `ProductManager` is a simple domain service defined as shown: - -````csharp -public class ProductManager : DomainService -{ - private readonly IRepository _productRepository; - - public ProductManager(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task CreateAsync( - [NotNull] string code, - [NotNull] string name, - float price = 0.0f, - int stockCount = 0) - { - var existingProduct = - await _productRepository.FirstOrDefaultAsync(p => p.Code == code); - - if (existingProduct != null) - { - throw new ProductCodeAlreadyExistsException(code); - } - - return await _productRepository.InsertAsync( - new Product( - GuidGenerator.Create(), - code, - name, - price, - stockCount - ) - ); - } -} -```` - -* It checks if given code is used before. Throws `ProductCodeAlreadyExistsException` so. -* If uses the `GuidGenerator` (`IGuidGenerator`) service to create a new `Guid`. -* It inserts the entity to the repository. - -So, with this design, uniqueness of the product code is guaranteed. - -`ProductCodeAlreadyExistsException` is a domain/business exception defined as like below: - -````csharp -public class ProductCodeAlreadyExistsException : BusinessException -{ - public ProductCodeAlreadyExistsException(string productCode) - : base("PM:000001", $"A product with code {productCode} has already exists!") - { - - } -} -```` - -`PM:000001` is a code for the exception type that is sent to the clients, so they can understand the error type. Not implemented for this case, but it is also possible to localize business exceptions. See the [exception handling documentation](../Exception-Handling.md). - -#### Application Layer - -Application layer of this module has two services: - -* `ProductAppService` is mainly used by the Backend Admin Application to manage (create, update, delete...) products. It requires permission to perform any operation. -* `PublicProductAppService` is used by the Public Web Site to show list of products to the visitors. It does not require any permission since most of the visitors are not logged in to the application. - -Notice that; instead of putting two application service into the same project, it might be a better principle to have separated application layers per application. But we unified them for simplicity in this solution. - -As an example, `ProductAppService` has the following method to update a product: - -````csharp -[Authorize(ProductManagementPermissions.Products.Update)] -public async Task UpdateAsync(Guid id, UpdateProductDto input) -{ - var product = await _productRepository.GetAsync(id); - - product.SetName(input.Name); - product.SetPrice(input.Price); - product.SetStockCount(input.StockCount); - - return ObjectMapper.Map(product); -} -```` - -* It defines the required permission (*ProductManagementPermissions.Products.Update* is a constant with value `ProductManagement.Update`) to perform this operation. -* Gets the id of the product and a DTO contains the values to update. -* Gets the related product entity from the repository. -* Uses the related methods (like `SetName`) of the `Product` class to change properties, because they are with private setters and the only way to change a value is to use an entity method. -* Returns an updated `ProductDto` to the client (client may need it for some reason) by using the [ObjectMapper](../Object-To-Object-Mapping.md). - -The implementation may vary based on the requirements. This implementation follows the [best practices offered here](../Best-Practices/Application-Services.md). - -#### Other Layers - -See other layers from the source code. - -## Infrastructure - -### Messaging and RabbitMQ - -Asynchronous Messaging is a key concept in distributed systems. It makes possible to communicate as a loosely coupled manner with fault tolerance. It does not require both sides to be online at the moment of messaging. So, it is a widely used communication pattern in microservice architecture. - -#### Distributed Event Bus - -Distributed Events (Event Bus) is a way of messaging where a service raise/trigger events while other services registers/listens to these events to be notified when an important event occurs. ABP makes distributed events easier to use by providing conventions, services and integrations. - -You have seen that the `Product` class publishing an event using the following code line: - -````csharp -AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount)); -```` - -`ProductStockCountChangedEto` was defined as shown below: - -````csharp -[Serializable] -public class ProductStockCountChangedEto : EtoBase -{ - public Guid Id { get; } - - public int OldCount { get; set; } - - public int CurrentCount { get; set; } - - private ProductStockCountChangedEto() - { - //Default constructor is needed for deserialization. - } - - public ProductStockCountChangedEto(Guid id, int oldCount, int currentCount) - { - Id = id; - OldCount = oldCount; - CurrentCount = currentCount; - } -} -```` - -This object stores necessary information about the event. Another service can easily register to this event by implementing the `IDistributedEventHandler` interface with the generic `ProductStockCountChangedEto` parameter: - -````csharp -public class MyHandler : IDistributedEventHandler -{ - public async Task HandleEventAsync(ProductStockCountChangedEto eventData) - { - var productId = eventData.Id; - //... - } -} -```` - -All the integration and communication are done by the ABP framework when you use the [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) package. If you need to publish events out of an entity, just inject the `IDistributedEventBus` and use the `PublishAsync` method. - -See the [Event Bus](../Event-Bus.md) documentation for more information about the distributed event system. - -#### RabbitMQ Configuration - -In this solution, [RabbitMQ](https://www.rabbitmq.com/) is used for messaging & distributed events. - -[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) package is required to integrate to the RabbitMQ for distributed event system. Then you need to add dependency to the `AbpEventBusRabbitMqModule` for your module. For example, `ProductServiceHostModule` declares this dependency. - -`AbpEventBusRabbitMqModule` gets configuration from the `appsettings.json` by default. For example, the Product Service has such a configuration: - -````json -"RabbitMQ": { - "Connections": { - "Default": { - "HostName": "localhost" - } - }, - "EventBus": { - "ClientName": "MsDemo_ProductService", - "ExchangeName": "MsDemo" - } -} -```` - -### Caching and Redis - -A distributed system obviously needs to a distributed and shared cache, instead of isolated in-memory caches for each service. - -[Redis](https://redis.io/) is used as a distributed cache in this solution. The solution uses Microsoft's standard [Microsoft.Extensions.Caching.Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Redis) package for integration. All applications and services uses Redis cache when you use and configure this package. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more. - -The solution also uses the [Microsoft.AspNetCore.DataProtection.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.StackExchangeRedis) package to share data protection keys between applications and services over Redis cache. - -### Logging, Serilog, Elasticsearch and Kibana - -This solution uses [Serilog](https://serilog.net/) as a logging library. It is a widely used library which has many data source integrations including [Elasticsearch](https://www.elastic.co/products/elasticsearch). - -Logging configurations are done in `Program.cs` files using a code block similar to the given below: - -````csharp -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.WithProperty("Application", "ProductService") - .Enrich.FromLogContext() - .WriteTo.File("Logs/logs.txt") - .WriteTo.Elasticsearch( - new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"])) - { - AutoRegisterTemplate = true, - AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6, - IndexFormat = "msdemo-log-{0:yyyy.MM}" - }) - .CreateLogger(); -```` - -This configures multiple log target: File and Elasticsearch. `Application` property is set to `ProductService` for this example. This is a way of distinguishing the logs of multiple services in a single database. You can then query logs by the `Application` name. - -Elasticsearch URL is read from the `appsettings.json` configuration file: - -````json -"ElasticSearch": { - "Url": "http://localhost:9200" -} -```` - -If you use Kibana, which is a Visualization tool that is well integrated to Elasticsearch, you can see some fancy UI about your logs: - -![microservice-sample-kibana-2](../images/microservice-sample-kibana-2.png) - -*Figure - A dashboard that shows log and error counts by service/application.* - -![microservice-sample-kibana-1](../images/microservice-sample-kibana-1.png) - -*Figure - A list of log entries* - -Kibana URL is `http://localhost:5601/` by default. - -### Audit Logging - -ABP provides automatic audit logging which saves every request in detail (who is the current user, what is the browser/client, what actions performed, which entities changed, even which properties of entities has been updated). See the [audit logging document](../Audit-Logging.md) for details. - -All of the services and applications are configured to write audit logs. Audit logs are saved to the MsDemo_Identity SQL database. So, you can query all audit logs of all applications from a single point. - -An Audit Log record has a `CorrelationId` property that can be used to track a request. When a service calls another service in a single web request, they both save audit logs with the same `CorrelationId`. See the `AbpAuditLogs` table in the database. - -### Multi-Tenancy - -The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data. diff --git a/docs/en/Settings.md b/docs/en/Settings.md deleted file mode 100644 index 61c55f99b4..0000000000 --- a/docs/en/Settings.md +++ /dev/null @@ -1,222 +0,0 @@ -# Settings - -[Configuration system](Configuration.md) is a good way to configure the application on startup. In addition to the configurations, ABP provides another way to set and get some application settings. - -A setting is a name-value pair stored in a dynamic data source, generally in a database. Setting system is extensible and there are pre-built providers for a user, a tenant, global and default. - -## Defining Settings - -A setting must be defined before its use. ABP was designed to be [modular](Module-Development-Basics.md), so different modules can have different settings. A module must create a class derived from the `SettingDefinitionProvider` in order to define its settings. An example setting definition provider is shown below: - -````csharp -public class EmailSettingProvider : SettingDefinitionProvider -{ - public override void Define(ISettingDefinitionContext context) - { - context.Add( - new SettingDefinition("Smtp.Host", "127.0.0.1"), - new SettingDefinition("Smtp.Port", "25"), - new SettingDefinition("Smtp.UserName"), - new SettingDefinition("Smtp.Password", isEncrypted: true), - new SettingDefinition("Smtp.EnableSsl", "false") - ); - } -} -```` - -ABP automatically discovers this class and registers the setting definitions. - -### SettingDefinition - -`SettingDefinition` class has the following properties: - -* **Name**: Unique name of the setting in the application. This is **the only mandatory property**. Used to get/set the value of this setting in the application code (It's a good idea to define a const string for a setting name instead of using a magic string). -* **DefaultValue**: A setting may have a default value. -* **DisplayName**: A localizable string that can be used to show the setting name on the UI. -* **Description**: A localizable string that can be used to show the setting description on the UI. -* **IsVisibleToClients**: A boolean value indicates that whether this setting value is available in the client side or not. Default value is false to prevent accidently publishing an internal critical setting value. -* **IsInherited**: A boolean value indicates that whether this setting value is inherited from other providers or not. Default value is true and fallbacks to the next provider if the setting value was not set for the requested provider (see the setting value providers section for more). -* **IsEncrypted**: A boolean value indicates that whether this setting value should be encrypted on save and decrypted on read. It makes possible to secure the setting value in the database. -* **Providers**: Can be used to restrict providers available for a particular setting (see the setting value providers section for more). -* **Properties**: A name/value collection to set custom properties about this setting those can be used later in the application code. - -### Change Setting Definitions of a Depended Module - -In some cases, you may want to change some properties of a settings defined in some other module that your application/module depends on. A setting definition provider can query and update setting definitions. - -The following example gets a setting defined by the [Volo.Abp.Emailing](Emailing.md) package and changes its properties: - -````csharp -public class MySettingDefinitionProvider : SettingDefinitionProvider -{ - public override void Define(ISettingDefinitionContext context) - { - var smtpHost = context.GetOrNull("Abp.Mailing.Smtp.Host"); - if (smtpHost != null) - { - smtpHost.DefaultValue = "mail.mydomain.com"; - smtpHost.DisplayName = - new LocalizableString( - typeof(MyLocalizationResource), - "SmtpServer_DisplayName" - ); - } - } -} -```` - -> Using constants for the setting names is a good practice and ABP packages do it. `Abp.Mailing.Smtp.Host` setting name is a constant defined by the `EmailSettingNames` class (in the `Volo.Abp.Emailing` namespace). - -## Reading the Setting Values - -### ISettingProvider - -`ISettingProvider` is used to get the value of a setting or get the values of all the settings. Example usages: - -````csharp -public class MyService -{ - private readonly ISettingProvider _settingProvider; - - //Inject ISettingProvider in the constructor - public MyService(ISettingProvider settingProvider) - { - _settingProvider = settingProvider; - } - - public async Task FooAsync() - { - //Get a value as string. - string userName = await _settingProvider.GetOrNullAsync("Smtp.UserName"); - - //Get a bool value and fallback to the default value (false) if not set. - bool enableSsl = await _settingProvider.GetAsync("Smtp.EnableSsl"); - - //Get a bool value and fallback to the provided default value (true) if not set. - bool enableSsl = await _settingProvider.GetAsync( - "Smtp.EnableSsl", defaultValue: true); - - //Get a bool value with the IsTrueAsync shortcut extension method - bool enableSsl = await _settingProvider.IsTrueAsync("Smtp.EnableSsl"); - - //Get an int value or the default value (0) if not set - int port = (await _settingProvider.GetAsync("Smtp.Port")); - - //Get an int value or null if not provided - int? port = (await _settingProvider.GetOrNullAsync("Smtp.Port"))?.To(); - } -} -```` - -> `ISettingProvider` is a very common service and some base classes (like `IApplicationService`) already property-inject it. You can directly use the `SettingProvider` property in such cases. - -### Reading Setting Values on the Client Side - -If a setting is allowed to be visible on the client side, current value of the setting can also be read from the client code. See the following documents to understand how to get the setting values in different UI types; - -* [MVC / Razor Pages](UI/AspNetCore/JavaScript-API/Settings.md) -* [Angular](UI/Angular/Settings.md) -* [Blazor](UI/Blazor/Settings.md) - -## Setting Value Providers - -Setting system is extensible, you can extend it by defining setting value providers to get setting values from any source and based on any condition. - -`ISettingProvider` uses the setting value providers to obtain a setting value. It fallbacks to the next value provider if a value provider can not get the setting value. - -There are 5 pre-built setting value providers registered by the order below: - -* `DefaultValueSettingValueProvider`: Gets the value from the default value of the setting definition, if set (see the SettingDefinition section above). -* `ConfigurationSettingValueProvider`: Gets the value from the [IConfiguration service](Configuration.md). -* `GlobalSettingValueProvider`: Gets the global (system-wide) value for a setting, if set. -* `TenantSettingValueProvider`: Gets the setting value for the current tenant, if set (see the [multi-tenancy](Multi-Tenancy.md) document). -* `UserSettingValueProvider`: Gets the setting value for the current user, if set (see the [current user](CurrentUser.md) document). - -> Setting fallback system works from bottom (user) to top (default). - -Global, Tenant and User setting value providers use the `ISettingStore` to read the value from the data source (see the section below). - -### Setting Values in the Application Configuration - -As mentioned in the previous section, `ConfigurationSettingValueProvider` reads the settings from the `IConfiguration` service, which can read values from the `appsettings.json` by default. So, the easiest way to configure setting values to define them in the `appsettings.json` file. - -For example, you can configure [IEmailSender](Emailing.md) settings as shown below: - -````json -{ - "Settings": { - "Abp.Mailing.DefaultFromAddress": "noreply@mydomain.com", - "Abp.Mailing.DefaultFromDisplayName": "My Application", - "Abp.Mailing.Smtp.Host": "mail.mydomain.com", - "Abp.Mailing.Smtp.Port": "547", - "Abp.Mailing.Smtp.UserName": "myusername", - "Abp.Mailing.Smtp.Password": "mySecretPassW00rd", - "Abp.Mailing.Smtp.EnableSsl": "True" - } -} -```` - -Setting values should be configured under the `Settings` section as like in this example. - -> `IConfiguration` is an .NET Core service and it can read values not only from the `appsettings.json`, but also from the environment, user secrets... etc. See [Microsoft's documentation]( https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/ ) for more. - -### Custom Setting Value Providers - -If you need to extend the setting system, you can define a class derived from the `SettingValueProvider` class. Example: - -````csharp -public class CustomSettingValueProvider : SettingValueProvider -{ - public override string Name => "Custom"; - - public CustomSettingValueProvider(ISettingStore settingStore) - : base(settingStore) - { - } - - public override Task GetOrNullAsync(SettingDefinition setting) - { - /* Return the setting value or null - Use the SettingStore or another data source */ - } -} -```` - -> Alternatively, you can implement the `ISettingValueProvider` interface. Remember to register it to the [dependency injection](Dependency-Injection.md) in this case. - -Every provider should have a unique Name (which is "Custom" here). Built-in providers use the given names: - -* `DefaultValueSettingValueProvider`: "**D**". -* `ConfigurationSettingValueProvider`: "**C**". -* `GlobalSettingValueProvider`: "**G**". -* `TenantSettingValueProvider`: "**T**". -* `UserSettingValueProvider`: "**U**". - -One-letter names were preferred to reduce the data size in the database (provider name is repeated in each row). - -Once you define a custom setting value provider, you need to explicitly register it to the `AbpSettingOptions`: - -````csharp -Configure(options => -{ - options.ValueProviders.Add(); -}); -```` - -This example adds it as the last item, so it will be the first value provider used by the `ISettingProvider`. You could add it to another index in the `options.ValueProviders` list. - -### ISettingStore - -While a setting value provider is free to use any source to get the setting value, the `ISettingStore` service is the default source of the setting values. Global, Tenant and User setting value providers use it. - -## ISettingEncryptionService - -`ISettingEncryptionService` is used to encrypt/decrypt setting values when `IsEncrypted` property of a setting definition was set to `true`. - -You can replace this service in the dependency injection system to customize the encryption/decryption process. Default implementation uses the `StringEncryptionService` which is implemented with the AES algorithm by default (see string [encryption document](String-Encryption.md) for more). - -## Setting Management Module - -The core setting system is pretty independent and doesn't make any assumption about how you manage (change) the setting values. Even the default `ISettingStore` implementation is the `NullSettingStore` which returns null for all setting values. - -The setting management module completes it (and implements `ISettingStore`) by managing setting values in a database. See the [Setting Management Module document](Modules/Setting-Management.md) for more. diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md deleted file mode 100644 index f441c64ae1..0000000000 --- a/docs/en/SignalR-Integration.md +++ /dev/null @@ -1,240 +0,0 @@ -# SignalR Integration - -> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides SignalR integration packages those simplify the integration and usage. - -## Installation - -### Server Side - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -#### Using the ABP CLI - -Open a command line window in the folder of your project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.AspNetCore.SignalR -``` - -> You typically want to add this package to the web or API layer of your application, depending on your architecture. - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.AspNetCore.SignalR). - -#### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: - - ``` - Install-Package Volo.Abp.AspNetCore.SignalR - ``` - - Or use the Visual Studio NuGet package management UI to install it. - -2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -``` - -> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. - -### Client Side - -Client side installation depends on your UI framework / client type. - -#### ASP.NET Core MVC / Razor Pages UI - -Run the following command in the root folder of your web project: - -```bash -yarn add @abp/signalr -``` - -> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. - -This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: - -```json -{ - ... - "dependencies": { - ... - "@abp/signalr": "~2.7.0" - } -} -``` - -Run the following [ABP CLI](CLI.md) command in the root folder of your web project: - -```bash -abp install-libs -``` - -This will copy the SignalR JavaScript files into your project: - -![signal-js-file](images/signal-js-file.png) - -Finally, add the following code to your page/view to include the `signalr.js` file - -```xml -@section scripts { - -} -``` - -It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. - -> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. - -That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. - -#### Other UI Frameworks / Clients - -Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. - -## The ABP Framework Integration - -This section covers the additional benefits when you use the ABP Framework integration packages. - -### Hub Route & Mapping - -ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. - -Example: - -```csharp -public class MessagingHub : Hub -{ - //... -} -``` - -The hub route will be `/signalr-hubs/messaging` for the `MessagingHub`: - -- Adding a standard `/signalr-hubs/` prefix -- Continue with the **kebab-case** hub name, without the `Hub` suffix. - -If you want to specify the route, you can use the `HubRoute` attribute: - -```csharp -[HubRoute("/my-messaging-hub")] -public class MessagingHub : Hub -{ - //... -} -``` - -### AbpHub Base Classes - -Instead of the standard `Hub` and `Hub` classes, you can inherit from the `AbpHub` or `AbpHub` which have useful base properties like `CurrentUser`. - -Example: - -```csharp -public class MessagingHub : AbpHub -{ - public async Task SendMessage(string targetUserName, string message) - { - var currentUserName = CurrentUser.UserName; //Access to the current user info - var txt = L["MyText"]; //Localization - } -} -``` - -> While you could inject the same properties into your hub constructor, this way simplifies your hub class. - -### Manual Registration / Mapping - -ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: - -```csharp -context.Services.AddTransient(); -``` - -When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. - -For manual mapping, you have two options: - -1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: - -```csharp -Configure(options => -{ - options.Hubs.Add( - new HubConfig( - typeof(MessagingHub), //Hub type - "/my-messaging/route", //Hub route (URL) - hubOptions => - { - //Additional options - hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - } - ) - ); -}); -``` - -This is a good way to provide additional SignalR options. - -If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: - -```csharp -Configure(options => -{ - options.Hubs.AddOrUpdate( - typeof(MessagingHub), //Hub type - config => //Additional configuration - { - config.RoutePattern = "/my-messaging-hub"; //override the default route - config.ConfigureActions.Add(hubOptions => - { - //Additional options - hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - }); - } - ); -}); -``` - -This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). - -2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). - -```csharp -app.UseConfiguredEndpoints(endpoints => -{ - endpoints.MapHub("/my-messaging-hub", options => - { - options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - }); -}); -``` - -### UserIdProvider - -ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. - -## Example Application - -See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. - -![signalr-demo-chat](images/signalr-demo-chat.png) - -## Remarks - -ABP Framework doesn't change the SignalR. It works in your ABP Framework based application just like any other ASP.NET Core application. - -Refer to the Microsoft's documentation to [host and scale](https://docs.microsoft.com/en-us/aspnet/core/signalr/scale) your application, integrate to [Azure](https://docs.microsoft.com/en-us/aspnet/core/signalr/publish-to-azure-web-app) or [Redis backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)... etc. - -## See Also - -- [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) -- [Real-Time Messaging In A Distributed Architecture Using ABP, SignalR & RabbitMQ](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) diff --git a/docs/en/Specifications.md b/docs/en/Specifications.md deleted file mode 100644 index 2ef9c4496e..0000000000 --- a/docs/en/Specifications.md +++ /dev/null @@ -1,257 +0,0 @@ -# Specifications - -Specification Pattern is used to define **named, reusable, combinable and testable filters** for entities and other business objects. - -> A Specification is a part of the Domain Layer. - -## Installation - -> This package is **already installed** when you use the startup templates. So, most of the times you don't need to manually install it. - -Install the [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) package to your project. You can use the [ABP CLI](CLI.md) *add-package* command in a command line terminal when the current folder is the root folder of your project (`.csproj`): - -````bash -abp add-package Volo.Abp.Specifications -```` - -## Defining the Specifications - -Assume that you've a Customer entity as defined below: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace MyProject -{ - public class Customer : AggregateRoot - { - public string Name { get; set; } - - public byte Age { get; set; } - - public long Balance { get; set; } - - public string Location { get; set; } - } -} -```` - -You can create a new Specification class derived from the `Specification`. - -**Example: A specification to select the customers with 18+ age:** - -````csharp -using System; -using System.Linq.Expressions; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class Age18PlusCustomerSpecification : Specification - { - public override Expression> ToExpression() - { - return c => c.Age >= 18; - } - } -} -```` - -You simply define a lambda [Expression](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) to define a specification. - -> Instead, you can directly implement the `ISpecification` interface, but the `Specification` base class much simplifies it. - -## Using the Specifications - -There are two common use cases of the specifications. - -### IsSatisfiedBy - -`IsSatisfiedBy` method can be used to check if a single object satisfies the specification. - -**Example: Throw exception if the customer doesn't satisfy the age specification** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class CustomerService : ITransientDependency - { - public async Task BuyAlcohol(Customer customer) - { - if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) - { - throw new Exception( - "This customer doesn't satisfy the Age specification!" - ); - } - - //TODO... - } - } -} -```` - -### ToExpression & Repositories - -`ToExpression()` method can be used to use the specification as Expression. In this way, you can use a specification to **filter entities while querying from the database**. - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace MyProject -{ - public class CustomerManager : DomainService, ITransientDependency - { - private readonly IRepository _customerRepository; - - public CustomerManager(IRepository customerRepository) - { - _customerRepository = customerRepository; - } - - public async Task> GetCustomersCanBuyAlcohol() - { - var queryable = await _customerRepository.GetQueryableAsync(); - var query = queryable.Where( - new Age18PlusCustomerSpecification().ToExpression() - ); - - return await AsyncExecuter.ToListAsync(query); - } - } -} -```` - -> Specifications are correctly translated to SQL/Database queries and executed efficiently in the DBMS side. While it is not related to the Specifications, see the [Repositories](Repositories.md) document if you want to know more about the `AsyncExecuter`. - -Actually, using the `ToExpression()` method is not necessary since the specifications are automatically casted to Expressions. This would also work: - -````csharp -var queryable = await _customerRepository.GetQueryableAsync(); -var query = queryable.Where( - new Age18PlusCustomerSpecification() -); -```` - -## Composing the Specifications - -One powerful feature of the specifications is that they are composable with `And`, `Or`, `Not` and `AndNot` extension methods. - -Assume that you have another specification as defined below: - -```csharp -using System; -using System.Linq.Expressions; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class PremiumCustomerSpecification : Specification - { - public override Expression> ToExpression() - { - return (customer) => (customer.Balance >= 100000); - } - } -} -``` - -You can combine the `PremiumCustomerSpecification` with the `Age18PlusCustomerSpecification` to query the count of premium adult customers as shown below: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class CustomerManager : DomainService, ITransientDependency - { - private readonly IRepository _customerRepository; - - public CustomerManager(IRepository customerRepository) - { - _customerRepository = customerRepository; - } - - public async Task GetAdultPremiumCustomerCountAsync() - { - return await _customerRepository.CountAsync( - new Age18PlusCustomerSpecification() - .And(new PremiumCustomerSpecification()).ToExpression() - ); - } - } -} -```` - -If you want to make this combination another reusable specification, you can create such a combination specification class deriving from the `AndSpecification`: - -````csharp -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class AdultPremiumCustomerSpecification : AndSpecification - { - public AdultPremiumCustomerSpecification() - : base(new Age18PlusCustomerSpecification(), - new PremiumCustomerSpecification()) - { - } - } -} -```` - -Now, you can re-write the `GetAdultPremiumCustomerCountAsync` method as shown below: - -````csharp -public async Task GetAdultPremiumCustomerCountAsync() -{ - return await _customerRepository.CountAsync( - new AdultPremiumCustomerSpecification() - ); -} -```` - -> You see the power of the specifications with these samples. If you change the `PremiumCustomerSpecification` later, say change the balance from `100.000` to `200.000`, all the queries and combined specifications will be effected by the change. This is a good way to reduce code duplication! - -## Discussions - -While the specification pattern is older than C# lambda expressions, it's generally compared to expressions. Some developers may think it's not needed anymore and we can directly pass expressions to a repository or to a domain service as shown below: - -````csharp -var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18); -```` - -Since ABP's [Repository](Repositories.md) supports Expressions, this is a completely valid use. You don't have to define or use any specification in your application and you can go with expressions. - -So, what's the point of a specification? Why and when should we consider to use them? - -### When To Use? - -Some benefits of using specifications: - -- **Reusable**: Imagine that you need the Premium Customer filter in many places in your code base. If you go with expressions and do not create a specification, what happens if you later change the "Premium Customer" definition? Say you want to change the minimum balance from $100,000 to $250,000 and add another condition to be a customer older than 3 years. If you'd used a specification, you just change a single class. If you repeated (copy/pasted) the same expression everywhere, you need to change all of them. -- **Composable**: You can combine multiple specifications to create new specifications. This is another type of reusability. -- **Named**: `PremiumCustomerSpecification` better explains the intent rather than a complex expression. So, if you have an expression that is meaningful in your business, consider using specifications. -- **Testable**: A specification is a separately (and easily) testable object. - -### When To Not Use? - -- **Non business expressions**: Do not use specifications for non business-related expressions and operations. -- **Reporting**: If you are just creating a report, do not create specifications, but directly use `IQueryable` & LINQ expressions. You can even use plain SQL, views or another tool for reporting. DDD does not necessarily care about reporting, so the way you query the underlying data store can be important from a performance perspective. \ No newline at end of file diff --git a/docs/en/Startup-Templates/Application-Single-Layer.md b/docs/en/Startup-Templates/Application-Single-Layer.md deleted file mode 100644 index 1b2e28be64..0000000000 --- a/docs/en/Startup-Templates/Application-Single-Layer.md +++ /dev/null @@ -1,110 +0,0 @@ -# Application (Single Layer) Startup Template - -## Introduction - -This template provides a simple solution structure with a single project. This document explains that solution structure in details. - -### The Difference Between the Application Startup Templates - -ABP's [Application Startup Template](Application.md) provides a well-organized and layered solution to create maintainable business applications based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. However, some developers find this template a little bit complex for simple and short-term applications. The single-layer application template has been created to provide a simpler development model for such applications. This template has the same functionality, features and modules on runtime with the [Application Startup Template](Application.md) but the development model is minimal and everything is in a single project (`.csproj`). - -## How to Start with It? - -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command for this startup template from the [Get Started](https://abp.io/get-started) page. In this section, we will use the ABP CLI. - -Firstly, install the ABP CLI if you haven't installed it before: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Then, use the `abp new` command in an empty folder to create a new solution: - -```bash -abp new Acme.BookStore -t app-nolayers -``` - -* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single-level, two-level or three-level naming. -* In this example, the `-t` (or `--template`) option specifies the template name. - -### Specify the UI Framework - -This template provides multiple UI frameworks: - -* `mvc`: ASP.NET Core MVC UI with Razor Pages (default) -* `blazor`: Blazor UI -* `blazor-server`: Blazor Server UI -* `angular`: Angular UI -* `none`: Without UI (for HTTP API development) - -Use the `-u` (or `--ui`) option to specify the UI framework while creating the solution: - -```bash -abp new Acme.BookStore -t app-nolayers -u angular -``` - -This example specifies the UI type (the `-u` option) as `angular`. You can also specify `mvc`, `blazor`, `blazor-server` or `none` for the UI type. - -### Specify the Database Provider - -This template supports the following database providers: - -- `ef`: Entity Framework Core (default) -- `mongodb`: MongoDB - -Use the `-d` (or `--database-provider`) option to specify the database provider while creating the solution: - -```bash -abp new Acme.BookStore -t app-nolayers -d mongodb -``` - -## Solution Structure - -If you don't specify any additional options while creating an `app-nolayers` template, you will have a solution as shown below: - -![](../images/bookstore-single-layer-solution-structure.png) - -In the next sections, we will explain the structure based on this example. Your startup solution can be slightly different based on your preferences. - -### Folder Structure - -Since this template provides a single-project solution, we've separated concerns into folders instead of projects. You can see the pre-defined folders as shown below: - -![](../images/single-layer-folder-structure.png) - -* Define your database mappings (for [EF Core](../Entity-Framework-Core.md) or [MongoDB](../MongoDB.md)) and [repositories](../Repositories.md) in the `Data` folder. -* Define your [entities](../Entities.md) in the `Entities` folder. -* Define your UI localization keys/values in the `Localization` folder. -* Define your UI menu items in the `Menus` folder. -* Define your [object-to-object mapping](../Object-To-Object-Mapping.md) classes in the `ObjectMapping` folder. -* Define your UI pages (Razor Pages) in the `Pages` folder (create `Controllers` and `Views` folder yourself if you prefer the MVC pattern). -* Define your [application services](../Application-Services.md) in the `Services` folder. - -### How to Run? - -Before running the application, you need to create the database and seed the initial data. To do that, you can run the following command in the directory of your project (in the same folder of the `.csproj` file): - -```bash -dotnet run --migrate-database -``` - -This command will create the database and seed the initial data for you. Then you can run the application with any IDE that supports .NET or by running the `dotnet run` command in the directory of your project. The default username is `admin` and the password is `1q2w3E*`. - -> While creating a database & applying migrations seem only necessary for relational databases, you should run this command even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. - -### The Angular UI - -If you choose `Angular` as the UI framework, the solution will be separated into two folders: - -* An `angular` folder that contains the Angular UI application, the client-side code. -* An `aspnet-core` folder that contains the ASP.NET Core solution (a single project), the server-side code. - -The server-side is similar to the solution described in the *Solution Structure* section above. This project serves the API, so the Angular application can consume it. - -The client-side application consumes the HTTP APIs as mentioned. You can see the folder structure of the Angular project shown below: - -![](../images/single-layer-angular-folder-structure.png) - - ## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/app-template) \ No newline at end of file diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md deleted file mode 100644 index fb5a1f1c07..0000000000 --- a/docs/en/Startup-Templates/Application.md +++ /dev/null @@ -1,464 +0,0 @@ -# Application Startup Template - -## Introduction - -This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. - -This document explains **the solution structure** and projects in details. If you want to start quickly, follow the guides below: - -* [The getting started document](../Getting-Started.md) explains how to create a new application in a few minutes. -* [The application development tutorial](../Tutorials/Part-1) explains step by step application development. - -## How to Start With? - -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. - -First, install the ABP CLI if you haven't installed it before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Then use the `abp new` command in an empty folder to create a new solution: - -````bash -abp new Acme.BookStore -t app -```` - -* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single-level, two-level or three-level naming. -* This example specified the template name (`-t` or `--template` option). However, `app` is already the default template if you didn't specify it. - -### Specify the UI Framework - -This template provides multiple UI frameworks: - -* `mvc`: ASP.NET Core MVC UI with Razor Pages (default) -* `blazor`: Blazor UI -* `blazor-server`: Blazor Server UI -* `angular`: Angular UI - -Use the `-u` or `--ui` option to specify the UI framework: - -````bash -abp new Acme.BookStore -u angular -```` - -### Specify the Database Provider - -This template supports the following database providers: - -- `ef`: Entity Framework Core (default) -- `mongodb`: MongoDB - -Use `-d` (or `--database-provider`) option to specify the database provider: - -````bash -abp new Acme.BookStore -d mongodb -```` - -### Specify the Mobile Application Framework - -This template supports the following mobile application frameworks: - -- `react-native`: React Native - -Use the `-m` (or `--mobile`) option to specify the mobile application framework: - -````bash -abp new Acme.BookStore -m react-native -```` - -If not specified, no mobile application will be created. - -## Solution Structure - -Based on the options you've specified, you will get a slightly different solution structure. - -### Default Structure - -If you don't specify any additional options, you will have a solution as shown below: - -![bookstore-rider-solution-v6](../images/solution-structure-solution-explorer-rider.png) - -Projects are organized in `src` and `test` folders. `src` folder contains the actual application which is layered based on [DDD](../Domain-Driven-Design.md) principles as mentioned before. - -The diagram below shows the layers & project dependencies of the application: - -![layered-project-dependencies](../images/layered-project-dependencies.png) - -Each section below will explain the related project & its dependencies. - -#### .Domain.Shared Project - -This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. - -A `BookType` enum and a `BookConsts` class (which may have some constant fields for the `Book` entity, like `MaxNameLength`) are good candidates for this project. - -* This project has no dependency on other projects in the solution. All other projects depend on this one directly or indirectly. - -#### .Domain Project - -This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value objects](../Value-Objects.md), [repository interfaces](../Repositories.md) and other domain objects. - -A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. - -* Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. - -#### .Application.Contracts Project - -This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It exists to separate the interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. - -An `IBookAppService` interface and a `BookCreationDto` class are good candidates for this project. - -* Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. - -#### .Application Project - -This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. - -A `BookAppService` class is a good candidate for this project. - -* Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. -* Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. - -#### .EntityFrameworkCore Project - -This is the integration project for the EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. - -* Depends on the `.Domain` project to be able to reference to entities and repository interfaces. - -> This project is available only if you are using EF Core as the database provider. If you select another database provider, its name will be different. - -#### .DbMigrator Project - -This is a console application that simplifies the execution of database migrations on development and production environments. When you run this application, it: - -* Creates the database if necessary. -* Applies the pending database migrations. -* Seeds initial data if needed. - -> This project has its own `appsettings.json` file. So, if you want to change the database connection string, remember to change this file too. - -Especially, seeding initial data is important at this point. ABP has a modular data seed infrastructure. See [its documentation](../Data-Seeding.md) for more about the data seeding. - -While creating database & applying migrations seem only necessary for relational databases, this project comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. - -* Depends on the `.EntityFrameworkCore` project (for EF Core) since it needs to access to the migrations. -* Depends on the `.Application.Contracts` project to be able to access permission definitions, because the initial data seeder grants all permissions to the admin role by default. - -#### .HttpApi Project - -This project is used to define your API Controllers. - -Most of the time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../API/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. - -* Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. - -#### .HttpApi.Client Project - -This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other types of applications, they can still use your APIs, either manually or using a tool in their own platform) - -Most of the time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature. - -`.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. - -* Depends on the `.Application.Contracts` project to be able to share the same application service interfaces and DTOs with the remote service. - -> You can delete this project & dependencies if you don't need to create C# client proxies for your APIs. - -#### .Web Project - -This project contains the User Interface (UI) of the application if you are using ASP.NET Core MVC UI. It contains Razor pages, JavaScript files, CSS files, images and so on... - -This project contains the main `appsettings.json` file that contains the connection string and other configurations of the application. - -* Depends on the `.HttpApi` project since the UI layer needs to use APIs and the application service interfaces of the solution. - -> If you check the source code of the `.Web.csproj` file, you will see the references to the `.Application` and the `.EntityFrameworkCore` projects. -> -> These references are actually not needed while coding your UI layer, because the UI layer normally doesn't depend on the EF Core or the Application layer's implementation. These startup templates are ready for tiered deployment, where the API layer is hosted on a separate server than the UI layer. -> -> However, if you don't choose the `--tiered` option, these references will be in the .Web project to be able to host the Web, API and application layers in a single application endpoint. -> -> This gives you the ability to use domain entities & repositories in your presentation layer. However, this is considered as a bad practice according to DDD. - -#### Test Projects - -The solution has multiple test projects, one for each layer: - -* `.Domain.Tests` is used to test the domain layer. -* `.Application.Tests` is used to test the application layer. -* `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. -* `.Web.Tests` is used to test the UI (if you are using ASP.NET Core MVC UI). -* `.TestBase` is a base (shared) project for all tests. - -In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a .NET application. - -Test projects are prepared for integration testing; - -* It is fully integrated into the ABP framework and all services in your application. -* It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library. -* Authorization is disabled, so any application service can be easily used in tests. - -You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all the initialization processes). - -#### How to Run? - -Set `.Web` as the startup project and run the application. The default username is `admin` and the password is `1q2w3E*`. - -See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) for more information. - -### Tiered Structure - -If you have selected the ASP.NET Core UI and specified the `--tiered` option, the solution created will be a tiered solution. The purpose of the tiered structure is to be able to **deploy Web applications and HTTP API to different servers**: - -![bookstore-visual-studio-solution-v3](../images/tiered-solution-servers.png) - -* Browser runs your UI by executing HTML, CSS & JavaScript. -* Web servers host static UI files (CSS, JavaScript, image... etc.) & dynamic components (e.g. Razor pages). It performs HTTP requests to the API server to execute the business logic of the application. -* The API Server hosts the HTTP APIs which then use the application & domain layers of the application to perform the business logic. -* Finally, database server hosts your database. - -So, the resulting solution allows a 4-tiered deployment, by comparing to 3-tiered deployment of the default structure explained before. - -> Unless you actually need such a 4-tiered deployment, it's suggested to go with the default structure which is simpler to develop, deploy and maintain. - -The solution structure is shown below: - -![bookstore-rider-solution-v6](../images/bookstore-rider-solution-tiered.png) - -As different from the default structure, two new projects come into play: `.AuthServer` & `.HttpApi.Host`. - -#### .AuthServer Project - -This project is used as an authentication server for other projects. `.Web` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the AuthServer. Then uses the access token to call the HTTP API server. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. - -![tiered-solution-applications](../images/tiered-solution-applications-authserver.png) - -ABP uses the [OpenIddict Module](../Modules/OpenIddict.md) that uses the open-source [OpenIddict-core](https://github.com/openiddict/openiddict-core) library for the authentication between applications. See [OpenIddict documentation](https://documentation.openiddict.com/) for details about the OpenIddict and OpenID Connect protocol. - -It has its own `appsettings.json` that contains database connection and other configurations. - -#### .HttpApi.Host Project - -This project is an application that hosts the API of the solution. It has its own `appsettings.json` that contains database connection and other configurations. - -#### .Web Project - -Just like the default structure, this project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... - -This project contains an `appsettings.json` file, but this time it does not have a connection string because it never connects to the database. Instead, it mainly contains the endpoint of the remote API server and the authentication server. - -#### Pre-requirements - -* [Redis](https://redis.io/): The applications use Redis as a distributed cache. So, you need to have Redis installed & running. - -#### How to Run? - -You should run the application with the given order: - -* First, run the `.AuthServer` since other applications depend on it. -* Then run the `.HttpApi.Host` since it is used by the `.Web` application. -* Finally, you can run the `.Web` project and login to the application (using `admin` as the username and `1q2w3E*` as the password). - -### Blazor UI -If you choose `Blazor` as the UI Framework (using the `-u blazor` or `-u blazor-server` option), the solution will have a project named `.Blazor`. This project contains the Blazor UI application. According to your choice, it will be a Blazor WebAssembly or Blazor Server application. If Blazor WebAssembly is selected, the solution will also have a `.HttpApi.Host`. This project is an ASP.NET Core application that hosts the backend application for the Blazor single page application. - -#### .Blazor Project (Server) -The Blazor Server project is similar to the ASP.NET Core MVC project. It replaces `.Web` project with `.Blazor` in the solution structure above. It has the same folder structure and the same application flow. Since it's an ASP.NET Core application, it can contain **.cshtml** files and **.razor** components at the same time. If routing matches a razor component, the Blazor UI will be used. Otherwise, the request will be handled by the MVC framework. - -![abp solution structure blazor server](../images/layered-project-dependencies-blazor-server.png) - -#### .Blazor Project (WebAssembly) -The Blazor WebAssembly project is a single page application that runs on the browser. You'll see it as `.Blazor` project in the solution. It uses the `.HttpApi.Host` project to communicate with the backend. It can't be used without the backend application. It contains only **.razor** components. It's a pure client-side application. It doesn't have any server-side code. Everything in this layer will be for the client side. - -![abp solution structure blazor wasm](../images/layered-project-dependencies-blazor-wasm.png) - -### Angular UI - -If you choose `Angular` as the UI framework (using the `-u angular` option), the solution is being separated into two folders: - -* `angular` folder contains the Angular UI application, the client-side code. -* `aspnet-core` folder contains the ASP.NET Core solution, the server-side code. - -The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the `Angular` application consumes it. - -Angular application folder structure looks like below: - -![angular-folder-structure](../images/angular-folder-structure.png) - - -Each of ABP Commercial modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: - -```bash -yarn list --pattern abp -``` - -Angular application module structure: - -![Angular template structure diagram](../images/angular-template-structure-diagram.png) - -#### AppModule - -`AppModule` is the root module of the application. Some of the ABP modules and some essential modules are imported to `AppModule`. - -ABP Config modules have also been imported to `AppModule` for initial requirements of the lazy-loadable ABP modules. - -#### AppRoutingModule - -There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. - -> Paths of ABP Modules should not be changed. - -You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages. - -```js -{ - path: 'dashboard', - loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), - canActivate: [authGuard, permissionGuard], - data: { - routes: { - name: 'ProjectName::Menu:Dashboard', - order: 2, - iconClass: 'fa fa-dashboard', - requiredPolicy: 'ProjectName.Dashboard.Host' - } as ABP.Route - } -} -``` -In the above example; -* If the user is not logged in, authGuard blocks access and redirects to the login page. -* permissionGuard checks the user's permission with the `requiredPolicy` property of the `routes` object. If the user is not authorized to access the page, the 403 page appears. -* The `name` property of `routes` is the menu link label. A localization key can be defined. -* The `iconClass` property of the `routes` object is the menu link icon class. -* The `requiredPolicy` property of the `routes` object is the required policy key to access the page. - -After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. - -#### Shared Module - -The modules that may be required for all modules have been imported to the `SharedModule`. You should import `SharedModule` to all modules. - -See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. - -#### Environments - -The files under the `src/environments` folder have the essential configuration of the application. - -#### Home Module - -Home module is an example lazy-loadable module that loads on the root address of the application. - -#### Styles - -The required style files are added to the `styles` array in `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time. - -#### Testing - -You should create your tests in the same folder as the file you want to test. - -See the [testing document](https://angular.io/guide/testing). - -#### Depended Packages - -* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library. -* [NGXS](https://www.ngxs.io/) is used as state management library. -* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC). -* [Chart.js](https://www.chartjs.org/) is used to create widgets. -* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms. - -### React Native - -If the `-m react-native` option is specified in the new project command, the solution includes the [React Native](https://reactnative.dev/) application in the `react-native` folder. - -The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the React Native application consumes it. - -The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. - -React Native application folder structure as like below: - -![react-native-folder-structure](../images/react-native-folder-structure.png) - -* `App.js` is the bootstrap component of the application. -* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations are defined in this file. -* [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder. -* [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the `src/hocs` folder. -* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in `src/hooks`. -* [Axios interceptors](https://github.com/axios/axios#interceptors) are created in the `src/interceptors` folder. -* Utility functions are exported from `src/utils` folder. - -#### Components - -Components that can be used on all screens are created in the `src/components` folder. All components have been created as a function that is able to use [hooks](https://reactjs.org/docs/hooks-intro.html). - -#### Screens - -![react-native-navigation-structure](../images/react-native-navigation-structure.png) - -Screens are created by creating folders that separate their names in the `src/screens` folder. Certain parts of some screens can be split into components. - -Each screen is used in a navigator in the `src/navigators` folder. - -#### Navigation - -[React Navigation](https://reactnavigation.org/) is used as a navigation library. Navigators are created in the `src/navigators`. A [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) navigator and several [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) navigators have been created in this folder. See the [above diagram](#screens) for the navigation structure. - -#### State Management - -[Redux](https://redux.js.org/) is used as a state management library. [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. - -Actions, reducers, sagas and selectors are created in the `src/store` folder. Store folder is as below: - -![react-native-store-folder](../images/react-native-store-folder.png) - -* [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file. -* [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store. -* [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store. -* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder. -* [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder. - -#### APIs - -[Axios](https://github.com/axios/axios) is used as an HTTP client library. An Axios instance has exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls. - -#### Theming - -[Native Base](https://nativebase.io/) is used as UI components library. Native Base components can customize easily. See the [Native Base customize](https://docs.nativebase.io/customizing-components) documentation. We followed the same way. - -* Native Base theme variables are in the `src/theme/variables` folder. -* Native Base component styles are in the `src/theme/components` folder. These files have been generated with Native Base's `ejectTheme` script. -* Styles of components override with the files under the `src/theme/overrides` folder. - -#### Testing - -Unit tests will be created. - -See the [Testing Overview](https://reactjs.org/docs/testing.html) document. - -#### Depended Libraries - -* [Native Base](https://nativebase.io/) is used as UI components library. -* [React Navigation](https://reactnavigation.org/) is used as navigation library. -* [Axios](https://github.com/axios/axios) is used as an HTTP client library. -* [Redux](https://redux.js.org/) is used as state management library. -* [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. -* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes. -* [Redux Persist](https://github.com/rt2zz/redux-persist) is used as state persistence. -* [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors. -* [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library. -* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) library allows loading fonts easily. -* [Formik](https://github.com/jaredpalmer/formik) is used to build forms. -* [Yup](https://github.com/jquense/yup) is used for form validations. - -## Social / External Logins - -If you want to configure social/external logins for your application, please follow the [Social/External Logins](../Authentication/Social-External-Logins.md) document. - -## What's Next? - -- [The getting started document](../Getting-Started.md) explains how to create a new application in a few minutes. -- [The application development tutorial](../Tutorials/Part-1.md) explains step by step application development. - -## See Also -* [Video tutorial](https://abp.io/video-courses/essentials/app-template) diff --git a/docs/en/Startup-Templates/Console.md b/docs/en/Startup-Templates/Console.md deleted file mode 100644 index b002eb3e5e..0000000000 --- a/docs/en/Startup-Templates/Console.md +++ /dev/null @@ -1,27 +0,0 @@ -# Console Application Startup Template - -This template is used to create a minimalist console application project. - -## How to Start With? - -First, install the [ABP CLI](../CLI.md) if you haven't installed before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Then use the `abp new` command in an empty folder to create a new solution: - -````bash -abp new Acme.MyConsoleApp -t console -```` - -`Acme.MyConsoleApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. - -## Solution Structure - -After you use the above command to create a solution, you will have a solution like shown below: - -![basic-console-application-solution](../images/basic-console-application-solution.png) - -* `HelloWorldService` is a sample service that implements the `ITransientDependency` interface to register this service to the [dependency injection](../Dependency-Injection.md) system. \ No newline at end of file diff --git a/docs/en/Startup-Templates/Index.md b/docs/en/Startup-Templates/Index.md deleted file mode 100644 index e30f2729e2..0000000000 --- a/docs/en/Startup-Templates/Index.md +++ /dev/null @@ -1,10 +0,0 @@ -# Startup Templates - -While you can start with an empty project and add needed packages manually, startup templates make easy and comfortable to start a new solution with the ABP framework. Click the name from the list below to see the documentation of the related startup template: - -* [**`app`**](Application.md): Application template. -* [**`app-nolayers`**](Application-Single-Layer.md): Application (single layer) template. -* [**`module`**](Module.md): Module/service template. -* [**`console`**](Console.md): Console template. -* [**`WPF`**](WPF.md): WPF template. -* [**`MAUI`**](MAUI.md): MAUI template. diff --git a/docs/en/Startup-Templates/MAUI.md b/docs/en/Startup-Templates/MAUI.md deleted file mode 100644 index ab171ddef1..0000000000 --- a/docs/en/Startup-Templates/MAUI.md +++ /dev/null @@ -1,27 +0,0 @@ -# MAUI Application Startup Template - -This template is used to create a minimalist MAUI application project. - -## How to Start With? - -First, install the [ABP CLI](../CLI.md) if you haven't installed before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Then use the `abp new` command in an empty folder to create a new solution: - -````bash -abp new Acme.MyMauiApp -t maui -```` - -`Acme.MyMauiApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. - -## Solution Structure - -After you use the above command to create a solution, you will have a solution like shown below: - -![basic-maui-application-solution](../images/basic-maui-application-solution.png) - -* `HelloWorldService` is a sample service that implements the `ITransientDependency` interface to register this service to the [dependency injection](../Dependency-Injection.md) system. \ No newline at end of file diff --git a/docs/en/Startup-Templates/Module.md b/docs/en/Startup-Templates/Module.md deleted file mode 100644 index aa33ac64df..0000000000 --- a/docs/en/Startup-Templates/Module.md +++ /dev/null @@ -1,250 +0,0 @@ -# Module Startup Template - -This template can be used to create a **reusable [application module](../Modules/Index.md)** based on the [module development best practices & conventions](../Best-Practices/Index.md). It is also suitable for creating **microservices** (with or without UI). - -## How to Start With? - -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. - -First, install the ABP CLI if you haven't installed before: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Then use the `abp new` command in an empty folder to create a new solution: - -```bash -abp new Acme.IssueManagement -t module -``` - -- `Acme.IssueManagement` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. - -### Without User Interface - -The template comes with MVC, Blazor & Angular user interfaces by default. You can use `--no-ui` option to not include any of these UI layers. - -````bash -abp new Acme.IssueManagement -t module --no-ui -```` - -## Solution Structure - -Based on the options you've specified, you will get a slightly different solution structure. If you don't specify any option, you will have a solution like shown below: - -![issuemanagement-module-solution](../images/issuemanagement-module-solution.png) - -Projects are organized as `src`, `test` and `host` folders: - -* `src` folder contains the actual module which is layered based on [DDD](../Domain-Driven-Design.md) principles. -* `test` folder contains unit & integration tests. -* `host` folder contains applications with different configurations to demonstrate how to host the module in an application. These are not a part of the module, but useful on development. - -The diagram below shows the layers & project dependencies of the module: - -![layered-project-dependencies-module](../images/layered-project-dependencies-module.png) - -Each section below will explain the related project & its dependencies. - -### .Domain.Shared Project - -This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. - -An `IssueType` enum and an `IssueConsts` class (which may have some constant fields for the `Issue` entity, like `MaxTitleLength`) are good candidates for this project. - -- This project has no dependency to other projects in the solution. All other projects depend on this directly or indirectly. - -### .Domain Project - -This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), value types, [repository interfaces](../Repositories.md) and other domain objects. - -An `Issue` entity, an `IssueManager` domain service and an `IIssueRepository` interface are good candidates for this project. - -- Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. - -### .Application.Contracts Project - -This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It does exists to separate interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. - -An `IIssueAppService` interface and an `IssueCreationDto` class are good candidates for this project. - -- Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. - -### .Application Project - -This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. - -An `IssueAppService` class is a good candidate for this project. - -- Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. -- Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. - -### .EntityFrameworkCore Project - -This is the integration project for EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. - -- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. - -> You can delete this project if you don't want to support EF Core for your module. - -### .MongoDB Project - -This is the integration project for MongoDB. - -- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. - -> You can delete this project if you don't want to support MongoDB for your module. - -### Test Projects - -The solution has multiple test projects, one for each layer: - -- `.Domain.Tests` is used to test the domain layer. -- `.Application.Tests` is used to test the application layer. -- `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. -- `.MongoDB.Tests` is used to test MongoDB configuration and custom repositories. -- `.TestBase` is a base (shared) project for all tests. - -In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a Dotnet application. - -Test projects are prepared for integration testing; - -- It is fully integrated to ABP framework and all services in your application. -- It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library. -- Authorization is disabled, so any application service can be easily used in tests. - -You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all initialization process). - -> Domain & Application tests are using EF Core. If you remove EF Core integration or you want to use MongoDB for testing these layers, you should manually change project references & module dependencies. - -### Host Projects - -The solution has a few host applications to run your module. Host applications are used to run your module in a fully configured application. It is useful on development. Host applications includes some other modules in addition to the module being developed: - -Host applications support two types of scenarios. - -#### Single (Unified) Application Scenario - -If your module has a UI, then `.Web.Unified` application is used to host the UI and API on a single point. It has its own `appsettings.json` file (that includes the database connection string) and EF Core database migrations. - -For the `.Web.Unified` application, there is a single database, named `YourProjectName_Unified` (like *IssueManagement_Unified* for this sample). - -> If you've selected the `--no-ui` option, this project will not be in your solution. - -##### How to Run? - -Set `host/YourProjectName.Web.Unified` as the startup project, run `Update-Database` command for the EF Core from Package Manager Console and run your application. Default username is `admin` and password is `1q2w3E*`. - -#### Separated Deployment & Databases Scenario - -In this scenario, there are three applications; - -* `.AuthServer` application is an authentication server used by other applications. It has its own `appsettings.json` that contains database connection and other configurations. -* `.HttpApi.Host` hosts the HTTP API of the module. It has its own `appsettings.json` that contains database connections and other configurations. -* `.Web.Host` host the UI of the module. This project contains an `appsettings.json` file, but it does not have a connection string because it never connects to the database. Instead, it mainly contains endpoint of the remote API server and the authentication server. - -The diagram below shows the relation of the applications: - -![tiered-solution-applications](../images/tiered-solution-applications.png) - -`.Web.Host` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the `.AuthServer`. Then uses the access token to call the `.HttpApi.Host`. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. - -##### Pre-requirements - -* [Redis](https://redis.io/): The applications use Redis as as distributed cache. So, you need to have Redis installed & running. - -##### How to Run? - -You should run the application with the given order: - -- First, run the `.AuthServer` since other applications depends on it. -- Then run the `.HttpApi.Host` since it is used by the `.Web.Host` application. -- Finally, you can run the `.Web.Host` project and login to the application using `admin` as the username and `1q2w3E*` as the password. - -## UI - -### Angular UI - -The solution will have a folder called `angular` in it. This is where the Angular client-side code is located. When you open that folder in an IDE, the folder structure will look like below: - -![Folder structure of ABP Angular module project](../images/angular-module-folder-structure.png) - -* _angular/projects/issue-management_ folder contains the Angular module project. -* _angular/projects/dev-app_ folder contains a development application that runs your module. - -The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API and the `Angular` demo application consumes it. You will not need to run the `.Web.Host` project though. - -#### How to Run the Angular Development App - -For module development, you will need the `dev-app` project up and running. So, here is how we can start the development server. - -First, we need to install dependencies: - -1. Open your terminal at the root folder, i.e. `angular`. -2. Run `yarn` or `npm install`. - -The dependencies will be installed and some of them are ABP modules published as NPM packages. To see all ABP packages, you can run the following command in the `angular` folder: - -```bash -yarn list --pattern abp -``` - -> There is no equivalent of this command in npm. - -The module you will develop depends on two of these ABP packages: _@abp/ng.core_ and _@abp/ng.theme.shared_. Rest of the ABP modules are included in _package.json_ because of the `dev-app` project. - -Once all dependencies are installed, follow the steps below to serve your development app: - -1. Make sure `.AuthServer` and `*.HttpApi.Host` projects are up and running. -2. Open your terminal at the root folder, i.e. `angular`. -3. Run `yarn start` or `npm start`. - -![ABP Angular module dev-app project](../images/angular-module-dev-app-project.png) - -The issue management page is empty in the beginning. You may change the content in `IssueManagementComponent` at the _angular/projects/issue-management/src/lib/issue-management.component.ts_ path and observe that the view changes accordingly. - -Now, let's have a closer look at some key elements of your project. - -#### Main Module - -`IssueManagementModule` at the _angular/projects/issue-management/src/lib/issue-management.module.ts_ path is the main module of your module project. There are a few things worth mentioning in it: - -- Essential ABP modules, i.e. `CoreModule` and `ThemeSharedModule`, are imported. -- `IssueManagementRoutingModule` is imported. -- `IssueManagementComponent` is declared. -- It is prepared for configurability. The `forLazy` static method enables [a configuration to be passed to the module when it is loaded by the router](https://volosoft.com/blog/how-to-configure-angular-modules-loaded-by-the-router). - - -#### Main Routing Module - -`IssueManagementRoutingModule` at the _angular/projects/issue-management/src/lib/issue-management-routing.module.ts_ path is the main routing module of your module project. It currently does two things: - -- Loads `DynamicLayoutComponent` at base path it is given. -- Loads `IssueManagementComponent` as child to the layout, again at the given base path. - -You can rearrange this module to load more than one component at different routes, but you need to update the route provider at _angular/projects/issue-management/config/src/providers/route.provider.ts_ to match the new routing structure with the routes in the menu. Please check [Modifying the Menu](../UI/Angular/Modifying-the-Menu.md) to see how route providers work. - -#### Config Module - -There is a config module at the _angular/projects/issue-management/config/src/issue-management-config.module.ts_ path. The static `forRoot` method of this module is supposed to be called at the route level. So, you may assume the following will take place: - -```js -@NgModule({ - imports: [ - /* other imports */ - - IssueManagementConfigModule.forRoot(), - ], - - /* rest of the module meta data */ -}) -export class AppModule {} -``` - -You can use this static method to configure an application that uses your module project. An example of such configuration is already implemented and the `ISSUE_MANAGEMENT_ROUTE_PROVIDERS` token is provided here. The method can take options which enables further configuration possibilities. - -The difference between the `forRoot` method of the config module and the `forLazy` method of the main module is that, for smallest bundle size, the former should only be used when you have to configure an app before your module is even loaded. - -#### Testing Angular UI - -Please see the [testing document](../UI/Angular/Testing.md). diff --git a/docs/en/Startup-Templates/WPF.md b/docs/en/Startup-Templates/WPF.md deleted file mode 100644 index 7eb6a5c73e..0000000000 --- a/docs/en/Startup-Templates/WPF.md +++ /dev/null @@ -1,27 +0,0 @@ -# WPF Application Startup Template - -This template is used to create a minimalist WPF application project. - -## How to Start With? - -First, install the [ABP CLI](../CLI.md) if you haven't installed before: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Then use the `abp new` command in an empty folder to create a new solution: - -````bash -abp new Acme.MyWpfApp -t wpf -```` - -`Acme.MyWpfApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. - -## Solution Structure - -After you use the above command to create a solution, you will have a solution like shown below: - -![basic-wpf-application-solution](../images/basic-wpf-application-solution.png) - -* `HelloWorldService` is a sample service that implements the `ITransientDependency` interface to register this service to the [dependency injection](../Dependency-Injection.md) system. \ No newline at end of file diff --git a/docs/en/String-Encryption.md b/docs/en/String-Encryption.md deleted file mode 100644 index 867190365d..0000000000 --- a/docs/en/String-Encryption.md +++ /dev/null @@ -1,125 +0,0 @@ -# String Encryption - -ABP Framework provides string encryption feature that allows to **Encrypt** and **Decrypt** strings. - -## Installation - -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. - -If installation is needed, it is suggested to use the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -```bash -abp add-package Volo.Abp.Security -``` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.Security](https://www.nuget.org/packages/Volo.Abp.Security) NuGet package to your project: - - `Install-Package Volo.Abp.Security` - -2. Add the `AbpSecurityModule` to the dependency list of your module: - - ```csharp - [DependsOn( - //...other dependencies - typeof(AbpSecurityModule) // <-- Add module dependency like that - )] - public class YourModule : AbpModule - { - } - ``` - -## Using String Encryption - -All encryption operations are included in `IStringEncryptionService`. You can inject it and start to use. - -```csharp - public class MyService : DomainService - { - protected IStringEncryptionService StringEncryptionService { get; } - - public MyService(IStringEncryptionService stringEncryptionService) - { - StringEncryptionService = stringEncryptionService; - } - - public string Encrypt(string value) - { - // To enrcypt a value - return StringEncryptionService.Encrypt(value); - } - - public string Decrpyt(string value) - { - // To decrypt a value - return StringEncryptionService.Decrypt(value); - } - } -``` - -### Using Custom PassPhrase - -`IStringEncryptionService` methods has **passPharase** parameter with default value and it uses default PassPhrase when you don't pass passPhrase parameter. - -```csharp -// Default Pass Phrase -var encryptedValue = StringEncryptionService.Encrypt(value); - -// Custom Pass Phrase -var encryptedValue = StringEncryptionService.Encrypt(value, "MyCustomPassPhrase"); - -// Encrypt & Decrypt have same parameters. -var decryptedValue = StringEncryptionService.Decrypt(value, "MyCustomPassPhrase"); -``` - -### Using Custom Salt - -`IStringEncryptionService` methods has **salt** parameter with default value and it uses default Salt when you don't pass the parameter. - -```csharp -// Default Salt -var encryptedValue = StringEncryptionService.Encrypt(value); - -// Custom Salt -var encryptedValue = StringEncryptionService.Encrypt(value, salt: Encoding.UTF8.GetBytes("MyCustomSalt")); - -// Encrypt & Decrypt have same parameters. -var decryptedValue = StringEncryptionService.Decrypt(value, salt: Encoding.UTF8.GetBytes("MyCustomSalt")); -``` - -*** - -## String Encryption Options - -Default values can be configured with `AbpStringEncryptionOptions` type. - -```csharp -Configure(opts => -{ - opts.DefaultPassPhrase = "MyStrongPassPhrase"; - opts.DefaultSalt = Encoding.UTF8.GetBytes("MyStrongSalt"); - opts.InitVectorBytes = Encoding.UTF8.GetBytes("YetAnotherStrongSalt"); - opts.Keysize = 512; -}); -``` - -- **DefaultPassPhrase**: Default password to encrypt/decrypt texts. It's recommended to set to another value for security. Default value: `gsKnGZ041HLL4IM8` - -- **DefaultSalt**: A value which is used as salt while encrypting/decrypting. - - Default value: `Encoding.ASCII.GetBytes("hgt!16kl")` - -- **InitVectorBytes:** This constant string is used as a "salt" value for the PasswordDeriveBytes function calls. This size of the IV (in bytes) must = (keysize / 8). Default keysize is 256, so the IV must be 32 bytes long. Using a 16 character string here gives us 32 bytes when converted to a byte array. - - Default value: `Encoding.ASCII.GetBytes("jkE49230Tf093b42")` - -- **Keysize:** This constant is used to determine the keysize of the encryption algorithm. - - Default value: `256` \ No newline at end of file diff --git a/docs/en/Testing.md b/docs/en/Testing.md deleted file mode 100644 index 6ad6bc5443..0000000000 --- a/docs/en/Testing.md +++ /dev/null @@ -1,833 +0,0 @@ -# Automated Testing - -## Introduction - -ABP Framework has been designed with testability in mind. There are some different levels of automated testing; - -* **Unit Tests**: You typically test a single class (or a very few classes together). These tests will be fast. However, you generally need to deal with mocking for the dependencies of your service(s). -* **Integration Tests**: You typically test a service, but this time you don't mock the fundamental infrastructure and services to see if they properly working together. -* **UI Tests**: You test the UI of the application, just like the users interact with your application. - -### Unit Tests vs Integration Tests - -Integration tests have some significant **advantages** compared to unit tests; - -* **Easier to write** since you don't work to establish mocking and dealing with the dependencies. -* Your test code runs with all the real services and infrastructure (including database mapping and queries), so it is much closer to the **real application test**. - -While they have some drawbacks; - -* They are **slower** compared to unit tests since all the infrastructure is prepared for each test case. -* A bug in a service may make multiple test cases broken, so it may be **harder to find the real problem** in some cases. - -We suggest to go mixed: Write unit or integration test where it is necessary and you find effective to write and maintain it. - -## The Application Startup Template - -The [Application Startup Template](Startup-Templates/Application.md) comes with the test infrastructure properly installed and configured for you. - -### The Test Projects - -See the following solution structure in the Visual Studio: - -![solution-test-projects](images/solution-test-projects.png) - -There are more than one test project, organized by the layers; - -* `Domain.Tests` is used to test your Domain Layer objects (like [Domain Services](Domain-Services.md) and [Entities](Entities.md)). -* `Application.Tests` is used to test your Application Layer (like [Application Services](Application-Services.md)). -* `EntityFrameworkCore.Tests` is used to implement abstract test classes and test your custom repository implementations or EF Core mappings (this project will be different if you use another [Database Provider](Data-Access.md)). -* `Web.Tests` is used to test the UI Layer (like Pages, Controllers and View Components). This project does exists only for MVC / Razor Page applications. -* `TestBase` contains some classes those are shared/used by the other projects. - -> `HttpApi.Client.ConsoleTestApp` is not an automated test application. It is an example Console Application that shows how to consume your HTTP APIs from a .NET Console Application. - -The following sections will introduce the base classes and other infrastructure included in these projects. - -### The Test Infrastructure - -The startup solution has the following libraries already installed; - -* [xUnit](https://xunit.net/) as the test framework. -* [NSubstitute](https://nsubstitute.github.io/) as the mocking library. -* [Shouldly](https://github.com/shouldly/shouldly) as the assertion library. - -While you are free to replace them with your favorite tools, this document and examples will be base on these tooling. - -## The Test Explorer - -You can use the Test Explorer to view and run the tests in Visual Studio. For other IDEs, see their own documentation. - -### Open the Test Explorer - -Open the *Test Explorer*, under the *Tests* menu, if it is not already open: - -![vs-test-explorer](images/vs-test-explorer.png) - -### Run the Tests - -Then you can click to the Run All or Run buttons to run the tests. The initial startup template has some sample tests for you: - -![vs-startup-template-tests](images/vs-startup-template-tests.png) - -### Run Tests In Parallel - -The test infrastructure is compatible to run the tests in parallel. It is **strongly suggested** to run all the tests in parallel, which is pretty faster then running them one by one. - -To enable it, click to the caret icon near to the settings (gear) button and select the *Run Tests In Parallel*. - -![vs-run-tests-in-parallel](images/vs-run-tests-in-parallel.png) - -## Unit Tests - -For Unit Tests, you don't need to much infrastructure. You typically instantiate your class and provide some pre-configured mocked objects to prepare your object to test. - -### Classes Without Dependencies - -In this simplest case, the class you want to test has no dependencies. In this case, you can directly instantiate your class, call its methods and make your assertions. - -#### Example: Testing an Entity - -Assume that you've an `Issue` [entity](Entities.md) as shown below: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace MyProject.Issues -{ - public class Issue : AggregateRoot - { - public string Title { get; set; } - public string Description { get; set; } - public bool IsLocked { get; set; } - public bool IsClosed { get; private set; } - public DateTime? CloseDate { get; private set; } - - public void Close() - { - IsClosed = true; - CloseDate = DateTime.UtcNow; - } - - public void Open() - { - if (!IsClosed) - { - return; - } - - if (IsLocked) - { - throw new IssueStateException("You can not open a locked issue!"); - } - - IsClosed = false; - CloseDate = null; - } - } -} - -```` - -Notice that the `IsClosed` and `CloseDate` properties have private setters to force some business rules by using the `Open()` and `Close()` methods; - -* Whenever you close an issue, the `CloseDate` should be set to the [current time](Timing.md). -* An issue can not be re-opened if it is locked. And if it is re-opened, the `CloseDate` should be set to `null`. - -Since the `Issue` entity is a part of the Domain Layer, we should test it in the `Domain.Tests` project. Create an `Issue_Tests` class inside the `Domain.Tests` project: - -````csharp -using Shouldly; -using Xunit; - -namespace MyProject.Issues -{ - public class Issue_Tests - { - [Fact] - public void Should_Set_The_CloseDate_Whenever_Close_An_Issue() - { - // Arrange - - var issue = new Issue(); - issue.CloseDate.ShouldBeNull(); // null at the beginning - - // Act - - issue.Close(); - - // Assert - - issue.IsClosed.ShouldBeTrue(); - issue.CloseDate.ShouldNotBeNull(); - } - } -} -```` - -This test follows the AAA (Arrange-Act-Assert) pattern; - -* **Arrange** part creates an `Issue` entity and ensures the `CloseDate` is `null` at the beginning. -* **Act** part executes the method we want to test for this case. -* **Assert** part checks if the `Issue` properties are same as we expect to be. - -`[Fact]` attribute is defined by the [xUnit](https://xunit.net/) library and marks a method as a test method. `Should...` extension methods are provided by the [Shouldly](https://github.com/shouldly/shouldly) library. You can directly use the `Assert` class of the xUnit, but Shouldly makes it much comfortable and straightforward. - -When you execute the tests, you will see that is passes successfully: - -![issue-first-test](images/issue-first-test.png) - -Let's add two more test methods: - -````csharp -[Fact] -public void Should_Allow_To_ReOpen_An_Issue() -{ - // Arrange - - var issue = new Issue(); - issue.Close(); - - // Act - - issue.Open(); - - // Assert - - issue.IsClosed.ShouldBeFalse(); - issue.CloseDate.ShouldBeNull(); -} - -[Fact] -public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() -{ - // Arrange - - var issue = new Issue(); - issue.Close(); - issue.IsLocked = true; - - // Act & Assert - - Assert.Throws(() => - { - issue.Open(); - }); -} -```` - -`Assert.Throws` checks if the executed code throws a matching exception. - -> See the xUnit & Shoudly documentations to learn more about these libraries. - -### Classes With Dependencies - -If your service has dependencies and you want to unit test this service, you need to mock the dependencies. - -#### Example: Testing a Domain Service - -Assume that you've an `IssueManager` [Domain Service](Domain-Services.md) that is defined as below: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Domain.Services; - -namespace MyProject.Issues -{ - public class IssueManager : DomainService - { - public const int MaxAllowedOpenIssueCountForAUser = 3; - - private readonly IIssueRepository _issueRepository; - - public IssueManager(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task AssignToUserAsync(Issue issue, Guid userId) - { - var issueCount = await _issueRepository.GetIssueCountOfUserAsync(userId); - - if (issueCount >= MaxAllowedOpenIssueCountForAUser) - { - throw new BusinessException( - code: "IM:00392", - message: $"You can not assign more" + - $"than {MaxAllowedOpenIssueCountForAUser} issues to a user!" - ); - } - - issue.AssignedUserId = userId; - } - } -} -```` - -`IssueManager` depends on the `IssueRepository` service, that will be mocked in this example. - -**Business Rule**: The example `AssignToUserAsync` doesn't allow to assign more than 3 (`MaxAllowedOpenIssueCountForAUser` constant) issues to a user. If you want to assign an issue in this case, you first need to unassign an existing issue. - -The test case below tries to make a valid assignment: - -````csharp -using System; -using System.Threading.Tasks; -using NSubstitute; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueManager_Tests - { - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Arrange - - var userId = Guid.NewGuid(); - - var fakeRepo = Substitute.For(); - fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1); - - var issueManager = new IssueManager(fakeRepo); - - var issue = new Issue(); - - // Act - - await issueManager.AssignToUserAsync(issue, userId); - - //Assert - - issue.AssignedUserId.ShouldBe(userId); - await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); - } - } -} -```` - -* `Substitute.For` creates a mock (fake) object that is passed into the `IssueManager` constructor. -* `fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1)` ensures that the `GetIssueCountOfUserAsync` method of the repository returns `1`. -* `issueManager.AssignToUserAsync` doesn't throw any exception since the repository returns `1` for the currently assigned issue count. -* `issue.AssignedUserId.ShouldBe(userId);` line checks if the `AssignedUserId` has the correct value. -* `await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);` checks if the `IssueManager` called the `GetIssueCountOfUserAsync` method exactly one time. - -Let's add a second test to see if it prevents to assign issues to a user more than the allowed count: - -````csharp -[Fact] -public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() -{ - // Arrange - - var userId = Guid.NewGuid(); - - var fakeRepo = Substitute.For(); - fakeRepo - .GetIssueCountOfUserAsync(userId) - .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); - - var issueManager = new IssueManager(fakeRepo); - - // Act & Assert - - var issue = new Issue(); - - await Assert.ThrowsAsync(async () => - { - await issueManager.AssignToUserAsync(issue, userId); - }); - - issue.AssignedUserId.ShouldBeNull(); - await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); -} -```` - -> For more information on the mocking, see the [NSubstitute](https://nsubstitute.github.io/) documentation. - -It is relatively easy to mock a single dependency. But, when your dependencies grow, it gets harder to setup the test objects and mock all the dependencies. See the *Integration Tests* section that doesn't require mocking the dependencies. - -### Tip: Share the Test Class Constructor - -[xUnit](https://xunit.net/) creates a **new test class instance** (`IssueManager_Tests` for this example) for each test method. So, you can move some *Arrange* code into the constructor to reduce the code duplication. The constructor will be executed for each test case and doesn't affect each other, even if they work in parallel. - -**Example: Refactor the `IssueManager_Tests` to reduce the code duplication** - -````csharp -using System; -using System.Threading.Tasks; -using NSubstitute; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueManager_Tests - { - private readonly Guid _userId; - private readonly IIssueRepository _fakeRepo; - private readonly IssueManager _issueManager; - private readonly Issue _issue; - - public IssueManager_Tests() - { - _userId = Guid.NewGuid(); - _fakeRepo = Substitute.For(); - _issueManager = new IssueManager(_fakeRepo); - _issue = new Issue(); - } - - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Arrange - _fakeRepo.GetIssueCountOfUserAsync(_userId).Returns(1); - - // Act - await _issueManager.AssignToUserAsync(_issue, _userId); - - //Assert - _issue.AssignedUserId.ShouldBe(_userId); - await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); - } - - [Fact] - public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() - { - // Arrange - _fakeRepo - .GetIssueCountOfUserAsync(_userId) - .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await _issueManager.AssignToUserAsync(_issue, _userId); - }); - - _issue.AssignedUserId.ShouldBeNull(); - await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); - } - } -} -```` - -> Keep your test code clean to create a maintainable test suite. - -## Integration Tests - -> You can also follow the [web application development tutorial](Tutorials/Part-1.md) to learn developing a full stack application, including the integration tests. - -### The Integration Test Infrastructure - -ABP Provides a complete infrastructure to write integration tests. All the ABP infrastructure and services will perform in your tests. The application startup template comes with the necessary infrastructure pre-configured for you; - -#### The Database - -The startup template is configured to use **in-memory SQLite** database for the EF Core (for MongoDB, it uses [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library). So, all the configuration and queries are performed against a real database and you can even test database transactions. - -Using in-memory SQLite database has two main advantages; - -* It is faster compared to an external DBMS. -* It create a **new fresh database** for each test case, so tests doesn't affect each other. - -> **Tip**: Do not use EF Core's In-Memory database for advanced integration tests. It is not a real DBMS and has many differences in details. For example, it doesn't support transaction and rollback scenarios, so you can't truly test the failing scenarios. On the other hand, In-Memory SQLite is a real DBMS and supports the fundamental SQL database features. - -### The Seed Data - -Writing tests against an empty database is not practical. In most cases, you need to some initial data in the database. For example, if you write a test class that query, update and delete the products, it would be helpful to have a few products in the database before executing the test case. - -ABP's [Data Seeding](Data-Seeding.md) system is a powerful way to seed the initial data. The application startup template has a *YourProject*TestDataSeedContributor class in the `.TestBase` project. You can fill it to have an initial data that you can use for each test method. - -**Example: Create some Issues as the seed data** - -````csharp -using System.Threading.Tasks; -using MyProject.Issues; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class MyProjectTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IIssueRepository _issueRepository; - - public MyProjectTestDataSeedContributor(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue one", - Description = "Test issue one description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue two", - Description = "Test issue two description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue three", - Description = "Test issue three description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue four", - Description = "Test issue four description", - AssignedUserId = TestData.User2Id - }); - } - } -} -```` - -Also created a static class to store the User `Ids`: - -````csharp -using System; - -namespace MyProject -{ - public static class TestData - { - public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B"); - public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999"); - } -} -```` - -In this way, we can use these known Issues and the User `Id`s to perform the tests. - -### Example: Testing a Domain Service - -`AbpIntegratedTest` class (defined in the [Volo.Abp.TestBase](https://www.nuget.org/packages/Volo.Abp.TestBase) package) is used to write tests integrated to the ABP Framework. `T` is the Type of the root module to setup and initialize the application. - -The application startup template has base classes in each test project, so you can derive from these base classes to make it easier. - -See the `IssueManager` tests are re-written as integration tests - -````csharp -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public abstract class IssueManager_Integration_Tests : MyProjectDomainTestBase - where TStartupModule : IAbpModule - { - private readonly IssueManager _issueManager; - private readonly Issue _issue; - - protected IssueManager_Integration_Tests() - { - _issueManager = GetRequiredService(); - _issue = new Issue - { - Title = "Test title", - Description = "Test description" - }; - } - - [Fact] - public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() - { - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await _issueManager.AssignToUserAsync(_issue, TestData.User1Id); - }); - - _issue.AssignedUserId.ShouldBeNull(); - } - - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Act - await _issueManager.AssignToUserAsync(_issue, TestData.User2Id); - - //Assert - _issue.AssignedUserId.ShouldBe(TestData.User2Id); - } - } -} -```` - -> The `IssueManager_Integration_Tests` class is an abstract class, and tests in this class are not seen on the tests explorer, see the **Implementing unit tests in EF Core and MongoDB** section below to learn how to list tests in the test explorer and run them. - -* First test method assigns the issue to the User 1, which has already assigned to 3 issues in the Data Seed code. So, it throws a `BusinessException`. -* Second test method assigns the issue to User 2, which has only 1 issue assigned. So, the method succeeds. - -This class typically locates in the `.Domain.Tests` project since it tests a class located in the `.Domain` project. It is derived from the `MyProjectDomainTestBase` which is already configured to properly run the tests. - -Writing such an integration test class is very straightforward. Another benefit is that you won't need to change the test class later when you add another dependency to the `IssueManager` class. - -### Example: Testing an Application Service - -Testing an [Application Service](Application-Services.md) is not so different. Assume that you've created an `IssueAppService` as defined below: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace MyProject.Issues -{ - public class IssueAppService : ApplicationService, IIssueAppService - { - private readonly IIssueRepository _issueRepository; - - public IssueAppService(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task> GetListAsync() - { - var issues = await _issueRepository.GetListAsync(); - - return ObjectMapper.Map, List>(issues); - } - } -} -```` - -*(assuming you've also defined the `IIssueAppService` and `IssueDto` and created the [object mapping](Object-To-Object-Mapping.md) between `Issue` and the `IssueDto`)* - -Now, you can write a test class inside the `.Application.Tests` project: - -````csharp -using System.Threading.Tasks; -using Shouldly; -using Xunit; - -namespace MyProject.Issues -{ - public abstract class IssueAppService_Tests : MyProjectApplicationTestBase - where TStartupModule : IAbpModule - { - private readonly IIssueAppService _issueAppService; - - protected IssueAppService_Tests() - { - _issueAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_All_Issues() - { - //Act - var issueDtos = await _issueAppService.GetListAsync(); - - //Assert - issueDtos.Count.ShouldBeGreaterThan(0); - } - } -} -```` - -> The `IssueAppService_Tests` class is an abstract, and tests in this class are not seen on the tests explorer, see the **Implementing unit tests in EF Core and MongoDB** section below to learn how to list tests in the test explorer and run them. - -It's that simple. This test method tests everything, including the application service, EF Core mapping, object to object mapping and the repository implementation. In this way, you can fully test the Application Layer and the Domain Layer of your solution. - -### Dealing with Unit of Work in Integration Tests - -ABP's [unit of work](Unit-Of-Work.md) system controls the database connection and transaction management in your application. It seamlessly works while you writing your application code, so you may not aware of it. - -In the ABP Framework, all the database operations must be performed inside a unit of work scope. When you test an [application service](Application-Services.md) method, the unit of work scope will be the scope of your application service method. If you are testing a [repository](Repositories.md) method, the unit of work scope will be the scope of your repository method. - -In some cases, you may need to manually control the unit of work scope. Consider the following test method: - -````csharp -public abstract class IssueRepository_Tests : MyProjectDomainTestBase - where TStartupModule : IAbpModule -{ - private readonly IRepository _issueRepository; - - protected IssueRepository_Tests() - { - _issueRepository = GetRequiredService>(); - } - - public async Task Should_Query_By_Title() - { - IQueryable queryable = await _issueRepository.GetQueryableAsync(); - var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - } -} -```` - -> The `IssueRepository_Tests` class is an abstract, and tests in this class are not seen on the tests explorer, see the **Implementing unit tests in EF Core and MongoDB** section below to learn how to list tests in the test explorer and run them. - -We are using `_issueRepository.GetQueryableAsync` to obtain an `IQueryable` object. Then, we are using the `FirstOrDefaultAsync` method to query an issue by its title. The database query is executed at this point, and you get an exception indicating that there is no active unit of work. - -To make that test properly working, you should manually start a unit of work scope as shown in the following example: - -````csharp -public abstract class IssueRepository_Tests : MyProjectDomainTestBase - where TStartupModule : IAbpModule -{ - private readonly IRepository _issueRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - protected IssueRepository_Tests() - { - _issueRepository = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - public async Task Should_Query_By_Title() - { - using (var uow = _unitOfWorkManager.Begin()) - { - IQueryable queryable = await _issueRepository.GetQueryableAsync(); - var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - await uow.CompleteAsync(); - } - } -} -```` - -We've used the `IUnitOfWorkManager` service to create a unit of work scope, then called the `FirstOrDefaultAsync` method inside that scope, so we don't have the problem anymore. - -> Note that we've tested the `FirstOrDefaultAsync` to demonstrate the unit of work problem. Normally, as a good principle, you should write tests only your own code. - -You could also use `WithUnitOfWorkAsync` to achieve the same functionality instead of writing the same using block in your tests. - -Here is the same test scenario using `WithUnitOfWorkAsync`: - -````csharp -public abstract class IssueRepository_Tests : MyProjectDomainTestBase - where TStartupModule : IAbpModule -{ - private readonly IRepository _issueRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - protected IssueRepository_Tests() - { - _issueRepository = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - public async Task Should_Query_By_Title() - { - await WithUnitOfWorkAsync(() => - { - IQueryable queryable = await _issueRepository.GetQueryableAsync(); - var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - }); - } -} -```` - -There are multiple overloads of the `WithUnitOfWorkAsync` method that you can use based on your specific needs. - -### Working with DbContext - -In some cases, you may want to directory work with the Entity Framework's `DbContext` object to perform database operations in your test methods. In this case, you can use `IDbContextProvider`service to obtain a `DbContext` instance inside a unit of work. - -The following example shows how you can create a `DbContext` object in a test method: - -````csharp -public abstract class MyDbContext_Tests : MyProjectDomainTestBase - where TStartupModule : IAbpModule -{ - private readonly IDbContextProvider _dbContextProvider; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - protected IssueRepository_Tests() - { - _dbContextProvider = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - public async Task Should_Query_By_Title() - { - using (var uow = _unitOfWorkManager.Begin()) - { - var dbContext = await _dbContextProvider.GetDbContextAsync(); - var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - await uow.CompleteAsync(); - } - } -} -```` - -> The `MyDbContext_Tests` class is an abstract, and tests in this class are not seen on the tests explorer, see the **Implementing unit tests in EF Core and MongoDB** section below to learn how to list tests in the test explorer and run them. - -Just like we've done in the *Dealing with Unit of Work in Integration Tests* section, we should perform `DbContext` operations inside an active unit of work. - -For [MongoDB](MongoDB.md), you can use the `IMongoDbContextProvider` service to obtain a `DbContext` object and directly use MongoDB APIs in your test methods. - -## Implementing unit tests in EF Core and MongoDB - -The unit test classes in the `.Domain.Test` and `.Application.Tests` projects are all abstract classes. Therefore, we need to implement the test classes in EF Core or MongoDB test projects to run the tests, otherwise they will be ignored. - -An example implementation for the `IssueManager_Integration_Tests` class in the `.EntityFrameworkCore.Tests` project is shown below: - -````csharp -namespace MyProject.EntityFrameworkCore.Applications; - -public class EfCoreIssueAppService_Tests : IssueAppService_Tests -{ - -} -```` - -> By deriving from the related abstract classes, now we can see the all tests in the test explorers and run them. - -![unitest-efcore-mongodb](images/unitest-efcore-mongodb.png) - -As you can see from the folder structure, all tests are clearly placed into the related subfolders, and they will be seen in the test explorer with this separation. Thus, you can clearly see which tests are related to which layers and projects. - -## UI Tests - -In general, there are two types of UI Tests; - -### Non Visual Tests - -Such tests completely depends on your UI Framework choice; - -* For an MVC / Razor Pages UI, you typically make request to the server, get some HTML and test if some expected DOM elements exist in the returned result. -* Angular has its own infrastructure and practices to test the components, views and services. - -See the following documents to learn Non Visual UI Testing; - -* [Testing in ASP.NET Core MVC / Razor Pages](UI/AspNetCore/Testing.md) -* [Testing in Angular](UI/Angular/Testing.md) -* [Testing in Blazor](UI/Blazor/Testing.md) - -### Visual Tests - -Visual Tests are used to interact with the application UI just like a real user does. It fully tests the application, including the visual appearance of the pages and components. - -Visual UI Testing is out of the scope for the ABP Framework. There are a lot of tooling in the industry (like [Selenium](https://www.selenium.dev/)) that you can use to test your application's UI. diff --git a/docs/en/Text-Templating-Razor.md b/docs/en/Text-Templating-Razor.md deleted file mode 100644 index bf911abd4a..0000000000 --- a/docs/en/Text-Templating-Razor.md +++ /dev/null @@ -1,580 +0,0 @@ -# Razor Integration - - -The Razor template is a standard C# class, so you can freely use the functions of C#, such as `dependency injection`, using `LINQ`, custom methods, and even using `Repository`. - - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.TextTemplating.Razor -```` - -> If you haven't done it yet, you first need to install the [ABP CLI](CLI.md). For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.TextTemplating.Razor). - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.TextTemplating.Razor](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Razor) NuGet package to your project: - -```` -Install-Package Volo.Abp.TextTemplating.Razor -```` - -2. Add the `AbpTextTemplatingRazorModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpTextTemplatingRazorModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## Add MetadataReference to CSharpCompilerOptions - -You need to add the `MetadataReference` of the type used in the template to `CSharpCompilerOptions's References`. - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.References.Add(MetadataReference.CreateFromFile(typeof(YourModule).Assembly.Location)); - }); -} -```` - -## Add MetadataReference for a template. - -You can add some `MetadataReference` to the template - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - services.Configure(options => - { - //Hello is template name. - options.TemplateReferences.Add("Hello", new List() - { - Assembly.Load("Microsoft.Extensions.Logging.Abstractions"), - Assembly.Load("Microsoft.Extensions.Logging") - } - .Select(x => MetadataReference.CreateFromFile(x.Location)) - .ToList()); - }); -} -```` - -## Defining Templates - -Before rendering a template, you should define it. Create a class inheriting from the `TemplateDefinitionProvider` base class: - -````csharp -public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider -{ - public override void Define(ITemplateDefinitionContext context) - { - context.Add( - new TemplateDefinition("Hello") //template name: "Hello" - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/Hello/Hello.cshtml", //template content path - isInlineLocalized: true - ) - ); - } -} -```` - -* `context` object is used to add new templates or get the templates defined by depended modules. Used `context.Add(...)` to define a new template. -* `TemplateDefinition` is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template). -* `/Demos/Hello/Hello.cshtml` is the path of the template file. -* `isInlineLocalized` is used to declare if you are using a single template for all languages (`true`) or different templates for each language (`false`). See the Localization section below for more. -* `WithRenderEngine` method is used to set the render engine of the template. - -### The Template Base - -Every `cshtml` template page needs to inherit `RazorTemplatePageBase` or `RazorTemplatePageBase`. -There are some useful properties in the base class that can be used in templates. eg: `Localizer`, `ServiceProvider`. - -### The Template Content - -`WithVirtualFilePath` indicates that we are using the [Virtual File System](Virtual-File-System.md) to store the template content. Create a `Hello.cshtml` file inside your project and mark it as "**embedded resource**" on the properties window: - -![hello-template-razor](images/hello-template-razor.png) - -Example `Hello.cshtml` content is shown below: - -```` -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -Hello @Model.Name -```` - -The `HelloModel` class is: -````csharp -namespace HelloModelNamespace -{ - public class HelloModel - { - public string Name { get; set; } - } -} -```` - -The [Virtual File System](Virtual-File-System.md) requires to add your files in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class: - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded("TextTemplateDemo"); -}); -```` - -* `TextTemplateDemoModule` is the module class that you define your template in. -* `TextTemplateDemo` is the root namespace of your project. - -## Rendering the Template - -`ITemplateRenderer` service is used to render a template content. - -### Example: Rendering a Simple Template - -````csharp -public class HelloDemo : ITransientDependency -{ - private readonly ITemplateRenderer _templateRenderer; - - public HelloDemo(ITemplateRenderer templateRenderer) - { - _templateRenderer = templateRenderer; - } - - public async Task RunAsync() - { - var result = await _templateRenderer.RenderAsync( - "Hello", //the template name - new HelloModel - { - Name = "John" - } - ); - - Console.WriteLine(result); - } -} -```` - -* `HelloDemo` is a simple class that injects the `ITemplateRenderer` in its constructor and uses it inside the `RunAsync` method. -* `RenderAsync` gets two fundamental parameters: - * `templateName`: The name of the template to be rendered (`Hello` in this example). - * `model`: An object that is used as the `model` inside the template (a `HelloModel` object in this example). - -The result shown below for this example: - -````csharp -Hello John :) -```` -## Localization - -It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections. - -### Inline localization - -Inline localization uses the [localization system](Localization.md) to localize texts inside templates. - -#### Example: Reset Password Link - -Assuming you need to send an email to a user to reset her/his password. Here, the model/template content: - -````csharp -namespace ResetMyPasswordModelNamespace -{ - public class ResetMyPasswordModel - { - public string Link { get; set; } - - public string Name { get; set; } - } -} -```` - -````html -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -@Localizer["ResetMyPassword", Model.Name] -```` - -`Localizer` service is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file: - -````json -"ResetMyPasswordTitle": "Reset my password", -"ResetMyPassword": "Hi {0}, Click here to reset your password" -```` - -You also need to declare the localization resource to be used with this template, inside your template definition provider class: - -````csharp -context.Add( - new TemplateDefinition( - "PasswordReset", //Template name - typeof(DemoResource) //LOCALIZATION RESOURCE - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/PasswordReset/PasswordReset.cshtml", //template content path - isInlineLocalized: true - ) -); -```` - -That's all. When you render this template like that: - -````csharp -var result = await _templateRenderer.RenderAsync( - "PasswordReset", //the template name - new PasswordResetModel - { - Name = "john", - Link = "https://abp.io/example-link?userId=123&token=ABC" - } -); -```` - -You will see the localized result: - -````html -Hi john, Click here to reset your password -```` - -> If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition. - -### Multiple Contents Localization - -Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations. - -#### Example: Welcome Email Template - -Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture. - -First, create a folder and put your templates inside it, like `en.cshtml`, `tr.cshtml`... one for each culture you support: - -![multiple-file-template-razor](images/multiple-file-template-razor.png) - -Then add your template definition in the template definition provider class: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en" - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", //template content folder - isInlineLocalized: false - ) -); -```` - -* Set **default culture name**, so it fallbacks to the default culture if there is no template for the desired culture. -* Specify **the template folder** rather than a single template file. -* Set `isInlineLocalized` to `false` for this case. - -That's all, you can render the template for the current culture: - -````csharp -var result = await _templateRenderer.RenderAsync("WelcomeEmail"); -```` - -> Skipped the modal for this example to keep it simple, but you can use models as just explained before. - -### Specify the Culture - -`ITemplateRenderer` service uses the current culture (`CultureInfo.CurrentUICulture`) if not specified. If you need, you can specify the culture as the `cultureName` parameter: - -````csharp -var result = await _templateRenderer.RenderAsync( - "WelcomeEmail", - cultureName: "en" -); -```` - -## Layout Templates - -Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET Core MVC / Razor Pages. - -### Example: Email HTML Layout Template - -For example, you may want to create a single layout for all of your email templates. - -First, create a template file just like before: - -````html -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase - - - - - - - @Body - - -```` - -* A layout template must have a `Body` part as a place holder for the rendered child content. - -The register your template in the template definition provider: - -````csharp -context.Add( - new TemplateDefinition( - "EmailLayout", - isLayout: true //SET isLayout! - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/EmailLayout/EmailLayout.cshtml", - isInlineLocalized: true - ) -); -```` - -Now, you can use this template as the layout of any other template: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en", - layout: "EmailLayout" //Set the LAYOUT - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", - isInlineLocalized: false - ) -); -```` - -## Global Context - -ABP passes the `model` that can be used to access to the model inside the template. You can pass more global variables if you need. - -An example template content: - -````html -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -A global object value: @GlobalContext["myGlobalObject"] -```` - -This template assumes that that is a `myGlobalObject` object in the template rendering context. You can provide it like shown below: - -````csharp -var result = await _templateRenderer.RenderAsync( - "GlobalContextUsage", - globalContext: new Dictionary - { - {"myGlobalObject", "TEST VALUE"} - } -); -```` - -The rendering result will be: - -```` -A global object value: TEST VALUE -```` - -## Replacing the Existing Templates - -It is possible to replace a template defined by a module that used in your application. In this way, you can customize the templates based on your requirements without changing the module code. - -### Option-1: Using the Virtual File System - -The [Virtual File System](Virtual-File-System.md) allows you to override any file by placing the same file into the same path in your project. - -#### Example: Replace the Standard Email Layout Template - -ABP Framework provides an [email sending system](Emailing.md) that internally uses the text templating to render the email content. It defines a standard email layout template in the `/Volo/Abp/Emailing/Templates/Layout.cshtml` path. The unique name of the template is `Abp.StandardEmailTemplates.Layout` and this string is defined as a constant on the `Volo.Abp.Emailing.Templates.StandardEmailTemplates` static class. - -Do the following steps to replace the template file with your own; - -**1)** Add a new file into the same location (`/Volo/Abp/Emailing/Templates/Layout.cshtml`) in your project: - -![replace-email-layout-razor](images/replace-email-layout-razor.png) - -**2)** Prepare your email layout template: - -````html -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase - - - - - - -

This my header

- - @Body - -
- This is my footer... -
- - -```` - -This example simply adds a header and footer to the template and renders the content between them (see the *Layout Templates* section above to understand it). - -**3)** Configure the embedded resources in the `.csproj` file - -* Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project. -* Add `true` into the `...` section of your `.csproj` file. -* Add the following code into your `.csproj` file: - -````xml - - - - -```` - -This makes the template files "embedded resource". - -**4)** Configure the virtual file system - -Configure the `AbpVirtualFileSystemOptions` in the `ConfigureServices` method of your [module](Module-Development-Basics.md) to add the embedded files into the virtual file system: - -```csharp -Configure(options => -{ - options.FileSets.AddEmbedded(); -}); -``` - -`BookStoreDomainModule` should be your module name, in this example code. - -> Be sure that your module (directly or indirectly) [depends on](Module-Development-Basics.md) the `AbpEmailingModule`. Because the VFS can override files based on the dependency order. - -Now, your template will be used when you want to render the email layout template. - -### Option-2: Using the Template Definition Provider - -You can create a template definition provider class that gets the email layout template and changes the virtual file path for the template. - -**Example: Use the `/MyTemplates/EmailLayout.cshtml` file instead of the standard template** - -```csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace MyProject -{ - public class MyTemplateDefinitionProvider - : TemplateDefinitionProvider, ITransientDependency - { - public override void Define(ITemplateDefinitionContext context) - { - var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); - - emailLayoutTemplate - .WithVirtualFilePath( - "/MyTemplates/EmailLayout.cshtml", - isInlineLocalized: true - ); - } - } -} -``` - -You should still add the file `/MyTemplates/EmailLayout.cshtml` to the virtual file system as explained before. This approach allows you to locate templates in any folder instead of the folder defined by the depended module. - -Beside the template content, you can manipulate the template definition properties, like `DisplayName`, `Layout` or `LocalizationSource`. - -## Advanced Features - -This section covers some internals and more advanced usages of the text templating system. - -### Template Content Provider - -`ITemplateRenderer` is used to render the template, which is what you want for most of the cases. However, you can use the `ITemplateContentProvider` to get the raw (not rendered) template contents. - -> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents. - -Example: - -````csharp -public class TemplateContentDemo : ITransientDependency -{ - private readonly ITemplateContentProvider _templateContentProvider; - - public TemplateContentDemo(ITemplateContentProvider templateContentProvider) - { - _templateContentProvider = templateContentProvider; - } - - public async Task RunAsync() - { - var result = await _templateContentProvider - .GetContentOrNullAsync("Hello"); - - Console.WriteLine(result); - } -} -```` - -The result will be the raw template content: - -```` -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -Hello @Model.Name -```` - -* `GetContentOrNullAsync` returns `null` if no content defined for the requested template. -* It can get a `cultureName` parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above). - -### Template Content Contributor - -`ITemplateContentProvider` service uses `ITemplateContentContributor` implementations to find template contents. There is a single pre-implemented content contributor, `VirtualFileTemplateContentContributor`, which gets template contents from the virtual file system as described above. - -You can implement the `ITemplateContentContributor` to read raw template contents from another source. - -Example: - -````csharp -public class MyTemplateContentProvider - : ITemplateContentContributor, ITransientDependency -{ - public async Task GetOrNullAsync(TemplateContentContributorContext context) - { - var templateName = context.TemplateDefinition.Name; - - //TODO: Try to find content from another source - return null; - } -} - -```` - -Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor. - -### Template Definition Manager - -`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). - -## See Also - -* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. -* [Localization system](Localization.md). -* [Virtual File System](Virtual-File-System.md). diff --git a/docs/en/Text-Templating-Scriban.md b/docs/en/Text-Templating-Scriban.md deleted file mode 100644 index 324fed7b14..0000000000 --- a/docs/en/Text-Templating-Scriban.md +++ /dev/null @@ -1,526 +0,0 @@ -# Scriban Integration - -## Installation - -It is suggested to use the [ABP CLI](CLI.md) to install this package. - -### Using the ABP CLI - -Open a command line window in the folder of the project (.csproj file) and type the following command: - -````bash -abp add-package Volo.Abp.TextTemplating.Scriban -```` - -### Manual Installation - -If you want to manually install; - -1. Add the [Volo.Abp.TextTemplating.Scriban](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Scriban) NuGet package to your project: - -```` -Install-Package Volo.Abp.TextTemplating.Scriban -```` - -2. Add the `AbpTextTemplatingScribanModule` to the dependency list of your module: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpTextTemplatingScribanModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## Defining Templates - -Before rendering a template, you should define it. Create a class inheriting from the `TemplateDefinitionProvider` base class: - -````csharp -public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider -{ - public override void Define(ITemplateDefinitionContext context) - { - context.Add( - new TemplateDefinition("Hello") //template name: "Hello" - .WithVirtualFilePath( - "/Demos/Hello/Hello.tpl", //template content path - isInlineLocalized: true - ) - .WithScribanEngine() - ); - } -} -```` - -* `context` object is used to add new templates or get the templates defined by depended modules. Used `context.Add(...)` to define a new template. -* `TemplateDefinition` is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template). -* `/Demos/Hello/Hello.tpl` is the path of the template file. -* `isInlineLocalized` is used to declare if you are using a single template for all languages (`true`) or different templates for each language (`false`). See the Localization section below for more. -* `WithRenderEngine` method is used to set the render engine of the template. - -### The Template Content - -`WithVirtualFilePath` indicates that we are using the [Virtual File System](Virtual-File-System.md) to store the template content. Create a `Hello.tpl` file inside your project and mark it as "**embedded resource**" on the properties window: - -![hello-template](images/hello-template.png) - -Example `Hello.tpl` content is shown below: - -```` -Hello {%{{{model.name}}}%} :) -```` - -The [Virtual File System](Virtual-File-System.md) requires to add your files in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class: - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded("TextTemplateDemo"); -}); -```` - -* `TextTemplateDemoModule` is the module class that you define your template in. -* `TextTemplateDemo` is the root namespace of your project. - -## Rendering the Template - -`ITemplateRenderer` service is used to render a template content. - -### Example: Rendering a Simple Template - -````csharp -public class HelloDemo : ITransientDependency -{ - private readonly ITemplateRenderer _templateRenderer; - - public HelloDemo(ITemplateRenderer templateRenderer) - { - _templateRenderer = templateRenderer; - } - - public async Task RunAsync() - { - var result = await _templateRenderer.RenderAsync( - "Hello", //the template name - new HelloModel - { - Name = "John" - } - ); - - Console.WriteLine(result); - } -} -```` - -* `HelloDemo` is a simple class that injects the `ITemplateRenderer` in its constructor and uses it inside the `RunAsync` method. -* `RenderAsync` gets two fundamental parameters: - * `templateName`: The name of the template to be rendered (`Hello` in this example). - * `model`: An object that is used as the `model` inside the template (a `HelloModel` object in this example). - -The result shown below for this example: - -````csharp -Hello John :) -```` - -### Anonymous Model - -While it is suggested to create model classes for the templates, it would be practical (and possible) to use anonymous objects for simple cases: - -````csharp -var result = await _templateRenderer.RenderAsync( - "Hello", - new - { - Name = "John" - } -); -```` - -In this case, we haven't created a model class, but created an anonymous object as the model. - -### PascalCase vs snake_case - -PascalCase property names (like `UserName`) is used as snake_case (like `user_name`) in the templates. - -## Localization - -It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections. - -### Inline localization - -Inline localization uses the [localization system](Localization.md) to localize texts inside templates. - -#### Example: Reset Password Link - -Assuming you need to send an email to a user to reset her/his password. Here, the template content: - -```` -{%{{{L "ResetMyPassword" model.name}}}%} -```` - -`L` function is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file: - -````json -"ResetMyPasswordTitle": "Reset my password", -"ResetMyPassword": "Hi {0}, Click here to reset your password" -```` - -You also need to declare the localization resource to be used with this template, inside your template definition provider class: - -````csharp -context.Add( - new TemplateDefinition( - "PasswordReset", //Template name - typeof(DemoResource) //LOCALIZATION RESOURCE - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/PasswordReset/PasswordReset.tpl", //template content path - isInlineLocalized: true - ) -); -```` - -That's all. When you render this template like that: - -````csharp -var result = await _templateRenderer.RenderAsync( - "PasswordReset", //the template name - new PasswordResetModel - { - Name = "john", - Link = "https://abp.io/example-link?userId=123&token=ABC" - } -); -```` - -You will see the localized result: - -````csharp -Hi john, Click here to reset your password -```` - -> If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition. - -### Multiple Contents Localization - -Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations. - -#### Example: Welcome Email Template - -Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture. - -First, create a folder and put your templates inside it, like `en.tpl`, `tr.tpl`... one for each culture you support: - -![multiple-file-template](images/multiple-file-template.png) - -Then add your template definition in the template definition provider class: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en" - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", //template content folder - isInlineLocalized: false - ) -); -```` - -* Set **default culture name**, so it fallbacks to the default culture if there is no template for the desired culture. -* Specify **the template folder** rather than a single template file. -* Set `isInlineLocalized` to `false` for this case. - -That's all, you can render the template for the current culture: - -````csharp -var result = await _templateRenderer.RenderAsync("WelcomeEmail"); -```` - -> Skipped the modal for this example to keep it simple, but you can use models as just explained before. - -### Specify the Culture - -`ITemplateRenderer` service uses the current culture (`CultureInfo.CurrentUICulture`) if not specified. If you need, you can specify the culture as the `cultureName` parameter: - -````csharp -var result = await _templateRenderer.RenderAsync( - "WelcomeEmail", - cultureName: "en" -); -```` - -## Layout Templates - -Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET Core MVC / Razor Pages. - -### Example: Email HTML Layout Template - -For example, you may want to create a single layout for all of your email templates. - -First, create a template file just like before: - -````xml - - - - - - - {%{{{content}}}%} - - -```` - -* A layout template must have a **{%{{{content}}}%}** part as a place holder for the rendered child content. - -The register your template in the template definition provider: - -````csharp -context.Add( - new TemplateDefinition( - "EmailLayout", - isLayout: true //SET isLayout! - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/EmailLayout/EmailLayout.tpl", - isInlineLocalized: true - ) -); -```` - -Now, you can use this template as the layout of any other template: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en", - layout: "EmailLayout" //Set the LAYOUT - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", - isInlineLocalized: false - ) -); -```` - -## Global Context - -ABP passes the `model` that can be used to access to the model inside the template. You can pass more global variables if you need. - -An example template content: - -```` -A global object value: {%{{{myGlobalObject}}}%} -```` - -This template assumes that that is a `myGlobalObject` object in the template rendering context. You can provide it like shown below: - -````csharp -var result = await _templateRenderer.RenderAsync( - "GlobalContextUsage", - globalContext: new Dictionary - { - {"myGlobalObject", "TEST VALUE"} - } -); -```` - -The rendering result will be: - -```` -A global object value: TEST VALUE -```` - -## Replacing the Existing Templates - -It is possible to replace a template defined by a module that used in your application. In this way, you can customize the templates based on your requirements without changing the module code. - -### Option-1: Using the Virtual File System - -The [Virtual File System](Virtual-File-System.md) allows you to override any file by placing the same file into the same path in your project. - -#### Example: Replace the Standard Email Layout Template - -ABP Framework provides an [email sending system](Emailing.md) that internally uses the text templating to render the email content. It defines a standard email layout template in the `/Volo/Abp/Emailing/Templates/Layout.tpl` path. The unique name of the template is `Abp.StandardEmailTemplates.Layout` and this string is defined as a constant on the `Volo.Abp.Emailing.Templates.StandardEmailTemplates` static class. - -Do the following steps to replace the template file with your own; - -**1)** Add a new file into the same location (`/Volo/Abp/Emailing/Templates/Layout.tpl`) in your project: - -![replace-email-layout](images/replace-email-layout.png) - -**2)** Prepare your email layout template: - -````html - - - - - - -

This my header

- - {%{{{content}}}%} - -
- This is my footer... -
- - -```` - -This example simply adds a header and footer to the template and renders the content between them (see the *Layout Templates* section above to understand it). - -**3)** Configure the embedded resources in the `.csproj` file - -* Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project. -* Add `true` into the `...` section of your `.csproj` file. -* Add the following code into your `.csproj` file: - -````xml - - - - -```` - -This makes the template files "embedded resource". - -**4)** Configure the virtual file system - -Configure the `AbpVirtualFileSystemOptions` in the `ConfigureServices` method of your [module](Module-Development-Basics.md) to add the embedded files into the virtual file system: - -```csharp -Configure(options => -{ - options.FileSets.AddEmbedded(); -}); -``` - -`BookStoreDomainModule` should be your module name, in this example code. - -> Be sure that your module (directly or indirectly) [depends on](Module-Development-Basics.md) the `AbpEmailingModule`. Because the VFS can override files based on the dependency order. - -Now, your template will be used when you want to render the email layout template. - -### Option-2: Using the Template Definition Provider - -You can create a template definition provider class that gets the email layout template and changes the virtual file path for the template. - -**Example: Use the `/MyTemplates/EmailLayout.tpl` file instead of the standard template** - -```csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace MyProject -{ - public class MyTemplateDefinitionProvider - : TemplateDefinitionProvider, ITransientDependency - { - public override void Define(ITemplateDefinitionContext context) - { - var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); - - emailLayoutTemplate - .WithVirtualFilePath( - "/MyTemplates/EmailLayout.tpl", - isInlineLocalized: true - ); - } - } -} -``` - -You should still add the file `/MyTemplates/EmailLayout.tpl` to the virtual file system as explained before. This approach allows you to locate templates in any folder instead of the folder defined by the depended module. - -Beside the template content, you can manipulate the template definition properties, like `DisplayName`, `Layout` or `LocalizationSource`. - -## Advanced Features - -This section covers some internals and more advanced usages of the text templating system. - -### Template Content Provider - -`ITemplateRenderer` is used to render the template, which is what you want for most of the cases. However, you can use the `ITemplateContentProvider` to get the raw (not rendered) template contents. - -> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents. - -Example: - -````csharp -public class TemplateContentDemo : ITransientDependency -{ - private readonly ITemplateContentProvider _templateContentProvider; - - public TemplateContentDemo(ITemplateContentProvider templateContentProvider) - { - _templateContentProvider = templateContentProvider; - } - - public async Task RunAsync() - { - var result = await _templateContentProvider - .GetContentOrNullAsync("Hello"); - - Console.WriteLine(result); - } -} -```` - -The result will be the raw template content: - -```` -Hello {%{{{model.name}}}%} :) -```` - -* `GetContentOrNullAsync` returns `null` if no content defined for the requested template. -* It can get a `cultureName` parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above). - -### Template Content Contributor - -`ITemplateContentProvider` service uses `ITemplateContentContributor` implementations to find template contents. There is a single pre-implemented content contributor, `VirtualFileTemplateContentContributor`, which gets template contents from the virtual file system as described above. - -You can implement the `ITemplateContentContributor` to read raw template contents from another source. - -Example: - -````csharp -public class MyTemplateContentProvider - : ITemplateContentContributor, ITransientDependency -{ - public async Task GetOrNullAsync(TemplateContentContributorContext context) - { - var templateName = context.TemplateDefinition.Name; - - //TODO: Try to find content from another source - return null; - } -} - -```` - -Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor. - -### Template Definition Manager - -`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers). - -## See Also - -* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. -* [Localization system](Localization.md). -* [Virtual File System](Virtual-File-System.md). diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md deleted file mode 100644 index 47e6c4cc80..0000000000 --- a/docs/en/Text-Templating.md +++ /dev/null @@ -1,37 +0,0 @@ -# Text Templating - -## Introduction - -ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object): - -Template + Model =renderer=> Rendered Content - -It is very similar to an ASP.NET Core Razor View (or Page): - -*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* - -You can use the rendered output for any purpose, like sending emails or preparing some reports. - -Template rendering engine is very powerful; - -* It supports **conditional logics**, **loops** and much more. -* Template content **can be localized**. -* You can define **layout templates** to be used as the layout while rendering other templates. -* You can pass arbitrary objects to the template context (beside the model) for advanced scenarios. - -ABP Framework provides two templating engines; - -* **[Razor](Text-Templating-Razor.md)** -* **[Scriban](Text-Templating-Scriban.md)** - -You can use different template engines in the same application, or even create a new custom template engine. - -## Source Code - -Get [the source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. - -## See Also - -* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. -* [Localization system](Localization.md). -* [Virtual File System](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/en/Themes/Basic.md b/docs/en/Themes/Basic.md deleted file mode 100644 index 819138de5f..0000000000 --- a/docs/en/Themes/Basic.md +++ /dev/null @@ -1,2 +0,0 @@ -This document has been moved to [here](../UI/AspNetCore/Basic-Theme.md). - diff --git a/docs/en/Themes/Index.md b/docs/en/Themes/Index.md deleted file mode 100644 index a588e37d3a..0000000000 --- a/docs/en/Themes/Index.md +++ /dev/null @@ -1,30 +0,0 @@ -# The Official Themes -ABP Framework provides a complete UI theming system. While you can build your own themes, you can use the following pre-built themes freely in your applications. - -## The Basic Theme -The Basic Theme is a minimalist theme that doesn't add any styling on top of the plain [Bootstrap](https://getbootstrap.com/) styles. You can take the Basic Theme as the base theme and build your own theme or styling on top of it. Here, a screenshot from the theme: - -![basic-theme-application-layout](../images/basic-theme-application-layout.png) - -### Documentation - -- [Basic Theme - MVC UI](../UI/AspNetCore/Basic-Theme.md) -- [Basic Theme - Blazor UI](../UI/Blazor/Basic-Theme.md) -- [Basic Theme - Angular UI](../UI/Angular/Basic-Theme.md) - -## The LeptonX Lite Theme -**LeptonX Lite** is the free version of the [LeptonX Theme](https://x.leptontheme.com/), which is a part of the ABP Commercial. Here, a screenshot from the theme: - -![LeptonX Lite application layout](../images/leptonxlite-theme-application-layout.jpeg) - -### Documentation - -- [LeptonX Lite - MVC UI](LeptonXLite/AspNetCore.md) -- [LeptonX Lite - Blazor UI](LeptonXLite/Blazor.md) -- [LeptonX Lite - Angular UI](LeptonXLite/Angular.md) - -## See Also - -* [Theming - MVC UI](../UI/AspNetCore/Theming.md) -* [Theming - Blazor UI](../UI/Blazor/Theming.md) -* [Theming - Angular UI](../UI/Angular/Theming.md) \ No newline at end of file diff --git a/docs/en/Themes/LeptonXLite/Angular.md b/docs/en/Themes/LeptonXLite/Angular.md deleted file mode 100644 index e7c8142bda..0000000000 --- a/docs/en/Themes/LeptonXLite/Angular.md +++ /dev/null @@ -1,304 +0,0 @@ -# LeptonX Lite Angular UI - -LeptonX Lite has implementation for the ABP Framework Angular Client. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). - -> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). - -> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. - -## Installation - -This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: - -To add `LeptonX-lite` into your project, - -- Install `@abp/ng.theme.lepton-x` - -```bash -yarn add @abp/ng.theme.lepton-x -``` - -- Install `bootstrap-icons` - -```bash -yarn add bootstrap-icons -``` - -- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one in the following link : - -* [Styles - Angular UI](../../UI/Angular/Theme-Configurations.md) - -Note: You should remove the old theme styles from "angular.json" if you are switching from "ThemeBasic" or "Lepton." -Look at the [Theme Configurations](../../UI/Angular/Theme-Configurations) list of styles. Depending on your theme, you can alter your styles in angular.json. - -- Finally, remove `ThemeBasicModule` from `app.module.ts`, and import the related modules in `app.module.ts` - -```js -import { ThemeLeptonXModule } from "@abp/ng.theme.lepton-x"; -import { SideMenuLayoutModule } from "@abp/ng.theme.lepton-x/layouts"; - -@NgModule({ - imports: [ - // ... - - // do not forget to remove ThemeBasicModule or other old theme module - // ThemeBasicModule.forRoot(), - ThemeLeptonXModule.forRoot(), - SideMenuLayoutModule.forRoot(), - ], - // ... -}) -export class AppModule {} -``` - -Note: If you employ [Resource Owner Password Flow](https://docs.abp.io/en/abp/latest/UI/Angular/Authorization#resource-owner-password-flow) for authorization, you should import the following module as well: - -```js -import { AccountLayoutModule } from "@abp/ng.theme.lepton-x/account"; - -@NgModule({ - // ... - imports: [ - // ... - AccountLayoutModule.forRoot(), - // ... - ], - // ... -}) -export class AppModule {} -``` - -To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss` - -```css -:root { - --lpx-logo: url("/assets/images/logo.png"); - --lpx-logo-icon: url("/assets/images/logo-icon.png"); - --lpx-brand: #edae53; -} -``` - -- `--lpx-logo` is used to place the logo in the menu. -- `--lpx-logo-icon` is a square icon used when the menu is collapsed. -- `--lpx-brand` is a color used throughout the application, especially on active elements. - -### Server Side - -In order to migrate to LeptonX on your server side projects (Host and/or AuthServer projects), please follow the [Server Side Migration](AspNetCore.md) document. - -## Customization - -### Layouts - -The Angular version of LeptonX Lite provides **layout components** for your **user interface** on [ABP Framework Theming](https://docs.abp.io/en/abp/latest/UI/Angular/Theming). You can use the layouts to **organize your user interface**. You can replace the **layout components** and some parts of the **layout components** with the [ABP replaceable component system](https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement). - -The main responsibility of a theme is to **provide** the layouts. There are **three pre-defined layouts that must be implemented by all the themes:** - -- **ApplicationLayoutComponent:** The **default** layout which is used by the **main** application pages. -- **AccountLayoutComponent:** Mostly used by the **account module** for **login**, **register**, **forgot password**... pages. -- **EmptyLayoutComponent:** The **Minimal** layout that **has no layout components** at all. - -The **Layout components** and all the replacable components are predefined in `eThemeLeptonXComponents` as enum. - -### How to replace a component - -```js -import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum -import { eThemeLeptonXComponents } from '@abp/ng.theme.lepton-x'; // imported eThemeLeptonXComponents enum - -//... - -@Component(/* component metadata */) -export class AppComponent { - constructor( - private replaceableComponents: ReplaceableComponentsService, // injected the service - ) { - this.replaceableComponents.add({ - component: YourNewApplicationLayoutComponent, - key: eThemeLeptonXComponents.ApplicationLayout, - }); - } -} -``` - -See the [Component Replacement](https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement) documentation for more information on how to replace components. - -### Brand Component - -The **brand component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. You can change the logo via css but if you want to change logo component, the key is `eThemeLeptonXComponents.Logo` - -```js -///... -this.replaceableComponents.add({ - component: YourNewLogoComponent, - key: eThemeLeptonXComponents.Logo, -}); -///... -``` - -![Brand component](../../images/leptonxlite-brand-component.png) - -## Breadcrumb Component - -On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. - -```js -///... -this.replaceableComponents.add({ - component: YourNewSidebarComponent, - key: eThemeLeptonXComponents.Breadcrumb, -}); -///... -``` - -![Breadcrumb component](../../images/leptonxlite-breadcrumb-component.png) - -## Navbar Component - -Sidebar menus have been used as a **directory for Related Pages** to a **Service** offering, **Navigation** items to a **specific service** or topic and even just as **Links** the user may be interested in. - -```js -///... -this.replaceableComponents.add({ - component: YourNewSidebarComponent, - key: eThemeLeptonXComponents.Navbar, -}); -///... -``` - -![Sidebar menu component](../../images/leptonxlite-sidebar-menu-component.png) - -## Page Alerts Component - -Provides contextual **feedback messages** for typical user actions with a handful of **available** and **flexible** **alert messages**. Alerts are available for any length of text, as well as an **optional dismiss button**. - -![Page alerts component](../../images/leptonxlite-page-alerts-component.png) - -```js -///... -this.replaceableComponents.add({ - component: YourNewPageAlertContainerComponent, - key: eThemeLeptonXComponents.PageAlertContainer, -}); -///... -``` - -## Toolbar Component - -![Breadcrumb component](../../images/leptonxlite-toolbar-component.png) - -Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. - -```js -///... -this.replaceableComponents.add({ - component: YourNewNavItemsComponent, - key: eThemeLeptonXComponents.NavItems, -}); -///... -``` - -## Toolbar Items - -There are two parts to the toolbar. The first is Language-Switch. The second is the User-Profile element. You can swap out each of these parts individually. - -## Language Switch Component - -Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** - -![Language switch component](../../images/leptonxlite-language-switch-component.png) - -```js -///... -this.replaceableComponents.add({ - component: YourNewLanguagesComponent, - key: eThemeLeptonXComponents.Languages, -}); -///... -``` - -## User Menu Component - -The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. - -![User menu component](../../images/leptonxlite-user-menu-component.png) - -```js -///... -this.replaceableComponents.add({ - component: YourNewCurrentUserComponent, - key: eThemeLeptonXComponents.CurrentUser, -}); -///... -``` - -Note: The language selection component in the Volo app is not replaceable. It is part of the settings menu. - -## Mobile Navbar Component - -The **mobile navbar component** is used to display the **navbar menu on mobile devices**. The mobile navbar component is a **dropdown menu** that contains language selection and user menu. - -![Mobile user menu component](../../images/leptonxlite-mobile-user-menu-component.png) - -```js -///... -this.replaceableComponents.add({ - component: YourNewMobileNavbarComponent, - key: eThemeLeptonXComponents.MobileNavbar, -}); -///... -``` - -## Mobile Navbar Items. - -There are two parts of the mobile navbar. The mobile navbar has Language-Switch and User-Profile. You can swap out each of these parts individually. - -The Mobile language-Selection component key is `eThemeLeptonXComponents.MobileLanguageSelection`. - -The Mobile User-Profile component key is `eThemeLeptonXComponents.MobileUserProfile`. - -## Footer Component - -![Angular Footer Component](../../images/angular-footer.png) - -The Footer is the section of content at the very bottom of the site. This section of the content can be modified. -Inject **FooterLinksService** and use the **setFooterInfo** method of **FooterLinksService** -to assign path or link and description. -**descUrl** and **footerLinks** are nullable. Constant **footerLinks** are on the right side of footer. - -```js -///... - -const footerLinks = [ - { - link: "/components/bootstrap/badge", - text: "Manage Your Profile", - }, - { - link: "/components/bootstrap/border", - text: "My Security Logs", - }, -]; - -const footerInfo: FooterNav = { - desc: "Home", - descUrl: "/components/home", - footerLinks: footerLinks, -}; - -this.footerLinksService.setFooterInfo(footerInfo); - -///... -``` - -If you want to change the footer component, the key is `eThemeLeptonXComponents.Footer` - -```js -///... -this.replaceableComponents.add({ - component: YourNewFooterComponent, - key: eThemeLeptonXComponents.Footer, -}); -///... -``` diff --git a/docs/en/Themes/LeptonXLite/AspNetCore.md b/docs/en/Themes/LeptonXLite/AspNetCore.md deleted file mode 100644 index 8bdf85704f..0000000000 --- a/docs/en/Themes/LeptonXLite/AspNetCore.md +++ /dev/null @@ -1,221 +0,0 @@ -# LeptonX Lite MVC UI -LeptonX Lite has implementation for the ABP Framework Razor Pages. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). - -> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). - -> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. - -## Installation - -This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: - -- Add the **Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite** package to your **Web** application. - -```bash -dotnet add package Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite --prerelease -``` - -- Remove the **Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic** reference from the project since it's not necessary after switching to LeptonX Lite. - -- Make sure the old theme is removed and LeptonX is added in your Module class. - -```diff -[DependsOn( - // Remove the BasicTheme module from DependsOn attribute -- typeof(AbpAspNetCoreMvcUiBasicThemeModule), - - // Add the LeptonX Lite module to DependsOn attribute -+ typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), -)] -``` - -- Replace `BasicThemeBundles` with `LeptonXLiteThemeBundles` in `AbpBundlingOptions`: - -```diff -Configure(options => -{ - options.StyleBundles.Configure( - // Remove the following line -- BasicThemeBundles.Styles.Global, - // Add the following line instead -+ LeptonXLiteThemeBundles.Styles.Global, - bundle => - { - bundle.AddFiles("/global-styles.css"); - } - ); -}); -``` - -## Customization - -### Layouts - -LeptonX Lite MVC provides **layouts** for your **user interface** based [ABP Framework Theming](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming). You can use **layouts** to **organize your user interface**. - -The main responsibility of a theme is to **provide** the layouts. There are **three pre-defined layouts that must be implemented by all the themes:** - -* **Application:** The **default** layout which is used by the **main** application pages. - -* **Account:** Mostly used by the **account module** for **login**, **register**, **forgot password**... pages. - -* **Empty:** The **Minimal** layout that **has no layout components** at all. - -**Layout names** are **constants** defined in the `LeptonXLiteTheme` class in the **MVC** project **root**. - -> The layout pages define under the `Themes/LeptonXLite/Layouts` folder and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -### Toolbars - -LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. - -- `LeptonXLiteToolbars.Main` -- `LeptonXLiteToolbars.MainMobile` - -```csharp -public class MyProjectNameMainToolbarContributor : IToolbarContributor -{ - public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) - { - if (context.Toolbar.Name == LeptonXLiteToolbars.Main) - { - context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); - } - - if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) - { - context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); - } - } -} -``` - -# LeptonX Lite MVC Components - -ABP **helps** you make **highly customizable UI**. You can easily **customize** your themes to fit your needs. **The Virtual File System** makes it possible to **manage files** that **do not physically** exist on the **file system** (disk). It's mainly used to embed **(js, css, image..)** files into assemblies and **use them like** physical files at runtime. An application (or another module) can **override** a **virtual file of a module** just like placing a file with the **same name** and **extension** into the **same folder** of the **virtual file**. - -LeptonX Lite is built on the [Abp Framework](https://abp.io/), so you can **easily** customize your Asp.Net Core MVC user interface by following [Abp Mvc UI Customization](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface). - -## Branding Component - -The **branding component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. - -![Brand component](../../images/leptonxlite-brand-component.png) - -### How to override the Branding Component in LeptonX Lite MVC - -* The **branding component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Brand/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **branding component (C# file)** is defined in the `Themes/LeptonXLite/Components/Brand/MainNavbarBrandViewComponent.cs` file and you can **override it** by creating a file with the **same name** and under the **same folder**. - -### How to override the favicon in LeptonX Lite MVC - -You can add a new favicon to the `~/wwwroot/favicon.svg` and `~/wwwroot/favicon.ico` paths to override the current favicon. - -## Breadcrumb Component - -On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. - -![Breadcrumb component](../../images/leptonxlite-breadcrumb-component.png) - -### How to override the Breadcrumb Component in LeptonX Lite MVC - -* The **breadcrumb component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Breadcrumbs/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **breadcrumb component (C# file)** is defined in the `Themes/LeptonXLite/Components/Breadcrumbs/BreadcrumbsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Sidebar Menu Component - -Sidebar menus have been used as **a directory for Related Pages** to a **Service** offering, **Navigation** items to a **specific service** or topic and even just as **Links** the user may be interested in. - -![Sidebar menu component](../../images/leptonxlite-sidebar-menu-component.png) - -### How to override the Sidebar Menu Component in LeptonX Lite MVC - -* **Sidebar menu page (.cshtml)** is defined in the `Themes/LeptonXLite/Components/Menu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* If you want to **override the menu component (C#)** you can override the `Themes/LeptonXLite/Components/Menu/MainMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -> The **sidebar menu** renders menu items **dynamically**. The **menu item** is a **partial view** and is defined in the `Themes/LeptonXLite/Components/Menu/_MenuItem.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Page Alerts Component - -Provides contextual **feedback messages** for typical user actions with the handful of **available** and **flexible** **alert messages**. Alerts are available for any length of text, as well as an **optional dismiss button**. - -![Page alerts component](../../images/leptonxlite-page-alerts-component.png) - -### How to override the Page Alerts Component in LeptonX Lite MVC - -* The **page alerts component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/PageAlerts/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **page alerts component (C#)** is defined in the `Themes/LeptonXLite/Components/PageAlerts/PageAlertsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Toolbar Component - -Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. - -### How to override the Toolbar Component in LeptonX Lite MVC - -* The **toolbar component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Toolbar/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **toolbar component (C#)** is defined in the `Themes/LeptonXLite/Components/Toolbar/ToolbarViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Toolbar Item Component - -The toolbar item is a **single item** that **contains** a **link**, an **icon**, a **label** etc.. - -### How to override the Toolbar Item Component in LeptonX Lite MVC - -* The **toolbar item component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/ToolbarItems/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **toolbar item component (C#)** is defined in the `Themes/LeptonXLite/Components/ToolbarItems/ToolbarItemsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -You can find the toolbar components below: - -## Language Switch Component - -Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** - -![Language switch component](../../images/leptonxlite-language-switch-component.png) - -### How to override the Language Switch Component in LeptonX Lite MVC - -* The **language switch component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/LanguageSwitch/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **language switch component (C#)** is defined in the `Themes/LeptonXLite/Components/LanguageSwitch/LanguageSwitchViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Mobile Language Switch Component - -The **mobile** **language switch component** is used to switch the language of the website **on mobile devices**. The mobile language switch component is a **dropdown menu** that **contains all the languages** of the website. - -![Mobil language switch component](../../images/leptonxlite-mobile-language-switch-component.png) - -### How to override the Mobile Language Switch Component in LeptonX Lite MVC - -* The **mobile language switch component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/MobileLanguageSwitch/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **mobile language switch component (C#)** is defined in the `Themes/LeptonXLite/Components/MobileLanguageSwitch/MobileLanguageSwitchViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## User Menu Component - -The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. - -![User menu component](../../images/leptonxlite-user-menu-component.png) - -### How to override the User Menu Component in LeptonX Lite MVC - -* The **user menu component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/UserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **user menu component (C#)** is defined in the `Themes/LeptonXLite/Components/UserMenu/UserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -## Mobile User Menu Component - -The **mobile user menu component** is used to display the **user menu on mobile devices**. The mobile user menu component is a **dropdown menu** that contains all the **options** of the **user menu**. - -![Mobile user menu component](../../images/leptonxlite-mobile-user-menu-component.png) - -### How to override the Mobile User Menu Component in LeptonX Lite MVC - -* The **mobile user menu component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/MobileUserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. - -* The **mobile user menu component (C#)** is defined in the `Themes/LeptonLite/Components/MobileUserMenu/MobileUserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. diff --git a/docs/en/Themes/LeptonXLite/Blazor.md b/docs/en/Themes/LeptonXLite/Blazor.md deleted file mode 100644 index d2565bea40..0000000000 --- a/docs/en/Themes/LeptonXLite/Blazor.md +++ /dev/null @@ -1,528 +0,0 @@ -# LeptonX Lite Blazor UI - -````json -//[doc-params] -{ - "UI": ["Blazor", "BlazorServer"] -} -```` - -LeptonX Lite has implementation for the ABP Framework Blazor WebAssembly & Blazor Server. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). - -> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). - -> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. - -## Installation - -This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: - -{{if UI == "Blazor"}} -- Complete the [MVC Razor Pages Installation](AspNetCore.md#installation) for the **HttpApi.Host** application first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and if Auth Server is separated, install to the **OpenIddict**_. - - -- Add **Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme** package to your **Blazor WebAssembly** application with the following command: - - ```bash - dotnet add package Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme --prerelease - ``` - -- Remove **Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme** reference from the project since it's not necessary after switching to LeptonX Lite. - -- Remove the old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule** type to the **DependsOn** attribute. - -```diff -[DependsOn( - // Remove BasicTheme module from DependsOn attribute -- typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeModule), - - // Add LeptonX Lite module to DependsOn attribute -+ typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule), -)] -``` - -- Change startup App component with the LeptonX one. - -```csharp -// Make sure the 'App' comes from 'Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite' namespace. -builder.RootComponents.Add("#ApplicationContainer"); -``` - -- Run the `abp bundle` command in your **Blazor** application folder. - -{{end}} - - -{{if UI == "BlazorServer"}} - -- Complete the [MVC Razor Pages Installation](AspNetCore.md#installation) first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and **AuthServer**_. - -- Add **Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme** package to your **Blazor server** application with the following command: - - ```bash - dotnet add package Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme --prerelease - ``` - -- Remove **Volo.Abp.AspNetCore.Components.Server.BasicTheme** reference from the project since it's not necessary after switching to LeptonX Lite. - - -- Remove old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsServerLeptonXLiteThemeModule** type to the **DependsOn** attribute. - - ```diff - [DependsOn( - // Remove BasicTheme module from DependsOn attribute - - typeof(AbpAspNetCoreComponentsServerBasicThemeModule), - - // Add LeptonX Lite module to DependsOn attribute - + typeof(AbpAspNetCoreComponentsServerLeptonXLiteThemeModule) - )] - ``` - -- Replace `BlazorBasicThemeBundles` with `BlazorLeptonXLiteThemeBundles` in `AbpBundlingOptions`: - ```diff - options.StyleBundles.Configure( - // Remove following line - - BlazorBasicThemeBundles.Styles.Global, - // Add following line instead - + BlazorLeptonXLiteThemeBundles.Styles.Global, - bundle => - { - bundle.AddFiles("/blazor-global-styles.css"); - //You can remove the following line if you don't use Blazor CSS isolation for components - bundle.AddFiles("/MyProjectName.Blazor.styles.css"); - }); - ``` - -- Update `_Host.cshtml` file. _(located under **Pages** folder by default.)_ - - - Add following usings to Locate **App** and **BlazorLeptonXLiteThemeBundles** classes. - ```csharp - @using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite - @using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Bundling - ``` - - Then replace script & style bundles as following: - ```diff - // Remove following line - - - // Add following line instead - + - ``` - - ```diff - // Remove following line - - - // Add following line instead - + - ``` - -{{end}} - - ---- - -## Customization - -### Layout - -* Create a razor page, like `MyMainLayout.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -@using Volo.Abp.DependencyInjection - -@inherits MainLayout -@attribute [ExposeServices(typeof(MainLayout))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Main Layout"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainLayout.razor.cs`, in your blazor application as shown below: - -```csharp -[ExposeServices(typeof(MainLayout))] -[Dependency(ReplaceServices = true)] -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - public partial class MyMainLayout - { - public string Name = "My Main Layout"; - } -} -``` - -> Don't forget to remove the repeated attributes from the razor page! -> Don't forget to remove the `@code` section from the razor page! - -### Toolbars - -LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. - -- `LeptonXLiteToolbars.Main` -- `LeptonXLiteToolbars.MainMobile` - -```csharp -public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) -{ - if (context.Toolbar.Name == LeptonXLiteToolbars.Main) - { - context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); - } - - if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) - { - context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); - } - - return Task.CompletedTask; -} -``` - -{{if UI == "BlazorServer"}} - -> _You can visit the [Toolbars Documentation](https://docs.abp.io/en/abp/latest/UI/Blazor/Toolbars) for better understanding._ - -{{end}} - -## Components - -LeptonX Blazor is built on the basis of components. You can use the components in your application as you wish, or you can customize the components by overriding them. If you want to override a component please follow the steps. - -### Branding Component - -The **branding component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. - -![](../../images/leptonxlite-brand-component.png) - -#### How to Override Branding Component - -* Create a razor page, like `MyBrandingComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -@using Volo.Abp.DependencyInjection - -@inherits Branding -@attribute [ExposeServices(typeof(Branding))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Branding Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyBrandingComponent.razor.cs`, in your blazor application as shown below: - -```csharp -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - public partial class MyBrandingComponent - { - public string Name = "My Branding Component"; - } -} -``` - -### How to override the favicon - -Startup templates contain `favicon.ico` files under the `wwwroot` folder of the Blazor application. You can change this file to override the current favicon. - -### Breadcrumb Component - -On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. - -![](../../images/leptonxlite-breadcrumb-component.png) - -#### How to Override the BreadCrumb Component - -* Create a razor page, like `MyBreadcrumbsComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -@using Volo.Abp.DependencyInjection - -@inherits Breadcrumbs -@attribute [ExposeServices(typeof(Breadcrumbs))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Breadcrumbs Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyBreadcrumbsComponent.razor.cs`, in your blazor application as shown below: -* -```csharp -using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(Breadcrumbs))] - [Dependency(ReplaceServices = true)] - public partial class MyBreadcrumbsComponent - { - public string Name = "My Breadcrumbs Component"; - } -} -``` - -### Main Menu Component - -Sidebar menus have been used as **a directory for Related Pages** for a **Service** offering, **Navigation** items for a **specific service** or topic and even just as **Links** the user may be interested in. - -![](../../images/leptonxlite-sidebar-menu-component.png) - -#### How to Override the Main Menu Component - -* Create a razor page, like `MyMainMenuComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Navigation; -@using Volo.Abp.DependencyInjection - -@inherits MainMenu -@attribute [ExposeServices(typeof(MainMenu))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Main Menu Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenu.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Navigation; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(MainMenu))] - [Dependency(ReplaceServices = true)] - public partial class MainMenu - { - public string Name = "My Main Menu Component"; - } -} -``` - -> The **main menu** renders the menu items **dynamically**. The **menu item** is a **razor component** named `MainMenuItem.razor.cs` in the same namespace with **main menu** and you can **override it** like the main menu. - -### Toolbar Items Component - -Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. - -#### How to Override the Toolbar Items Component - -* Create a razor page, like `MyToolbarItemsComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -@using Volo.Abp.DependencyInjection - -@inherits ToolbarItemsComponent -@attribute [ExposeServices(typeof(ToolbarItemsComponent))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Toolbar Items Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyToolbarItemsComponent.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(ToolbarItemsComponent))] - [Dependency(ReplaceServices = true)] - public partial class MyToolbarItemsComponent - { - public string Name = "My Toolbar Items Component"; - } -} -``` - -### Language Switch Component - -Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** - -![](../../images/leptonxlite-language-switch-component.png) - -#### How to Override the Language Switch Component - -* Create a razor page, like `MyLanguageSwitchComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -@using Volo.Abp.DependencyInjection - -@inherits LanguageSwitchComponent -@attribute [ExposeServices(typeof(LanguageSwitchComponent))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Language Switch Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyLanguageSwitchComponent.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(LanguageSwitchComponent))] - [Dependency(ReplaceServices = true)] - public partial class MyLanguageSwitchComponent - { - public string Name = "My Language Switch Component"; - } -} -``` - -### Mobile Language Switch Component - -The **mobile** **language switch component** is used to switch the language of the website **on mobile devices**. The mobile language switch component is a **dropdown menu** that **contains all the languages** of the website. - -![](../../images/leptonxlite-mobile-language-switch-component.png) - -#### How to Override the Mobile Language Switch Component - -* Create a razor page, like `MyMobilLanguageSwitchComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -@using Volo.Abp.DependencyInjection - -@inherits MobilLanguageSwitchComponent -@attribute [ExposeServices(typeof(MobilLanguageSwitchComponent))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Mobile Language Switch Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobilLanguageSwitchComponent.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(MobilLanguageSwitchComponent))] - [Dependency(ReplaceServices = true)] - public partial class MyMobilLanguageSwitchComponent - { - public string Name = "My Mobile Language Switch Component"; - } -} -``` - -### User Menu Component - -The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. - -![](../../images/leptonxlite-user-menu-component.png) - -#### How to Override the User Menu Component - -* Create a razor page, like `MyUserMenuComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -@using Volo.Abp.DependencyInjection - -@inherits MobilLanguageSwitchComponent -@attribute [ExposeServices(typeof(MobilLanguageSwitchComponent))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My User Menu Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyUserMenuComponent.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(UserMenuComponent))] - [Dependency(ReplaceServices = true)] - public partial class MyUserMenuComponent - { - public string Name = "My User Menu Component"; - } -} -``` - -### Mobile User Menu Component - -The **mobile user menu component** is used to display the **user menu on mobile devices**. The mobile user menu component is a **dropdown menu** that contains all the **options** of the **user menu**. - -![](../../images/leptonxlite-mobile-user-menu-component.png) - -#### How to override the Mobile User Menu Component - -* Create a razor page, like `MyMobileUserMenuComponent.razor`, in your blazor application as shown below: - -```html -@using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -@using Volo.Abp.DependencyInjection - -@inherits MobilUserMenuComponent -@attribute [ExposeServices(typeof(MobilUserMenuComponent))] -@attribute [Dependency(ReplaceServices = true)] - -@Name - -@code { - string Name = "My Mobile User Menu Component"; -} -``` - -* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobileUserMenuComponent.razor.cs`, in your blazor application as shown below: - -```csharp -using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; -using Volo.Abp.DependencyInjection; - -namespace LeptonXLite.DemoApp.Blazor.MyComponents -{ - [ExposeServices(typeof(MobileUserMenuComponent))] - [Dependency(ReplaceServices = true)] - public partial class MyMobileUserMenuComponent - { - public string Name = "My Mobile User Menu Component"; - } -} -``` diff --git a/docs/en/Timing.md b/docs/en/Timing.md deleted file mode 100644 index 9b3eb0cca9..0000000000 --- a/docs/en/Timing.md +++ /dev/null @@ -1,113 +0,0 @@ -# Timing - -Working with times & [time zones](https://en.wikipedia.org/wiki/Time_zone) is always tricky, especially if you need to build a **global system** that is used by users in **different time zones**. - -ABP provides a basic infrastructure to make it easy and handle automatically wherever possible. This document covers the ABP Framework services and systems related to time and time zones. - -> If you are creating a local application that runs in a single time zone region, you may not need all these systems. But even in this case, it is suggested to use the `IClock` service introduced in this document. - -## IClock - -`DateTime.Now` returns a `DateTime` object with the **local date & time of the server**. A `DateTime` object **doesn't store the time zone information**. So, you can not know the **absolute date & time** stored in this object. You can only make **assumptions**, like assuming that it was created in UTC+05 time zone. The things especially gets complicated when you save this value to a database and read later, or send it to a client in a **different time zone**. - -One solution to this problem is always use `DateTime.UtcNow` and assume all `DateTime` objects as UTC time. In this way, you can convert it to the time zone of the target client when needed. - -`IClock` provides an abstraction while getting the current time, so you can control the kind of the date time (UTC or local) in a single point in your application. - -**Example: Getting the current time** - -````csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Timing; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IClock _clock; - - public MyService(IClock clock) - { - _clock = clock; - } - - public void Foo() - { - //Get the current time! - var now = _clock.Now; - } - } -} -```` - -* Inject the `IClock` service when you need to get the current time. Common base classes (like ApplicationService) already injects it and provides as a base property - so, you can directly use as `Clock`. -* Use the `Now` property to get the current time. - -> Most of the times, `IClock` is the only service you need to know and use in your application. - -### Clock Options - -`AbpClockOptions` is the [options](Options.md) class that used to set the clock kind. - -**Example: Use UTC Clock** - -````csharp -Configure(options => -{ - options.Kind = DateTimeKind.Utc; -}); -```` - -Write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). - -> Default `Kind` is `Unspecified`, that actually make the Clock as it doesn't exists at all. Either make it `Utc` or `Local` if you want to get benefit of the Clock system. - -### DateTime Normalization - -Other important function of the `IClock` is to normalize `DateTime` objects. - -**Example usage:** - -````csharp -DateTime dateTime = ...; //Get from somewhere -var normalizedDateTime = Clock.Normalize(dateTime) -```` - -`Normalize` method works as described below: - -* Converts the given `DateTime` to the UTC (by using the `DateTime.ToUniversalTime()` method) if current Clock is UTC and given `DateTime` is local. -* Converts the given `DateTime` to the local (by using the `DateTime.ToLocalTime()` method) if current Clock is local and given `DateTime` is UTC. -* Sets `Kind` of the given `DateTime` (using the `DateTime.SpecifyKind(...)` method) to the `Kind` of the current Clock if given `DateTime`'s `Kind` is `Unspecified`. - -`Normalize` method is used by the ABP Framework when the it gets a `DateTime` that is not created by `IClock.Now` and may not be compatible with the current Clock type. Examples; - -* `DateTime` type binding in the ASP.NET Core MVC model binding. -* Saving data to and reading data from database via [Entity Framework Core](Entity-Framework-Core.md). -* Working with `DateTime` objects on [JSON deserialization](Json-Serialization.md). - -#### DisableDateTimeNormalization Attribute - -`DisableDateTimeNormalization` attribute can be used to disable the normalization operation for desired classes or properties. - -### Other IClock Properties - -In addition to the `Now`, `IClock` service has the following properties: - -* `Kind`: Returns a `DateTimeKind` for the currently used clock type (`DateTimeKind.Utc`, `DateTimeKind.Local` or `DateTimeKind.Unspecified`). -* `SupportsMultipleTimezone`: Returns `true` if currently used clock is UTC. - -## Time Zones - -This section covers the ABP Framework infrastructure related to managing time zones. - -### TimeZone Setting - -ABP Framework defines **a setting**, named `Abp.Timing.TimeZone`, that can be used to set and get the time zone for a user, [tenant](Multi-Tenancy.md) or globally for the application. The default value is `UTC`. - -See the [setting documentation](Settings.md) to learn more about the setting system. - -### ITimezoneProvider - -`ITimezoneProvider` is a service to simple convert [Windows Time Zone Id](https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values) values to [Iana Time Zone Name](https://www.iana.org/time-zones) values and vice verse. It also provides methods to get list of these time zones and get a `TimeZoneInfo` with a given name. - -It has been implemented using the [TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter) library. diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/Angular/Part-I.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/Angular/Part-II.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/Angular/Part-III.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md deleted file mode 100644 index 2867a3159f..0000000000 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md +++ /dev/null @@ -1,8 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [With Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md deleted file mode 100644 index b98395d6ac..0000000000 --- a/docs/en/Tutorials/Part-1.md +++ /dev/null @@ -1,523 +0,0 @@ -# Web Application Development Tutorial - Part 1: Creating the Server Side -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -````json -//[doc-nav] -{ - "Next": { - "Name": "The Book List Page", - "Path": "Tutorials/Part-2" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the database provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts: - -- **Part 1: Creating the server side (this part)** -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -> After downloading the source code, you might need to run some commands before running the application. See the _After Creating the Solution_ section below for more information. - -{{if UI == "MVC" && DB == "EF"}} - -### Video Tutorial - -This part is also recorded as a video tutorial and **published on YouTube**. - -{{end}} - -## Creating the Solution - -Before starting the development, create a new solution named `Acme.BookStore` and run it by following the [getting started tutorial](../Getting-Started.md). - -## After Creating the Solution - -### Installing the Client-Side Packages - -[ABP CLI](../CLI.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. - -However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. - -For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: - -```bash -abp install-libs -``` - -> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. - -{{if UI=="Blazor" || UI=="BlazorServer"}} - -### Bundling and Minification - -`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](../CLI.md). - -However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your `Blazor.Client` project: - -```bash -abp bundle -``` - -> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../UI/Blazor/Global-Scripts-Styles.md). - -{{end}} - -## Create the Book Entity - -**Domain layer** in the startup template is separated into two projects: - -- `Acme.BookStore.Domain` contains your [entities](../Entities.md), [domain services](../Domain-Services.md) and other core domain objects. -- `Acme.BookStore.Domain.Shared` contains `constants`, `enums` or other domain related objects that can be shared with clients. - -So, define your entities in the domain layer (`Acme.BookStore.Domain` project) of the solution. - -The main entity of the application is the `Book`. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain` project and add a `Book` class inside it: - -````csharp -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore.Books; - -public class Book : AuditedAggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } -} -```` - -* ABP Framework has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is a [Domain Driven Design](../Domain-Driven-Design.md) concept which can be thought as a root entity that is directly queried and worked on (see the [entities document](../Entities.md) for more). -* The `Book` entity inherits from the `AuditedAggregateRoot` which adds some base [auditing](../Audit-Logging.md) properties (like `CreationTime`, `CreatorId`, `LastModificationTime`...) on top of the `AggregateRoot` class. ABP automatically manages these properties for you. -* `Guid` is the **primary key type** of the `Book` entity. - -> This tutorial leaves the entity properties with **public get/set** for the sake of simplicity. See the [entities document](../Entities.md) if you want to learn more about DDD best practices. - -### BookType Enum - -The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it: - -````csharp -namespace Acme.BookStore.Books; - -public enum BookType -{ - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry -} -```` - -The final folder/file structure should be as shown below: - -![bookstore-book-and-booktype](images/bookstore-book-and-booktype.png) - -### Add the Book Entity to the DbContext - -{{if DB == "EF"}} - -EF Core requires that you relate the entities with your `DbContext`. The easiest way to do so is adding a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: - -````csharp -public class BookStoreDbContext : AbpDbContext -{ - public DbSet Books { get; set; } - //... -} -```` - -{{end}} - -{{if DB == "Mongo"}} - -Add a `IMongoCollection Books` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: - -```csharp -public class BookStoreMongoDbContext : AbpMongoDbContext -{ - public IMongoCollection Books => Collection(); - //... -} -``` - -{{end}} - -{{if DB == "EF"}} - -### Map the Book Entity to a Database Table - -Navigate to the `OnModelCreating` method in the `BookStoreDbContext` class and add the mapping code for the `Book` entity: - -````csharp -using Acme.BookStore.Books; -... - -namespace Acme.BookStore.EntityFrameworkCore; - -public class BookStoreDbContext : - AbpDbContext, - IIdentityDbContext, - ITenantManagementDbContext -{ - ... - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - ... - - /* Configure your own tables/entities inside here */ - - builder.Entity(b => - { - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", - BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - }); - } -} -```` - -* `BookStoreConsts` has constant values for the schema and table prefixes for your tables. You don't have to use it, but it's suggested to control the table prefixes in a single point. -* The `ConfigureByConvention()` method gracefully configures/maps the inherited properties. Always use it for all your entities. - -### Add Database Migration - -The startup solution is configured to use [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. - -Open a command-line terminal in the directory of the `Acme.BookStore.EntityFrameworkCore` project and type the following command: - -```bash -dotnet ef migrations add Created_Book_Entity -``` - -This will add a new migration class to the project: - -![bookstore-efcore-migration](./images/bookstore-efcore-migration.png) - -> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. - -{{end}} - -### Add Sample Seed Data - -> It's good to have some initial data in the database before running the application. This section introduces the [Data Seeding](../Data-Seeding.md) system of the ABP framework. You can skip this section if you don't want to create the data seeding, but it is suggested to follow along and learn this useful ABP Framework feature. - -Create a class that implements the `IDataSeedContributor` interface in the `*.Domain` project by copying the following code: - -```csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore; - -public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency -{ - private readonly IRepository _bookRepository; - - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } - } -} -``` - -* This code simply uses the `IRepository` (the default [repository](../Repositories.md)) to insert two books to the database in case there weren't any books in it. - -### Update the Database - -Run the `Acme.BookStore.DbMigrator` application to update the database: - -![bookstore-dbmigrator-on-solution](images/bookstore-dbmigrator-on-solution.png) - -`.DbMigrator` is a console application that can be run to **migrate the database schema** and **seed the data** on **development** and **production** environments. - -## Create the Application Service - -The application layer is separated into two projects: - -* `Acme.BookStore.Application.Contracts` contains your [DTO](../Data-Transfer-Objects.md)s and [application service](../Application-Services.md) interfaces. -* `Acme.BookStore.Application` contains the implementations of your application services. - -In this section, you will create an application service to get, create, update and delete books using the `CrudAppService` base class of the ABP Framework. - -### BookDto - -`CrudAppService` base class requires to define the fundamental DTOs for the entity. Create a `Books` folder (namespace) in the `Acme.BookStore.Application.Contracts` project and add a `BookDto` class inside it: - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books; - -public class BookDto : AuditedEntityDto -{ - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } -} -```` - -* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) for more details. -* The `BookDto` is used to transfer the book data to the presentation layer in order to show the book information on the UI. -* The `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` entity defined above. - -It will be needed to map the `Book` entities to the `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper pre-configured. So, you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore; - -public class BookStoreApplicationAutoMapperProfile : Profile -{ - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } -} -```` - -> See the [object to object mapping](../Object-To-Object-Mapping.md) document for details. - -### CreateUpdateBookDto - -Create a `CreateUpdateBookDto` class in the `Books` folder (namespace) of the `Acme.BookStore.Application.Contracts` project: - -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Books; - -public class CreateUpdateBookDto -{ - [Required] - [StringLength(128)] - public string Name { get; set; } = string.Empty; - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } -} -```` - -* This `DTO` class is used to get a book information from the user interface while creating or updating the book. -* It defines data annotation attributes (like `[Required]`) to define validations for the properties. `DTO`s are [automatically validated](https://docs.abp.io/en/abp/latest/Validation) by the ABP framework. - -As done to the `BookDto` above, we should define the mapping from the `CreateUpdateBookDto` object to the `Book` entity. The final class will be as shown below: - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore; - -public class BookStoreApplicationAutoMapperProfile : Profile -{ - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } -} -```` - -### IBookAppService - -Next step is to define an interface for the application service. Create an `IBookAppService` interface in the `Books` folder (namespace) of the `Acme.BookStore.Application.Contracts` project: - -````csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Books; - -public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book -{ - -} -```` - -* Defining interfaces for the application services **are not required** by the framework. However, it's suggested as a best practice. -* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually (which will be done for the authors in the next parts). -* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method (like using different DTOs for create and update). - -### BookAppService - -It is time to implement the `IBookAppService` interface. Create a new class, named `BookAppService` in the `Books` namespace (folder) of the `Acme.BookStore.Application` project: - -````csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books; - -public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService -{ - public BookAppService(IRepository repository) - : base(repository) - { - - } -} -```` - -* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD (create, read, update, delete) methods defined by the `ICrudAppService`. -* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](https://docs.abp.io/en/abp/latest/Repositories). -* `BookAppService` uses `IObjectMapper` service ([see](../Object-To-Object-Mapping.md)) to map the `Book` objects to the `BookDto` objects and `CreateUpdateBookDto` objects to the `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. We have defined the mappings before, so it will work as expected. - -## Auto API Controllers - -In a typical ASP.NET Core application, you create **API Controllers** to expose the application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them over HTTP. - -ABP can [**automagically**](../API/Auto-API-Controllers.md) configure your application services as MVC API Controllers by convention. - -### Swagger UI - -The startup template is configured to run the [Swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application ({{if UI=="MVC"}}`Acme.BookStore.Web`{{else}}`Acme.BookStore.HttpApi.Host`{{end}}) by pressing `CTRL+F5` and navigate to `https://localhost:/swagger/` on your browser. Replace `` with your own port number. - -You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: - -![bookstore-swagger](./images/bookstore-swagger.png) - -Swagger has a nice interface to test the APIs. - -If you try to execute the `[GET] /api/app/book` API to get a list of books, the server returns such a JSON result: - -````json -{ - "totalCount": 2, - "items": [ - { - "name": "The Hitchhiker's Guide to the Galaxy", - "type": 7, - "publishDate": "1995-09-27T00:00:00", - "price": 42, - "lastModificationTime": null, - "lastModifierId": null, - "creationTime": "2020-07-03T21:04:18.4607218", - "creatorId": null, - "id": "86100bb6-cbc1-25be-6643-39f62806969c" - }, - { - "name": "1984", - "type": 3, - "publishDate": "1949-06-08T00:00:00", - "price": 19.84, - "lastModificationTime": null, - "lastModifierId": null, - "creationTime": "2020-07-03T21:04:18.3174016", - "creatorId": null, - "id": "41055277-cce8-37d7-bb37-39f62806960b" - } - ] -} -```` - -That's pretty cool since we haven't written a single line of code to create the API controller, but now we have a fully working REST API! \ No newline at end of file diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md deleted file mode 100644 index 91476b8cd8..0000000000 --- a/docs/en/Tutorials/Part-10.md +++ /dev/null @@ -1,1236 +0,0 @@ -# Web Application Development Tutorial - Part 10: Book to Author Relation -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Previous": { - "Name": "Authors: User Interface", - "Path": "Tutorials/Part-9" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- **Part 10: Book to Author Relation (this part)** - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -## Introduction - -We have created `Book` and `Author` functionalities for the book store application. However, currently there is no relation between these entities. - -In this tutorial, we will establish a **1 to N** relation between the `Author` and the `Book` entities. - -## Add Relation to The Book Entity - -Open the `Books/Book.cs` in the `Acme.BookStore.Domain` project and add the following property to the `Book` entity: - -````csharp -public Guid AuthorId { get; set; } -```` - -{{if DB=="EF"}} - -> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will be doing below) which makes your application code simpler. - -{{end}} - -## Database & Data Migration - -Added a new, required `AuthorId` property to the `Book` entity. But, **what about the existing books** on the database? They currently don't have `AuthorId`s and this will be a problem when we try to run the application. - -This is a **typical migration problem** and the decision depends on your case; - -* If you haven't published your application to the production yet, you can just delete existing books in the database, or you can even delete the entire database in your development environment. -* You can update the existing data programmatically on data migration or seed phase. -* You can manually handle it on the database. - -We prefer to **delete the database** {{if DB=="EF"}}(you can run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. - -{{if DB=="EF"}} - -### Update the EF Core Mapping - -Locate to `OnModelCreating` method in the `BookStoreDbContext` class that under the `EntityFrameworkCore` folder of the `Acme.BookStore.EntityFrameworkCore` project and change the `builder.Entity` part as shown below: - -````csharp -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - - // ADD THE MAPPING FOR THE RELATION - b.HasOne().WithMany().HasForeignKey(x => x.AuthorId).IsRequired(); -}); -```` - -### Add New EF Core Migration - -The startup solution is configured to use [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. - -Open a command-line terminal in the directory of the `Acme.BookStore.EntityFrameworkCore` project and type the following command: - -````bash -dotnet ef migrations add Added_AuthorId_To_Book -```` - -This should create a new migration class with the following code in its `Up` method: - -````csharp -migrationBuilder.AddColumn( - name: "AuthorId", - table: "AppBooks", - type: "uniqueidentifier", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - -migrationBuilder.CreateIndex( - name: "IX_AppBooks_AuthorId", - table: "AppBooks", - column: "AuthorId"); - -migrationBuilder.AddForeignKey( - name: "FK_AppBooks_AppAuthors_AuthorId", - table: "AppBooks", - column: "AuthorId", - principalTable: "AppAuthors", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); -```` - -* Adds an `AuthorId` field to the `AppBooks` table. -* Creates an index on the `AuthorId` field. -* Declares the foreign key to the `AppAuthors` table. - -> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. - -{{end}} - -## Change the Data Seeder - -Since the `AuthorId` is a required property of the `Book` entity, current data seeder code can not work. Open the `BookStoreDataSeederContributor` in the `Acme.BookStore.Domain` project and change as the following: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore; - -public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency -{ - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - var orwell = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - var douglas = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = orwell.Id, // SET THE AUTHOR - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = douglas.Id, // SET THE AUTHOR - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } -} -```` - -The only change is that we set the `AuthorId` properties of the `Book` entities. - -> Delete existing books or delete the database before executing the `DbMigrator`. See the *Database & Data Migration* section above for more info. - -{{if DB=="EF"}} - -You can now run the `.DbMigrator` console application to **migrate** the **database schema** and **seed** the initial data. - -{{else if DB=="Mongo"}} - -You can now run the `.DbMigrator` console application to **seed** the initial data. - -{{end}} - -## Application Layer - -We will change the `BookAppService` to support the Author relation. - -### Data Transfer Objects - -Let's begin from the DTOs. - -#### BookDto - -Open the `BookDto` class in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add the following properties: - -```csharp -public Guid AuthorId { get; set; } -public string AuthorName { get; set; } -``` - -The final `BookDto` class should be following: - -```csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books; - -public class BookDto : AuditedEntityDto -{ - public Guid AuthorId { get; set; } - - public string AuthorName { get; set; } - - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } -} -``` - -#### CreateUpdateBookDto - -Open the `CreateUpdateBookDto` class in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add an `AuthorId` property as shown: - -````csharp -public Guid AuthorId { get; set; } -```` - -#### AuthorLookupDto - -Create a new class, `AuthorLookupDto`, inside the `Books` folder of the `Acme.BookStore.Application.Contracts` project: - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books; - -public class AuthorLookupDto : EntityDto -{ - public string Name { get; set; } -} -```` - -This will be used in a new method that will be added to the `IBookAppService`. - -### IBookAppService - -Open the `IBookAppService` interface in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add a new method, named `GetAuthorLookupAsync`, as shown below: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Books; - -public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book -{ - // ADD the NEW METHOD - Task> GetAuthorLookupAsync(); -} -```` - -This new method will be used from the UI to get a list of authors and fill a dropdown list to select the author of a book. - -### BookAppService - -Open the `BookAppService` class in the `Books` folder of the `Acme.BookStore.Application` project and replace the file content with the following code: - -{{if DB=="EF"}} - -```csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Entities; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books; - -[Authorize(BookStorePermissions.Books.Default)] -public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService -{ - private readonly IAuthorRepository _authorRepository; - - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } - - public override async Task GetAsync(Guid id) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - where book.Id == id - select new { book, author }; - - //Execute the query and get the book with author - var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); - if (queryResult == null) - { - throw new EntityNotFoundException(typeof(Book), id); - } - - var bookDto = ObjectMapper.Map(queryResult.book); - bookDto.AuthorName = queryResult.author.Name; - return bookDto; - } - - public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - select new {book, author}; - - //Paging - query = query - .OrderBy(NormalizeSorting(input.Sorting)) - .Skip(input.SkipCount) - .Take(input.MaxResultCount); - - //Execute the query and get a list - var queryResult = await AsyncExecuter.ToListAsync(query); - - //Convert the query result to a list of BookDto objects - var bookDtos = queryResult.Select(x => - { - var bookDto = ObjectMapper.Map(x.book); - bookDto.AuthorName = x.author.Name; - return bookDto; - }).ToList(); - - //Get the total count with another query - var totalCount = await Repository.GetCountAsync(); - - return new PagedResultDto( - totalCount, - bookDtos - ); - } - - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } - - private static string NormalizeSorting(string sorting) - { - if (sorting.IsNullOrEmpty()) - { - return $"book.{nameof(Book.Name)}"; - } - - if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) - { - return sorting.Replace( - "authorName", - "author.Name", - StringComparison.OrdinalIgnoreCase - ); - } - - return $"book.{sorting}"; - } -} -``` - -Let's see the changes we've done: - -* Added `[Authorize(BookStorePermissions.Books.Default)]` to authorize the methods we've newly added/overrode (remember, authorize attribute is valid for all the methods of the class when it is declared for a class). -* Injected `IAuthorRepository` to query from the authors. -* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`. - * Used a simple LINQ expression to join books and authors and query them together for the given book id. - * Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. It is a way to use asynchronous LINQ extensions without depending on the database provider API. Check the [repository documentation](../Repositories.md) to understand why we've used it. - * Throws an `EntityNotFoundException` which results an `HTTP 404` (not found) result if requested book was not present in the database. - * Finally, created a `BookDto` object using the `ObjectMapper`, then assigning the `AuthorName` manually. -* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. The logic is similar to the previous method, so you can easily understand the code. -* Created a new method: `GetAuthorLookupAsync`. This simple gets all the authors. The UI uses this method to fill a dropdown list and select and author while creating/editing books. - -{{else if DB=="Mongo"}} - -```csharp -using System; -using System.Collections.Generic; -using System.Linq.Dynamic.Core; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books; - -[Authorize(BookStorePermissions.Books.Default)] -public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService -{ - private readonly IAuthorRepository _authorRepository; - - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; - } - - public async override Task GetAsync(Guid id) - { - var book = await Repository.GetAsync(id); - var bookDto = ObjectMapper.Map(book); - - var author = await _authorRepository.GetAsync(book.AuthorId); - bookDto.AuthorName = author.Name; - - return bookDto; - } - - public async override Task> - GetListAsync(PagedAndSortedResultRequestDto input) - { - //Set a default sorting, if not provided - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Book.Name); - } - - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Get the books - var books = await AsyncExecuter.ToListAsync( - queryable - .OrderBy(input.Sorting) - .Skip(input.SkipCount) - .Take(input.MaxResultCount) - ); - - //Convert to DTOs - var bookDtos = ObjectMapper.Map, List>(books); - - //Get a lookup dictionary for the related authors - var authorDictionary = await GetAuthorDictionaryAsync(books); - - //Set AuthorName for the DTOs - bookDtos.ForEach(bookDto => bookDto.AuthorName = - authorDictionary[bookDto.AuthorId].Name); - - //Get the total count with another query (required for the paging) - var totalCount = await Repository.GetCountAsync(); - - return new PagedResultDto( - totalCount, - bookDtos - ); - } - - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } - - private async Task> - GetAuthorDictionaryAsync(List books) - { - var authorIds = books - .Select(b => b.AuthorId) - .Distinct() - .ToArray(); - - var queryable = await _authorRepository.GetQueryableAsync(); - - var authors = await AsyncExecuter.ToListAsync( - queryable.Where(a => authorIds.Contains(a.Id)) - ); - - return authors.ToDictionary(x => x.Id, x => x); - } -} -``` - -Let's see the changes we've done: - -* Added `[Authorize(BookStorePermissions.Books.Default)]` to authorize the methods we've newly added/overrode (remember, authorize attribute is valid for all the methods of the class when it is declared for a class). -* Injected `IAuthorRepository` to query from the authors. -* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`. -* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. This code separately queries the authors from database and sets the name of the authors in the application code. Instead, you could create a custom repository method and perform a join query or take the power of the MongoDB API to get the books and their authors in a single query, which would be more performant. -* Created a new method: `GetAuthorLookupAsync`. This simple gets all the authors. The UI uses this method to fill a dropdown list and select and author while creating/editing books. - -{{end}} - -### Object to Object Mapping Configuration - -Introduced the `AuthorLookupDto` class and used object mapping inside the `GetAuthorLookupAsync` method. So, we need to add a new mapping definition inside the `BookStoreApplicationAutoMapperProfile.cs` file of the `Acme.BookStore.Application` project: - -````csharp -CreateMap(); -```` - -## Unit Tests - -Some of the unit tests will fail since we made some changed on the `AuthorAppService`. Open the `BookAppService_Tests` in the `Books` folder of the `Acme.BookStore.Application.Tests` project and change the content as the following: - -```csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Modularity; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books; - -public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IBookAppService _bookAppService; - private readonly IAuthorAppService _authorAppService; - - protected BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - _authorAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984" && - b.AuthorName == "George Orwell"); - } - - [Fact] - public async Task Should_Create_A_Valid_Book() - { - var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); - var firstAuthor = authors.Items.First(); - - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - AuthorId = firstAuthor.Id, - Name = "New test book 42", - Price = 10, - PublishDate = System.DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); - } -} -``` - -* Changed the assertion condition in the `Should_Get_List_Of_Books` from `b => b.Name == "1984"` to `b => b.Name == "1984" && b.AuthorName == "George Orwell"` to check if the author name was filled. -* Changed the `Should_Create_A_Valid_Book` method to set the `AuthorId` while creating a new book, since it is required anymore. - -## The User Interface - -{{if UI=="MVC"}} - -### The Book List - -Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add an `authorName` column between the `name` and `type` columns: - -````js -... -{ - title: l('Name'), - data: "name" -}, - -// ADDED the NEW AUTHOR NAME COLUMN -{ - title: l('Author'), - data: "authorName" -}, - -{ - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType.' + data); - } -}, -... -```` - -When you run the application, you can see the *Author* column on the table: - -![bookstore-added-author-to-book-list](images/bookstore-added-author-to-book-list-2.png) - -### Create Modal - -Open the `Pages/Books/CreateModal.cshtml.cs` in the `Acme.BookStore.Web` project and change the file content as shown below: - -```csharp -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Books; - -public class CreateModalModel : BookStorePageModel -{ - [BindProperty] - public CreateBookViewModel Book { get; set; } - - public List Authors { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel( - IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - Book = new CreateBookViewModel(); - - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync( - ObjectMapper.Map(Book) - ); - return NoContent(); - } - - public class CreateBookViewModel - { - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } - - [Required] - [StringLength(128)] - public string Name { get; set; } = string.Empty; - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } - } -} -``` - -* Changed type of the `Book` property from `CreateUpdateBookDto` to the new `CreateBookViewModel` class defined in this file. The main motivation of this change to customize the model class based on the User Interface (UI) requirements. We didn't want to use UI-related `[SelectItems(nameof(Authors))]` and `[DisplayName("Author")]` attributes inside the `CreateUpdateBookDto` class. -* Added `Authors` property that is filled inside the `OnGetAsync` method using the `IBookAppService.GetAuthorLookupAsync` method defined before. -* Changed the `OnPostAsync` method to map `CreateBookViewModel` object to a `CreateUpdateBookDto` object since `IBookAppService.CreateAsync` expects a parameter of this type. - -### Edit Modal - -Open the `Pages/Books/EditModal.cshtml.cs` in the `Acme.BookStore.Web` project and change the file content as shown below: - -```csharp -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Books; - -public class EditModalModel : BookStorePageModel -{ - [BindProperty] - public EditBookViewModel Book { get; set; } - - public List Authors { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync(Guid id) - { - var bookDto = await _bookAppService.GetAsync(id); - Book = ObjectMapper.Map(bookDto); - - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync( - Book.Id, - ObjectMapper.Map(Book) - ); - - return NoContent(); - } - - public class EditBookViewModel - { - [HiddenInput] - public Guid Id { get; set; } - - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } - - [Required] - [StringLength(128)] - public string Name { get; set; } = string.Empty; - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } - } -} -``` - -* Changed type of the `Book` property from `CreateUpdateBookDto` to the new `EditBookViewModel` class defined in this file, just like done before for the create modal above. -* Moved the `Id` property inside the new `EditBookViewModel` class. -* Added `Authors` property that is filled inside the `OnGetAsync` method using the `IBookAppService.GetAuthorLookupAsync` method. -* Changed the `OnPostAsync` method to map `EditBookViewModel` object to a `CreateUpdateBookDto` object since `IBookAppService.UpdateAsync` expects a parameter of this type. - -These changes require a small change in the `EditModal.cshtml`. Remove the `` tag since we no longer need to it (since moved it to the `EditBookViewModel`). The final content of the `EditModal.cshtml` should be following: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - -```` - -### Object to Object Mapping Configuration - -The changes above requires to define some object to object mappings. Open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and add the following mapping definitions inside the constructor: - -```csharp -CreateMap(); -CreateMap(); -CreateMap(); -``` - -You can run the application and try to create a new book or update an existing book. You will see a drop down list on the create/update form to select the author of the book: - -![bookstore-added-authors-to-modals](images/bookstore-added-authors-to-modals-2.png) - -{{else if UI=="NG"}} - -### Service Proxy Generation - -Since the HTTP APIs have been changed, you need to update Angular client side [service proxies](../UI/Angular/Service-Proxies.md). Before running `generate-proxy` command, your host must be up and running. - -Run the following command in the `angular` folder (you may need to stop the angular application): - -```bash -abp generate-proxy -t ng -``` -This command will update the service proxy files under the `/src/app/proxy/` folder. - -### The Book List - -Book list page change is trivial. Open the `/src/app/book/book.component.html` and add the following column definition between the `Name` and `Type` columns: - -````html - -```` - -When you run the application, you can see the *Author* column on the table: - -![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular-2.png) - -### Create/Edit Forms - -The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below: - -![bookstore-angular-author-selection](images/bookstore-angular-author-selection-2.png) - -Added the Author dropdown as the first element in the form. - -Open the `/src/app/book/book.component.ts` and and change the content as shown below: - -````js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions, AuthorLookupDto } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; - - selectedBook = {} as BookDto; - - authors$: Observable; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder, - private confirmation: ConfirmationService - ) { - this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items)); - } - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.selectedBook = {} as BookDto; - this.buildForm(); - this.isModalOpen = true; - } - - editBook(id: string) { - this.bookService.get(id).subscribe((book) => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - authorId: [this.selectedBook.authorId || null, Validators.required], - name: [this.selectedBook.name || null, Validators.required], - type: [this.selectedBook.type || null, Validators.required], - publishDate: [ - this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - Validators.required, - ], - price: [this.selectedBook.price || null, Validators.required], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - const request = this.selectedBook.id - ? this.bookService.update(this.selectedBook.id, this.form.value) - : this.bookService.create(this.form.value); - - request.subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } - - delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.bookService.delete(id).subscribe(() => this.list.get()); - } - }); - } -} -```` - -* Added imports for the `AuthorLookupDto`, `Observable` and `map`. -* Added `authors$: Observable;` field after the `selectedBook`. -* Added `this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));` into the constructor. -* Added ` authorId: [this.selectedBook.authorId || null, Validators.required],` into the `buildForm()` function. - -Open the `/src/app/book/book.component.html` and add the following form group just before the book name form group: - -````html -
- * - -
-```` - -That's all. Just run the application and try to create or edit an author. - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -### The Book List - -It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the `Acme.BookStore.Blazor.Client` project and add the following `DataGridColumn` definition just after the `Name` (book name) column: - -````xml - -```` - -When you run the application, you can see the *Author* column on the table: - -![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors-2.png) - -### Create Book Modal - -Add the following field to the `@code` section of the `Books.razor` file: - -````csharp -IReadOnlyList authorList = Array.Empty(); -```` - -Override the `OnInitializedAsync` method and adding the following code: - -````csharp -protected override async Task OnInitializedAsync() -{ - await base.OnInitializedAsync(); - authorList = (await AppService.GetAuthorLookupAsync()).Items; -} -```` - -* It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. - -Override the `OpenCreateModalAsync` method and adding the following code: - -````csharp -protected override async Task OpenCreateModalAsync() -{ - if (!authorList.Any()) - { - throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); - } - - await base.OpenCreateModalAsync(); - NewEntity.AuthorId = authorList.First().Id; -} -```` - -The final `@code` block should be the following: - -````csharp -@code -{ - //ADDED A NEW FIELD - IReadOnlyList authorList = Array.Empty(); - - public Books() // Constructor - { - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } - - //GET AUTHORS ON INITIALIZATION - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - authorList = (await AppService.GetAuthorLookupAsync()).Items; - } - - protected override async Task OpenCreateModalAsync() - { - if (!authorList.Any()) - { - throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); - } - - await base.OpenCreateModalAsync(); - NewEntity.AuthorId = authorList.First().Id; - } -} -```` - -Finally, add the following `Field` definition into the `ModalBody` of the *Create* modal, as the first item, before the `Name` field: - -````xml - - @L["Author"] - - -```` - -This requires to add a new localization key to the `en.json` file: - -````js -"AnAuthorIsRequiredForCreatingBook": "An author is required to create a book" -```` - -You can run the application to see the *Author Selection* while creating a new book: - -![book-create-modal-with-author](images/book-create-modal-with-author-2.png) - -### Edit Book Modal - -Add the following `Field` definition into the `ModalBody` of the *Edit* modal, as the first item, before the `Name` field: - -````xml - - @L["Author"] - - -```` - -That's all. We are reusing the `authorList` defined for the *Create* modal. - -{{end}} diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md deleted file mode 100644 index a34bbfc2a6..0000000000 --- a/docs/en/Tutorials/Part-2.md +++ /dev/null @@ -1,684 +0,0 @@ -# Web Application Development Tutorial - Part 2: The Book List Page -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -````json -//[doc-nav] -{ - "Next": { - "Name": "Creating, Updating and Deleting Books", - "Path": "Tutorials/Part-3" - }, - "Previous": { - "Name": "Creating the Server Side", - "Path": "Tutorials/Part-1" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts: - -- [Part 1: Creating the server side](Part-1.md) -- **Part 2: The book list page (this part)** -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -{{if UI == "MVC" && DB == "EF"}} - -### Video Tutorial - -This part is also recorded as a video tutorial and **published on YouTube**. - -{{end}} - -{{if UI == "MVC"}} - -## Dynamic JavaScript Proxies - -It's common to call the HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. - -ABP **dynamically** creates **[JavaScript Proxies](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)** for all the API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. - -### Testing in the Developer Console - -You can easily test the JavaScript proxies using your favorite browser's **Developer Console**. Run the application, open your browser's **developer tools** (*shortcut is generally F12*), switch to the **Console** tab, type the following code and press enter: - -````js -acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); }); -```` - -* `acme.bookStore.books` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). -* `book` is the conventional name for the `BookAppService` (removed `AppService` postfix and converted to camelCase). -* `getList` is the conventional name for the `GetListAsync` method defined in the `CrudAppService` base class (removed `Async` postfix and converted to camelCase). -* The `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional with default values, so you can send an empty object). -* The `getList` function returns a `promise`. You can pass a callback to the `then` (or `done`) function to get the result returned from the server. - -Running this code produces the following output: - -![bookstore-javascript-proxy-console](images/bookstore-javascript-proxy-console.png) - -You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: - -![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png) - -Let's **create a new book** using the `create` function: - -````js -acme.bookStore.books.book.create({ - name: 'Foundation', - type: 7, - publishDate: '1951-05-24', - price: 21.5 - }).then(function (result) { - console.log('successfully created the book with id: ' + result.id); - }); -```` - -> If you downloaded the source code of the tutorial and are following the steps from the sample, you should also pass the `authorId` parameter to the create method for **creating a new book**. - -You should see a message in the console that looks something like this: - -````text -successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 -```` - -Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. - -We will use these dynamic proxy functions in the next sections to communicate with the server. - -{{end}} - -## Localization - -Before starting the UI development, we first want to prepare the localization texts (you normally do this when needed while developing your application). - -Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: - -![bookstore-localization-files](images/bookstore-localization-files-v2.png) - -Open the `en.json` (*the English translations*) file and change the content as shown below: - -````json -{ - "Culture": "en", - "Texts": { - "Menu:Home": "Home", - "Welcome": "Welcome", - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", - "Menu:BookStore": "Book Store", - "Menu:Books": "Books", - "Actions": "Actions", - "Close": "Close", - "Delete": "Delete", - "Edit": "Edit", - "PublishDate": "Publish date", - "NewBook": "New book", - "Name": "Name", - "Type": "Type", - "Price": "Price", - "CreationTime": "Creation time", - "AreYouSure": "Are you sure?", - "AreYouSureToDelete": "Are you sure you want to delete this item?", - "Enum:BookType.0": "Undefined", - "Enum:BookType.1": "Adventure", - "Enum:BookType.2": "Biography", - "Enum:BookType.3": "Dystopia", - "Enum:BookType.4": "Fantastic", - "Enum:BookType.5": "Horror", - "Enum:BookType.6": "Science", - "Enum:BookType.7": "Science fiction", - "Enum:BookType.8": "Poetry" - } -} -```` - -* Localization key names are arbitrary. You can set any name. We prefer some conventions for specific text types; - * Add `Menu:` prefix for menu items. - * Use `Enum:.` or `.` naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums in some proper cases. - -If a text is not defined in the localization file, it **falls back** to the localization key (as ASP.NET Core's standard behavior). - -> ABP's localization system is built on the [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. Check the [localization document](../Localization.md) for details. - -{{if UI == "MVC"}} - -## Create a Books Page - -It's time to create something visible and usable! Instead of the classic MVC, we will use the [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. - -Create a `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project. Add a new Razor Page by right clicking the Books folder then selecting **Add > Razor Page** menu item. Name it as `Index`: - -![bookstore-add-index-page](images/bookstore-add-index-page-v2.png) - -Open the `Index.cshtml` and change the whole content as shown below: - -````html -@page -@using Acme.BookStore.Web.Pages.Books -@model IndexModel - -

Books

-```` - -`Index.cshtml.cs` content should be like that: - -```csharp -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Acme.BookStore.Web.Pages.Books; - -public class IndexModel : PageModel -{ - public void OnGet() - { - - } -} -``` - -### Add Books Page to the Main Menu - -Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ) - ) -); -```` - -Run the project, login to the application with the username `admin` and the password `1q2w3E*` and you can see that the new menu item has been added to the main menu: - -![bookstore-menu-items](images/bookstore-new-menu-item-2.png) - -When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page. - -### Book List - -We will use the [Datatables.net](https://datatables.net/) jQuery library to show the book list. Datatables library completely works via AJAX, it is fast, popular and provides a good user experience. - -> Datatables library is configured in the startup template, so you can directly use it in any page without including any style or script file for your page. - -#### Index.cshtml - -Change the `Pages/Books/Index.cshtml` as the following: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@section scripts -{ - -} - - -

@L["Books"]

-
- - - -
-```` - -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to the standard `script` tag. It handles **minification** and **versioning**. Check the [bundling & minification document](../UI/AspNetCore/Bundling-Minification.md) for details. -* `abp-card` is a tag helper for Twitter Bootstrap's [card component](https://getbootstrap.com/docs/4.5/components/card/). There are other useful tag helpers provided by the ABP Framework to easily use most of [bootstrap](https://getbootstrap.com/)'s components. You could use the regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by the help of the IntelliSense and compiles time type checking. For further information, check the [tag helpers](../UI/AspNetCore/Tag-Helpers/Index.md) document. - -#### Index.js - -Create an `Index.js` file under the `Pages/Books` folder: - -![bookstore-index-js-file](images/bookstore-index-js-file-v3.png) - -The content of the file is shown below: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType.' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } - } - ] - }) - ); -}); -```` - -* `abp.localization.getResource` gets a function that is used to localize text using the same JSON file defined on the server side. In this way, you can share the localization values with the client side. -* `abp.libs.datatables.normalizeConfiguration` is a helper function defined by the ABP Framework. There's no requirement to use it, but it simplifies the [Datatables](https://datatables.net/) configuration by providing conventional default values for missing options. -* `abp.libs.datatables.createAjax` is another helper function to adapt the ABP's dynamic JavaScript API proxies to the [Datatable](https://datatables.net/)'s expected parameter format -* `acme.bookStore.books.book.getList` is the dynamic JavaScript proxy function introduced before. -* [luxon](https://moment.github.io/luxon/) library is also a standard library that is pre-configured in the solution, so you can use to perform date/time operations easily. - -> See [Datatables documentation](https://datatables.net/manual/) for all configuration options. - -## Run the Final Application - -You can run the application! The final UI of this part is shown below: - -![Book list](images/bookstore-book-list-4.png) - -This is a fully working, server side paged, sorted and localized table of books. - -{{else if UI == "NG"}} - -## Install NPM packages - -> Notice: This tutorial is based on the ABP Framework v3.1.0+ If your project version is older, then please upgrade your solution. Check the [migration guide](../UI/Angular/Migration-Guide-v3.md) if you are upgrading an existing project with v2.x. - -If you haven't done it before, open a new command line interface (terminal window) and go to your `angular` folder and then run the `yarn` command to install the NPM packages: - -```bash -yarn -``` - -## Create a Books Page - -It's time to create something visible and usable! There are some tools that we will use when developing the Angular frontend application: - -- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. -- [Ngx-Datatable](https://swimlane.gitbook.io/ngx-datatable/) will be used as the datatable library. - -Run the following command line to create a new module, named `BookModule` in the root folder of the angular application: - -```bash -yarn ng generate module book --module app --routing --route books -``` - -This command should produce the following output: - -````bash -> yarn ng generate module book --module app --routing --route books - -yarn run v1.19.1 -$ ng generate module book --module app --routing --route books -CREATE src/app/book/book-routing.module.ts (336 bytes) -CREATE src/app/book/book.module.ts (335 bytes) -CREATE src/app/book/book.component.html (19 bytes) -CREATE src/app/book/book.component.spec.ts (614 bytes) -CREATE src/app/book/book.component.ts (268 bytes) -CREATE src/app/book/book.component.scss (0 bytes) -UPDATE src/app/app-routing.module.ts (1289 bytes) -Done in 3.88s. -```` - -### BookModule - -Open the `/src/app/book/book.module.ts` and replace the content as shown below: - -````js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { BookRoutingModule } from './book-routing.module'; -import { BookComponent } from './book.component'; - -@NgModule({ - declarations: [BookComponent], - imports: [ - BookRoutingModule, - SharedModule - ] -}) -export class BookModule { } - -```` - -* Added the `SharedModule`. `SharedModule` exports some common modules needed to create user interfaces. -* `SharedModule` already exports the `CommonModule`, so we've removed the `CommonModule`. - -### Routing - -The generated code places the new route definition to the `src/app/app-routing.module.ts` file as shown below: - -````js -const routes: Routes = [ - // other route definitions... - { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) }, -]; -```` - -Now, open the `src/app/route.provider.ts` file and replace the `configureRoutes` function declaration as shown below: - -```js -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', - order: 1, - layout: eLayoutType.application, - }, - { - path: '/book-store', - name: '::Menu:BookStore', - iconClass: 'fas fa-book', - order: 2, - layout: eLayoutType.application, - }, - { - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - }, - ]); - }; -} -``` - -`RoutesService` is a service provided by the ABP Framework to configure the main menu and the routes. - -* `path` is the URL of the route. -* `name` is the localized menu item name (check the [localization document](../UI/Angular/Localization.md) for details). -* `iconClass` is the icon of the menu item (you can use [Font Awesome](https://fontawesome.com/) icons by default). -* `order` is the order of the menu item. -* `layout` is the layout of the BooksModule's routes (there are three types of pre-defined layouts: `eLayoutType.application`, `eLayoutType.account` or `eLayoutType.empty`). - -For more information, check the [RoutesService document](../UI/Angular/Modifying-the-Menu.md#via-routesservice). - -### Service Proxy Generation - -[ABP CLI](../CLI.md) provides a `generate-proxy` command that generates client proxies for your HTTP APIs to make your HTTP APIs easy to consume by the client side. Before running the `generate-proxy` command, your host must be up and running. - -> **Warning**: There is a problem with IIS Express; it doesn't allow connecting to the application from another process. If you are using Visual Studio, select the `Acme.BookStore.HttpApi.Host` instead of IIS Express in the run button drop-down list, as shown in the figure below: - -![vs-run-without-iisexpress](images/vs-run-without-iisexpress.png) - -Once the host application is running, execute the following command in the `angular` folder: - -```bash -abp generate-proxy -t ng -``` - -This command will create the following files under the `/src/app/proxy/books` folder: - -![Generated files](images/generated-proxies-3.png) - -### BookComponent - -Open the `/src/app/book/book.component.ts` file and replace the content as below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto } from '@proxy/books'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } -} -``` - -* We imported and injected the generated `BookService`. -* We are using the [ListService](../UI/Angular/List-Service.md), a utility service from the ABP Framework which provides easy pagination, sorting and searching. - -Open the `/src/app/book/book.component.html` and replace the content as shown below: - -```html -
-
-
-
-
- {%{{{ '::Menu:Books' | abpLocalization }}}%} -
-
-
-
-
-
- - - - - {%{{{ '::Enum:BookType.' + row.type | abpLocalization }}}%} - - - - - {%{{{ row.publishDate | date }}}%} - - - - - {%{{{ row.price | currency }}}%} - - - -
-
-``` - -Now you can see the final result on your browser: - -![Book list final result](images/bookstore-book-list-angular.png) - -{{else if UI == "Blazor" || UI == "BlazorServer"}} - -## Create a Books Page - -It's time to create something visible and usable! Right click on the `Pages` folder under the `Acme.BookStore.Blazor.Client` project and add a new **razor component**, named `Books.razor`: - -![blazor-add-books-component](images/blazor-add-books-component.png) - -Replace the contents of this component as shown below: - -````html -@page "/books" - -

Books

- -@code { - -} -```` - -### Add the Books Page to the Main Menu - -Open the `BookStoreMenuContributor` class in the `Blazor.Client` project add the following code to the end of the `ConfigureMainMenuAsync` method: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - ) - ) -); -```` - -Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu: - -![blazor-menu-bookstore](images/bookstore-new-menu-item-2.png) - -When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page. - -### Book List - -We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including Bootstrap. - -ABP Framework provides a generic base class - `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff. - -Open the `Books.razor` and replace the content as the following: - -````xml -@page "/books" -@using Volo.Abp.Application.Dtos -@using Acme.BookStore.Books -@using Acme.BookStore.Localization -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L -@inherits AbpCrudPageBase - - - -

@L["Books"]

-
- - - - - - - @L[$"Enum:BookType.{context.Type}"] - - - - - @context.PublishDate.ToShortDateString() - - - - - - - @context.CreationTime.ToLongDateString() - - - - - -
-```` - -> If you see some syntax errors, you can ignore them if your application is properly built and running. Visual Studio still has some bugs with Blazor. - -* Inherited from `AbpCrudPageBase` which implements all the CRUD details for us. -* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class. -* Injected `IStringLocalizer` (as `L` object) and used for localization. - -While the code above is pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. - -#### About the AbpCrudPageBase - -We will continue benefitting from `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../API/Dynamic-CSharp-API-Clients.md) system of the ABP Framework). We will do it manually for the authors page to demonstrate how to call the server side HTTP APIs in your Blazor applications. - -## Run the Final Application - -You can run the application! The final UI of this part is shown below: - -![blazor-bookstore-book-list](images/blazor-bookstore-book-list-2.png) - -This is a fully working, server side paged, sorted and localized table of books. - -{{end # UI }} diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md deleted file mode 100644 index e1972bc9f0..0000000000 --- a/docs/en/Tutorials/Part-3.md +++ /dev/null @@ -1,1571 +0,0 @@ -# Web Application Development Tutorial - Part 3: Creating, Updating and Deleting Books -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Integration Tests", - "Path": "Tutorials/Part-4" - }, - "Previous": { - "Name": "The Book List Page", - "Path": "Tutorials/Part-2" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts: - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- **Part 3: Creating, updating and deleting books (this part)** -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -{{if UI == "MVC" && DB == "EF"}} - -### Video Tutorial - -This part is also recorded as a video tutorial and **published on YouTube**. - -{{end}} - -{{if UI == "MVC"}} - -## Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like the image below: - -![bookstore-create-dialog](./images/bookstore-create-dialog-3.png) - -### Create the Modal Form - -Create a new razor page named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project. - -![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) - -#### CreateModal.cshtml.cs - -Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace it with the following code: - -````C# -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public void OnGet() - { - Book = new CreateUpdateBookDto(); - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* This class is derived from the `BookStorePageModel` instead of the standard `PageModel`. `BookStorePageModel` indirectly inherits the `PageModel` and adds some common properties & methods that can be shared in your page model classes. -* `[BindProperty]` attribute on the `Book` property binds post request data to this property. -* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. -* It creates a new `CreateUpdateBookDto` object in the `OnGet` method. ASP.NET Core can work without creating a new instance like that. However, it doesn't create an instance for you and if your class has some default value assignments or code execution in the class constructor, they won't work. For this case, we set default values for some of the `CreateUpdateBookDto` properties. - -#### CreateModal.cshtml - -Open the `CreateModal.cshtml` file and paste the code below: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model CreateModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - -```` - -* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateUpdateBookDto` model class. -* `abp-model` attribute indicates the model object where it's the `Book` property in this case. -* `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page). - -> Tip: `Layout` should be `null` just as done in this example since we don't want to include all the layout for the modals when they are loaded via AJAX. - -### Add the "New book" Button - -Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag as below: - -````html - - - - @L["Books"] - - - - - - -```` - -The final content of `Index.cshtml` is shown below: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@section scripts -{ - -} - - - - - - @L["Books"] - - - - - - - - - - -```` - -This adds a new button called **New book** to the **top-right** of the table: - -![bookstore-new-book-button](./images/bookstore-new-book-button-3.png) - -Open the `Pages/Books/Index.js` file and add the following code right after the `Datatable` configuration: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` is a helper class to manage modals on the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. -* `createModal.onResult(...)` used to refresh the data table after creating a new book. -* `createModal.open();` is used to open the model to create a new book. - -The final content of the `Index.js` file should be like this: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType.' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - dataFormat: "datetime" - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - dataFormat: "datetime" - } - ] - }) - ); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -Now, you can **run the application** and add some new books using the new modal form. - -## Updating a Book - -Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: - -![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) - -### EditModal.cshtml.cs - -Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace it with the following code: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books; - -public class EditModalModel : BookStorePageModel -{ - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } -} -```` - -* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get the `Id` value from the query string parameter of the request. -* In the `OnGetAsync` method, we get the `BookDto` from the `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`. -* The `OnPostAsync` uses `BookAppService.UpdateAsync(...)` to update the entity. - -### Mapping from BookDto to CreateUpdateBookDto - -To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` file in the `Acme.BookStore.Web` project and change it as shown below: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile -{ - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } -} -```` - -* We have just added `CreateMap();` to define this mapping. - -> Notice that we do the mapping definition in the web layer as a best practice since it is only needed in this layer. - -### EditModal.cshtml - -Replace `EditModal.cshtml` content with the following content: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - - -```` - -This page is very similar to `CreateModal.cshtml`, except: - -* It includes an `abp-input` for the `Id` property to store the `Id` of the editing book (which is a hidden input). -* It uses `Books/EditModal` as the post URL. - -### Add "Actions" Dropdown to the Table - -We will add a dropdown button to the table named *Actions*. - -Open the `Pages/Books/Index.js` file and replace the content as below: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType.' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - dataFormat: "datetime" - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - dataFormat: "datetime" - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* Added a new `ModalManager` named `editModal` to open the edit modal dialog. -* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button. -* The "*Edit*" action simply calls `editModal.open()` to open the edit dialog. -* The `editModal.onResult(...)` callback refreshes the data table when you close the edit modal. - -You can run the application and edit any book by selecting the edit action on a book. - -The final UI looks as below: - -![bookstore-books-table-actions](./images/bookstore-edit-button-3.png) - -> Notice that you don't see the "Actions" button in the figure below. Instead, you see an "Edit" button. ABP is smart enough to show a single simple button instead of a actions dropdown button when the dropdown has only a single item. After the next section, it will turn to a drop down button. - -## Deleting a Book - -Open the `Pages/Books/Index.js` file and add a new item to the `rowAction` `items`: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.books.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* The `confirmMessage` option is used to ask a confirmation question before executing the `action`. -* The `acme.bookStore.books.book.delete(...)` method makes an AJAX request to the server to delete a book. -* `abp.notify.info()` shows a notification after the delete operation. - -Since we've used two new localization texts (`BookDeletionConfirmationMessage` and `SuccessfullyDeleted`) you need to add these to the localization file (`en.json` under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project): - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?", -"SuccessfullyDeleted": "Successfully deleted!" -```` - -The final `Index.js` content is shown below: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l( - 'BookDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - acme.bookStore.books.book - .delete(data.record.id) - .then(function() { - abp.notify.info( - l('SuccessfullyDeleted') - ); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType.' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - dataFormat: "datetime" - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - dataFormat: "datetime" - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -You can run the application and try to delete a book. - -{{end}} - -{{if UI == "NG"}} - -## Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. - -### BookComponent - -Open `/src/app/book/book.component.ts` and replace the content as below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto } from '@proxy/books'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - isModalOpen = false; // add this line - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - // add new method - createBook() { - this.isModalOpen = true; - } -} -``` - -* We defined a property called `isModalOpen` and a method called `createBook`. - - -Open `/src/app/book/book.component.html` and make the following changes: - -```html -
-
-
-
-
{%{{{ '::Menu:Books' | abpLocalization }}}%}
-
-
- - -
- -
- -
-
-
-
- -
-
- - - - -

{%{{{ '::NewBook' | abpLocalization }}}%}

-
- - - - - - -
-``` - -* Added a `New book` button to the card header.. -* Added the `abp-modal` which renders a modal to allow user to create a new book. `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. - -You can open your browser and click the **New book** button to see the new modal. - -![Empty modal for new book](./images/bookstore-empty-new-book-modal-2.png) - -### Create a Reactive Form - -[Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. - -Open `/src/app/book/book.component.ts` and replace the content as below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // add bookTypeOptions -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; // add this line - - // add bookTypes as a list of BookType enum members - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder // inject FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.buildForm(); // add this line - this.isModalOpen = true; - } - - // add buildForm method - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } - - // add save method - save() { - if (this.form.invalid) { - return; - } - - this.bookService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`. -* Added a `form: FormGroup` property. -* Added a `bookTypes` property as a list of `BookType` enum members. That will be used in form options. -* Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms. -* Added a `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method. -* Added a `save` method. - -Open `/src/app/book/book.component.html` and replace ` ` with the following code part: - -```html - -
-
- * - -
- -
- * - -
- -
- * - -
- -
- * - -
-
-
-``` - -Also replace ` ` with the following code part: - -````html - - - - - - -```` - -### Datepicker - -We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. So, we need to arrange the dependencies related to this component. - -Open `/src/app/book/book.module.ts` and replace the content as below: - -```js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { BookRoutingModule } from './book-routing.module'; -import { BookComponent } from './book.component'; -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; // add this line - -@NgModule({ - declarations: [BookComponent], - imports: [ - BookRoutingModule, - SharedModule, - NgbDatepickerModule, // add this line - ] -}) -export class BookModule { } -``` - -* We imported `NgbDatepickerModule` to be able to use the date picker. - -Open `/src/app/book/book.component.ts` and replace the content as below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -// added this line -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ - ListService, - { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter } // add this line - ], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.buildForm(); - this.isModalOpen = true; - } - - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - this.bookService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* Imported ` NgbDateNativeAdapter` and `NgbDateAdapter`. -* We added a new provider `NgbDateAdapter` that converts the Datepicker value to `Date` type. Check out the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. - -Now, you can open your browser to see the changes: - -![Save button to the modal](./images/bookstore-new-book-form-v3.png) - -## Updating a Book - -Open `/src/app/book/book.component.ts` and replace the content as shown below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - selectedBook = {} as BookDto; // declare selectedBook - - form: FormGroup; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.selectedBook = {} as BookDto; // reset the selected book - this.buildForm(); - this.isModalOpen = true; - } - - // Add editBook method - editBook(id: string) { - this.bookService.get(id).subscribe((book) => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: [this.selectedBook.type || null, Validators.required], - publishDate: [ - this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - Validators.required, - ], - price: [this.selectedBook.price || null, Validators.required], - }); - } - - // change the save method - save() { - if (this.form.invalid) { - return; - } - - const request = this.selectedBook.id - ? this.bookService.update(this.selectedBook.id, this.form.value) - : this.bookService.create(this.form.value); - - request.subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* We declared a variable named `selectedBook` as `BookDto`. -* We added an `editBook` method. This method fetches the book with the given `id` and sets it to `selectedBook` object. -* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data. -* We replaced the `createBook` method so it sets `selectedBook` to an empty object. -* We changed the `save` method to handle both of create and update operations. - -### Add "Actions" Dropdown to the Table - -Open `/src/app/book/book.component.html`  and add the following `ngx-datatable-column` definition as the first column in the `ngx-datatable`: - -```html - - -
- -
- -
-
-
-
-``` - -Added an "Actions" dropdown as the first column of the table that is shown below: - -![Action buttons](./images/bookstore-actions-buttons-2.png) - -Also, change the `ng-template #abpHeader` section as shown below: - -```html - -

{%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}

-
-``` - -This template will show the **Edit** text for edit record operation, **New Book** for new record operation in the title. - -## Deleting a Book - -Open the `/src/app/book/book.component.ts` file and inject the `ConfirmationService`. - -Replace the constructor as below: - -```js -// ... - -// add new imports -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; - -//change the constructor -constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder, - private confirmation: ConfirmationService // inject the ConfirmationService -) {} - -// Add a delete method -delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.bookService.delete(id).subscribe(() => this.list.get()); - } - }); -} -``` - -* We imported `ConfirmationService`. -* We injected `ConfirmationService` to the constructor. -* Added a `delete` method. - -> Check out the [Confirmation Popup documentation](../UI/Angular/Confirmation-Service) for more about this service. - -### Add a Delete Button - - -Open `/src/app/book/book.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: - -```html -
- - -
-``` - -The final actions dropdown UI looks like below: - -![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown-2.png) - -Clicking the "Delete" action calls the `delete` method which then shows a confirmation popup as shown below: - -![bookstore-confirmation-popup](./images/bookstore-confirmation-popup-2.png) - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -## Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. Since we've inherited from the `AbpCrudPageBase`, we only need to develop the view part. - -### Add a "New Button" Button - -Open the `Books.razor` and replace the `` section with the following code: - -````xml - - - -

@L["Books"]

-
- - - -
-
-```` - -This will change the card header by adding a "New book" button to the right side: - -![blazor-add-book-button](./images/blazor-add-book-button-2.png) - -Now, we can add a modal that will be opened when we click the button. - -### Book Creation Modal - -Open the `Books.razor` and add the following code to the end of the page: - -````xml - - - -
- - @L["NewBook"] - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
-
-
-```` - -This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at the top of the file, just before the `@inherits...` line: - -````csharp -@inject AbpBlazorMessageLocalizerHelper LH -```` - -* The form implements validation and the `AbpBlazorMessageLocalizerHelper` is used to simply localize the validation messages. -* The `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` methods are defined by the base class. Check out the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components. - -That's all. Run the application and try to add a new book: - -![blazor-new-book-modal](./images/blazor-new-book-modal-2.png) - -## Updating a Book - -Editing a book is similar to creating a new book. - -### Actions Dropdown - -Open the `Books.razor` and add the following `DataGridEntityActionsColumn` section inside the `DataGridColumns` as the first item: - -````xml - - - - - - - -```` - -* `OpenEditModalAsync` is defined in the base class which takes the entity (book) to edit. - -The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it: - -![blazor-edit-book-action](./images/blazor-edit-book-action-3.png) - -### Edit Modal - -We can now define a modal to edit the book. Add the following code to the end of the `Books.razor` page: - -````xml - - - -
- - @EditingEntity.Name - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
-
-
-```` - -### AutoMapper Configuration - -The base `AbpCrudPageBase` uses the [object to object mapping](../Object-To-Object-Mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. - -Open the `BookStoreBlazorAutoMapperProfile` inside the `Acme.BookStore.Blazor.Client` project and change the content as the following: - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Blazor.Client; - -public class BookStoreBlazorAutoMapperProfile : Profile -{ - public BookStoreBlazorAutoMapperProfile() - { - CreateMap(); - } -} -```` - -* We've just added the `CreateMap();` line to define the mapping. - -### Test the Editing Modal - -You can now run the application and try to edit a book. - -![blazor-edit-book-modal](./images/blazor-edit-book-modal-2.png) - -> Tip: Try to leave the *Name* field empty and submit the form to show the validation error message. - -## Deleting a Book - -Open the `Books.razor` page and add the following `EntityAction` code under the "Edit" action inside `EntityActions`: - -````xml - -```` - -* `DeleteEntityAsync` is defined in the base class that deletes the entity by performing a call to the server. -* `ConfirmationMessage` is a callback to show a confirmation message before executing the action. -* `GetDeleteConfirmationMessage` is defined in the base class. You can override this method (or pass another value to the `ConfirmationMessage` parameter) to customize the localization message. - -The "Actions" button becomes a dropdown since it has two actions now: - -![blazor-delete-book-action](./images/blazor-delete-book-action-2.png) - -Run the application and try to delete a book. - -## Full CRUD UI Code - -Here's the complete code to create the book management CRUD page, that has been developed in the last two parts: - -````xml -@page "/books" -@using Volo.Abp.Application.Dtos -@using Acme.BookStore.Books -@using Acme.BookStore.Localization -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Components.Web -@inject IStringLocalizer L -@inject AbpBlazorMessageLocalizerHelper LH -@inherits AbpCrudPageBase - - - - - -

@L["Books"]

-
- - - -
-
- - - - - - - - - - - - - - - @L[$"Enum:BookType.{context.Type}"] - - - - - @context.PublishDate.ToShortDateString() - - - - - - - @context.CreationTime.ToLongDateString() - - - - - -
- - - - -
- - @L["NewBook"] - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
-
-
- - - - -
- - @EditingEntity.Name - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
-
-
-```` - -{{end}} - diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md deleted file mode 100644 index 4172072c74..0000000000 --- a/docs/en/Tutorials/Part-4.md +++ /dev/null @@ -1,300 +0,0 @@ -# Web Application Development Tutorial - Part 4: Integration Tests -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Authorization", - "Path": "Tutorials/Part-5" - }, - "Previous": { - "Name": "Creating, Updating and Deleting Books", - "Path": "Tutorials/Part-3" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- **Part 4: Integration tests (this part)** -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -{{if UI == "MVC" && DB == "EF"}} - -### Video Tutorial - -This part is also recorded as a video tutorial and **published on YouTube**. - -{{end}} - -## Test Projects in the Solution - -This part covers the **server side** tests. There are several test projects in the solution: - -![bookstore-test-projects-v2](./images/bookstore-test-projects-mvc.png) - -> Test projects slightly differs based on your UI and Database selection. For example, if you select MongoDB, then the `Acme.BookStore.EntityFrameworkCore.Tests` will be `Acme.BookStore.MongoDB.Tests`. - -Each project is used to test the related project. Test projects use the following libraries for testing: - -* [Xunit](https://github.com/xunit/xunit) as the main test framework. -* [Shoudly](https://github.com/shouldly/shouldly) as the assertion library. -* [NSubstitute](http://nsubstitute.github.io/) as the mocking library. - -{{if DB=="EF"}} - -> The test projects are configured to use **SQLite in-memory** as the database. A separate database instance is created and seeded (with the [data seed system](../Data-Seeding.md)) to prepare a fresh database for every test. - -{{else if DB=="Mongo"}} - -> **[EphemeralMongo](https://github.com/asimmon/ephemeral-mongo)** library is used to mock the MongoDB database. A separate database instance is created and seeded (with the [data seed system](../Data-Seeding.md)) to prepare a fresh database for every test. - -{{end}} - -## Adding Test Data - -If you had created a data seed contributor as described in the [first part](Part-1.md), the same data will be available in your tests. So, you can skip this section. If you haven't created the seed contributor, you can use the `BookStoreTestDataSeedContributor` to seed the same data to be used in the tests below. - -## Testing the BookAppService - -Add a new test class, named `BookAppService_Tests` in the `Books` namespace (folder) of the `Acme.BookStore.Application.Tests` project: - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Modularity; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books; - -public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IBookAppService _bookAppService; - - protected BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } -} -```` - -{{if DB == "EF"}} -Add a new implementation class of `BookAppService_Tests` class, named `EfCoreBookAppService_Tests` in the `EntityFrameworkCore\Applications\Books` namespace (folder) of the `Acme.BookStore.EntityFrameworkCore.Tests` project: - -````csharp -using Acme.BookStore.Books; -using Xunit; - -namespace Acme.BookStore.EntityFrameworkCore.Applications.Books; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class EfCoreBookAppService_Tests : BookAppService_Tests -{ - -} -```` -{{end}} - -{{if DB == "Mongo"}} -Add a new implementation class of `BookAppService_Tests` class, named `MongoDBBookAppService_Tests` in the `MongoDb\Applications\Books` namespace (folder) of the `Acme.BookStore.MongoDB.Tests` project: - -````csharp -using Acme.BookStore.MongoDB; -using Acme.BookStore.Books; -using Xunit; - -namespace Acme.BookStore.MongoDb.Applications.Books; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class MongoDBBookAppService_Tests : BookAppService_Tests -{ - -} -```` -{{end}} - -* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of books. -* We can safely check the book "1984" by its name, because we know that this books is available in the database since we've added it in the seed data. - -Add a new test method to the `BookAppService_Tests` class that creates a new **valid** book: - -````csharp -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -Add a new test that tries to create an invalid book and fails: - -````csharp -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* Since the `Name` is empty, ABP will throw an `AbpValidationException`. - -The final test class should be as shown below: - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Modularity; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books; - -public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IBookAppService _bookAppService; - - protected BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } - - [Fact] - public async Task Should_Create_A_Valid_Book() - { - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); - } -} -```` - -Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: - -![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) - -Congratulations, the **green icons** indicates that the tests have been successfully passed! \ No newline at end of file diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md deleted file mode 100644 index c881df7294..0000000000 --- a/docs/en/Tutorials/Part-5.md +++ /dev/null @@ -1,580 +0,0 @@ -# Web Application Development Tutorial - Part 5: Authorization -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Authors: Domain Layer", - "Path": "Tutorials/Part-6" - }, - "Previous": { - "Name": "Integration Tests", - "Path": "Tutorials/Part-4" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- **Part 5: Authorization (this part)** -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -{{if UI == "MVC" && DB == "EF"}} - -### Video Tutorial - -This part is also recorded as a video tutorial and **published on YouTube**. - -{{end}} - -## Permissions - -ABP Framework provides an [authorization system](../Authorization.md) based on the ASP.NET Core's [authorization infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). One major feature added on top of the standard authorization infrastructure is the **permission system** which allows to define permissions and enable/disable per role, user or client. - -### Permission Names - -A permission must have a unique name (a `string`). The best way is to define it as a `const`, so we can reuse the permission name. - -Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: - -````csharp -namespace Acme.BookStore.Permissions; - -public static class BookStorePermissions -{ - public const string GroupName = "BookStore"; - - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } -} -```` - -This is a hierarchical way of defining permission names. For example, "create book" permission name was defined as `BookStore.Books.Create`. ABP doesn't force you to a structure, but we find this way useful. - -### Permission Definitions - -You should define permissions before using them. - -Open the `BookStorePermissionDefinitionProvider` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: - -````csharp -using Acme.BookStore.Localization; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Localization; - -namespace Acme.BookStore.Permissions; - -public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider -{ - public override void Define(IPermissionDefinitionContext context) - { - var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); - - var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); - booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); - booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); - booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } -} -```` - -This class defines a **permission group** (to group permissions on the UI, will be seen below) and **4 permissions** inside this group. Also, **Create**, **Edit** and **Delete** are children of the `BookStorePermissions.Books.Default` permission. A child permission can be selected **only if the parent was selected**. - -Finally, edit the localization file (`en.json` under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project) to define the localization keys used above: - -````json -"Permission:BookStore": "Book Store", -"Permission:Books": "Book Management", -"Permission:Books.Create": "Creating new books", -"Permission:Books.Edit": "Editing the books", -"Permission:Books.Delete": "Deleting the books" -```` - -> Localization key names are arbitrary and there is no forcing rule. But we prefer the convention used above. - -### Permission Management UI - -Once you define the permissions, you can see them on the **permission management modal**. - -Go to the *Administration -> Identity -> Roles* page, select *Permissions* action for the admin role to open the permission management modal: - -![bookstore-permissions-ui](images/bookstore-permissions-ui-2.png) - -Grant the permissions you want and save the modal. - -> **Tip**: New permissions are automatically granted to the admin role if you run the `Acme.BookStore.DbMigrator` application. - -## Authorization - -Now, you can use the permissions to authorize the book management. - -### Application Layer & HTTP API - -Open the `BookAppService` class and set the policy names as the permission names defined above: - -````csharp -using System; -using Acme.BookStore.Permissions; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books; - -public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService -{ - public BookAppService(IRepository repository) - : base(repository) - { - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } -} -```` - -Added code to the constructor. Base `CrudAppService` automatically uses these permissions on the CRUD operations. This makes the **application service** secure, but also makes the **HTTP API** secure since this service is automatically used as an HTTP API as explained before (see [auto API controllers](../API/Auto-API-Controllers.md)). - -> You will see the declarative authorization, using the `[Authorize(...)]` attribute, later while developing the author management functionality. - -{{if UI == "MVC"}} - -### Razor Page - -While securing the HTTP API & the application service prevents unauthorized users to use the services, they can still navigate to the book management page. While they will get authorization exception when the page makes the first AJAX call to the server, we should also authorize the page for a better user experience and security. - -Open the `BookStoreWebModule` and add the following code block inside the `ConfigureServices` method: - -````csharp -Configure(options => -{ - options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default); - options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create); - options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit); -}); -```` - -Now, unauthorized users are redirected to the **login page**. - -#### Hide the New Book Button - -The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission. - -![bookstore-new-book-button-small](images/bookstore-new-book-button-small-2.png) - -Open the `Pages/Books/Index.cshtml` file and change the content as shown below: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Permissions -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.AspNetCore.Authorization -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@inject IAuthorizationService AuthorizationService -@section scripts -{ - -} - - - - - - @L["Books"] - - - @if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create)) - { - - } - - - - - - - -```` - -* Added `@inject IAuthorizationService AuthorizationService` to access to the authorization service. -* Used `@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))` to check the book creation permission to conditionally render the *New Book* button. - -### JavaScript Side - -Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions: - -![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions-2.png) - -We should hide an action if the current user has not granted for the related permission. Datatables row actions has a `visible` option that can be set to `false` to hide the action item. - -Open the `Pages/Books/Index.js` inside the `Acme.BookStore.Web` project and add a `visible` option to the `Edit` action as shown below: - -````js -{ - text: l('Edit'), - visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION - action: function (data) { - editModal.open({ id: data.record.id }); - } -} -```` - -Do same for the `Delete` action: - -````js -visible: abp.auth.isGranted('BookStore.Books.Delete') -```` - -* `abp.auth.isGranted(...)` is used to check a permission that is defined before. -* `visible` could also be get a function that returns a `bool` if the value will be calculated later, based on some conditions. - -### Menu Item - -Even we have secured all the layers of the book management page, it is still visible on the main menu of the application. We should hide the menu item if the current user has no permission. - -Open the `BookStoreMenuContributor` class, find the code block below: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ) - ) -); -```` - -And replace this code block with the following: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ).RequirePermissions(BookStorePermissions.Books.Default) // Check the permission! - ) -); -```` - -We've only added the `.RequirePermissions(BookStorePermissions.Books.Default)` extension method call for the inner menu item. - -{{else if UI == "NG"}} - -### Angular Guard Configuration - -First step of the UI is to prevent unauthorized users to see the "Books" menu item and enter to the book management page. - -Open the `/src/app/book/book-routing.module.ts` and replace with the following content: - -````js -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import { authGuard, permissionGuard } from '@abp/ng.core'; -import { BookComponent } from './book.component'; - -const routes: Routes = [ - { path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class BookRoutingModule {} -```` - -* Imported `authGuard` and `permissionGuard` from the `@abp/ng.core`. -* Added `canActivate: [authGuard, permissionGuard]` to the route definition. - -Open the `/src/app/route.provider.ts` and add `requiredPolicy: 'BookStore.Books'` to the `/books` route. The `/books` route block should be following: - -````js -{ - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Books', -} -```` - -### Hide the New Book Button - -The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission. - -![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png) - -Open the `/src/app/book/book.component.html` file and replace the create button HTML content as shown below: - -````html - - -```` - -* Just added `*abpPermission="'BookStore.Books.Create'"` that hides the button if the current user has no permission. - -### Hide the Edit and Delete Actions - -Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions: - -![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions-2.png) - -We should hide an action if the current user has not granted for the related permission. - -Open the `/src/app/book/book.component.html` file and replace the edit and delete buttons contents as shown below: - -````html - - - - - -```` - -* Added `*abpPermission="'BookStore.Books.Edit'"` that hides the edit action if the current user has no editing permission. -* Added `*abpPermission="'BookStore.Books.Delete'"` that hides the delete action if the current user has no delete permission. - -{{else if UI == "Blazor"}} - -### Authorize the Razor Component - -Open the `/Pages/Books.razor` file in the `Acme.BookStore.Blazor.Client` project and add an `Authorize` attribute just after the `@page` directive and the following namespace imports (`@using` lines), as shown below: - -````html -@page "/books" -@attribute [Authorize(BookStorePermissions.Books.Default)] -@using Acme.BookStore.Permissions -@using Microsoft.AspNetCore.Authorization -... -```` - -Adding this attribute prevents to enter this page if the current hasn't logged in or hasn't granted for the given permission. In case of attempt, the user is redirected to the login page. - -### Show/Hide the Actions - -The book management page has a *New Book* button and *Edit* and *Delete* actions for each book. We should hide these buttons/actions if the current user has not granted for the related permissions. - -The base `AbpCrudPageBase` class already has the necessary functionality for these kind of operations. - -#### Set the Policy (Permission) Names - -Add the following code block to the end of the `Books.razor` file: - -````csharp -@code -{ - public Books() // Constructor - { - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } -} -```` - -The base `AbpCrudPageBase` class automatically checks these permissions on the related operations. It also defines the given properties for us if we need to check them manually: - -* `HasCreatePermission`: True, if the current user has permission to create the entity. -* `HasUpdatePermission`: True, if the current user has permission to edit/update the entity. -* `HasDeletePermission`: True, if the current user has permission to delete the entity. - -> **Blazor Tip**: While adding the C# code into a `@code` block is fine for small code parts, it is suggested to use the code behind approach to develop a more maintainable code base when the code block becomes longer. We will use this approach for the authors part. - -#### Hide the New Book Button - -Wrap the *New Book* button by an `if` block as shown below: - -````xml -@if (HasCreatePermission) -{ - -} -```` - -#### Hide the Edit/Delete Actions - -`EntityAction` component defines `Visible` attribute (parameter) to conditionally show the action. - -Update the `EntityActions` section as shown below: - -````xml - - - - -```` - -#### About the Permission Caching - -You can run and test the permissions. Remove a book related permission from the admin role to see the related button/action disappears from the UI. - -**ABP Framework caches the permissions** of the current user in the client side. So, when you change a permission for yourself, you need to manually **refresh the page** to take the effect. If you don't refresh and try to use the prohibited action you get an HTTP 403 (forbidden) response from the server. - -> Changing a permission for a role or user immediately available on the server side. So, this cache system doesn't cause any security problem. - -### Menu Item - -Even we have secured all the layers of the book management page, it is still visible on the main menu of the application. We should hide the menu item if the current user has no permission. - -Open the `BookStoreMenuContributor` class in the `Acme.BookStore.Blazor.Client` project, find the code block below: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - ) - ) -); -```` - -And replace this code block with the following: - -````csharp -var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" -); - -context.Menu.AddItem(bookStoreMenu); - -//CHECK the PERMISSION -if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); -} -```` - -You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return value. The final `ConfigureMainMenuAsync` method should be the following: - -````csharp -private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) -{ - var l = context.GetLocalizer(); - - context.Menu.Items.Insert( - 0, - new ApplicationMenuItem( - "BookStore.Home", - l["Menu:Home"], - "/", - icon: "fas fa-home" - ) - ); - - var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ); - - context.Menu.AddItem(bookStoreMenu); - - //CHECK the PERMISSION - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); - } -} -```` - -{{end}} \ No newline at end of file diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md deleted file mode 100644 index 55b9d2689e..0000000000 --- a/docs/en/Tutorials/Part-6.md +++ /dev/null @@ -1,281 +0,0 @@ -# Web Application Development Tutorial - Part 6: Authors: Domain Layer -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Authors: Database Integration", - "Path": "Tutorials/Part-7" - }, - "Previous": { - "Name": "Authorization", - "Path": "Tutorials/Part-5" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- **Part 6: Authors: Domain layer (this part)** -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -## Introduction - -In the previous parts, we've used the ABP infrastructure to easily build some services; - -* Used the [CrudAppService](../Application-Services.md) base class instead of manually developing an application service for standard create, read, update and delete operations. -* Used [generic repositories](../Repositories.md) to completely automate the database layer. - -For the "Authors" part; - -* We will **do some of the things manually** to show how you can do it in case of need. -* We will implement some **Domain Driven Design (DDD) best practices**. - -> **The development will be done layer by layer to concentrate on an individual layer in one time. In a real project, you will develop your application feature by feature (vertical) as done in the previous parts. In this way, you will experience both approaches.** - -## The Author Entity - -Create an `Authors` folder (namespace) in the `Acme.BookStore.Domain` project and add an `Author` class inside it: - -````csharp -using System; -using JetBrains.Annotations; -using Volo.Abp; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore.Authors; - -public class Author : FullAuditedAggregateRoot -{ - public string Name { get; private set; } - public DateTime BirthDate { get; set; } - public string ShortBio { get; set; } - - private Author() - { - /* This constructor is for deserialization / ORM purpose */ - } - - internal Author( - Guid id, - string name, - DateTime birthDate, - string? shortBio = null) - : base(id) - { - SetName(name); - BirthDate = birthDate; - ShortBio = shortBio; - } - - internal Author ChangeName(string name) - { - SetName(name); - return this; - } - - private void SetName(string name) - { - Name = Check.NotNullOrWhiteSpace( - name, - nameof(name), - maxLength: AuthorConsts.MaxNameLength - ); - } -} -```` - -* Inherited from `FullAuditedAggregateRoot` which makes the entity [soft delete](../Data-Filtering.md) (that means when you delete it, it is not deleted in the database, but just marked as deleted) with all the [auditing](../Entities.md) properties. -* `private set` for the `Name` property restricts to set this property from out of this class. There are two ways of setting the name (in both cases, we validate the name): - * In the constructor, while creating a new author. - * Using the `ChangeName` method to update the name later. -* The `constructor` and the `ChangeName` method is `internal` to force to use these methods only in the domain layer, using the `AuthorManager` that will be explained later. -* `Check` class is an ABP Framework utility class to help you while checking method arguments (it throws `ArgumentException` on an invalid case). - -`AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project: - -````csharp -namespace Acme.BookStore.Authors; - -public static class AuthorConsts -{ - public const int MaxNameLength = 64; -} - -```` - -Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later. - -## AuthorManager: The Domain Service - -`Author` constructor and `ChangeName` methods are `internal`, so they can be used only in the domain layer. Create an `AuthorManager` class in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: - -````csharp -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Volo.Abp; -using Volo.Abp.Domain.Services; - -namespace Acme.BookStore.Authors; - -public class AuthorManager : DomainService -{ - private readonly IAuthorRepository _authorRepository; - - public AuthorManager(IAuthorRepository authorRepository) - { - _authorRepository = authorRepository; - } - - public async Task CreateAsync( - string name, - DateTime birthDate, - string? shortBio = null) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var existingAuthor = await _authorRepository.FindByNameAsync(name); - if (existingAuthor != null) - { - throw new AuthorAlreadyExistsException(name); - } - - return new Author( - GuidGenerator.Create(), - name, - birthDate, - shortBio - ); - } - - public async Task ChangeNameAsync( - Author author, - string newName) - { - Check.NotNull(author, nameof(author)); - Check.NotNullOrWhiteSpace(newName, nameof(newName)); - - var existingAuthor = await _authorRepository.FindByNameAsync(newName); - if (existingAuthor != null && existingAuthor.Id != author.Id) - { - throw new AuthorAlreadyExistsException(newName); - } - - author.ChangeName(newName); - } -} -```` - -* `AuthorManager` forces to create an author and change name of an author in a controlled way. The application layer (will be introduced later) will use these methods. - -> **DDD tip**: Do not introduce domain service methods unless they are really needed and perform some core business rules. For this case, we needed this service to be able to force the unique name constraint. - -Both methods checks if there is already an author with the given name and throws a special business exception, `AuthorAlreadyExistsException`, defined in the `Acme.BookStore.Domain` project (in the `Authors` folder) as shown below: - -````csharp -using Volo.Abp; - -namespace Acme.BookStore.Authors; - -public class AuthorAlreadyExistsException : BusinessException -{ - public AuthorAlreadyExistsException(string name) - : base(BookStoreDomainErrorCodes.AuthorAlreadyExists) - { - WithData("name", name); - } -} -```` - -`BusinessException` is a special exception type. It is a good practice to throw domain related exceptions when needed. It is automatically handled by the ABP Framework and can be easily localized. `WithData(...)` method is used to provide additional data to the exception object that will later be used on the localization message or for some other purpose. - -Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below: - -````csharp -namespace Acme.BookStore; - -public static class BookStoreDomainErrorCodes -{ - public const string AuthorAlreadyExists = "BookStore:00001"; -} -```` - -This is a unique string represents the error code thrown by your application and can be handled by client applications. For users, you probably want to localize it. Open the `Localization/BookStore/en.json` inside the `Acme.BookStore.Domain.Shared` project and add the following entry: - -````json -"BookStore:00001": "There is already an author with the same name: {name}" -```` - -Whenever you throw an `AuthorAlreadyExistsException`, the end user will see a nice error message on the UI. - -## IAuthorRepository - -`AuthorManager` injects the `IAuthorRepository`, so we need to define it. Create this new interface in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Authors; - -public interface IAuthorRepository : IRepository -{ - Task FindByNameAsync(string name); - - Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null - ); -} -```` - -* `IAuthorRepository` extends the standard `IRepository` interface, so all the standard [repository](../Repositories.md) methods will also be available for the `IAuthorRepository`. -* `FindByNameAsync` was used in the `AuthorManager` to query an author by name. -* `GetListAsync` will be used in the application layer to get a listed, sorted and filtered list of authors to show on the UI. - -We will implement this repository in the next part. - -> Both of these methods might **seem unnecessary** since the standard repositories already provide generic querying methods and you can easily use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it. - -## Conclusion - -This part covered the domain layer of the authors functionality of the book store application. The main files created/updated in this part was highlighted in the picture below: - -![bookstore-author-domain-layer](images/bookstore-author-domain-layer.png) \ No newline at end of file diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md deleted file mode 100644 index 9f0c51b5e9..0000000000 --- a/docs/en/Tutorials/Part-7.md +++ /dev/null @@ -1,244 +0,0 @@ -# Web Application Development Tutorial - Part 7: Authors: Database Integration -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Authors: Application Layer", - "Path": "Tutorials/Part-8" - }, - "Previous": { - "Name": "Authors: Domain Layer", - "Path": "Tutorials/Part-6" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- **Part 7: Authors: Database Integration (this part)** -- [Part 8: Authors: Application Layer](Part-8.md) -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -## Introduction - -This part explains how to configure the database integration for the `Author` entity introduced in the previous part. - -{{if DB=="EF"}} - -## DB Context - -Open the `BookStoreDbContext` in the `Acme.BookStore.EntityFrameworkCore` project and add the following `DbSet` property: - -````csharp -public DbSet Authors { get; set; } -```` - -Then locate to the `OnModelCreating` method in `BookStoreDbContext` class in the same project and add the following lines to the end of the method: - -````csharp -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", - BookStoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Name) - .IsRequired() - .HasMaxLength(AuthorConsts.MaxNameLength); - - b.HasIndex(x => x.Name); -}); -```` - -This is just like done for the `Book` entity before, so no need to explain again. - -## Create a new Database Migration - -The startup solution is configured to use [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. - -Open a command-line terminal in the directory of the `Acme.BookStore.EntityFrameworkCore` project and type the following command: - -````bash -dotnet ef migrations add Added_Authors -```` - -This will add a new migration class to the project: - -![bookstore-efcore-migration-authors](./images/bookstore-efcore-migration-authors.png) - -You can apply changes to the database using the following command, in the same command-line terminal: - -````bash -dotnet ef database update -```` - -> If you are using Visual Studio, you may want to use the `Add-Migration Added_Authors` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. - -{{else if DB=="Mongo"}} - -## DB Context - -Open the `BookStoreMongoDbContext` in the `MongoDb` folder of the `Acme.BookStore.MongoDB` project and add the following property to the class: - -````csharp -public IMongoCollection Authors => Collection(); -```` - -{{end}} - -## Implementing the IAuthorRepository - -{{if DB=="EF"}} - -Create a new class, named `EfCoreAuthorRepository` inside the `Acme.BookStore.EntityFrameworkCore` project (in the `Authors` folder) and paste the following code: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Acme.BookStore.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -namespace Acme.BookStore.Authors; - -public class EfCoreAuthorRepository - : EfCoreRepository, - IAuthorRepository -{ - public EfCoreAuthorRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var dbSet = await GetDbSetAsync(); - return await dbSet - .WhereIf( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } -} -```` - -* Inherited from the `EfCoreRepository`, so it inherits the standard repository method implementations. -* `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. -* `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. - -> See the [EF Core Integration document](../Entity-Framework-Core.md) for more information on the EF Core based repositories. - -{{else if DB=="Mongo"}} - -Create a new class, named `MongoDbAuthorRepository` inside the `Acme.BookStore.MongoDB` project (in the `Authors` folder) and paste the following code: - -```csharp -using System; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Collections.Generic; -using System.Threading.Tasks; -using Acme.BookStore.MongoDB; -using MongoDB.Driver; -using MongoDB.Driver.Linq; -using Volo.Abp.Domain.Repositories.MongoDB; -using Volo.Abp.MongoDB; - -namespace Acme.BookStore.Authors; - -public class MongoDbAuthorRepository - : MongoDbRepository, - IAuthorRepository -{ - public MongoDbAuthorRepository( - IMongoDbContextProvider dbContextProvider - ) : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable - .WhereIf>( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .As>() - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } -} -``` - -* Inherited from the `MongoDbRepository`, so it inherits the standard repository method implementations. -* `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. -* `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. - -> See the [MongoDB Integration document](../MongoDB.md) for more information on the MongoDB based repositories. - -{{end}} \ No newline at end of file diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md deleted file mode 100644 index 5263cbc44e..0000000000 --- a/docs/en/Tutorials/Part-8.md +++ /dev/null @@ -1,606 +0,0 @@ -# Web Application Development Tutorial - Part 8: Authors: Application Layer -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Authors: User Interface", - "Path": "Tutorials/Part-9" - }, - "Previous": { - "Name": "Authors: Database Integration", - "Path": "Tutorials/Part-7" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- **Part 8: Author: Application Layer (this part)** -- [Part 9: Authors: User Interface](Part-9.md) -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -## Introduction - -This part explains to create an application layer for the `Author` entity created before. - -## IAuthorAppService - -We will first create the [application service](../Application-Services.md) interface and the related [DTO](../Data-Transfer-Objects.md)s. Create a new interface, named `IAuthorAppService`, in the `Authors` namespace (folder) of the `Acme.BookStore.Application.Contracts` project: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Authors; - -public interface IAuthorAppService : IApplicationService -{ - Task GetAsync(Guid id); - - Task> GetListAsync(GetAuthorListDto input); - - Task CreateAsync(CreateAuthorDto input); - - Task UpdateAsync(Guid id, UpdateAuthorDto input); - - Task DeleteAsync(Guid id); -} -```` - -* `IApplicationService` is a conventional interface that is inherited by all the application services, so the ABP Framework can identify the service. -* Defined standard methods to perform CRUD operations on the `Author` entity. -* `PagedResultDto` is a pre-defined DTO class in the ABP Framework. It has an `Items` collection and a `TotalCount` property to return a paged result. -* Preferred to return an `AuthorDto` (for the newly created author) from the `CreateAsync` method, while it is not used by this application - just to show a different usage. - -This interface is using the DTOs defined below (create them for your project). - -### AuthorDto - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Authors; - -public class AuthorDto : EntityDto -{ - public string Name { get; set; } - - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } -} -```` - -* `EntityDto` simply has an `Id` property with the given generic argument. You could create an `Id` property yourself instead of inheriting the `EntityDto`. - -### GetAuthorListDto - -````csharp -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Authors; - -public class GetAuthorListDto : PagedAndSortedResultRequestDto -{ - public string? Filter { get; set; } -} -```` - -* `Filter` is used to search authors. It can be `null` (or empty string) to get all the authors. -* `PagedAndSortedResultRequestDto` has the standard paging and sorting properties: `int MaxResultCount`, `int SkipCount` and `string Sorting`. - -> ABP Framework has such base DTO classes to simplify and standardize your DTOs. See the [DTO documentation](../Data-Transfer-Objects.md) for all. - -### CreateAuthorDto - -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Authors; - -public class CreateAuthorDto -{ - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } = string.Empty; - - [Required] - public DateTime BirthDate { get; set; } - - public string? ShortBio { get; set; } -} -```` - -Data annotation attributes can be used to validate the DTO. See the [validation document](../Validation.md) for details. - -### UpdateAuthorDto - -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Authors; - -public class UpdateAuthorDto -{ - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } = string.Empty; - - [Required] - public DateTime BirthDate { get; set; } - - public string? ShortBio { get; set; } -} -```` - -> We could share (re-use) the same DTO among the create and the update operations. While you can do it, we prefer to create different DTOs for these operations since we see they generally be different by the time. So, code duplication is reasonable here compared to a tightly coupled design. - -## AuthorAppService - -It is time to implement the `IAuthorAppService` interface. Create a new class, named `AuthorAppService` in the `Authors` namespace (folder) of the `Acme.BookStore.Application` project: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Authors; - -[Authorize(BookStorePermissions.Authors.Default)] -public class AuthorAppService : BookStoreAppService, IAuthorAppService -{ - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public AuthorAppService( - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - //...SERVICE METHODS WILL COME HERE... -} -```` - -* `[Authorize(BookStorePermissions.Authors.Default)]` is a declarative way to check a permission (policy) to authorize the current user. See the [authorization document](../Authorization.md) for more. `BookStorePermissions` class will be updated below, don't worry for the compile error for now. -* Derived from the `BookStoreAppService`, which is a simple base class comes with the startup template. It is derived from the standard `ApplicationService` class. -* Implemented the `IAuthorAppService` which was defined above. -* Injected the `IAuthorRepository` and `AuthorManager` to use in the service methods. - -Now, we will introduce the service methods one by one. Copy the explained method into the `AuthorAppService` class. - -### GetAsync - -````csharp -public async Task GetAsync(Guid id) -{ - var author = await _authorRepository.GetAsync(id); - return ObjectMapper.Map(author); -} -```` - -This method simply gets the `Author` entity by its `Id`, converts to the `AuthorDto` using the [object to object mapper](../Object-To-Object-Mapping.md). This requires to configure the AutoMapper, which will be explained later. - -### GetListAsync - -````csharp -public async Task> GetListAsync(GetAuthorListDto input) -{ - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Author.Name); - } - - var authors = await _authorRepository.GetListAsync( - input.SkipCount, - input.MaxResultCount, - input.Sorting, - input.Filter - ); - - var totalCount = input.Filter == null - ? await _authorRepository.CountAsync() - : await _authorRepository.CountAsync( - author => author.Name.Contains(input.Filter)); - - return new PagedResultDto( - totalCount, - ObjectMapper.Map, List>(authors) - ); -} -```` - -* Default sorting is "by author name" which is done in the beginning of the method in case of it wasn't sent by the client. -* Used the `IAuthorRepository.GetListAsync` to get a paged, sorted and filtered list of authors from the database. We had implemented it in the previous part of this tutorial. Again, it actually was not needed to create such a method since we could directly query over the repository, but wanted to demonstrate how to create custom repository methods. -* Directly queried from the `AuthorRepository` while getting the count of the authors. If a filter is sent, then we are using it to filter entities while getting the count. -* Finally, returning a paged result by mapping the list of `Author`s to a list of `AuthorDto`s. - -### CreateAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Create)] -public async Task CreateAsync(CreateAuthorDto input) -{ - var author = await _authorManager.CreateAsync( - input.Name, - input.BirthDate, - input.ShortBio - ); - - await _authorRepository.InsertAsync(author); - - return ObjectMapper.Map(author); -} -```` - -* `CreateAsync` requires the `BookStorePermissions.Authors.Create` permission (in addition to the `BookStorePermissions.Authors.Default` declared for the `AuthorAppService` class). -* Used the `AuthorManager` (domain service) to create a new author. -* Used the `IAuthorRepository.InsertAsync` to insert the new author to the database. -* Used the `ObjectMapper` to return an `AuthorDto` representing the newly created author. - -> **DDD tip**: Some developers may find useful to insert the new entity inside the `_authorManager.CreateAsync`. We think it is a better design to leave it to the application layer since it better knows when to insert it to the database (maybe it requires additional works on the entity before insert, which would require to an additional update if we perform the insert in the domain service). However, it is completely up to you. - -### UpdateAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Edit)] -public async Task UpdateAsync(Guid id, UpdateAuthorDto input) -{ - var author = await _authorRepository.GetAsync(id); - - if (author.Name != input.Name) - { - await _authorManager.ChangeNameAsync(author, input.Name); - } - - author.BirthDate = input.BirthDate; - author.ShortBio = input.ShortBio; - - await _authorRepository.UpdateAsync(author); -} -```` - -* `UpdateAsync` requires the additional `BookStorePermissions.Authors.Edit` permission. -* Used the `IAuthorRepository.GetAsync` to get the author entity from the database. `GetAsync` throws `EntityNotFoundException` if there is no author with the given id, which results a `404` HTTP status code in a web application. It is a good practice to always bring the entity on an update operation. -* Used the `AuthorManager.ChangeNameAsync` (domain service method) to change the author name if it was requested to change by the client. -* Directly updated the `BirthDate` and `ShortBio` since there is not any business rule to change these properties, they accept any value. -* Finally, called the `IAuthorRepository.UpdateAsync` method to update the entity on the database. - -{{if DB == "EF"}} - -> **EF Core Tip**: Entity Framework Core has a **change tracking** system and **automatically saves** any change to an entity at the end of the unit of work (You can simply think that the ABP Framework automatically calls `SaveChanges` at the end of the method). So, it will work as expected even if you don't call the `_authorRepository.UpdateAsync(...)` in the end of the method. If you don't consider to change the EF Core later, you can just remove this line. - -{{end}} - -### DeleteAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Delete)] -public async Task DeleteAsync(Guid id) -{ - await _authorRepository.DeleteAsync(id); -} -```` - -* `DeleteAsync` requires the additional `BookStorePermissions.Authors.Delete` permission. -* It simply uses the `DeleteAsync` method of the repository. - -## Permission Definitions - -You can't compile the code since it is expecting some constants declared in the `BookStorePermissions` class. - -Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: - -````csharp -namespace Acme.BookStore.Permissions; - -public static class BookStorePermissions -{ - public const string GroupName = "BookStore"; - - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } - - // *** ADDED a NEW NESTED CLASS *** - public static class Authors - { - public const string Default = GroupName + ".Authors"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } -} -```` - -Then open the `BookStorePermissionDefinitionProvider` in the same project and add the following lines at the end of the `Define` method: - -````csharp -var authorsPermission = bookStoreGroup.AddPermission( - BookStorePermissions.Authors.Default, L("Permission:Authors")); -authorsPermission.AddChild( - BookStorePermissions.Authors.Create, L("Permission:Authors.Create")); -authorsPermission.AddChild( - BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit")); -authorsPermission.AddChild( - BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete")); -```` - -Finally, add the following entries to the `Localization/BookStore/en.json` inside the `Acme.BookStore.Domain.Shared` project, to localize the permission names: - -````csharp -"Permission:Authors": "Author Management", -"Permission:Authors.Create": "Creating new authors", -"Permission:Authors.Edit": "Editing the authors", -"Permission:Authors.Delete": "Deleting the authors" -```` - -## Object to Object Mapping - -`AuthorAppService` is using the `ObjectMapper` to convert the `Author` objects to `AuthorDto` objects. So, we need to define this mapping in the AutoMapper configuration. - -Open the `BookStoreApplicationAutoMapperProfile` class inside the `Acme.BookStore.Application` project and add the following line to the constructor: - -````csharp -CreateMap(); -```` - -## Data Seeder - -As just done for the books before, it would be good to have some initial author entities in the database. This will be good while running the application first time, but also it is very useful for the automated tests. - -Open the `BookStoreDataSeederContributor` in the `Acme.BookStore.Domain` project and change the file content with the code below: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore; - -public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency -{ - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } - - // ADDED SEED DATA FOR AUTHORS - - if (await _authorRepository.GetCountAsync() <= 0) - { - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - } - } -} -```` - -{{if DB=="EF"}} - -You can now run the `.DbMigrator` console application to **migrate** the **database schema** and **seed** the initial data. - -{{else if DB=="Mongo"}} - -You can now run the `.DbMigrator` console application to **seed** the initial data. - -{{end}} - -## Testing the Author Application Service - -Finally, we can write some tests for the `IAuthorAppService`. Add a new class, named `AuthorAppService_Tests` in the `Authors` namespace (folder) of the `Acme.BookStore.Application.Tests` project: - -````csharp -using System; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Modularity; -using Xunit; - -namespace Acme.BookStore.Authors; - -public abstract class AuthorAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IAuthorAppService _authorAppService; - - protected AuthorAppService_Tests() - { - _authorAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_All_Authors_Without_Any_Filter() - { - var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); - - result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldContain(author => author.Name == "Douglas Adams"); - } - - [Fact] - public async Task Should_Get_Filtered_Authors() - { - var result = await _authorAppService.GetListAsync( - new GetAuthorListDto {Filter = "George"}); - - result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); - } - - [Fact] - public async Task Should_Create_A_New_Author() - { - var authorDto = await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Edward Bellamy", - BirthDate = new DateTime(1850, 05, 22), - ShortBio = "Edward Bellamy was an American author..." - } - ); - - authorDto.Id.ShouldNotBe(Guid.Empty); - authorDto.Name.ShouldBe("Edward Bellamy"); - } - - [Fact] - public async Task Should_Not_Allow_To_Create_Duplicate_Author() - { - await Assert.ThrowsAsync(async () => - { - await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Douglas Adams", - BirthDate = DateTime.Now, - ShortBio = "..." - } - ); - }); - } - - //TODO: Test other methods... -} -```` - -{{if DB == "EF"}} -Add a new implementation class of `AuthorAppService_Tests` class, named `EfCoreAuthorAppService_Tests` in the `EntityFrameworkCore\Applications\Authors` namespace (folder) of the `Acme.BookStore.EntityFrameworkCore.Tests` project: - -````csharp -using Acme.BookStore.Authors; -using Xunit; - -namespace Acme.BookStore.EntityFrameworkCore.Applications.Authors; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class EfCoreAuthorAppService_Tests : AuthorAppService_Tests -{ - -} -```` -{{end}} - -{{if DB == "Mongo"}} -Add a new implementation class of `AuthorAppService_Tests` class, named `MongoDBAuthorAppService_Tests` in the `MongoDb\Applications\Authors` namespace (folder) of the `Acme.BookStore.MongoDB.Tests` project: - -````csharp -using Acme.BookStore.MongoDB; -using Acme.BookStore.Authors; -using Xunit; - -namespace Acme.BookStore.MongoDb.Applications.Authors; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class MongoDBAuthorAppService_Tests : AuthorAppService_Tests -{ - -} -```` -{{end}} - -Created some tests for the application service methods, which should be clear to understand. diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md deleted file mode 100644 index 433d48360f..0000000000 --- a/docs/en/Tutorials/Part-9.md +++ /dev/null @@ -1,1260 +0,0 @@ -# Web Application Development Tutorial - Part 9: Authors: User Interface -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` - -````json -//[doc-nav] -{ - "Next": { - "Name": "Book to Author Relation", - "Path": "Tutorials/Part-10" - }, - "Previous": { - "Name": "Authors: Application Layer", - "Path": "Tutorials/Part-8" - } -} -```` - -## About This Tutorial - -In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: - -* **{{DB_Value}}** as the ORM provider. -* **{{UI_Value}}** as the UI Framework. - -This tutorial is organized as the following parts; - -- [Part 1: Creating the server side](Part-1.md) -- [Part 2: The book list page](Part-2.md) -- [Part 3: Creating, updating and deleting books](Part-3.md) -- [Part 4: Integration tests](Part-4.md) -- [Part 5: Authorization](Part-5.md) -- [Part 6: Authors: Domain layer](Part-6.md) -- [Part 7: Authors: Database Integration](Part-7.md) -- [Part 8: Authors: Application Layer](Part-8.md) -- **Part 9: Authors: User Interface (this part)** -- [Part 10: Book to Author Relation](Part-10.md) - -### Download the Source Code - -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: - -* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). - -## Introduction - -This part explains how to create a CRUD page for the `Author` entity introduced in the previous parts. - -{{if UI == "MVC"}} - -## The Authors List Page - -Create a new razor page, `Index.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. - -### Index.cshtml - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Permissions -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.AspNetCore.Authorization -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L -@inject IAuthorizationService AuthorizationService -@model IndexModel - -@section scripts -{ - -} - - - - - - @L["Authors"] - - - @if (await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Create)) - { - - } - - - - - - - -```` - -This is a simple page similar to the Books page we had created before. It imports a JavaScript file which will be introduced below. - -### Index.cshtml.cs - -````csharp -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Acme.BookStore.Web.Pages.Authors; - -public class IndexModel : PageModel -{ - public void OnGet() - { - - } -} -```` - -### Index.js - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal'); - - var dataTable = $('#AuthorsTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - visible: - abp.auth.isGranted('BookStore.Authors.Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - visible: - abp.auth.isGranted('BookStore.Authors.Delete'), - confirmMessage: function (data) { - return l( - 'AuthorDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - acme.bookStore.authors.author - .delete(data.record.id) - .then(function() { - abp.notify.info( - l('SuccessfullyDeleted') - ); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('BirthDate'), - data: "birthDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewAuthorButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -Briefly, this JavaScript page; - -* Creates a Data table with `Actions`, `Name` and `BirthDate` columns. - * `Actions` column is used to add *Edit* and *Delete* actions. - * `BirthDate` provides a `render` function to format the `DateTime` value using the [luxon](https://moment.github.io/luxon/) library. -* Uses the `abp.ModalManager` to open *Create* and *Edit* modal forms. - -This code is very similar to the Books page created before, so we will not explain it more. - -### Localizations - -This page uses some localization keys we need to declare. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -Notice that we've added more keys. They will be used in the next sections. - -### Add to the Main Menu - -Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add a new *Authors* menu item under the *Book Store* menu item. The following code (in the `ConfigureMainMenuAsync` method) shows the final code part: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ).RequirePermissions(BookStorePermissions.Books.Default) - ).AddItem( // ADDED THE NEW "AUTHORS" MENU ITEM UNDER THE "BOOK STORE" MENU - new ApplicationMenuItem( - "BooksStore.Authors", - l["Menu:Authors"], - url: "/Authors" - ).RequirePermissions(BookStorePermissions.Authors.Default) - ) -); -```` - -### Run the Application - -Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: - -![bookstore-author-permissions](images/bookstore-author-permissions-3.png) - -As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: - -![bookstore-authors-page](images/bookstore-authors-page-3.png) - -The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet. - -> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. - -## Create Modal - -Create a new razor page, `CreateModal.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. - -### CreateModal.cshtml - -```html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model CreateModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} -
- - - - - - - - - -
-``` - -We had used [dynamic forms](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) of the ABP Framework for the books page before. We could use the same approach here, but we wanted to show how to do it manually. Actually, not so manually, because we've used `abp-input` tag helper in this case to simplify creating the form elements. - -You can definitely use the standard Bootstrap HTML structure, but it requires to write a lot of code. `abp-input` automatically adds validation, localization and other standard elements based on the data type. - -### CreateModal.cshtml.cs - -```csharp -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Authors; - -public class CreateModalModel : BookStorePageModel -{ - [BindProperty] - public CreateAuthorViewModel Author { get; set; } - - private readonly IAuthorAppService _authorAppService; - - public CreateModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } - - public void OnGet() - { - Author = new CreateAuthorViewModel(); - } - - public async Task OnPostAsync() - { - var dto = ObjectMapper.Map(Author); - await _authorAppService.CreateAsync(dto); - return NoContent(); - } - - public class CreateAuthorViewModel - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } = string.Empty; - - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } - - [TextArea] - public string? ShortBio { get; set; } - } -} -``` - -This page model class simply injects and uses the `IAuthorAppService` to create a new author. The main difference between the book creation model class is that this one is declaring a new class, `CreateAuthorViewModel`, for the view model instead of re-using the `CreateAuthorDto`. - -The main reason of this decision was to show you how to use a different model class inside the page. But there is one more benefit: We added two attributes to the class members, which were not present in the `CreateAuthorDto`: - -* Added `[DataType(DataType.Date)]` attribute to the `BirthDate` which shows a date picker on the UI for this property. -* Added `[TextArea]` attribute to the `ShortBio` which shows a multi-line text area instead of a standard textbox. - -In this way, you can specialize the view model class based on your UI requirements without touching to the DTO. As a result of this decision, we have used `ObjectMapper` to map `CreateAuthorViewModel` to `CreateAuthorDto`. To be able to do that, you need to add a new mapping code to the `BookStoreWebAutoMapperProfile` constructor: - -````csharp -using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile -{ - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - - // ADD a NEW MAPPING - CreateMap(); - } -} -```` - -"New author" button will work as expected and open a new model when you run the application again: - -![bookstore-new-author-modal](images/bookstore-new-author-modal-2.png) - -## Edit Modal - -Create a new razor page, `EditModal.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. - -### EditModal.cshtml - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} -
- - - - - - - - - - -
-```` - -### EditModal.cshtml.cs - -```csharp -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Authors; - -public class EditModalModel : BookStorePageModel -{ - [BindProperty] - public EditAuthorViewModel Author { get; set; } - - private readonly IAuthorAppService _authorAppService; - - public EditModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } - - public async Task OnGetAsync(Guid id) - { - var authorDto = await _authorAppService.GetAsync(id); - Author = ObjectMapper.Map(authorDto); - } - - public async Task OnPostAsync() - { - await _authorAppService.UpdateAsync( - Author.Id, - ObjectMapper.Map(Author) - ); - - return NoContent(); - } - - public class EditAuthorViewModel - { - [HiddenInput] - public Guid Id { get; set; } - - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } = string.Empty; - - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } - - [TextArea] - public string? ShortBio { get; set; } - } -} -``` - -This class is similar to the `CreateModal.cshtml.cs` while there are some main differences; - -* Uses the `IAuthorAppService.GetAsync(...)` method to get the editing author from the application layer. -* `EditAuthorViewModel` has an additional `Id` property which is marked with the `[HiddenInput]` attribute that creates a hidden input for this property. - -This class requires to add two object mapping declarations to the `BookStoreWebAutoMapperProfile` class: - -```csharp -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile -{ - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - - CreateMap(); - - // ADD THESE NEW MAPPINGS - CreateMap(); - CreateMap(); - } -} -``` - -That's all! You can run the application and try to edit an author. - -{{else if UI == "NG"}} - -## The Author Management Page - -Run the following command line to create a new module, named `AuthorModule` in the root folder of the angular application: - -```bash -yarn ng generate module author --module app --routing --route authors -``` - -This command should produce the following output: - -```bash -> yarn ng generate module author --module app --routing --route authors - -yarn run v1.19.1 -$ ng generate module author --module app --routing --route authors -CREATE src/app/author/author-routing.module.ts (344 bytes) -CREATE src/app/author/author.module.ts (349 bytes) -CREATE src/app/author/author.component.html (21 bytes) -CREATE src/app/author/author.component.spec.ts (628 bytes) -CREATE src/app/author/author.component.ts (276 bytes) -CREATE src/app/author/author.component.scss (0 bytes) -UPDATE src/app/app-routing.module.ts (1396 bytes) -Done in 2.22s. -``` - -### AuthorModule - -Open the `/src/app/author/author.module.ts` and replace the content as shown below: - -```js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { AuthorRoutingModule } from './author-routing.module'; -import { AuthorComponent } from './author.component'; -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - declarations: [AuthorComponent], - imports: [SharedModule, AuthorRoutingModule, NgbDatepickerModule], -}) -export class AuthorModule {} -``` - -- Added the `SharedModule`. `SharedModule` exports some common modules needed to create user interfaces. -- `SharedModule` already exports the `CommonModule`, so we've removed the `CommonModule`. -- Added `NgbDatepickerModule` that will be used later on the author create and edit forms. - -### Menu Definition - -Open the `src/app/route.provider.ts` file and add the following menu definition: - -````js -{ - path: '/authors', - name: '::Menu:Authors', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Authors', -} -```` - -The final `configureRoutes` function declaration should be following: - -```js -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', - order: 1, - layout: eLayoutType.application, - }, - { - path: '/book-store', - name: '::Menu:BookStore', - iconClass: 'fas fa-book', - order: 2, - layout: eLayoutType.application, - }, - { - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Books', - }, - { - path: '/authors', - name: '::Menu:Authors', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Authors', - }, - ]); - }; -} -``` - -### Service Proxy Generation - -[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your HTTP APIs from the client side. Before running `generate-proxy` command, your host must be up and running. - -Run the following command in the `angular` folder: - -```bash -abp generate-proxy -t ng -``` - -This command generates the service proxy for the author service and the related model (DTO) classes: - -![bookstore-angular-service-proxy-author](images/bookstore-angular-service-proxy-author-2.png) - -### AuthorComponent - -Open the `/src/app/author/author.component.ts` file and replace the content as below: - -```js -import { Component, OnInit } from '@angular/core'; -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { AuthorService, AuthorDto } from '@proxy/authors'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; - -@Component({ - selector: 'app-author', - templateUrl: './author.component.html', - styleUrls: ['./author.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class AuthorComponent implements OnInit { - author = { items: [], totalCount: 0 } as PagedResultDto; - - isModalOpen = false; - - form: FormGroup; - - selectedAuthor = {} as AuthorDto; - - constructor( - public readonly list: ListService, - private authorService: AuthorService, - private fb: FormBuilder, - private confirmation: ConfirmationService - ) {} - - ngOnInit(): void { - const authorStreamCreator = (query) => this.authorService.getList(query); - - this.list.hookToQuery(authorStreamCreator).subscribe((response) => { - this.author = response; - }); - } - - createAuthor() { - this.selectedAuthor = {} as AuthorDto; - this.buildForm(); - this.isModalOpen = true; - } - - editAuthor(id: string) { - this.authorService.get(id).subscribe((author) => { - this.selectedAuthor = author; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - name: [this.selectedAuthor.name || '', Validators.required], - birthDate: [ - this.selectedAuthor.birthDate ? new Date(this.selectedAuthor.birthDate) : null, - Validators.required, - ], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - if (this.selectedAuthor.id) { - this.authorService - .update(this.selectedAuthor.id, this.form.value) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } else { - this.authorService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } - } - - delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure') - .subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.authorService.delete(id).subscribe(() => this.list.get()); - } - }); - } -} -``` - -Open the `/src/app/author/author.component.html` and replace the content as below: - -````html -
-
-
-
-
- {%{{{ '::Menu:Authors' | abpLocalization }}}%} -
-
-
-
- -
-
-
-
-
- - - -
- -
- - -
-
-
-
- - - - {%{{{ row.birthDate | date }}}%} - - -
-
-
- - - -

{%{{{ (selectedAuthor.id ? '::Edit' : '::NewAuthor') | abpLocalization }}}%}

-
- - -
-
- * - -
- -
- * - -
-
-
- - - - - - -
-```` - -### Localizations - -This page uses some localization keys we need to declare. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -### Run the Application - -Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: - -![bookstore-author-permissions](images/bookstore-author-permissions-2.png) - -As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: - -![bookstore-authors-page](images/bookstore-angular-authors-page-2.png) - -That's all! This is a fully working CRUD page, you can create, edit and delete authors. - -> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -## The Author Management Page - -### Authors Razor Component - -Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStore.Blazor.Client` project with the following content: - -````xml -@page "/authors" -@using Acme.BookStore.Authors -@using Acme.BookStore.Localization -@using Volo.Abp.AspNetCore.Components.Web -@inherits BookStoreComponentBase -@inject IAuthorAppService AuthorAppService -@inject AbpBlazorMessageLocalizerHelper LH - - - - -

@L["Authors"]

-
- - @if (CanCreateAuthor) - { - - } - -
-
- - - - - - - - @L["Actions"] - - - @if (CanEditAuthor) - { - - @L["Edit"] - - } - @if (CanDeleteAuthor) - { - - @L["Delete"] - - } - - - - - - - - @context.BirthDate.ToShortDateString() - - - - - -
- - - - -
- - @L["NewAuthor"] - - - - - - - @L["Name"] - - - - - - - - - @L["BirthDate"] - - - - - @L["ShortBio"] - - - - - - - - - - - - - -
-
-
- - - - -
- - @EditingAuthor.Name - - - - - - - @L["Name"] - - - - - - - - - @L["BirthDate"] - - - - - @L["ShortBio"] - - - - - - - - - - - - - -
-
-
-```` - -* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`, but uses its own implementation. -* Injects the `IAuthorAppService` to consume the server side HTTP APIs from the UI. We can directly inject application service interfaces and use just like regular method calls by the help of [Dynamic C# HTTP API Client Proxy System](../API/Dynamic-CSharp-API-Clients.md), which performs REST API calls for us. See the `Authors` class below to see the usage. -* Injects the `IAuthorizationService` to check [permissions](../Authorization.md). -* Injects the `IObjectMapper` for [object to object mapping](../Object-To-Object-Mapping.md). - -Create a new code behind file, `Authors.razor.cs`, under the `Pages` folder, with the following content: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Blazorise; -using Blazorise.DataGrid; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Blazor.Client.Pages; - -public partial class Authors -{ - private IReadOnlyList AuthorList { get; set; } - - private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; - private int CurrentPage { get; set; } - private string CurrentSorting { get; set; } - private int TotalCount { get; set; } - - private bool CanCreateAuthor { get; set; } - private bool CanEditAuthor { get; set; } - private bool CanDeleteAuthor { get; set; } - - private CreateAuthorDto NewAuthor { get; set; } - - private Guid EditingAuthorId { get; set; } - private UpdateAuthorDto EditingAuthor { get; set; } - - private Modal CreateAuthorModal { get; set; } - private Modal EditAuthorModal { get; set; } - - private Validations CreateValidationsRef; - - private Validations EditValidationsRef; - - public Authors() - { - NewAuthor = new CreateAuthorDto(); - EditingAuthor = new UpdateAuthorDto(); - } - - protected override async Task OnInitializedAsync() - { - await SetPermissionsAsync(); - await GetAuthorsAsync(); - } - - private async Task SetPermissionsAsync() - { - CanCreateAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Create); - - CanEditAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Edit); - - CanDeleteAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Delete); - } - - private async Task GetAuthorsAsync() - { - var result = await AuthorAppService.GetListAsync( - new GetAuthorListDto - { - MaxResultCount = PageSize, - SkipCount = CurrentPage * PageSize, - Sorting = CurrentSorting - } - ); - - AuthorList = result.Items; - TotalCount = (int)result.TotalCount; - } - - private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) - { - CurrentSorting = e.Columns - .Where(c => c.SortDirection != SortDirection.Default) - .Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : "")) - .JoinAsString(","); - CurrentPage = e.Page - 1; - - await GetAuthorsAsync(); - - await InvokeAsync(StateHasChanged); - } - - private void OpenCreateAuthorModal() - { - CreateValidationsRef.ClearAll(); - - NewAuthor = new CreateAuthorDto(); - CreateAuthorModal.Show(); - } - - private void CloseCreateAuthorModal() - { - CreateAuthorModal.Hide(); - } - - private void OpenEditAuthorModal(AuthorDto author) - { - EditValidationsRef.ClearAll(); - - EditingAuthorId = author.Id; - EditingAuthor = ObjectMapper.Map(author); - EditAuthorModal.Show(); - } - - private async Task DeleteAuthorAsync(AuthorDto author) - { - var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; - if (!await Message.Confirm(confirmMessage)) - { - return; - } - - await AuthorAppService.DeleteAsync(author.Id); - await GetAuthorsAsync(); - } - - private void CloseEditAuthorModal() - { - EditAuthorModal.Hide(); - } - - private async Task CreateAuthorAsync() - { - if (await CreateValidationsRef.ValidateAll()) - { - await AuthorAppService.CreateAsync(NewAuthor); - await GetAuthorsAsync(); - CreateAuthorModal.Hide(); - } - } - - private async Task UpdateAuthorAsync() - { - if (await EditValidationsRef.ValidateAll()) - { - await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); - await GetAuthorsAsync(); - EditAuthorModal.Hide(); - } - } -} -```` - -This class typically defines the properties and methods used by the `Authors.razor` page. - -### Object Mapping - -`Authors` class uses the `IObjectMapper` in the `OpenEditAuthorModal` method. So, we need to define this mapping. - -Open the `BookStoreBlazorAutoMapperProfile.cs` in the `Acme.BookStore.Blazor.Client` project and add the following mapping code in the constructor: - -````csharp -CreateMap(); -```` - -You will need to declare a `using Acme.BookStore.Authors;` statement to the beginning of the file. - -### Add to the Main Menu - -Open the `BookStoreMenuContributor.cs` in the `Acme.BookStore.Blazor.Client` project and add the following code to the end of the `ConfigureMainMenuAsync` method: - -````csharp -if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Authors", - l["Menu:Authors"], - url: "/authors" - )); -} -```` - -### Localizations - -We should complete the localizations we've used above. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -### Run the Application - -Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: - -![bookstore-author-permissions](images/bookstore-author-permissions-2.png) - -As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: - -![bookstore-authors-page](images/bookstore-authors-page-3.png) - -That's all! This is a fully working CRUD page, you can create, edit and delete the authors. - -> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. - -{{end}} \ No newline at end of file diff --git a/docs/en/Tutorials/Todo/Index.md b/docs/en/Tutorials/Todo/Index.md deleted file mode 100644 index 28725fd2c8..0000000000 --- a/docs/en/Tutorials/Todo/Index.md +++ /dev/null @@ -1,889 +0,0 @@ -# Quick Start - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -This is a single-part quick-start tutorial to build a simple todo application with the ABP Framework. Here's a screenshot from the final application: - -![todo-list](todo-list.png) - -You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). - -This documentation has a video tutorial on **YouTube**!! You can watch it here: - -{{if UI=="MVC" && DB =="EF"}} - - - -{{else if UI=="Blazor" && DB=="EF"}} - - - -{{else if UI=="BlazorServer" && DB=="EF"}} - - - -{{else if UI=="NG" && DB=="EF"}} - - - -{{else if UI=="MVC" && DB=="Mongo"}} - - - -{{else if UI=="BlazorServer" && DB=="Mongo"}} - - - -{{else if UI=="Blazor" && DB=="Mongo"}} - - - -{{else if UI=="NG" && DB=="Mongo"}} - - - -{{end}} - -## Pre-Requirements - -* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. -* [Node v16.x](https://nodejs.org/) - -{{if DB=="Mongo"}} - -* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) - -{{end}} - -## Install ABP CLI Tool - -We will use the [ABP CLI](../../CLI.md) to create new ABP solutions. You can run the following command on a terminal window to install this dotnet tool: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -## Create Your ABP Solution - -Create an empty folder, open a command-line terminal and execute the following command in the terminal: - -````bash -abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} -```` - -{{if UI=="NG"}} - -This will create a new solution, named *TodoApp* with `angular` and `aspnet-core` folders. Once the solution is ready, open the ASP.NET Core solution in your favorite IDE. - -{{else}} - -This will create a new solution, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. - -{{end}} - -### Create the Database - -If you are using Visual Studio, right click on the `TodoApp.DbMigrator` project, select *Set as StartUp Project*, then hit *Ctrl+F5* to run it without debugging. It will create the initial database and seed the initial data. - -{{if DB=="EF"}} - -> Some IDEs (e.g. Rider) may have problems for the first run since *DbMigrator* adds the initial migration and re-compiles the project. In this case, open a command-line terminal in the folder of the `.DbMigrator` project and execute the `dotnet run` command. - -{{end}} - -### Before Running the Application - -#### Installing the Client-Side Packages - -[ABP CLI](../../CLI.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. - -However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. - -For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: - -```bash -abp install-libs -``` - -> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. - -{{if UI=="Blazor" || UI=="BlazorServer"}} - -#### Bundling and Minification - -`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](../../CLI.md). - -However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your blazor application: - -```bash -abp bundle -``` - -> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../../UI/Blazor/Global-Scripts-Styles.md). - -{{end}} - -### Run the Application - -{{if UI=="MVC" || UI=="BlazorServer"}} - -It is good to run the application before starting the development. Ensure the {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the initial UI: - -{{else if UI=="Blazor"}} - -It is good to run the application before starting the development. The solution has two main applications; - -* `TodoApp.HttpApi.Host` hosts the server-side HTTP API. -* `TodoApp.Blazor` is the client-side Blazor WebAssembly application. - -Ensure the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): - -![todo-swagger-ui-initial](todo-swagger-ui-initial.png) - -You can explore and test your HTTP API with this UI. Now, we can set the `TodoApp.Blazor` as the startup project and run it to open the actual Blazor application UI: - -{{else if UI=="NG"}} - -It is good to run the application before starting the development. The solution has two main applications: - -* `TodoApp.HttpApi.Host` (in the .NET solution) host the server-side HTTP API. -* `angular` folder contains the Angular application. - -Ensure that the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): - -![todo-swagger-ui-initial](todo-swagger-ui-initial.png) - -You can explore and test your HTTP API with this UI. If it works, we can run the Angular client application. - -You can run the application using the following command: - -````bash -npm start -```` - -This command takes time, but eventually runs and opens the application in your default browser: - -{{end}} - -![todo-ui-initial](todo-ui-initial.png) - -You can click on the *Login* button, use `admin` as the username and `1q2w3E*` as the password to login to the application. - -All ready. We can start coding! - -## Domain Layer - -This application has a single [entity](../../Entities.md) and we'll start by creating it. Create a new `TodoItem` class inside the *TodoApp.Domain* project: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace TodoApp -{ - public class TodoItem : BasicAggregateRoot - { - public string Text { get; set; } - } -} -```` - -`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. - -## Database Integration - -{{if DB=="EF"}} - -Next step is to setup the [Entity Framework Core](../../Entity-Framework-Core.md) configuration. - -### Mapping Configuration - -Open the `TodoAppDbContext` class in the `EntityFrameworkCore` folder of the *TodoApp.EntityFrameworkCore* project and add a new `DbSet` property to this class: - -````csharp -public DbSet TodoItems { get; set; } -```` - -Then navigate to the `OnModelCreating` method in the `TodoAppDbContext` class and add the mapping code for the `TodoItem ` entity: - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - ... - - /* Configure your own tables/entities inside here */ - builder.Entity(b => - { - b.ToTable("TodoItems"); - }); -} -```` - -We've mapped the `TodoItem` entity to the `TodoItems` table in the database. - -### Code First Migrations - -The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. - -Open a command-line terminal in the directory of the *TodoApp.EntityFrameworkCore* project and type the following command: - -````bash -dotnet ef migrations add Added_TodoItem -```` - -This will add a new migration class to the project: - -![todo-efcore-migration](todo-efcore-migration.png) - -You can apply changes to the database using the following command, in the same command-line terminal: - -````bash -dotnet ef database update -```` - -> If you are using Visual Studio, you may want to use the `Add-Migration Added_TodoItem` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} is the startup project and `TodoApp.EntityFrameworkCore` is the *Default Project* in PMC. - -{{else if DB=="Mongo"}} - -Next step is to setup the [MongoDB](../../MongoDB.md) configuration. Open the `TodoAppMongoDbContext` class in the `MongoDb` folder of the *TodoApp.MongoDB* project and make the following changes: - -1. Add a new property to the class: - -````csharp -public IMongoCollection TodoItems => Collection(); -```` - -2. Add the following code inside the `CreateModel` method: - -````csharp -modelBuilder.Entity(b => -{ - b.CollectionName = "TodoItems"; -}); -```` - -{{end}} - -Now, we can use the ABP repositories to save and retrieve the todo items, as we'll do in the next section. - -## Application Layer - -An [Application Service](../../Application-Services.md) is used to perform the use cases of the application. We need to perform the following use cases: - -* Get the list of the todo items -* Create a new todo item -* Delete an existing todo item - -### Application Service Interface - -We can start by defining an interface for the application service. Create a new `ITodoAppService` interface in the *TodoApp.Application.Contracts* project, as shown below: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace TodoApp -{ - public interface ITodoAppService : IApplicationService - { - Task> GetListAsync(); - Task CreateAsync(string text); - Task DeleteAsync(Guid id); - } -} -```` - -### Data Transfer Object - -`GetListAsync` and `CreateAsync` methods return `TodoItemDto`. `ApplicationService` typically gets and returns DTOs ([Data Transfer Objects](../../Data-Transfer-Objects.md)) instead of entities. So, we should define the DTO class here. Create a new `TodoItemDto` class inside the *TodoApp.Application.Contracts* project: - -````csharp -using System; - -namespace TodoApp -{ - public class TodoItemDto - { - public Guid Id { get; set; } - public string Text { get; set; } - } -} -```` - -This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`. - -### Application Service Implementation - -Create a `TodoAppService` class inside the *TodoApp.Application* project, as shown below: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace TodoApp -{ - public class TodoAppService : ApplicationService, ITodoAppService - { - private readonly IRepository _todoItemRepository; - - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... - } -} -```` - -This class inherits from the `ApplicationService` class of the ABP Framework and implements the `ITodoAppService` that was defined before. ABP provides default generic [repositories](../../Repositories.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../Dependency-Injection.md) `IRepository`, which is the default repository for the `TodoItem` entity. We will use it to implement the use cases described before. - -#### Getting Todo Items - -Let's start by implementing the `GetListAsync` method: - -````csharp -public async Task> GetListAsync() -{ - var items = await _todoItemRepository.GetListAsync(); - return items - .Select(item => new TodoItemDto - { - Id = item.Id, - Text = item.Text - }).ToList(); -} -```` - -We are simply getting the complete `TodoItem` list from the database, mapping them to `TodoItemDto` objects and returning as the result. - -#### Creating a New Todo Item - -Next method is `CreateAsync` and we can implement it as shown below: - -````csharp -public async Task CreateAsync(string text) -{ - var todoItem = await _todoItemRepository.InsertAsync( - new TodoItem {Text = text} - ); - - return new TodoItemDto - { - Id = todoItem.Id, - Text = todoItem.Text - }; -} -```` - -The repository's `InsertAsync` method inserts the given `TodoItem` to the database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. - -#### Deleting a Todo Item - -Finally, we can implement the `DeleteAsync` as the following code block: - -````csharp -public async Task DeleteAsync(Guid id) -{ - await _todoItemRepository.DeleteAsync(id); -} -```` - -The application service is ready to be used from the UI layer. - -## User Interface Layer - -It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI: - -![todo-list](todo-list.png) - -> **We will keep the UI side minimal for this tutorial to make the tutorial simple and focused. See the [web application development tutorial](../Part-1.md) to build real-life pages with all aspects.** - -{{if UI=="MVC"}} - -### Index.cshtml.cs - -Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* project and replace the content with the following code block: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TodoApp.Web.Pages -{ - public class IndexModel : TodoAppPageModel - { - public List TodoItems { get; set; } - - private readonly ITodoAppService _todoAppService; - - public IndexModel(ITodoAppService todoAppService) - { - _todoAppService = todoAppService; - } - - public async Task OnGetAsync() - { - TodoItems = await _todoAppService.GetListAsync(); - } - } -} -```` - -This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. - -### Index.cshtml - -Open the `Index.cshtml` file in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: - -````xml -@page -@model TodoApp.Web.Pages.IndexModel -@section styles { - -} -@section scripts { - -} -
- - - - TODO LIST - - - - -
-
-
- -
-
-
- -
-
- -
    - @foreach (var todoItem in Model.TodoItems) - { -
  • - @todoItem.Text -
  • - } -
-
-
-
-```` - -We are using ABP's [card tag helper](../../UI/AspNetCore/Tag-Helpers/Cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers](../../UI/AspNetCore/Tag-Helpers/Index.md) make it much easier and type safe. - -This page imports a CSS and a JavaScript file, so we should also create them. - -### Index.js - -Open the `Index.js` file in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: - -````js -$(function () { - - // DELETING ITEMS ///////////////////////////////////////// - $('#TodoList').on('click', 'li i', function(){ - var $li = $(this).parent(); - var id = $li.attr('data-id'); - - todoApp.todo.delete(id).then(function(){ - $li.remove(); - abp.notify.info('Deleted the todo item.'); - }); - }); - - // CREATING NEW ITEMS ///////////////////////////////////// - $('#NewItemForm').submit(function(e){ - e.preventDefault(); - - var todoText = $('#NewItemText').val(); - todoApp.todo.create(todoText).then(function(result){ - $('
  • ') - .html(' ' + result.text) - .appendTo($('#TodoList')); - $('#NewItemText').val(''); - }); - }); -}); -```` - -In the first part, we are subscribing to the click events of the trash icons near the todo items, deleting the related item on the server and showing a notification on the UI. Also, we are removing the deleted item from the DOM, so we don't need to refresh the page. - -In the second part, we are creating a new todo item on the server. If it succeeds, we are then manipulating the DOM to insert a new `
  • ` element to the todo list. This way we don't need to refresh the whole page after creating a new todo item. - -The interesting part here is how we communicate with the server. See the *Dynamic JavaScript Proxies & Auto API Controllers* section to understand how it works. But now, let's continue and complete the application. - -### Index.css - -As the final touch, open the `Index.css` file in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the application again and see the result. - -### Dynamic JavaScript Proxies & Auto API Controllers - -In the `Index.js` file, we've used the `todoApp.todo.delete(...)` and `todoApp.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP Framework, thanks to the [Dynamic JavaScript Client Proxy](../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. - -However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to API Controllers by convention. - -If you open the [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: - -![todo-api](todo-api.png) - -{{else if UI=="Blazor" || UI=="BlazorServer"}} - -### Index.razor.cs - -Open the `Index.razor.cs` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: - -````csharp -using Microsoft.AspNetCore.Components; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TodoApp.Blazor.Pages -{ - public partial class Index - { - [Inject] - private ITodoAppService TodoAppService { get; set; } - - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } - - protected override async Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } - - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } - } -} -```` - -This class uses `ITodoAppService` to perform operations for the todo items. It manipulates the `TodoItems` list after create and delete operations. This way, we don't need to refresh the whole todo list from the server. - -{{if UI=="Blazor"}} - -See the *Dynamic C# Proxies & Auto API Controllers* section below to learn how we could inject and use the application service interface from the Blazor application which is running on the browser! But now, let's continue and complete the application. - -{{end # Blazor}} - -### Index.razor - -Open the `Index.razor` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: - -````xml -@page "/" -@inherits TodoAppComponentBase -
    - - - - TODO LIST - - - - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
      - @foreach (var todoItem in TodoItems) - { -
    • - @todoItem.Text -
    • - } -
    -
    -
    -
    -```` - -### Index.razor.css - -As the final touch, open the `Index.razor.css` file in the `Pages` folder of the *TodoApp.Blazor* project and replace it with the following content: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the application again to see the result. - -{{if UI=="Blazor"}} - -### Dynamic C# Proxies & Auto API Controllers - -In the `Index.razor.cs` file, we've injected (with the `[Inject]` attribute) and used the `ITodoAppService` just like using a local service. Remember that the Blazor application is running on the browser while the implementation of this application service is running on the server. - -The magic is done by the ABP Framework's [Dynamic C# Client Proxy](../../API/Dynamic-CSharp-API-Clients.md) system. It uses the standard `HttpClient` and performs HTTP API requests to the remote server. It also handles all the standard tasks for us, including authorization, JSON serialization and exception handling. - -However, you may ask that we haven't created any API Controller, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to API Controllers by convention. - -If you run the `TodoApp.HttpApi.Host` application, you can see the Todo API: - -![todo-api](todo-api.png) - -{{end # Blazor}} - -{{else if UI=="NG"}} - -### Service Proxy Generation - -ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. - -You first need to run the `TodoApp.HttpApi.Host` project since the proxy generator reads API definitions from the server application. - -> **Warning**: There is an issue with IIS Express: it doesn't allow connecting to the application from another process. If you are using Visual Studio, select the `TodoApp.HttpApi.Host` instead of IIS Express in the run button drop-down list, as shown in the figure below: - -![run-without-iisexpress](run-without-iisexpress.png) - -Once you run the `TodoApp.HttpApi.Host` project, open a command-line terminal in the `angular` folder and type the following command: - -````bash -abp generate-proxy -t ng -```` - -If everything goes well, it should generate an output as shown below: - -````bash -CREATE src/app/proxy/generate-proxy.json (170978 bytes) -CREATE src/app/proxy/README.md (1000 bytes) -CREATE src/app/proxy/todo.service.ts (794 bytes) -CREATE src/app/proxy/models.ts (66 bytes) -CREATE src/app/proxy/index.ts (58 bytes) -```` - -We can then use `todoService` to use the server-side HTTP APIs, as we'll do in the next section. - -### home.component.ts - -Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: - -````js -import { ToasterService } from '@abp/ng.theme.shared'; -import { Component, OnInit } from '@angular/core'; -import { TodoItemDto, TodoService } from '@proxy'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'] -}) -export class HomeComponent implements OnInit { - - todoItems: TodoItemDto[]; - newTodoText: string; - - constructor( - private todoService: TodoService, - private toasterService: ToasterService) - { } - - ngOnInit(): void { - this.todoService.getList().subscribe(response => { - this.todoItems = response; - }); - } - - create(): void{ - this.todoService.create(this.newTodoText).subscribe((result) => { - this.todoItems = this.todoItems.concat(result); - this.newTodoText = null; - }); - } - - delete(id: string): void { - this.todoService.delete(id).subscribe(() => { - this.todoItems = this.todoItems.filter(item => item.id !== id); - this.toasterService.info('Deleted the todo item.'); - }); - } -} - -```` - -We've used `todoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side. - -### home.component.html - -Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: - -````html -
    -
    -
    -
    TODO LIST
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
      -
    • - {%{{{ todoItem.text }}}%} -
    • -
    -
    -
    -
    -```` - -### home.component.scss - -As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the application again to see the result. - -{{end}} - -## Conclusion - -In this tutorial, we've built a very simple application to warm up for the ABP Framework. If you are looking to build a serious application, please check the [web application development tutorial](../Part-1.md) which covers all the aspects of real-life web application development. - -## Source Code - -You can find source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). - -## See Also - -* [Web Application Development Tutorial](../Part-1.md) diff --git a/docs/en/Tutorials/Todo/Overall.md b/docs/en/Tutorials/Todo/Overall.md deleted file mode 100644 index 2fc92ae2ac..0000000000 --- a/docs/en/Tutorials/Todo/Overall.md +++ /dev/null @@ -1,14 +0,0 @@ -# Quick Start: Overall - -**Welcome to the ABP Framework**. This is a single-part, quick-start tutorial to build a simple application. Start with this tutorial if you want to quickly understand how ABP Framework works. - -## Select the Solution Architecture - -This tutorial has multiple versions. Please select the one best fits for you: - -* **[Single-Layer Solution](Single-Layer/Index.md)**: Creates a single-project solution. Recommended for building an application with a **simpler and easy to understand** architecture. -* **[Layered Solution Architecture](Index.md)**: A fully layered (multiple projects) solution based on [Domain Driven Design](../../Domain-Driven-Design.md) practices. Recommended for long-term projects that need a **maintainable and extensible** codebase. - -## See Also - -* Check the [Web Application Development Tutorial](../Part-1.md) to see a real-life web application development in a layered architecture. diff --git a/docs/en/Tutorials/Todo/Single-Layer/Index.md b/docs/en/Tutorials/Todo/Single-Layer/Index.md deleted file mode 100644 index b04080e635..0000000000 --- a/docs/en/Tutorials/Todo/Single-Layer/Index.md +++ /dev/null @@ -1,875 +0,0 @@ -# Quick Start - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -This is a single-part quick-start tutorial to build a simple todo application with the ABP Framework. Here's a screenshot from the final application: - -![todo-list](../todo-list.png) - -You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). - -{{if UI=="Blazor"}} -We are currently preparing a video tutorial for Blazor UI. You can watch other tutorials for the three UI types from [here](https://www.youtube.com/playlist?list=PLsNclT2aHJcPqZxk7D4tU8LtTeCFcN_ci). -{{else}} -This documentation has a video tutorial on **YouTube**!! You can watch it here: -{{end}} - -{{if UI=="MVC" && DB =="EF"}} - - - -{{else if UI=="BlazorServer" && DB=="EF"}} - - - -{{else if UI=="NG" && DB=="EF"}} - - - -{{else if UI=="MVC" && DB=="Mongo"}} - - - -{{else if UI=="BlazorServer" && DB=="Mongo"}} - - - -{{else if UI=="NG" && DB=="Mongo"}} - - - -{{end}} - -## Pre-Requirements - -* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. -* [Node v16.x](https://nodejs.org/) - -{{if DB=="Mongo"}} - -* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) - -{{end}} - -## Creating a New Solution - -In this tutorial, we will use the [ABP CLI](../../../CLI.md) to create the sample application with the ABP Framework. You can run the following command in a command-line terminal to install the **ABP CLI**, if you haven't installed it yet: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Then create an empty folder, open a command-line terminal and execute the following command in the terminal: - -````bash -abp new TodoApp -t app-nolayers{{if UI=="BlazorServer"}} -u blazor-server{{else if UI=="Blazor"}} -u blazor{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} -```` - -{{if UI=="NG"}} - -This will create a new solution, named *TodoApp*, with `angular` and `aspnet-core` folders. Once the solution is ready, open the solution (in the `aspnet-core` folder) with your favorite IDE. - -{{else if UI=="Blazor"}} - -This will create a new solution with three projects: - -* A `blazor` application that contains the Blazor code, the client-side. -* A `host` application, hosts and serves the `blazor` application. -* A `contracts` project, shared library between these two projects. - -Once the solution is ready, open it in your favorite IDE. - -{{else}} - -This will create a new solution with a single project, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. - -{{end}} - -### Create the Database - -You can run the following command in the {{if UI=="Blazor"}} directory of your `TodoApp.Host` project {{else}}root directory of your project (in the same folder of the `.csproj` file){{end}} to create the database and seed the initial data: - -```bash -dotnet run --migrate-database -``` - -This command will create the database and seed the initial data for you. Then you can run the application. - -### Before Running the Application - -#### Installing the Client-Side Packages - -[ABP CLI](../../../CLI.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. - -However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. - -For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: - -```bash -abp install-libs -``` - -> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. - -{{if UI=="Blazor" || UI=="BlazorServer"}} - -#### Bundling and Minification - -`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](../../../CLI.md). - -However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your blazor application: - -```bash -abp bundle -``` - -> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../../../UI/Blazor/Global-Scripts-Styles.md). - -{{end}} - -### Run the Application - -{{if UI=="MVC" || UI=="BlazorServer"}} - -It is good to run the application before starting the development. Running the application is pretty straight-forward, you can run the application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project: - -{{else if UI=="Blazor"}} - -It is good to run the application before starting the development. Running the application is pretty straight-forward, you just need to run the `TodoApp.Host` application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project. - -> **Note:** The `host` application hosts and serves the `blazor` application. Therefore, you should run the `host` application only. - -After the application runs, open the application in your default browser: - -{{else if UI=="NG"}} - -It is good to run the application before starting the development. The solution has two main applications: - -* `TodoApp` (in the .NET solution) hosts the server-side HTTP API, so the Angular application can consume it. (server-side application) -* `angular` folder contains the Angular application. (client-side application) - -Firstly, run the `TodoApp` project in your favorite IDE (or run the `dotnet run` CLI command on your project directory) to see the server-side HTTP API on [Swagger UI](https://swagger.io/tools/swagger-ui/): - -![todo-swagger-ui-initial](./todo-single-layer-ui-initial.png) - -You can explore and test your HTTP API with this UI. If it works, then we can run the Angular client application. - -You can run the application using the following (or `yarn start`) command: - -````bash -npm start -```` - -This command takes time, but eventually runs and opens the application in your default browser: - -{{end}} - -![todo-ui-initial](../todo-ui-initial.png) - -You can click on the *Login* button and use `admin` as the username and `1q2w3E*` as the password to login to the application. - -All right. We can start coding! - -## Defining the Entity - -This application will have a single [entity](../../../Entities.md) and we can start by creating it. So, create a new `TodoItem` class under the `Entities` folder of {{if UI=="Blazor"}}the `TodoApp.Host` project{{else}}the project{{end}}: - -````csharp -using Volo.Abp.Domain.Entities; - -namespace TodoApp{{if UI=="Blazor"}}.{{end}}Entities; - -public class TodoItem : BasicAggregateRoot -{ - public string Text { get; set; } -} -```` - -`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. - -## Database Integration - -{{if DB=="EF"}} - -Next step is to setup the [Entity Framework Core](../../../Entity-Framework-Core.md) configuration. - -### Mapping Configuration - -Open the `TodoAppDbContext` class (in the `Data` folder) and add a new `DbSet` property to this class: - -````csharp -public DbSet TodoItems { get; set; } -```` - -Then navigate to the `OnModelCreating` method in the same class and add the following mapping code for the `TodoItem ` entity: - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - ... - - /* Configure your own tables/entities inside here */ - builder.Entity(b => - { - b.ToTable("TodoItems"); - }); -} -```` - -We've mapped the `TodoItem` entity to the `TodoItems` table in the database. The next step is to create a migration and apply the changes to the database. - -### Code First Migrations - -The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. - -Open a command-line terminal in the {{if UI=="Blazor"}} directory of your `TodoApp.Host` project {{else}}root directory of your project (in the same folder of the `.csproj` file){{end}} and type the following command: - -````bash -dotnet ef migrations add Added_TodoItem -```` - -This will add a new migration class to the project. You should see the new migration in the `Migrations` folder: - -![todo-efcore-migration](todo-efcore-migration-single-layer.png) - -Then, you can apply changes to the database using the following command, in the same command-line terminal: - -````bash -dotnet ef database update -```` - -{{else if DB=="Mongo"}} - -The next step is to setup the [MongoDB](../../../MongoDB.md) configuration. Open the `TodoAppDbContext` class (under the **Data** folder) in your project and make the following changes: - -1. Add a new property to the class: - -````csharp -public IMongoCollection TodoItems => Collection(); -```` - -2. Add the following code inside the `CreateModel` method: - -````csharp -modelBuilder.Entity(b => -{ - b.CollectionName = "TodoItems"; -}); -```` - -{{end}} - -After the database integrations, now we can start to create application service methods and implement our use-cases. - -## Creating the Application Service - -An [application service](../../../Application-Services.md) is used to perform the use cases of the application. We need to perform the following use cases in this application: - -* Get the list of the todo items -* Create a new todo item -* Delete an existing todo item - -Before starting to implement these use cases, first we need to create a DTO class that will be used in the application service. - -### Creating the Data Transfer Object (DTO) - -[Application services](../../../Application-Services.md) typically get and return DTOs ([Data Transfer Objects](../../../Data-Transfer-Objects.md)) instead of entities. So, create a new `TodoItemDto` class under the `Services/Dtos` folder{{if UI=="Blazor"}} of your `TodoApp.Contracts` project{{end}}: - -```csharp -namespace TodoApp.Services.Dtos; - -public class TodoItemDto -{ - public Guid Id { get; set; } - public string Text { get; set; } -} -``` - -This is a very simple DTO class that has the same properties as the `TodoItem` entity. Now, we are ready to implement our use-cases. - -{{if UI=="Blazor"}} - -### The Application Service Interface - -Create a `ITodoAppService` interface under the `Services` folder of the `TodoApp.Contracts` project, as shown below: - -```csharp -using TodoApp.Services.Dtos; -using Volo.Abp.Application.Services; - -namespace TodoApp.Services; - -public interface ITodoAppService : IApplicationService -{ - Task> GetListAsync(); - - Task CreateAsync(string text); - - Task DeleteAsync(Guid id); -} -``` - -{{end}} - -### The Application Service Implementation - -Create a `TodoAppService` class under the `Services` folder of {{if UI=="Blazor"}}your `TodoApp.Host` project{{else}}your project{{end}}, as shown below: - -```csharp -using TodoApp.Services; -{{if UI=="Blazor"}} -using TodoApp.Services.Dtos; -using TodoApp.Entities; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -{{else}} -using TodoAppEntities; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -{{end}} - -namespace TodoApp.Services; - -public class TodoAppService : TodoAppAppService{{if UI=="Blazor"}}, ITodoAppService{{end}} -{ - private readonly IRepository _todoItemRepository; - - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... -} -``` - -This class inherits from the `TodoAppAppService`, which inherits from the `ApplicationService` class of the ABP Framework and implements our use-cases. ABP provides default generic [repositories](../../../Repositories.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../../Dependency-Injection.md) `IRepository`, which is the default repository for the `TodoItem` entity. We will use it to implement our use cases. - -#### Getting the Todo Items - -Let's start by implementing the `GetListAsync` method, which is used to get a list of todo items: - -````csharp -public async Task> GetListAsync() -{ - var items = await _todoItemRepository.GetListAsync(); - return items - .Select(item => new TodoItemDto - { - Id = item.Id, - Text = item.Text - }).ToList(); -} -```` - -We are simply getting the `TodoItem` list from the repository, mapping them to the `TodoItemDto` objects and returning as the result. - -#### Creating a New Todo Item - -The next method is `CreateAsync` and we can implement it as shown below: - -````csharp -public async Task CreateAsync(string text) -{ - var todoItem = await _todoItemRepository.InsertAsync( - new TodoItem {Text = text} - ); - - return new TodoItemDto - { - Id = todoItem.Id, - Text = todoItem.Text - }; -} -```` - -The repository's `InsertAsync` method inserts the given `TodoItem` to the database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. - -#### Deleting a Todo Item - -Finally, we can implement the `DeleteAsync` as the following code block: - -````csharp -public async Task DeleteAsync(Guid id) -{ - await _todoItemRepository.DeleteAsync(id); -} -```` - -The application service is ready to be used from the UI layer. So, let's implement it. - -## User Interface - -It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI: - -![todo-list](../todo-list.png) - -{{if UI=="MVC"}} - -### Index.cshtml.cs - -Open the `Index.cshtml.cs` file in the `Pages` folder and replace the content with the following code block: - -```csharp -using TodoApp.Services; -using TodoApp.Services.Dtos; -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; - -namespace TodoApp.Pages; - -public class IndexModel : AbpPageModel -{ - public List TodoItems { get; set; } - - private readonly TodoAppService _todoAppService; - - public IndexModel(TodoAppService todoAppService) - { - _todoAppService = todoAppService; - } - - public async Task OnGetAsync() - { - TodoItems = await _todoAppService.GetListAsync(); - } -} -``` - -This class uses `TodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. - -### Index.cshtml - -Open the `Index.cshtml` file in the `Pages` folder and replace it with the following content: - -```xml -@page -@model TodoApp.Pages.IndexModel - -@section styles { - -} -@section scripts { - -} - -
    - - - - TODO LIST - - - - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
      - @foreach (var todoItem in Model.TodoItems) - { -
    • - @todoItem.Text -
    • - } -
    -
    -
    -
    -``` - -We are using ABP's [card tag helper](../../../UI/AspNetCore/Tag-Helpers/Cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers](../../../UI/AspNetCore/Tag-Helpers/Index.md) make it much easier and type safe. - -This page imports a CSS and a JavaScript file, so we should also create them. - -### Index.cshtml.js - -Open the `Index.cshtml.js` file in the `Pages` folder and replace with the following content: - -````js -$(function () { - - // DELETING ITEMS ///////////////////////////////////////// - $('#TodoList').on('click', 'li i', function(){ - var $li = $(this).parent(); - var id = $li.attr('data-id'); - - todoApp.services.todo.delete(id).then(function(){ - $li.remove(); - abp.notify.info('Deleted the todo item.'); - }); - }); - - // CREATING NEW ITEMS ///////////////////////////////////// - $('#NewItemForm').submit(function(e){ - e.preventDefault(); - - var todoText = $('#NewItemText').val(); - todoApp.services.todo.create(todoText).then(function(result){ - $('
  • ') - .html(' ' + result.text) - .appendTo($('#TodoList')); - $('#NewItemText').val(''); - }); - }); -}); -```` - -In the first part, we subscribed to the click events of the trash icons near the todo items, deleted the related item on the server and showed a notification on the UI. Also, we removed the deleted item from the DOM, so we wouldn't need to refresh the page. - -In the second part, we created a new todo item on the server. If it succeeded, we would then manipulate the DOM to insert a new `
  • ` element to the todo list. This way, we wouldn't need to refresh the whole page after creating a new todo item. - -The interesting part here is how we communicate with the server. See the *Dynamic JavaScript Proxies & Auto API Controllers* section to understand how it works. But now, let's continue and complete the application. - -### Index.cshtml.css - -As for the final touch, open the `Index.cshtml.css` file in the `Pages` folder and replace with the following content: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the application again and see the result. - -### Dynamic JavaScript Proxies & Auto API Controllers - -In the `Index.cshtml.js` file, we've used the `todoApp.services.todo.delete(...)` and `todoApp.services.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP Framework, thanks to the [Dynamic JavaScript Client Proxy](../../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. - -> `services` keyword comes from the namespace (`namespace TodoApp.Services;`). It's a naming convention. - -However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to **API Controllers** by convention. - -If you open [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: - -![todo-api](../todo-api.png) - -{{else if UI=="Blazor" || UI=="BlazorServer"}} - -### Index.razor.cs - -Open the `Index.razor.cs` file in the `Pages` folder{{if UI=="Blazor"}} in your `Todo.Blazor` project{{end}} and replace the content with the following code block: - -```csharp -{{if UI=="Blazor"}} -using Microsoft.AspNetCore.Components; -using TodoApp.Services; -using TodoApp.Services.Dtos; -{{else}} -using Microsoft.AspNetCore.Components; -using TodoApp.Services; -using TodoApp.Services.Dtos; -{{end}} - -namespace TodoApp.Pages; - -public partial class Index -{ - [Inject] - private {{if UI=="Blazor"}}ITodoAppService{{else}}TodoAppService{{end}} TodoAppService { get; set; } - - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } - - protected override async Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } - - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } -} -``` - -This class uses the {{if UI=="Blazor"}}`ITodoAppService`{{else}}`TodoAppService`{{end}} to get the list of todo items. It manipulates the `TodoItems` list after create and delete operations. This way, we don't need to refresh the whole todo list from the server. - -### Index.razor - -Open the `Index.razor` file in the `Pages` folder and replace the content with the following code block: - -```xml -@page "/" -@inherits TodoAppComponentBase - -
    - - - - TODO LIST - - - - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
      - @foreach (var todoItem in TodoItems) - { -
    • - - @todoItem.Text -
    • - } -
    -
    -
    -
    -``` - -### Index.razor.css - -As the final touch, open the `Index.razor.css` file in the `Pages` folder and replace it with the following content: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the {{if UI=="Blazor"}}`TodoApp.Host` project{{else}}application{{end}} again to see the result. - -{{else if UI=="NG"}} - -### Service Proxy Generation - -ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. - -You first need to run the `TodoApp` project since the proxy generator reads API definitions from the server application. - -Once you run the `TodoApp` project (**Swagger API Definition** will be shown), open a command-line terminal in the directory of `angular` folder and run the following command: - -```bash -abp generate-proxy -t ng -``` - -If everything goes well, it should generate an output as shown below: - -```bash -CREATE src/app/proxy/generate-proxy.json (182755 bytes) -CREATE src/app/proxy/README.md (1000 bytes) -CREATE src/app/proxy/services/todo.service.ts (833 bytes) -CREATE src/app/proxy/services/dtos/models.ts (71 bytes) -CREATE src/app/proxy/services/dtos/index.ts (26 bytes) -CREATE src/app/proxy/services/index.ts (81 bytes) -CREATE src/app/proxy/index.ts (61 bytes) -``` - -Then, we can use the `TodoService` to use the server-side HTTP APIs, as we'll do in the next section. - -### home.component.ts - -Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: - -```ts -import { ToasterService } from "@abp/ng.theme.shared"; -import { Component, OnInit } from '@angular/core'; -import { TodoItemDto } from "@proxy/services/dtos"; -import { TodoService } from "@proxy/services"; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'], -}) - -export class HomeComponent implements OnInit { - - todoItems: TodoItemDto[]; - newTodoText: string; - - constructor( - private todoService: TodoService, - private toasterService: ToasterService) - { } - - ngOnInit(): void { - this.todoService.getList().subscribe(response => { - this.todoItems = response; - }); - } - - create(): void{ - this.todoService.create(this.newTodoText).subscribe((result) => { - this.todoItems = this.todoItems.concat(result); - this.newTodoText = null; - }); - } - - delete(id: string): void { - this.todoService.delete(id).subscribe(() => { - this.todoItems = this.todoItems.filter(item => item.id !== id); - this.toasterService.info('Deleted the todo item.'); - }); - } -} -``` - -We've used `TodoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side. - -### home.component.html - -Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: - -````html -
    -
    -
    -
    TODO LIST
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
      -
    • - {%{{{ todoItem.text }}}%} -
    • -
    -
    -
    -
    -```` - -### home.component.scss - -As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -This is a simple styling for the todo page. We believe that you can do much better :) - -Now, you can run the application again to see the result. - -{{end}} - -## Conclusion - -In this tutorial, we've built a very simple application to warm up with the ABP Framework. - -## Source Code - -You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). - -## See Also - -* Check the [Web Application Development Tutorial](../../Part-1.md) to see a real-life web application development in a layered architecture using the [Application Startup Template](../../../Startup-Templates/Application.md). diff --git a/docs/en/Tutorials/images/bookstore-homepage.png b/docs/en/Tutorials/images/bookstore-homepage.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/en/Tutorials/images/bookstore-homepage.png and /dev/null differ diff --git a/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png and /dev/null differ diff --git a/docs/en/UI/Angular/Account-Module.md b/docs/en/UI/Angular/Account-Module.md deleted file mode 100644 index 28852f6b4d..0000000000 --- a/docs/en/UI/Angular/Account-Module.md +++ /dev/null @@ -1,148 +0,0 @@ -# Angular UI Account Module - -Angular UI account module is available as of v4.3. It contains some pages (login, register, My account, etc.). - -If you add the account module to your project; - -- "My account" link in the current user dropdown on the top bar will redirect the user to a page in the account module. -- You can switch the authentication flow to the resource owner password flow. - - -### Account Module Implementation - -Install the `@abp/ng.account` NPM package by running the below command: - -```bash -npm install @abp/ng.account -``` - -> Make sure v4.3 or higher version is installed. - -Open the `app.module.ts` and add `AccountConfigModule.forRoot()` to the imports array as shown below: - -```js -// app.module.ts - -import { AccountConfigModule } from '@abp/ng.account/config'; -//... - -@NgModule({ - imports: [ - //... - AccountConfigModule.forRoot() - ], - //... -}) -export class AppModule {} -``` - -Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: - -```js -// app-routing.module.ts -const routes: Routes = [ - //... - { - path: 'account', - loadChildren: () => import('@abp/ng.account').then(m => m.AccountModule.forLazy()), - }, - //... -export class AppRoutingModule {} -``` - -### Account Public Module Implementation for Commercial Templates - -The pro startup template comes with `@volo/abp.ng.account` package. You should update the package version to v4.3 or higher version. The package can be updated by running the following command: - -```bash -npm install @volo/abp.ng.account -``` -> Make sure v4.3 or higher version is installed. - -Open the `app.module.ts` and add `AccountPublicConfigModule.forRoot()` to the imports array as shown below: - -> Ensure that the `Account Layout Module` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.` when you try to access the account pages. Otherwise, you can skip adding the `AccountLayoutModule` step. - - -```js -// app.module.ts - -import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config'; -// if you are using or want to use Lepton X, you should add AccountLayoutModule -// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account' - -//... - -@NgModule({ - imports: [ - //... - AccountPublicConfigModule.forRoot(), - // AccountLayoutModule.forRoot() // Only for Lepton X - ], - //... -}) -export class AppModule {} -``` - -Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: - -```js -// app-routing.module.ts -const routes: Routes = [ - //... - { - path: 'account', - loadChildren: () => import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy()), - }, - //... -export class AppRoutingModule {} -``` - -### My Account Page - -Before v4.3, the "My account" link in the current user dropdown on the top bar redirected the user to MVC's profile management page. As of v4.3, if you added the account module to your project, the same link will land on a page in the Angular UI account module instead. - -### Personal Info Page Confirm Message - -When the user changes their own data on the personal settings tab in My Account, The data can not update the CurrentUser key of Application-Configuration. The information of the user is stored in claims. The only way to apply this information to the CurrentUser of Application-Configuration is user should log out and log in. When the Refresh-Token feature is implemented, it will be fixed. So We've added a confirmation alert. - -If you want to disable these warning, You should set `isPersonalSettingsChangedConfirmationActive` false - -```js -// app-routing.module.ts -const routes: Routes = [ - //... - { - path: 'account', - loadChildren: () => import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy({ isPersonalSettingsChangedConfirmationActive:false })), - }, - //... -export class AppRoutingModule {} -``` - -### Security Logs Page [COMMERCIAL] - -Before v4.3, the "Security Logs" link in the current user dropdown on the top bar redirected the user to MVC's security logs page. As of v4.3, if you added the account module to your project, the same link will land on a page in the Angular UI account public module instead. - -### Resource Owner Password Flow - -OAuth is preconfigured as authorization code flow in Angular application templates by default. If you added the account module to your project, you can switch the flow to resource owner password flow by changing the OAuth configuration in the _environment.ts_ files as shown below: - -```js -import { Config } from '@abp/ng.core'; - -export const environment = { - // other options removed for sake of brevity - - oAuthConfig: { - issuer: 'https://localhost:44305', // AuthServer url - clientId: 'MyProjectName_App', - dummyClientSecret: '1q2w3e*', - scope: 'offline_access MyProjectName', - }, - - // other options removed for sake of brevity -} as Config.Environment; -``` - -See the [Authorization in Angular UI](./Authorization.md) document for more details. diff --git a/docs/en/UI/Angular/Authorization.md b/docs/en/UI/Angular/Authorization.md deleted file mode 100644 index d2e8a61d51..0000000000 --- a/docs/en/UI/Angular/Authorization.md +++ /dev/null @@ -1,181 +0,0 @@ -## Authorization in Angular UI - -OAuth is preconfigured in Angular application templates. So, when you start a project using the CLI (or Suite, for that matter), authorization already works. ABP Angular UI packages are using [angular-oauth2-oidc library](https://github.com/manfredsteyer/angular-oauth2-oidc#logging-in) for managing OAuth in the Angular client. -You can find **OAuth configuration** in the _environment.ts_ files. - -### Authorization Code Flow - -```js -import { Config } from '@abp/ng.core'; - -const baseUrl = 'http://localhost:4200'; - -export const environment = { - // other options removed for sake of brevity - - oAuthConfig: { - issuer: 'https://localhost:44305', - redirectUri: baseUrl, - clientId: 'MyProjectName_App', - responseType: 'code', - scope: 'offline_access MyProjectName', - }, - - // other options removed for sake of brevity -} as Config.Environment; - -``` - -This configuration results in an [OAuth authorization code flow with PKCE](https://tools.ietf.org/html/rfc7636). -According to this flow, the user is redirected to an external login page which is built with MVC. So, if you need **to customize the login page**, please follow [this community article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). - -### Resource Owner Password Flow - -If you have used the [Angular UI account module](./Account-Module) in your project, you can switch to the resource owner password flow by changing the OAuth configuration in the _environment.ts_ files as shown below: - -```js -import { Config } from '@abp/ng.core'; - -export const environment = { - // other options removed for sake of brevity - - oAuthConfig: { - issuer: 'https://localhost:44305', - clientId: 'MyProjectName_App', - dummyClientSecret: '1q2w3e*', - scope: 'offline_access MyProjectName', - }, - - // other options removed for sake of brevity -} as Config.Environment; -``` - -According to this flow, the user is redirected to the login page in the account module. - -### Error Filtering - -In [AuthFlowStrategy](https://github.com/abpframework/abp/blob/21e70fd66154d4064d03b1a438f20a2e4318715e/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts#L24) class, there is a method called `listenToOauthErrors` that listens to `OAuthErrorEvent` errors. This method clears the localStorage for OAuth keys. However, in certain cases, we might want to skip this process. To achieve this, we can use the `AuthErrorFilterService`. -The `AuthErrorFilterService` is an abstract service that needs to be replaced with a custom implementation - -> By default, this service is replaced in the `@abp/ng.oauth` package - -### Usage - -#### 1.Create an auth-filter.provider - -```js -import { APP_INITIALIZER, inject } from '@angular/core'; -import { AuthErrorFilter, AuthErrorEvent, AuthErrorFilterService } from '@abp/ng.core'; -import { eCustomersAuthFilterNames } from '../enums'; - -export const CUSTOMERS_AUTH_FILTER_PROVIDER = [ - { provide: APP_INITIALIZER, useFactory: configureAuthFilter, multi: true }, -]; - -type Reason = object & { error: { grant_type: string | undefined } }; - -function configureAuthFilter() { - const errorFilterService = inject( - AuthErrorFilterService, AuthErrorEvent>, - ); - const filter: AuthErrorFilter = { - id: eCustomersAuthFilterNames.LinkedUser, - executable: true, - execute: (event: AuthErrorEvent) => { - const { reason } = event; - const { - error: { grant_type }, - } = (reason || {}); - - return !!grant_type && grant_type === eCustomersAuthFilterNames.LinkedUser; - }, - }; - - return () => errorFilterService.add(filter); -} -``` - -- `AuthErrorFilter:` is a model for filter object and it have 3 properties - - `id:` a unique key in the list for the filter object - - `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list - - `execute:` a function that stores the skip logic - -#### 2.Add to the FeatureConfigModule - -```js -import { ModuleWithProviders, NgModule } from "@angular/core"; -import { CUSTOMERS_AUTH_FILTER_PROVIDER } from "./providers/auth-filter.provider"; - -@NgModule() -export class CustomersConfigModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: CustomersConfigModule, - providers: [CUSTOMERS_AUTH_FILTER_PROVIDER], - }; - } -} -``` - -Now it'll skip the clearing of OAuth storage keys for `LinkedUser` grant_type if any `OAuthErrorEvent` occurs - -#### Replace with custom implementation - -Use the `AbstractAuthErrorFilter` class for signs of process. - -#### Example - -`my-auth-error-filter.service.ts` - -```js -import { Injectable, signal } from '@angular/core'; -import { MyAuthErrorEvent } from 'angular-my-auth-oidc'; -import { AbstractAuthErrorFilter, AuthErrorFilter } from '@abp/ng.core'; - -@Injectable({ providedIn: 'root' }) -export class OAuthErrorFilterService extends AbstractAuthErrorFilter< - AuthErrorFilter, - MyAuthErrorEvent -> { - protected readonly _filters = signal>>([]); - readonly filters = this._filters.asReadonly(); - - get(id: string): AuthErrorFilter { - return this._filters().find(({ id: _id }) => _id === id); - } - - add(filter: AuthErrorFilter): void { - this._filters.update(items => [...items, filter]); - } - - patch(item: Partial>): void { - const _item = this.filters().find(({ id }) => id === item.id); - if (!_item) { - return; - } - - Object.assign(_item, item); - } - - remove(id: string): void { - const item = this.filters().find(({ id: _id }) => _id === id); - if (!item) { - return; - } - - this._filters.update(items => items.filter(({ id: _id }) => _id !== id)); - } - - run(event: MyAuthErrorEvent): boolean { - return this.filters() - .filter(({ executable }) => !!executable) - .map(({ execute }) => execute(event)) - .some(item => item); - } -} - -``` - -## See Also - -* [Video tutorials](https://abp.io/video-courses/essentials/authorization) \ No newline at end of file diff --git a/docs/en/UI/Angular/Basic-Theme.md b/docs/en/UI/Angular/Basic-Theme.md deleted file mode 100644 index 651be9b637..0000000000 --- a/docs/en/UI/Angular/Basic-Theme.md +++ /dev/null @@ -1,107 +0,0 @@ -# Angular UI: Basic Theme - -The Basic Theme is a theme implementation for the Angular UI. It is a minimalist theme that doesn't add any styling on top of the plain [Bootstrap](https://getbootstrap.com/). You can take the Basic Theme as the **base theme** and build your own theme or styling on top of it. See the *Customization* section. - -> If you are looking for a professional, enterprise ready theme, you can check the [Lepton Theme](https://commercial.abp.io/themes), which is a part of the [ABP Commercial](https://commercial.abp.io/). - -> See the [Theming document](Theming.md) to learn about themes. - -## Installation - -If you need to manually this theme, follow the steps below: - -* Install the [@abp/ng.theme.basic](https://www.npmjs.com/package/@abp/ng.theme.basic) NPM package to your Angular project. -* Open the `src/app/app.module.ts` file, import `ThemeBasicModule` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule.forRoot()` to the `imports` array. -* Open the `src/app/shared/shared.module` file, import `ThemeBasicModule` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule` to the `imports` and `exports` array. - -The `ThemeBasicModule` is registered own layouts (`ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`) to a service which is exposed by `@abp/ng.core` package on application initialization. - -## Application Layout - -![basic-theme-application-layout](../../images/basic-theme-application-layout.png) - -Application Layout implements the following parts, in addition to the common parts mentioned above; - -* Logo area -* Routes area -* Language selection & user menu -* [Page Alerts](Page-Alerts.md) - -See Application Layout components: - -![application layout components](./images/layout-components.png) - -### How to Use a Layout - -Routes should be added to the menu by calling `add` method `RoutesService`. A layout can be set in the object of your route. See the [modifying the menu](Modifying-the-Menu#how-to-add-a-navigation-element) for more information. - -## Customization - -You have two options two customize this theme: - -### Overriding Styles / Components - -In this approach, you continue to use the theme as an NPM package and customize the parts you need to. There are several ways to customize it; - -#### Override the Styles - -You can simply override the styles in the global styles (`src/styles.scss`) file of your application. - -#### Override the Components - -See the [Component Replacement](Component-Replacement.md) to learn how you can replace components, customize and extend the user interface. - -### Copy & Customize - -You can run the following [ABP CLI](../../CLI.md) command in **Angular** project directory to copy the source code to your solution: - -`abp add-package @abp/ng.theme.basic --with-source-code` - ----- - -Or, you can download the [source code](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-basic) of the Basic Theme, manually copy the project content into your project (`projects/theme-basic` folder), open `angular.json` file and add configuration below to the `projects` object: - -```json -{ - "projects": { - ... - "theme-basic": { - "projectType": "library", - "root": "projects/theme-basic", - "sourceRoot": "projects/theme-basic/src", - "prefix": "abp", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:ng-packagr", - "options": { - "tsConfig": "projects/theme-basic/tsconfig.lib.json", - "project": "projects/theme-basic/ng-package.json" - }, - "configurations": { - "production": { - "tsConfig": "projects/theme-basic/tsconfig.lib.prod.json" - } - } - } - } - } - } -} -``` - -Then, open the `tsconfig.json` file and add new paths as follows: - -```json -"paths": { - ... - "@abp/ng.theme.basic": ["projects/theme-basic/src/public-api.ts"], - "@abp/ng.theme.basic/testing": ["projects/theme-basic/testing/src/public-api.ts"] -} -``` - - -You can now freely customize the theme based on your application requirements. - -## See Also - -* [Theming](Theming.md) diff --git a/docs/en/UI/Angular/Breadcrumb.md b/docs/en/UI/Angular/Breadcrumb.md deleted file mode 100644 index 58ae826278..0000000000 --- a/docs/en/UI/Angular/Breadcrumb.md +++ /dev/null @@ -1,50 +0,0 @@ -## Breadcrumb Component - -ABP provides a component that listens to the angular router's `NavigationEnd` -event and creates inputs for `BreadcrumbItemsComponent`. This component is used in -ABP components with [`PageComponent`](./Page-Component.md). - -## Breadcrumb Items Component - -`BreadcrumbItemsComponent` is used to display breadcrumb items. It can be useful -when you want to display breadcrumb items in a different way than the default. - -### Usage - -Example of overriding the default template of `PageComponent`: - -```html - - - - - -``` - -```js -import { Component } from "@angular/core"; -import { ABP } from "@abp/ng.core"; - -@Component({ - /* component metadata */ -}) -export class YourComponent { - breadCrumbItems: ABP.Route[] = [ - { - name: "Item 1", - }, - { - name: "Item 2", - path: "/path", - }, - ]; -} -``` - -### Inputs - -- items: Partial[] : Array of ABP.Route objects. The source code of ABP.Route can be found in [github](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L69). - -## See Also - -- [Page Component](./Page-Component.md) diff --git a/docs/en/UI/Angular/Caps-Lock-Directive.md b/docs/en/UI/Angular/Caps-Lock-Directive.md deleted file mode 100644 index 64a3fc42ca..0000000000 --- a/docs/en/UI/Angular/Caps-Lock-Directive.md +++ /dev/null @@ -1,80 +0,0 @@ -# Caps Lock Directive - -In password inputs, You may want to show if Caps Lock is on. To make this even easier, you can use the `TrackCapsLockDirective` which has been exposed by the `@abp/ng.core` package. - - -## Getting Started - -`TrackCapsLockDirective` is standalone. In order to use the `TrackCapsLockDirective` in an HTML template, import it to related module or your standalone component: - -**Importing to NgModule** -```ts -import { TrackCapsLockDirective } from '@abp/ng.core'; - -@NgModule({ - //... - declarations: [ - ..., - TestComponent - ], - imports: [ - ..., - TrackCapsLockDirective - ], -}) -export class MyFeatureModule {} -``` - -## Usage - -The `TrackCapsLockDirective` is very easy to use. The directive's selector is **`abpCapsLock`**. By adding the `abpCapsLock` event to an element, you can track the status of Caps Lock. You can use this to warn user. - -See an example usage: - -**NgModule Component usage** -```ts -@Component({ - selector: 'test-component', - template: ` -
    - - - icon -
    - ` -}) -export class TestComponent{ - capsLock = false; -} -``` - -**Standalone Component usage** -```ts -import { TrackCapsLockDirective } from '@abp/ng.core' - -@Component({ - selector: 'standalone-component', - standalone: true, - template: ` -
    - - - icon -
    - `, - imports: [TrackCapsLockDirective] -}) -export class StandaloneComponent{ - capsLock = false; -} -``` - -The `abpCapsLock` event has been added to the `` element. Press Caps Lock to activate the `TrackCapsLockDirective`. - -See the result: - -![Show Password directive](./images/CapsLockDirective1.png) - -To see Caps Lock icon press Caps Lock. - -![Show Password directive](./images/CapsLockDirective2.png) diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md deleted file mode 100644 index d21489d232..0000000000 --- a/docs/en/UI/Angular/Component-Replacement.md +++ /dev/null @@ -1,669 +0,0 @@ -# Component Replacement - -You can replace some ABP components with your custom components. - -The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_. - -## How to Replace a Component - -Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. - -Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below: - -```js -import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum -//... - -@Component(/* component metadata */) -export class AppComponent { - constructor( - private replaceableComponents: ReplaceableComponentsService, // injected the service - ) { - this.replaceableComponents.add({ - component: YourNewRoleComponent, - key: eIdentityComponents.Roles, - }); - } -} -``` - -![Example Usage](./images/component-replacement.gif) - - -## How to Replace a Layout - -Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced the same way. - -> A layout component template should contain `` element. - -The example below describes how to replace the `ApplicationLayoutComponent`: - -Run the following command to generate a layout in `angular` folder: - -```bash -yarn ng generate component my-application-layout -``` - -Add the following code in your layout template (`my-application-layout.component.html`) where you want the page to be loaded. - -```html - -``` - -Open `app.component.ts` in `src/app` folder and modify it as shown below: - -```js -import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys -import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent - -@Component(/* component metadata */) -export class AppComponent { - constructor( - private replaceableComponents: ReplaceableComponentsService, // injected the service - ) { - this.replaceableComponents.add({ - component: MyApplicationLayoutComponent, - key: eThemeBasicComponents.ApplicationLayout, - }); - } -} -``` - -> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. - -### Layout Components - -![Layout Components](./images/layout-components.png) - -### How to Add a New Layout Component - -To add a new layout component, you need to follow these steps: - -#### Step 1: Create a New Angular Component - -This component should have a 'router-outlet' for dynamic content loading. You can create a new component using the Angular CLI. Run the following command in your terminal: - -```bash -ng generate component new-layout -``` -This command will create a new component named `new-layout`. Now, open the new-layout.component.html file and add a `router-outlet` to it: - -```html - -``` -This 'router-outlet' will act as a placeholder that Angular dynamically fills based on the current router state. - -note: (don't forget: you should add the app in the app.module.ts file) - -#### Step 2: Define a Variable for the Layout Component - -Although this step is optional, it can be useful if you're going to use the layout component's value multiple times. You can define a variable for the layout component like this: - -```javascript -export const eCustomLayout = { - key: 'CustomLayout', - component: 'CustomLayoutComponent', -}; -``` -In this variable, `key` is a unique identifier for the layout component, and `component` is the name of the layout component. -You can use this variable when you need to refer to the layout component. - -#### Step 3: Add the Layout Component to the ABP Replaceable-System - -Next, you need to add the new layout component to the `ReplaceableComponentsService`. This service allows you to replace a component with another one dynamically. - -You can do this by defining a provider for `APP_INITIALIZER` that uses a factory function. In this function, you inject the `ReplaceableComponentsService` and use its `add` method to add the new layout component. - -Here's how you can do it: - -```javascript -export const CUSTOM_LAYOUT_PROVIDERS = [ - { provide: APP_INITIALIZER, useFactory: configureLayoutFn, deps: [ReplaceableComponentsService], multi: true }, - -]; -function configureLayoutFn() { - const service= inject( ReplaceableComponentsService) - return () =>{ - service.add({ - key: eCustomLayout.component, - component: CustomLayoutComponent, - }) - } -} -``` -In this code, `configureLayoutFn` is a factory function that adds the new layout component to the `ReplaceableComponentsService`. The `APP_INITIALIZER` provider runs this function when the application starts. - -note: (don't forget: you should add the CUSTOM_LAYOUT_PROVIDERS in the app.module.ts file) - -#### Step 4: Define the Application's Dynamic Layouts - -Finally, you need to define the application's dynamic layouts. This is a map where the keys are the layout keys and the values are the layout components. - -You can add the new layout to the existing layouts like this: - -```javascript -export const myDynamicLayouts = new Map([...DEFAULT_DYNAMIC_LAYOUTS, [eCustomLayout.key, eCustomLayout.component]]); -``` - -#### Step 5: Pass the Dynamic Layouts to the CoreModule - -The final step is to pass the dynamic layouts to the `CoreModule` using the `forRoot` method. This method allows you to configure the module with a static method. - -Here's how you can do it: - -```javascript -@NgModule({ - declarations: [AppComponent], - imports: [ - // other imports... - CoreModule.forRoot({ - dynamicLayouts: myDynamicLayouts, - environment, - registerLocaleFn: registerLocale(), - }), - // other imports... - NewLayoutComponent - ], - providers: [APP_ROUTE_PROVIDER, CUSTOM_LAYOUT_PROVIDERS], - bootstrap: [AppComponent], -}) -export class AppModule {} -``` -In this code, `myDynamicLayouts` is the map of dynamic layouts you defined earlier. We pass this map to the `CoreModule` using the `forRoot` method. - - -Now that you have defined the new layout, you can use it in the router definition. You do this by adding a new route that uses the new layout. - -Here's how you can do it: - -```javascript -// route.provider.ts -import { eCustomLayout } from './custom-layout/custom-layout.provider'; -import { RoutesService, eLayoutType } from '@abp/ng.core'; -import { APP_INITIALIZER } from '@angular/core'; - -export const APP_ROUTE_PROVIDER = [ - { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, -]; - -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', - order: 1, - layout: eLayoutType.application, - }, - { - path: '/dashboard', - name: '::Menu:Dashboard', - iconClass: 'fas fa-chart-line', - order: 2, - layout: eCustomLayout.key as eLayoutType, - requiredPolicy: 'MyProjectName.Dashboard.Host || MyProjectName.Dashboard.Tenant', - }, - ]); - }; -} -``` - -#### How to Replace LogoComponent - -![LogoComponent](./images/logo-component.png) - -Run the following command in `angular` folder to create a new component called `LogoComponent`. - -```bash -yarn ng generate component logo --inlineTemplate --inlineStyle -``` - - -Open the generated `logo.component.ts` in `src/app/logo` folder and replace its content with the following: - -```js -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-logo', - template: ` - - - logo - - `, -}) -export class LogoComponent {} -``` - -Open `app.component.ts` in `src/app` folder and modify it as shown below: - -```js -import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { LogoComponent } from './logo/logo.component'; // imported LogoComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService - - ngOnInit() { - //... - - this.replaceableComponents.add({ - component: LogoComponent, - key: eThemeBasicComponents.Logo, - }); - } -} -``` - -The final UI looks like below: - -![New logo](./images/replaced-logo-component.png) - -#### How to Replace RoutesComponent - -![RoutesComponent](./images/routes-component.png) - -Run the following command in `angular` folder to create a new component called `RoutesComponent`. - -```bash -yarn ng generate component routes -``` - -Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following: - -```js -import { Component, HostBinding } from '@angular/core'; - -@Component({ - selector: 'app-routes', - templateUrl: 'routes.component.html', -}) -export class RoutesComponent { - @HostBinding('class.mx-auto') - marginAuto = true; - - get smallScreen() { - return window.innerWidth < 992; - } -} -``` - -Import the `SharedModule` to the `imports` array of `AppModule`: - -```js -// app.module.ts - -import { SharedModule } from './shared/shared.module'; - -@NgModule({ - imports: [ - //... - SharedModule - ] -)} -``` - -Open the generated `routes.component.html` in `src/app/routes` folder and replace its content with the following: - -```html - -``` - -Open `app.component.ts` in `src/app` folder and modify it as shown below: - -```js -import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { RoutesComponent } from './routes/routes.component'; // imported RoutesComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService - - ngOnInit() { - //... - - this.replaceableComponents.add({ - component: RoutesComponent, - key: eThemeBasicComponents.Routes, - }); - } -} -``` - -The final UI looks like below: - -![New routes](./images/replaced-routes-component.png) - -#### How to Replace NavItemsComponent - -![NavItemsComponent](./images/nav-items-component.png) - -Run the following command in `angular` folder to create a new component called `NavItemsComponent`. - -```bash -yarn ng generate component nav-items -``` - -Open the generated `nav-items.component.ts` in `src/app/nav-items` folder and replace the content with the following: - -```js -import { - AuthService, - ConfigStateService, - CurrentUserDto, - LanguageInfo, - NAVIGATE_TO_MANAGE_PROFILE, - SessionStateService, -} from '@abp/ng.core'; -import { Component, Inject } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import snq from 'snq'; - -@Component({ - selector: 'app-nav-items', - templateUrl: 'nav-items.component.html', -}) -export class NavItemsComponent { - currentUser$: Observable = this.configState.getOne$('currentUser'); - selectedTenant$ = this.sessionState.getTenant$(); - - languages$: Observable = this.configState.getDeep$('localization.languages'); - - get smallScreen(): boolean { - return window.innerWidth < 992; - } - - get defaultLanguage$(): Observable { - return this.languages$.pipe( - map( - languages => - snq( - () => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName - ), - '' - ) - ); - } - - get dropdownLanguages$(): Observable { - return this.languages$.pipe( - map( - languages => - snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)), - [] - ) - ); - } - - get selectedLangCulture(): string { - return this.sessionState.getLanguage(); - } - - constructor( - @Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile, - private configState: ConfigStateService, - private authService: AuthService, - private sessionState: SessionStateService - ) {} - - onChangeLang(cultureName: string) { - this.sessionState.setLanguage(cultureName); - } - - navigateToLogin() { - this.authService.navigateToLogin(); - } - - logout() { - this.authService.logout().subscribe(); - } -} -``` - -Import the `SharedModule` to the `imports` array of `AppModule`: - -```js -// app.module.ts - -import { SharedModule } from './shared/shared.module'; - -@NgModule({ - imports: [ - //... - SharedModule - ] -)} -``` - -Open the generated `nav-items.component.html` in `src/app/nav-items` folder and replace the content with the following: - -```html - -``` - -Open `app.component.ts` in `src/app` folder and modify it as shown below: - -```js -import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService -import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService - - ngOnInit() { - //... - - this.replaceableComponents.add({ - component: NavItemsComponent, - key: eThemeBasicComponents.NavItems, - }); - } -} -``` - -The final UI looks like below: - -![New nav-items](./images/replaced-nav-items-component.png) - -## See Also - -- [How Replaceable Components Work with Extensions](./How-Replaceable-Components-Work-with-Extensions.md) -- [How to Replace PermissionManagementComponent](./Permission-Management-Component-Replacement.md) diff --git a/docs/en/UI/Angular/Config-State-Service.md b/docs/en/UI/Angular/Config-State-Service.md deleted file mode 100644 index 668b6f8568..0000000000 --- a/docs/en/UI/Angular/Config-State-Service.md +++ /dev/null @@ -1,135 +0,0 @@ -# Config State Service - -`ConfigStateService` is a singleton service, i.e. provided in root level of your application, and keeps the application configuration response in the internal store. - -## Before Use - -In order to use the `ConfigStateService` you must inject it in your class as a dependency. - -```js -import { ConfigStateService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private config: ConfigStateService) {} -} -``` - -You do not have to provide the `ConfigStateService` at module or component/directive level, because it is already **provided in root**. - -## Get Methods - -`ConfigStateService` has numerous get methods which allow you to get a specific configuration or all configurations. - -Get methods with "$" at the end of the method name (e.g. `getAll$`) return an RxJs stream. The streams are triggered when set or patched the state. - -### How to Get All Configurations - -You can use the `getAll` or `getAll$` method of `ConfigStateService` to get all of the applcation configuration response object. It is used as follows: - -```js -// this.config is instance of ConfigStateService - -const config = this.config.getAll(); - -// or -this.config.getAll$().subscribe(config => { - // use config here -}) -``` - -### How to Get a Specific Configuration - -You can use the `getOne` or `getOne$` method of `ConfigStateService` to get a specific configuration property. For that, the property name should be passed to the method as parameter. - -```js -// this.config is instance of ConfigStateService - -const currentUser = this.config.getOne("currentUser"); - -// or -this.config.getOne$("currentUser").subscribe(currentUser => { - // use currentUser here -}) -``` - -On occasion, you will probably want to be more specific than getting just the current user. For example, here is how you can get the `tenantId`: - -```js -const tenantId = this.config.getDeep("currentUser.tenantId"); - -// or -this.config.getDeep$("currentUser.tenantId").subscribe(tenantId => { - // use tenantId here -}) -``` - -or by giving an array of keys as parameter: - -```js -const tenantId = this.config.getDeep(["currentUser", "tenantId"]); -``` - -FYI, `getDeep` is able to do everything `getOne` does. Just keep in mind that `getOne` is slightly faster. - -### How to Get a Feature - -You can use the `getFeature` or `getFeature$` method of `ConfigStateService` to get a feature value. For that, the feature name should be passed to the method as parameter. - -```js -// this.config is instance of ConfigStateService - -const enableLdapLogin = this.config.getFeature("Account.EnableLdapLogin"); - -// or -this.config.getFeature$("Account.EnableLdapLogin").subscribe(enableLdapLogin => { - // use enableLdapLogin here -}) -``` - -> For more information, see the [features document](./Features). - -### How to Get a Setting - -You can use the `getSetting` or `getSetting$` method of `ConfigStateService` to get a setting. For that, the setting name should be passed to the method as parameter. - -```js -// this.config is instance of ConfigStateService - -const twoFactorBehaviour = this.config.getSetting("Abp.Identity.TwoFactor.Behaviour"); - -// or -this.config.getSetting$("Abp.Identity.TwoFactor.Behaviour").subscribe(twoFactorBehaviour => { - // use twoFactorBehaviour here -}) -``` - -> For more information, see the [settings document](./Settings). - -#### State Properties - -Please refer to `ApplicationConfigurationDto` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [models.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts#L11). - - -## Set State - -`ConfigStateService` has a method named `setState` which allow you to set the state value. - -You can get the application configuration response and set the `ConfigStateService` state value as shown below: - -```js -import {AbpApplicationConfigurationService, ConfigStateService} from '@abp/ng.core'; - -constructor(private abpApplicationConfigurationService: AbpApplicationConfigurationService, private config: ConfigStateService) { - this.abpApplicationConfigurationService.get({ includeLocalizationResources: false }).subscribe(config => { - this.config.setState(config); - }) -} -``` - -## See Also - -- [Settings](./Settings.md) -- [Features](./Features.md) diff --git a/docs/en/UI/Angular/Config-State.md b/docs/en/UI/Angular/Config-State.md deleted file mode 100644 index d6774bb0cb..0000000000 --- a/docs/en/UI/Angular/Config-State.md +++ /dev/null @@ -1 +0,0 @@ -**ConfigState has been deprecated.** Use the [ConfigStateService](./Config-State-Service) instead. \ No newline at end of file diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md deleted file mode 100644 index 53b5dae397..0000000000 --- a/docs/en/UI/Angular/Confirmation-Service.md +++ /dev/null @@ -1,201 +0,0 @@ -# Confirmation Popup - -You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project. - -## Getting Started - -You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private confirmation: ConfirmationService) {} -} -``` - -## Usage - -You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup. - -### How to Display a Confirmation Popup - -```js -const confirmationStatus$ = this.confirmation.success("Message", "Title"); -``` - -- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`. -- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum. - -### How to Listen Closing Event - -You can subscribe to the confirmation closing event like below: - -```js -import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; - -constructor(private confirmation: ConfirmationService) {} - -this.confirmation - .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) - .subscribe((status: Confirmation.Status) => { - // your code here - }); -``` - -- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md) -- `Confirmation.Status` is an enum and has three properties; - - `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. - - `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. - - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape or clicking the backdrop. - -If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: - -```js -this.confirmation.error("You are not authorized.", "Error"); -``` - -### How to Display a Confirmation Popup With Given Options - -Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: - -```js -const options: Partial = { - hideCancelBtn: false, - hideYesBtn: false, - dismissible: false, - cancelText: "Close", - yesText: "Confirm", - messageLocalizationParams: ["Demo"], - titleLocalizationParams: [], - // You can customize icon - // icon: 'fa fa-exclamation-triangle', // or - // iconTemplate : '' -} - -this.confirmation.warn( - "AbpIdentity::RoleDeletionConfirmationMessage", - "Are you sure?", - options -); -``` - -- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false`. -- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false`. -- `dismissible` option allows dismissing the confirmation popup by pressing escape or clicking the backdrop. Default value is `true`. -- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel`. -- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. -- `messageLocalizationParams` is the interpolation parameters for the localization of the message. -- `titleLocalizationParams` is the interpolation parameters for the localization of the title. -- `icon` is the custom class of the icon. Default value is `undefined`. -- `iconTemplate` is the template for icon. Default value is `undefined`. - -With the options above, the confirmation popup looks like this: - -![confirmation](./images/confirmation.png) - -You are able to pass in an HTML string as title, message, or button texts. Here is an example: - -```js -const options: Partial = { - yesText: 'Yes, delete it', -}; - -this.confirmation.warn( - ` - Role Demo will be deleted -
    - Do you confirm that? - `, - 'Are you sure?', - options -); -``` - -Since the values are HTML now, localization should be handled manually. Check out the [LocalizationService](./Localization#using-the-localization-service) to see how you can accomplish that. - -> Please note that all strings will be sanitized by Angular and not every HTML string will work. Only values that are considered as "safe" by Angular will be displayed. - -### How to Remove a Confirmation Popup - -The open confirmation popup can be removed manually via the `clear` method: - -```js -this.confirmation.clear(); -``` - -### How to Change Icons of The Confirmation Popup - -You can change icons with the token of "confirmationIcons" in ThemeSharedModule in the app.module.ts. The changes will affect all confirmation popup in the project. - -```js -... -ThemeSharedModule.forRoot({ - confirmationIcons: { - info: 'fa fa-info-circle', - success: 'fa fa-check-circle', - warning: 'fa fa-exclamation-triangle', - error: 'fa fa-times-circle', - default: 'fa fa-question-circle', - }, -}), -... -``` - - -## API - -### success - -```js -success( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -> See the [`LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/localization.ts#L6) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) - -### warn - -```js -warn( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### error - -```js -error( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### info - -```js -info( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### clear - -```js -clear( - status: Confirmation.Status = Confirmation.Status.dismiss -): void -``` - -- `status` parameter is the value of the confirmation closing event. diff --git a/docs/en/UI/Angular/Container-Strategy.md b/docs/en/UI/Angular/Container-Strategy.md deleted file mode 100644 index 3610c5ddd8..0000000000 --- a/docs/en/UI/Angular/Container-Strategy.md +++ /dev/null @@ -1,101 +0,0 @@ -# ContainerStrategy - -`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. - - - -## API - -`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. - - -### constructor - -```js -constructor( - public containerRef: ViewContainerRef, - private index?: number, // works only in InsertIntoContainerStrategy -) -``` - -- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. - - -### getIndex - -```js -getIndex(): number -``` - -This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. - - -### prepare - -```js -prepare(): void -``` - -This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). - - - -## ClearContainerStrategy - -`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. - - - -## InsertIntoContainerStrategy - -`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. - - - -## Predefined Container Strategies - -Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. - - -### Clear - -```js -CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) -``` - -Clears given container before content projection. - - -### Append - -```js -CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) -``` - -Projected content will be appended to the container. - - -### Prepend - -```js -CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) -``` - -Projected content will be prepended to the container. - - -### Insert - -```js -CONTAINER_STRATEGY.Insert( - containerRef: ViewContainerRef, - index: number, -) -``` - -Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). - - -## See Also - -- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md deleted file mode 100644 index ee64f57e20..0000000000 --- a/docs/en/UI/Angular/Content-Projection-Service.md +++ /dev/null @@ -1,73 +0,0 @@ -# Projecting Angular Content - -You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. - -## Getting Started - -You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. - -```js -import { ContentProjectionService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private contentProjectionService: ContentProjectionService) {} -} -``` - -## Usage - -You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. - -### How to Project Components to Root Level - -If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. - -```js -const strategy = PROJECTION_STRATEGY.AppendComponentToBody( - SomeOverlayComponent, - { someOverlayProp: "SOME_VALUE" } -); - -const componentRef = this.contentProjectionService.projectContent(strategy); -``` - -In the example above, `SomeOverlayComponent` component will placed at the **end** of `` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. - -> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. - -### How to Project Components and Templates into a Container - -If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. - -```js -const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( - SomeComponent, - viewContainerRefOfTarget, - { someProp: "SOME_VALUE" } -); - -const componentRef = this.contentProjectionService.projectContent(strategy); -``` - -In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`. - -> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. - -Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. - -## API - -### projectContent - -```js -projectContent | TemplateRef>( - projectionStrategy: ProjectionStrategy, - injector = this.injector, -): ComponentRef | EmbeddedViewRef -``` - -- `projectionStrategy` parameter is the primary focus here and is explained above. -- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. diff --git a/docs/en/UI/Angular/Content-Security-Strategy.md b/docs/en/UI/Angular/Content-Security-Strategy.md deleted file mode 100644 index f32317c9ca..0000000000 --- a/docs/en/UI/Angular/Content-Security-Strategy.md +++ /dev/null @@ -1,74 +0,0 @@ -# ContentSecurityStrategy - -`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). - - - - -## API - - -### constructor - -```js -constructor(public nonce?: string) -``` - -- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. - - -### applyCSP - -```js -applyCSP(element: HTMLScriptElement | HTMLStyleElement): void -``` - -This method maps the aforementioned properties to the given `element`. - - - - -## LooseContentSecurityPolicy - -`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `` element will place at the **end** of `` and `scriptElement` will be an `HTMLScriptElement`. - -Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. - -> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. - -### How to Insert Styles - -If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `` element will place at the **end** of `` and `styleElement` will be an `HTMLStyleElement`. - -Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. - -> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. - -### How to Remove Inserted Scripts & Styles - -If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element. - -```js -import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - private styleElement: HTMLStyleElement; - - constructor(private domInsertionService: DomInsertionService) {} - - ngOnInit() { - this.styleElement = this.domInsertionService.insertContent( - CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') - ); - } - - ngOnDestroy() { - this.domInsertionService.removeContent(this.styleElement); - } -} -``` - -In the example above, `` element **will be removed** from `` when the component is destroyed. - -## API - -### insertContent - -```js -insertContent( - contentStrategy: ContentStrategy, -): T -``` - -- `contentStrategy` parameter is the primary focus here and is explained above. -- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy. - -### removeContent - -```js -removeContent(element: HTMLScriptElement | HTMLStyleElement): void -``` - -- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method. - -### has - -```js -has(content: string): boolean -``` - -The `has` method returns a boolean value that indicates the given content has already been added to the DOM or not. - -- `content` parameter is the content of the inserted `HTMLScriptElement` or `HTMLStyleElement` element. diff --git a/docs/en/UI/Angular/Dom-Strategy.md b/docs/en/UI/Angular/Dom-Strategy.md deleted file mode 100644 index e7b6c68b0f..0000000000 --- a/docs/en/UI/Angular/Dom-Strategy.md +++ /dev/null @@ -1,90 +0,0 @@ -# DomStrategy - -`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. - - -## API - - -### constructor - -```js -constructor( - public target?: HTMLElement, - public position?: InsertPosition -) -``` - -- `target` is an HTMLElement (_default: document.head_). -- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). - - -### insertElement - -```js -insertElement(element: HTMLElement): void -``` - -This method inserts given `element` to `target` based on the `position`. - - - -## Predefined Dom Strategies - -Predefined dom strategies are accessible via `DOM_STRATEGY` constant. - - -### AppendToBody - -```js -DOM_STRATEGY.AppendToBody() -``` - -`insertElement` will place the given `element` at the end of ``. - - -### AppendToHead - -```js -DOM_STRATEGY.AppendToHead() -``` - -`insertElement` will place the given `element` at the end of ``. - - -### PrependToHead - -```js -DOM_STRATEGY.PrependToHead() -``` - -`insertElement` will place the given `element` at the beginning of ``. - - -### AfterElement - -```js -DOM_STRATEGY.AfterElement(target: HTMLElement) -``` - -`insertElement` will place the given `element` after (as a sibling to) the `target`. - - -### BeforeElement - -```js -DOM_STRATEGY.BeforeElement(target: HTMLElement) -``` - -`insertElement` will place the given `element` before (as a sibling to) the `target`. - - - - -## See Also - -- [DomInsertionService](./Dom-Insertion-Service.md) -- [LazyLoadService](./Lazy-Load-Service.md) -- [LoadingStrategy](./Loading-Strategy.md) -- [ContentStrategy](./Content-Strategy.md) -- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/en/UI/Angular/Dynamic-Form-Extensions.md b/docs/en/UI/Angular/Dynamic-Form-Extensions.md deleted file mode 100644 index 2fb73a6fac..0000000000 --- a/docs/en/UI/Angular/Dynamic-Form-Extensions.md +++ /dev/null @@ -1,346 +0,0 @@ -# Dynamic Form (or Form Prop) Extensions for Angular UI - - -## Introduction - -Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below: - -Form Prop Extension Example: 'Date of Birth' Field - -You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contributor for an edit form. - -## How to Set Up - -In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../Modules/Identity.md) and validate it. - -### Step 1. Create Form Prop Contributors - -The following code prepares two constants named `identityCreateFormPropContributors` and `identityEditFormPropContributors`, ready to be imported and used in your root module: - -```js -// src/app/form-prop-contributors.ts - -import { - eIdentityComponents, - IdentityCreateFormPropContributors, -} from '@abp/ng.identity'; -import { IdentityUserDto } from '@abp/ng.identity/proxy'; -import { ePropType, FormProp, FormPropList } from '@abp/ng.components/extensible'; -import { Validators } from '@angular/forms'; - -const birthdayProp = new FormProp({ - type: ePropType.Date, - name: 'birthday', - displayName: 'AbpIdentity::Birthday', - validators: () => [Validators.required], -}); - -export function birthdayPropContributor(propList: FormPropList) { - propList.addByIndex(birthdayProp, 4); -} - -export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = { - // enum indicates the page to add contributors to - [eIdentityComponents.Users]: [ - birthdayPropContributor, - // You can add more contributors here - ], -}; - -export const identityEditFormPropContributors = identityCreateFormPropContributors; -// you may define different contributors for edit form if you like - -``` - - -The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addByIndex` method, which adds the given value to the specified index of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). - -### Step 2. Import and Use Form Prop Contributors - -Import `identityCreateFormPropContributors` and `identityEditFormPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: - -```js -// src/app/app-routing.module.ts - -// other imports -import { - identityCreateFormPropContributors, - identityEditFormPropContributors, -} from './form-prop-contributors'; - -const routes: Routes = [ - // other routes - - { - path: 'identity', - loadChildren: () => - import('@abp/ng.identity').then(m => - m.IdentityModule.forLazy({ - createFormPropContributors: identityCreateFormPropContributors, - editFormPropContributors: identityEditFormPropContributors, - }) - ), - }, - - // other routes -]; -``` - -That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `IdentityModule`. - -## Object Extensions - -Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior. - -## API - -### PropData\ - -`PropData` is the shape of the parameter passed to all callbacks or predicates in a `FormProp`. - -It has the following properties: - -- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleFormPropComponent`, including, but not limited to, its parent components. - - ```js - { - type: ePropType.Enum, - name: 'myField', - options: data => { - const restService = data.getInjected(RestService); - const usersComponent = data.getInjected(UsersComponent); - - // Use restService and usersComponent public props and methods here - } - }, - ``` - -- **record** is the row data, i.e. current value of the selected item to edit. This property is _available only on edit forms_. - - ```js - { - type: ePropType.String, - name: 'myProp', - readonly: data => data.record.someOtherProp, - } - ``` - -### PropCallback\ - -`PropCallback` is the type of the callback function that can be passed to a `FormProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation: - -```js -type PropCallback = (data?: PropData) => R; -``` - -### PropPredicate\ - -`PropPredicate` is the type of the predicate function that can be passed to a `FormProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation: - -```js -type PropPredicate = (data?: PropData) => boolean; -``` - -### FormPropOptions\ - -`FormPropOptions` is the type that defines required and optional properties you have to pass in order to create a form prop. - -Its type definition is as follows: - -```js -type FormPropOptions = { - type: ePropType; - name: string; - displayName?: string; - id?: string; - permission?: string; - visible?: PropPredicate; - readonly?: PropPredicate; - disabled?: PropPredicate; - validators?: PropCallback; - asyncValidators?: PropCallback; - defaultValue?: boolean | number | string | Date; - options?: PropCallback[]>>; - autocomplete?: string; - isExtra? boolean; - formText?: string; - tooltip?: FormPropTooltip; -}; -``` - -As you see, passing `type` and `name` is enough to create a form prop. Here is what each property is good for: - -- **type** is the type of the prop value. It defines which input is rendered for the prop in the form. (_required_) -- **name** is the property name (or key) which will be used to read the value of the prop. (_required_) -- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`) -- **id** will be set as the `for` attribute of the label and the `id` attribute of the input for the field. (_default:_ `options.name`) -- **permission** is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (_default:_ `undefined`) -- **visible** is a predicate that will be used to decide if this prop should be displayed on the form or not. (_default:_ `() => true`) -- **readonly** is a predicate that will be used to decide if this prop should be readonly or not. (_default:_ `() => false`) -- **disabled** is a predicate that will be used to decide if this prop should be disabled or not. (_default:_ `() => false`) -- **validators** is a callback that returns validators for the prop. (_default:_ `() => []`) -- **asyncValidators** is a callback that returns async validators for the prop. (_default:_ `() => []`) -- **defaultValue** is the initial value the field will have. (_default:_ `null`) -- **options** is a callback that is called when a dropdown is needed. It must return an observable. (_default:_ `undefined`) -- **autocomplete** will be set as the `autocomplete` attribute of the input for the field. Please check [possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#Values). (_default:_ `'off'`) -- **isExtra** indicates this prop is an object extension. When `true`, the value of the field will be mapped from and to `extraProperties` of the entity. (_default:_ `undefined`) -- **formText** is the definition of the field. Placed under the field. (_default:_ `undefined`) -- **tooltip** is the tooltip for the field placed near of the label (_default:_ `undefined`) - -> Important Note: Do not use `record` property of `PropData` in create form predicates and callbacks, because it will be `undefined`. You can use it on edit form contributors though. - -You may find a full example below. - -### FormProp\ - -`FormProp` is the class that defines your form props. It takes a `FormPropOptions` and sets the default values to the properties, creating a form prop that can be passed to a form contributor. - -```js -const options: FormPropOptions = { - type: ePropType.Enum, - name: 'myProp', - displayName: 'Default::MyPropName', - id: 'my-prop', - permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical - visible: data => { - const store = data.getInjected(Store); - const selectSensitiveDataVisibility = ConfigState.getSetting( - 'Abp.Identity.IsSensitiveDataVisible' // hypothetical - ); - - return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; - }, - readonly: data => data.record.someProp, - disabled: data => data.record.someOtherProp, - validators: () => [Validators.required], - asyncValidators: data => { - const http = data.getInjected(HttpClient); - - function validate(control: AbstractControl): Observable { - if (control.pristine) return of(null); - - return http - .get('https://api.my-brand.io/hypothetical/endpoint/' + control.value) - .pipe(map(response => (response.valid ? null : { invalid: true }))); - } - - return [validate]; - }, - defaultValue: 0, - options: data => { - const service = data.getInjected(MyIdentityService); - - return service.getMyPropOptions() - .pipe( - map(({items}) => items.map( - item => ({key: item.name, value: item.id }) - )), - ); - }, - autocomplete: 'off', - isExtra: true, - template: undefined | Type, // Custom angular component - tooltip: { text: 'Default::MyPropName_Tooltip', placement: 'top' }, - formText: 'Default::MyPropName_Description', -}; - -const prop = new FormProp(options); -``` -FormProp has the template option since version 6.0. it can accept custom angular component. -The component can access PropData and Prop. -Example of the custom prop component. -```js -import { - EXTENSIBLE_FORM_VIEW_PROVIDER, - EXTENSIONS_FORM_PROP, - EXTENSIONS_FORM_PROP_DATA, -} from '@abp/ng.components/extensible'; - - -@Component({ - selector: 'my-custom-custom-prop', - templateUrl: './my-custom-custom-prop.component.html', - viewProviders: [EXTENSIBLE_FORM_VIEW_PROVIDER], //you should add this, otherwise form-group doesn't work. -}) -export class MyCustomPropComponent { - constructor( - @Inject(EXTENSIONS_FORM_PROP) private formProp: FormProp, - @Inject(EXTENSIONS_FORM_PROP_DATA) private propData: ProfileDto, - ...) - ... -} -``` - -It also has two static methods to create its instances: - -- **FormProp.create\\(options: FormPropOptions\\)** is used to create an instance of `FormProp`. - ```js - const prop = FormProp.create(options); - ``` -- **FormProp.createMany\\(options: FormPropOptions\\[\]\)** is used to create multiple instances of `FormProp` with given array of `FormPropOptions`. - ```js - const props = FormProp.createMany(optionsArray); - ``` - -### FormPropList\ - -`FormPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). - -The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: - -```js -export function reorderUserContributors( - propList: FormPropList, -) { - // drop email node - const emailPropNode = propList.dropByValue( - 'AbpIdentity::EmailAddress', - (prop, displayName) => prop.displayName === displayName, - ); - - // add it back after phoneNumber - propList.addAfter( - emailPropNode.value, - 'phoneNumber', - (value, name) => value.name === name, - ); -} -``` - -### CreateFormPropContributorCallback\ - -`CreateFormPropContributorCallback` is the type that you can pass as **create form** prop contributor callbacks to static `forLazy` methods of the modules. - -```js -export function myPropCreateContributor( - propList: FormPropList, -) { - // add myProp as 2nd field from the start - propList.add(myProp).byIndex(1); -} - -export const identityCreateFormPropContributors = { - [eIdentityComponents.Users]: [myPropCreateContributor], -}; -``` - -### EditFormPropContributorCallback\ - -`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules. - -```js -export function myPropEditContributor( - propList: FormPropList, -) { - // add myProp as 2nd field from the end - propList.add(myProp).byIndex(-1); -} - -export const identityEditFormPropContributors = { - [eIdentityComponents.Users]: [myPropEditContributor], -}; -``` - -## See Also - -- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Entity-Action-Extensions.md b/docs/en/UI/Angular/Entity-Action-Extensions.md deleted file mode 100644 index ead1a39b72..0000000000 --- a/docs/en/UI/Angular/Entity-Action-Extensions.md +++ /dev/null @@ -1,409 +0,0 @@ -# Entity Action Extensions for Angular UI - -## Introduction - -Entity action extension system allows you to add a new action to the action menu for an entity. A "Click Me" action was added to the user management page below: - -Entity Action Extension Example: 'Click Me!' Action - -You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code. - -## How to Set Up - -In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../Modules/Identity.md). - -### Step 1. Create Entity Action Contributors - -The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module: - -```ts -// src/app/entity-action-contributors.ts - -import { eIdentityComponents, IdentityEntityActionContributors } from '@abp/ng.identity'; -import { IdentityUserDto } from '@abp/ng.identity/proxy'; -import { EntityAction, EntityActionList } from '@abp/ng.components/extensible'; - -const alertUserName = new EntityAction({ - text: 'Click Me!', - action: (data) => { - // Replace alert with your custom code - alert(data.record.userName); - }, - // See EntityActionOptions in API section for all options -}); - -export function alertUserNameContributor(actionList: EntityActionList) { - actionList.addTail(alertUserName); -} - -export const identityEntityActionContributors: IdentityEntityActionContributors = { - // enum indicates the page to add contributors to - [eIdentityComponents.Users]: [ - alertUserNameContributor, - // You can add more contributors here - ], -}; -``` - -The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addTail` method, which adds the given value to the end of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). - -### Step 2. Import and Use Entity Action Contributors - -Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: - -```js -// src/app/app-routing.module.ts - -// other imports -import { identityEntityActionContributors } from './entity-action-contributors'; - -const routes: Routes = [ - // other routes - - { - path: 'identity', - loadChildren: () => - import('@abp/ng.identity').then(m => - m.IdentityModule.forLazy({ - entityActionContributors: identityEntityActionContributors, - }) - ), - }, - - // other routes -]; -``` - -That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `IdentityModule`. - -## How to Place a Custom Modal and Trigger It by Entity Actions - -Let's employ dependency injection to extend the functionality of `IdentityModule` and add a quick view action for the User entity. We will take a lazy-loaded approach. - -Entity Action Extension Example: Custom Modal - -1. Create a folder at this path: `src/app/identity-extended` - -2. Add an entity action similar to this: - ```js - // src/app/identity-extended/entity-action-contributors.ts - - import { - eIdentityComponents, - IdentityEntityActionContributors, - IdentityUserDto, - } from '@abp/ng.identity'; - import { EntityAction, EntityActionList } from '@abp/ng.components/extensible'; - import { IdentityExtendedComponent } from './identity-extended.component'; - - const quickViewAction = new EntityAction({ - text: 'Quick View', - action: data => { - const component = data.getInjected(IdentityExtendedComponent); - component.openUserQuickView(data.record); - }, - }); - - export function customModalContributor(actionList: EntityActionList) { - actionList.addTail(quickViewAction); - } - - export const identityEntityActionContributors: IdentityEntityActionContributors = { - // enum indicates the page to add contributors to - [eIdentityComponents.Users]: [ - customModalContributor, - // You can add more contributors here - ], - }; - ``` - -3. Create a parent component to the identity module. - ```js - // src/app/identity-extended/identity-extended.component.ts - - import { IdentityUserDto } from '@abp/ng.identity'; - import { Component } from '@angular/core'; - - @Component({ - selector: 'app-identity-extended', - templateUrl: './identity-extended.component.html', - }) - export class IdentityExtendedComponent { - isUserQuickViewVisible: boolean; - - user: IdentityUserDto; - - openUserQuickView(record: IdentityUserDto) { - this.user = new Proxy(record, { - get: (target, prop) => target[prop] || '—', - }); - this.isUserQuickViewVisible = true; - } - } - ``` - -4. Add a router outlet and a modal to the parent component. - ```html - - - - - - -

    {%{{{ user.userName }}}%}

    -
    - - - - - - - - - - - - - - - - - - - - - -
    {%{{{ 'AbpIdentity::DisplayName:Name' | abpLocalization }}}%}{%{{{ user.name }}}%}
    {%{{{ 'AbpIdentity::DisplayName:Surname' | abpLocalization }}}%}{%{{{ user.surname }}}%}
    {%{{{ 'AbpIdentity::EmailAddress' | abpLocalization }}}%}{%{{{ user.email }}}%}
    {%{{{ 'AbpIdentity::PhoneNumber' | abpLocalization }}}%}{%{{{ user.phoneNumber }}}%}
    -
    - - - - -
    - ``` - -5. Add a module for the component and load `IdentityModule` as seen below: - ```js - // src/app/identity-extended/identity-extended.module.ts - - import { CoreModule } from '@abp/ng.core'; - import { IdentityModule } from '@abp/ng.identity'; - import { ThemeSharedModule } from '@abp/ng.theme.shared'; - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - import { identityEntityActionContributors } from './entity-action-contributors'; - import { IdentityExtendedComponent } from './identity-extended.component'; - - @NgModule({ - imports: [ - CoreModule, - ThemeSharedModule, - RouterModule.forChild([ - { - path: '', - component: IdentityExtendedComponent, - children: [ - { - path: '', - loadChildren: () => - IdentityModule.forLazy({ - entityActionContributors: identityEntityActionContributors, - }), - }, - ], - }, - ]), - ], - declarations: [IdentityExtendedComponent], - }) - export class IdentityExtendedModule {} - ``` - -6. Load `IdentityExtendedModule` instead of `IdentityModule` in your root routing module. - ```js - // src/app/app-routing.module.ts - - const routes: Routes = [ - // other routes - - { - path: 'identity', - loadChildren: () => - import('./identity-extended/identity-extended.module') - .then(m => m.IdentityExtendedModule), - }, - - // other routes - ]; - ``` - -That's it. As you see, we reached the `IdentityExtendedComponent` through dependency injection and called one of its methods in our action. The specific user was also available via `data.record`, so we were able to display a summary view. - -## API - -### ActionData\ - -`ActionData` is the shape of the parameter passed to all callbacks or predicates in an `EntityAction`. - -It has the following properties: - -- **record** is the row data, i.e. current value rendered in the table. - - ```js - { - text: 'Click Me!', - action: data => { - alert(data.record.userName); - }, - } - ``` - -- **index** is the table index where the record is at. - -- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `GridActionsComponent`, including, but not limited to, its parent component. - - ```js - { - text: 'Click Me!', - action: data => { - const restService = data.getInjected(RestService); - - // Use restService public props and methods here - }, - visible: data => { - const usersComponent = data.getInjected(UsersComponent); - - // Use usersComponent public props and methods here - }, - } - ``` - -### ActionCallback\ - -`ActionCallback` is the type of the callback function that can be passed to an `EntityAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation: - -```js -type ActionCallback = (data?: ActionData) => R; -``` - -### ActionPredicate\ - -`ActionPredicate` is the type of the predicate function that can be passed to an `EntityAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation: - -```js -type ActionPredicate = (data?: ActionData) => boolean; -``` - -### EntityActionOptions\ - -`EntityActionOptions` is the type that defines required and optional properties you have to pass in order to create an entity action. - -Its type definition is as follows: - -```js -type EntityActionOptions = { - action: ActionCallback, - text: string, - icon?: string, - permission?: string, - visible?: ActionPredicate, - btnClass?: string, - btnStyle?: string, - showOnlyIcon?: boolean, - tooltip?: FormPropTooltip; -}; -``` - -As you see, passing `action` and `text` is enough to create an entity action. Here is what each property is good for: - -- **action** is a callback that is called when the grid action is clicked. (_required_) -- **text** is the button text which will be localized. (_required_) -- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`) -- **permission** is the permission context which will be used to decide if this type of grid action should be displayed to the user or not. (_default:_ `undefined`) -- **visible** is a predicate that will be used to decide if the current record should have this grid action or not. (_default:_ `() => true`) -- **btnClass** is the classes that will be applied to the button. (_default:_ `'btn btn-primary text-center'`) -- **btnStyle** is the styles that will be applied to the button. (_default:_ `''`) -- **showOnlyIcon** is shows only the icon itself. (_default:_ `false`) -- **tooltip** is only available in single entity action button. Adds an tooltip for button. (_default:_ undefined) - -You may find a full example below. - -### EntityAction\ - -`EntityAction` is the class that defines your entity actions. It takes an `EntityActionOptions` and sets the default values to the properties, creating an entity action that can be passed to an entity contributor. - -```js -const options: EntityActionOptions = { - action: data => { - const component = data.getInjected(IdentityExtendedComponent); - component.unlock(data.record.id); - }, - text: 'AbpIdentity::Unlock', - icon: 'fa fa-unlock', - permission: 'AbpIdentity.Users.Update', - visible: data => data.record.isLockedOut, - btnClass:'btn btn-warning text-center', - btnStyle: '', //Adds inline style - showOnlyIcon: true, - tooltip: { text: 'AbpIdentity::Edit', placement: 'top' } -}; - -const action = new EntityAction(options); -``` - -It also has two static methods to create its instances: - -- **EntityAction.create\\(options: EntityActionOptions\\)** is used to create an instance of `EntityAction`. - ```js - const action = EntityAction.create(options); - ``` -- **EntityAction.createMany\\(options: EntityActionOptions\\[\]\)** is used to create multiple instances of `EntityAction` with given array of `EntityActionOptions`. - ```js - const actions = EntityAction.createMany(optionsArray); - ``` - -### EntityActionList\ - -`EntityActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). - -The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: - -```js -export function reorderUserContributors( - actionList: EntityActionList, -) { - // drop "Unlock" button - const unlockActionNode = actionList.dropByValue( - 'AbpIdentity::Unlock', - (action, text) => action.text === text, - ); - - // add it back to the head of the list - actionList.addHead(unlockActionNode.value); -} -``` - -### EntityActionContributorCallback\ - -`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules. - -```js -// lockUserContributor should have EntityActionContributorCallback type - -export function lockUserContributor( - actionList: EntityActionList, -) { - // add lockUser as 3rd action - actionList.add(lockUser).byIndex(2); -} - -export const identityEntityActionContributors = { - [eIdentityComponents.Users]: [lockUserContributor], -}; -``` - -## See Also - -- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md deleted file mode 100644 index 026c5c5fc7..0000000000 --- a/docs/en/UI/Angular/Environment.md +++ /dev/null @@ -1,177 +0,0 @@ -# Environment - -Every application needs some **environment** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for ABP as well. - -Current `Environment` configuration holds sub config classes as follows: - -```js -export interface Environment { - apis: Apis; - application: Application; - oAuthConfig: AuthConfig; - production: boolean; - remoteEnv?: RemoteEnv; -} -``` - -## Apis - -```js -export interface Apis { - [key: string]: ApiConfig; - default: ApiConfig; -} - -export interface ApiConfig { - [key: string]: string; - rootNamespace?: string; - url: string; -} -``` - -Api config has to have a default config and it may have some additional ones for different modules. -I.e. you may want to connect to different Apis for different modules. - -Take a look at following example - -```json -{ - // ... - "apis": { - "default": { - "url": "https://localhost:8080" - }, - "AbpIdentity": { - "url": "https://localhost:9090" - } - } - // ... -} -``` - -When an api from `AbpIdentity` is called, the request will be sent to `"https://localhost:9090"`. -Everything else will be sent to `"https://localhost:8080"` - -- `rootNamespace` **(new)** : Root namespace of the related API. e.g. Acme.BookStore - -## Application - -```js -export interface Application { - name: string; - baseUrl?: string; - logoUrl?: string; -} -``` - -- `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided. -- `logoUrl`: Url of the application logo. It is used by `logo.component` -- `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver) - -## AuthConfig - -For authentication, we use angular-oauth2-oidc. Please check their [docs](https://github.com/manfredsteyer/angular-oauth2-oidc) out - -## RemoteEnvironment - -Some applications need to integrate an existing config into the `environment` used throughout the application. -Abp Framework supports this out of box. - -To integrate an existing config json into the `environment`, you need to set `remoteEnv` - -```js -export type customMergeFn = ( - localEnv: Partial, - remoteEnv: any -) => Config.Environment; - -export interface RemoteEnv { - url: string; - mergeStrategy: "deepmerge" | "overwrite" | customMergeFn; - method?: string; - headers?: ABP.Dictionary; -} -``` - -- `url` \*: Required. The url to be used to retrieve environment config -- `mergeStrategy` \*: Required. Defines how the local and the remote `environment` json will be merged - - `deepmerge`: Both local and remote `environment` json will be merged recursively. If both configs have same nested path, the remote `environment` will be prioritized. - - `overwrite`: Remote `environment` will be used and local environment will be ignored. - - `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial` and `remoteEnv` and it needs to return a `Config.Environment` object. -- `method`: HTTP method to be used when retrieving environment config. Default: `GET` -- `headers`: If extra headers are needed for the request, it can be set through this field. - -## EnvironmentService - -` EnvironmentService` is a singleton service, i.e. provided in root level of your application, and keeps the environment in the internal store. - -### Before Use - -In order to use the `EnvironmentService` you must inject it in your class as a dependency. - -```js -import { EnvironmentService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private environment: EnvironmentService) {} -} -``` - -You do not have to provide the `EnvironmentService` at module or component/directive level, because it is already **provided in root**. - -### Get Methods - -`EnvironmentService` has numerous get methods which allow you to get a specific value or all environment object. - -Get methods with "$" at the end of the method name (e.g. `getEnvironment$`) return an RxJs stream. The streams are triggered when set or patched the state. - -#### How to Get Environment Object - -You can use the `getEnvironment` or `getEnvironment$` method of `EnvironmentService` to get all of the environment object. It is used as follows: - -```js -// this.environment is instance of EnvironmentService - -const environment = this.environment.getEnvironment(); - -// or -this.environment.getEnvironment$().subscribe((environment) => { - // use environment here -}); -``` - -#### How to Get API URL - -The `getApiUrl` or `getApiUrl$` method is used to get a specific API URL from the environment object. This is how you can use it: - -```js -// this.environment is instance of EnvironmentService - -const apiUrl = this.environment.getApiUrl(); -// environment.apis.default.url - -this.environment.getApiUrl$("search").subscribe((searchUrl) => { - // environment.apis.search.url -}); -``` - -This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used. - -#### How to Set the Environment - -`EnvironmentService` has a method named `setState` which allows you to set the state value. - -```js -// this.environment is instance of EnvironmentService - -this.environment.setState(newEnvironmentObject); -``` - -Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. - -#### Environment Properties - -Please refer to `Environment` type for all the properties. It can be found in the [environment.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/environment.ts#L4). diff --git a/docs/en/UI/Angular/Extensions-Overall.md b/docs/en/UI/Angular/Extensions-Overall.md deleted file mode 100644 index 2bb9862b1d..0000000000 --- a/docs/en/UI/Angular/Extensions-Overall.md +++ /dev/null @@ -1,34 +0,0 @@ -## Angular UI Extensions - -Angular UI extensions system allows you to add a new action to the actions menu, a new column to the data table, a new action to the toolbar of a page, and add a new field to the create and/or edit forms. - -See the documents below for the details: - -* [Entity Action Extensions](Entity-Action-Extensions.md) -* [Data Table Column (or Entity Prop) Extensions](Data-Table-Column-Extensions.md) -* [Page Toolbar Extension](Page-Toolbar-Extensions.md) -* [Dynamic Form (or Form Prop) Extensions](Dynamic-Form-Extensions.md) - -## Extensible Table Component - -Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible table. - -````ts - - -```` - - * ` actionsText : ` ** Column name of action column. **Type** : string - * ` data : ` Items shows in your table. **Type** : Array - * ` list : ` Instance of ListService. **Type** : ListService - * `actionsColumnWidth : ` Width of your action column. **Type** : number - * ` actionsTemplate : ` Template of the action when "click this button" or whatever. Generally ng-template. **Type** : TemplateRef - * ` recordsTotal : ` Count of the record total. **Type** : number - * ` tableActivate : ` The Output(). A cell or row was focused via the keyboard or a mouse click. **Type** : EventEmitter() diff --git a/docs/en/UI/Angular/Feature-Libraries.md b/docs/en/UI/Angular/Feature-Libraries.md deleted file mode 100644 index bdfff93371..0000000000 --- a/docs/en/UI/Angular/Feature-Libraries.md +++ /dev/null @@ -1,92 +0,0 @@ -# About Feature Libraries - -ABP has an ever-growing number of feature modules and [introducing a new one](../../Module-Development-Basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them. - -## Feature Library Content - -Each library has at least two modules: - -1. The main module contains all components, services, types, enums, etc. to deliver the required UI when the feature is loaded. From here on, we will refer to these modules as **"feature module"**. -2. There is also a **"config module"** per library which helps us configure applications to run these modules or make them accessible. - -## How to Add a Feature Library to Your Project - - - -The manual setup of a feature library has three steps: - -### 1. Install the Library - -Feature libraries are usually published as an npm package. If a library you want to use does not exist in your project, you may install it via the following command: - -```shell -yarn add @my-company-name/my-project-name -``` - -...or... - -```shell -npm install @my-company-name/my-project-name -``` - -The `my-company-name` and `my-project-name` parts are going to change according to the package you want to use. For example, if we want to install the ABP Identity module, the package installation will be as seen below: - -```shell -yarn add @abp/ng.identity -``` - -> Identity is used just as an example. If you have initiated your project with ABP CLI or ABP Suite, the identity library will already be installed and configured in your project. - -### 2. Import the Config Module - -As of ABP v3.0, every lazy-loaded module has a config module available via a secondary entry point on the same package. Importing them in your root module looks like this: - -```js -import { IdentityConfigModule } from "@abp/ng.identity/config"; - -@NgModule({ - imports: [ - // other imports - IdentityConfigModule.forRoot(), - ], - // providers, declarations, and bootstrap -}) -export class AppModule {} -``` - -We need the config modules for actions required before feature modules are loaded (lazily). For example, the above import configures the menu to display links to identity pages. - -Furthermore, depending on the library, the `.forRoot` static method may receive some options that configure how the feature works. - -### 3. Import the Feature Module - -Finally, the feature module should be [loaded lazily via Angular router](https://angular.io/guide/lazy-loading-ngmodules). If you open the `/src/app/app-routing.module.ts` file, you should see `IdentityModule` is loaded exactly as follows: - -```js -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; - -const routes: Routes = [ - // other routes - { - path: "identity", - loadChildren: () => - import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()), - }, - // other routes -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], -}) -export class AppRoutingModule {} -``` - -When you load the identity feature like this, the "Users" page, for example, will have a route path of `/identity/users`. [1](#f-modify-route) - -Depending on the library, the `.forLazy` static method may also receive some options that configure how the feature works. - ---- - -1 _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./Modifying-the-Menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ [↩](#a-modify-route) diff --git a/docs/en/UI/Angular/Features.md b/docs/en/UI/Angular/Features.md deleted file mode 100644 index 3278bfd988..0000000000 --- a/docs/en/UI/Angular/Features.md +++ /dev/null @@ -1,33 +0,0 @@ -# Features - -You can get the value of a feature on the client-side using the [config state service](./Config-State.md) if it is allowed by the feature definition on the server-side. - -> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system. - -## Before Use - -To use the `ConfigStateService`, you must inject it in your class as a dependency. You do not have to provide the service explicitly, because it is already **provided in root**. - -```js -import { ConfigStateService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private config: ConfigStateService) {} -} -``` - -## How to Get a Specific Feature - -You can use the `getFeature` method of `ConfigStateService` to get a specific feature from the configuration state. Here is an example: - -```js -// this.config is instance of ConfigStateService - -const defaultLang = this.config.getFeature("Identity.TwoFactor"); -// 'Optional' -``` - -You can then check the value of the feature to perform your logic. Please note that **feature keys are case-sensitive**. diff --git a/docs/en/UI/Angular/Form-Validation.md b/docs/en/UI/Angular/Form-Validation.md deleted file mode 100644 index a178fa213c..0000000000 --- a/docs/en/UI/Angular/Form-Validation.md +++ /dev/null @@ -1,198 +0,0 @@ -# Form Validation - -Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npmjs.com/package/@ngx-validate/core) and helper texts are shown automatically based on validation rules and error blueprints. You do not have to add any elements or components to your templates. The library handles that for you. Here is how the experience is: - -The ngx-validate library validates an Angular reactive form and an error text appears under each wrong input based on the validation rule and the error blueprint. - -## How to Add New Error Messages - -You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module. - -```js -import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; -import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; - -@NgModule({ - imports: [ - ThemeSharedModule.forRoot({ - validation: { - blueprints: { - uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", - }, - }, - - // rest of theme shared config - }), - - // other imports - ], - - // rest of the module metadata -}) -export class AppModule {} -``` - -Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. - -```js -import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; -import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; - -@NgModule({ - providers: [ - { - provide: VALIDATION_BLUEPRINTS, - useValue: { - ...DEFAULT_VALIDATION_BLUEPRINTS, - uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", - }, - }, - - // other providers - ], - - // rest of the module metadata -}) -export class AppModule {} -``` - -When a [validator](https://angular.io/guide/form-validation#defining-custom-validators) or an [async validator](https://angular.io/guide/form-validation#creating-asynchronous-validators) returns an error with the key given to the error blueprints (`uniqueUsername` here), the validation library will be able to display an error message after localizing according to the given key and interpolation params. The result will look like this: - -An already taken username is entered while creating new user and a custom error message appears under the input after validation. - -In this example; - -- Localization key is `::AlreadyExists`. -- The interpolation param is `username`. -- Localization resource is defined as `"AlreadyExists": "Sorry, “{0}” already exists."`. -- And the validator should return `{ uniqueUsername: { username: "admin" } }` as the error object. - -## How to Change Existing Error Messages - -You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs. - -```json -"RequiredInput": "Oops! We need this input." -``` - -To use this instead of the built-in required input message, all you need to do is the following. - -```js -import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; -import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; - -@NgModule({ - imports: [ - ThemeSharedModule.forRoot({ - validation: { - blueprints: { - required: "::RequiredInput", - }, - }, - - // rest of theme shared config - }), - - // other imports - ], - - // rest of the module metadata -}) -export class AppModule {} -``` - -Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. - -```js -import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; -import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; - -@NgModule({ - providers: [ - { - provide: VALIDATION_BLUEPRINTS, - useValue: { - ...DEFAULT_VALIDATION_BLUEPRINTS, - required: "::RequiredInput", - }, - }, - - // other providers - ], - - // rest of the module metadata -}) -export class AppModule {} -``` - -The error message will look like this: - -A required field is cleared and the custom error message appears under the input. - -## How to Disable Validation on a Form - -If you want to validate a form manually, you can always disable automatic validation on it. All you need to do is place `skipValidation` on the form element. - -```html -
    - -
    -``` - -## How to Disable Validation on a Specific Field - -Validation works on any element or component with a `formControl` or `formControlName` directive. You can disable automatic validation on a specific field by placing `skipValidation` on the input element or component. - -```html - -``` - -## How to Use a Custom Error Component - -First, build a custom error component. Extending the existing `ValidationErrorComponent` would make it easier. - -```js -import { ValidationErrorComponent } from "@abp/ng.theme.basic"; -import { ChangeDetectionStrategy, Component } from "@angular/core"; - -@Component({ - selector: "app-validation-error", - template: ` -
    - {%{{{ error.message | abpLocalization: error.interpoliteParams }}}%} -
    - `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ErrorComponent extends ValidationErrorComponent {} -``` - -Then, declare and provide it in your root module. - -```js -import { VALIDATION_ERROR_TEMPLATE } from "@ngx-validate/core"; - -@NgModule({ - // rest of the module metadata - - declarations: [ - // other declarables - ErrorComponent, - ], - providers: [ - // other providers - { - provide: VALIDATION_ERROR_TEMPLATE, - useValue: ErrorComponent, - }, - ], -}) -export class AppModule {} -``` - -The error message will be bold and italic now: - -A required field is cleared and a bold and italic error message appears. diff --git a/docs/en/UI/Angular/GlobalFeatures.md b/docs/en/UI/Angular/GlobalFeatures.md deleted file mode 100644 index c8f9ba78df..0000000000 --- a/docs/en/UI/Angular/GlobalFeatures.md +++ /dev/null @@ -1,47 +0,0 @@ -# Angular: Global Features API - -The `ConfigStateService.getGlobalFeatures` API allows you to get the enabled features of the [Global Features](../../Global-Features.md) on the client side. - -> This document only explains the JavaScript API. See the [Global Features](../../Global-Features.md) document to understand the ABP Global Features system. - -## Usage - -````js - -import { ConfigStateService } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent implements OnInit { - constructor(private config: ConfigStateService) {} - - ngOnInit(): void { - // Gets all enabled global features. - const getGlobalFeatures = this.config.getGlobalFeatures(); - - //Example result is: `{ enabledFeatures: [ 'Shopping.Payment', 'Ecommerce.Subscription' ] }` - - // or - this.config.getGlobalFeatures$().subscribe(getGlobalFeatures => { - // use getGlobalFeatures here - }) - - // Check the global feature is enabled - this.config.getGlobalFeatureIsEnabled('Ecommerce.Subscription') - - //Example result is `true` - - this.config.getGlobalFeatureIsEnabled('My.Subscription') - - //Example result is `false` - - // or - this.config.getGlobalFeatureIsEnabled$('Ecommerce.Subscription').subscribe((isEnabled:boolean) => { - // use isEnabled here - }) - } -} - - diff --git a/docs/en/UI/Angular/HTTP-Error-Handling.md b/docs/en/UI/Angular/HTTP-Error-Handling.md deleted file mode 100644 index 8b4077fb53..0000000000 --- a/docs/en/UI/Angular/HTTP-Error-Handling.md +++ /dev/null @@ -1,229 +0,0 @@ -# HTTP Error Handling - -### Error Configurations - -ABP offers a configurations for errors handling like below - -```ts -import { ThemeSharedModule } from '@abp/ng.theme.shared'; -import { MyCustomRouteErrorComponent } from './my-custom-route.component'; - -@NgModule({ - imports: [ - ThemeSharedModule.forRoot({ - httpErrorConfig: { - skipHandledErrorCodes: [403], - errorScreen: { - forWhichErrors: [404], - component: CustomErrorComponent, - hideCloseIcon: false - } - } - }), - ... - ], -}) -export class AppModule {} -``` - -- `ErrorScreenErrorCodes` the error codes that you can pass to `skipHandledErrorCodes` and `forWhichErrors`. -- `skipHandledErrorCodes` the error codes those you don't want to handle it. -- `errorScreen` the screen that you want to show when a route error occurs. - - `component` component that you want to show. - - `forWhichErrors` same as `ErrorScreenErrorCodes` - - `hideCloseIcon` hides close icon in default ABP component. - -## Custom HTTP Error Handler -When the `RestService` is used, all HTTP errors are reported to the [`HttpErrorReporterService`](./HTTP-Error-Reporter-Service), and then `ErrorHandler`, a service exposed by the `@abp/ng.theme.shared` package automatically handles the errors. - -### Function Method `Deprecated` - -A custom HTTP error handler can be registered to an injection token named `HTTP_ERROR_HANDLER`. If a custom handler function is registered, the `ErrorHandler` executes that function. - -See an example: - -```ts -// http-error-handler.ts -import { ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core'; -import { ToasterService } from '@abp/ng.theme.shared'; -import { HttpErrorResponse } from '@angular/common/http'; -import { Injector } from '@angular/core'; -import { of, EMPTY } from 'rxjs'; -import { Error404Component } from './error404/error404.component'; - -export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { - if (httpError.status === 400) { - const toaster = injector.get(ToasterService); - toaster.error(httpError.error?.error?.message || 'Bad request!', '400'); - return EMPTY; - } - - if (httpError.status === 404) { - const contentProjection = injector.get(ContentProjectionService); - contentProjection.projectContent(PROJECTION_STRATEGY.AppendComponentToBody(Error404Component)); - return EMPTY; - } - - return of(httpError); -} - -// app.module.ts -import { Error404Component } from './error404/error404.component'; -import { handleHttpErrors } from './http-error-handling'; -import { HTTP_ERROR_HANDLER, ... } from '@abp/ng.theme.shared'; - -@NgModule({ - // ... - providers: [ - // ... - { provide: HTTP_ERROR_HANDLER, useValue: handleHttpErrors } - ], - declarations: [ - //... - Error404Component], -}) -export class AppModule {} -``` - -In the example above: - -- Created a function named `handleHttpErrors` and defined as value of the `HTTP_ERROR_HANDLER` provider in app.module. After this, the function executes when an HTTP error occurs. -- 400 bad request errors is handled. When a 400 error occurs. - -- Since `of(httpError)` is returned at bottom of the `handleHttpErrors`, the `ErrorHandler` will handle the HTTP errors except 400 and 404 errors. - -**Note 1:** If you put `return EMPTY` to next line of handling an error, default error handling will not work for that error. [EMPTY](https://rxjs.dev/api/index/const/EMPTY) can be imported from `rxjs`. - -```ts -export function handleHttpErrors( - injector: Injector, - httpError: HttpErrorResponse -) { - if (httpError.status === 403) { - // handle 403 errors here - return EMPTY; // put return EMPTY to skip default error handling - } -} -``` - -**Note 2:** If you put `return of(httpError)`, default error handling will work. - -- `of` is a function. It can be imported from `rxjs`. -- `httpError` is the second parameter of the error handler function which is registered to the `HTTP_ERROR_HANDLER` provider. Type of the `httpError` is `HttpErrorResponse`. - -```ts -import { of } from "rxjs"; - -export function handleHttpErrors( - injector: Injector, - httpError: HttpErrorResponse -) { - if (httpError.status === 500) { - // handle 500 errors here - } - - // you can return the of(httpError) at bottom of the function to run the default handler of ABP for HTTP errors that you didn't handle above. - return of(httpError); -} -``` - -### Service Method - -You can provide **more than one handler** with services, a custom HTTP error handler service can be registered with injection token named **`CUSTOM_ERROR_HANDLERS`**. ABP has some default [error handlers](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts). - -### How To Add New Handler Service - -ABP error handler services are implements the interface of **CustomHttpErrorHandlerService**. - -**Interface of `CUSTOM_ERROR_HANDLERS`** - -```ts -interface CustomHttpErrorHandlerService { - readonly priority: number; - canHandle(error: unknown): boolean; - execute(): void; -} -``` - -- **`priority`** ABP sorts the services according to the number of the priority variable. Higher priority will be checked first. -- **`canHandle`** Check if the service can handle the error. Returns boolean. -- **`execute`** If the service can handle the error, then run the execute method. - -**In Summary** - -- Services are sorted by their priority number. -- Start from highest priority service and run canHandle() method. Pick the service if can handle the error, if not check next service. -- If the service found, run the execute method of a service. Done. - -See an example: - -```ts -// custom-error-handler.service.ts -import { inject, Injectable } from "@angular/core"; -import { HttpErrorResponse } from "@angular/common/http"; -import { CustomHttpErrorHandlerService } from "@abp/ng.theme.shared"; -import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from "@abp/ng.theme.shared"; -import { ToasterService } from "@abp/ng.theme.shared"; - -@Injectable({ providedIn: "root" }) -export class MyCustomErrorHandlerService - implements CustomHttpErrorHandlerService -{ - // You can write any number here, ex: 9999 - readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.veryHigh; - protected readonly toaster = inject(ToasterService); - private error: HttpErrorResponse | undefined = undefined; - - // What kind of error should be handled by this service? You can decide it in this method. If error is suitable to your case then return true; otherwise return false. - canHandle(error: unknown): boolean { - if (error instanceof HttpErrorResponse && error.status === 400) { - this.error = error; - return true; - } - return false; - } - - // If this service is picked from ErrorHandler, this execute method will be called. - execute() { - this.toaster.error( - this.error.error?.error?.message || "Bad request!", - "400" - ); - } -} -``` - -```ts - -// app.module.ts -import { CUSTOM_ERROR_HANDLERS, ... } from '@abp/ng.theme.shared'; -import { MyCustomErrorHandlerService } from './custom-error-handler.service'; - -@NgModule({ - // ... - providers: [ - // ... - { - provide: CUSTOM_ERROR_HANDLERS, - useExisting: MyCustomErrorHandlerService, - multi: true, - } - ] -}) -export class AppModule {} -``` - -In the example above: - -- Created a service named `MyCustomErrorHandlerService`, and provided via `useExisting` key because we dont want another instance of it. And set `multi` key to true because ABP default error handlers are also provided with **CUSTOM_ERROR_HANDLERS** injection token. - -- 400 errors are handled from custom `MyCustomErrorHandlerService`. When a 400 error occurs, backend error message will be displayed as shown below: - -![custom-error-handler-toaster-message](images/custom-error-handler-toaster-message.jpg) - -### Notes - -- If your service cannot handle the error. Then ABP will check the next Error Service. -- If none of the service handle the error. Then basic confirmation message about the error will be shown to the user. -- You can provide more than one service, with CUSTOM_ERROR_HANDLER injection token. -- If you want your custom service to be evaluated (checked) earlier, set the priority variable high. diff --git a/docs/en/UI/Angular/HTTP-Error-Reporter-Service.md b/docs/en/UI/Angular/HTTP-Error-Reporter-Service.md deleted file mode 100644 index 5f02589751..0000000000 --- a/docs/en/UI/Angular/HTTP-Error-Reporter-Service.md +++ /dev/null @@ -1,71 +0,0 @@ -# HTTP Error Reporter Service - -`HttpErrorReporterService` is a service which is exposed by `@abp/ng.core` package. HTTP errors can be reported by using this service. The service emits an event when an error is reported and keeps the errors as an array. The [`RestService`](./HTTP-Requests#restservice) uses the `HttpErrorReporterService` for reporting errors. - -See the example below to learn how to report an error: - -```ts -import { HttpErrorReporterService } from '@abp/ng.core'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { of } from 'rxjs'; -import { catchError } from 'rxjs/operators'; - -@Injectable() -export class SomeService { - constructor(private http: HttpClient, private httpErrorReporter: HttpErrorReporterService) {} - - getData() { - return this.http.get('http://example.com/get-data').pipe( - catchError(err => { - this.httpErrorReporter.reportError(err); - return of(null); - }), - ); - } -} -``` - -See the following example to learn listening the reported errors: - -```ts -import { HttpErrorReporterService } from '@abp/ng.core'; -import { HttpErrorResponse } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class MyErrorHandler { - constructor(private httpErrorReporter: HttpErrorReporterService) { - this.handleErrors(); - } - - handleErrors() { - this.httpErrorReporter.reporter$.subscribe((err: HttpErrorResponse) => { - // handle the errors here - }); - } -} -``` - - -## API - - -### `reporter$: Observable` - -`reporter$` is a getter, returns an observable. It emits an event when a new error is reported. The event value type is `HttpErrorResponse`. - - -### `errors$: Observable` - -`errors$` is a getter, returns an observable. It emits an event when a new error is reported. The event value is all errors reported at runtime. - -### `errors: HttpErrorResponse` - -`errors` is a getter that returns all errors reported. - - -### `reportError(error: HttpErrorResponse): void` - -`reportError` is a method. The errors can be reported via this. -When an error is reported, the method triggers the `reports$` and `errors$` observables to emit an event. \ No newline at end of file diff --git a/docs/en/UI/Angular/HTTP-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md deleted file mode 100644 index 9bef5cb8f2..0000000000 --- a/docs/en/UI/Angular/HTTP-Requests.md +++ /dev/null @@ -1,190 +0,0 @@ -# How to Make HTTP Requests - -## About HttpClient - -Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project. - -However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned. - -```js -getConfig() { - this.http.get(this.configUrl).subscribe( - config => this.updateConfig(config), - error => { - // Handle error here - }, - ); -} -``` - -Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable. - -An `HttpInterceptor` is able to catch `HttpErrorResponse` and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details. - -## RestService - -ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by default. - -### Getting Started with RestService - -In order to use the `RestService`, you must inject it in your class as a dependency. - -```js -import { RestService } from '@abp/ng.core'; - -@Injectable({ - /* class metadata here */ -}) -class DemoService { - constructor(private rest: RestService) {} -} -``` - -You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**. - -### How to Make a Request with RestService - -You can use the `request` method of the `RestService` is for HTTP requests. Here is an example: - -```js -getFoo(id: number) { - const request: Rest.Request = { - method: 'GET', - url: '/api/some/path/to/foo/' + id, - }; - - return this.rest.request(request); -} -``` - -The `request` method always returns an `Observable`. Therefore you can do the following wherever you use `getFoo` method: - -```js -doSomethingWithFoo(id: number) { - this.demoService.getFoo(id).subscribe( - foo => { - // Do something with foo. - } - ) -} -``` - -**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error. - -As you see, `request` method gets a request options object with `Rest.Request` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one: - -```js -postFoo(body: Foo) { - const request: Rest.Request = { - method: 'POST', - url: '/api/some/path/to/foo', - body - }; - - return this.rest.request(request); -} -``` - -You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request` type, which has only a few changes compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular. - -### How to Disable Default Error Handler of RestService - -The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself: - -```js -deleteFoo(id: number) { - const request: Rest.Request = { - method: 'DELETE', - url: '/api/some/path/to/foo/' + id, - }; - - return this.rest.request(request, { skipHandleError: true }); -} -``` - -`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription. - -```js -removeFooFromList(id: number) { - this.demoService.deleteFoo(id).subscribe( - foo => { - // Do something with foo. - }, - error => { - // Do something with error. - } - ) -} -``` - -### How to Get a Specific API Endpoint From Application Config - -Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration. - -```js -putFoo(body: Foo, id: string) { - const request: Rest.Request = { - method: 'PUT', - url: '/' + id, - body - }; - - return this.rest.request(request, {apiName: 'foo'}); -} -``` - -`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows: - -```js -// environment.ts - -export const environment = { - apis: { - default: { - url: "https://localhost:44305", - }, - foo: { - url: "https://localhost:44305/api/some/path/to/foo", - }, - }, - - /* rest of the environment variables here */ -}; -``` - -### How to Observe Response Object or HTTP Events Instead of Body - -`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object. - -```js -getSomeCustomHeaderValue() { - const request: Rest.Request = { - method: 'GET', - url: '/api/some/path/that/sends/some-custom-header', - }; - - return this.rest.request>( - request, - {observe: Rest.Observe.Response}, - ).pipe( - map(response => response.headers.get('Some-Custom-Header')) - ); -} -``` - -You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). - -### How to Skip HTTP interceptors and ABP headers - -The ABP Framework adds several HTTP headers to the HttpClient, such as the "Auth token" or "tenant Id". -The ABP Server must possess the information but the ABP user may not want to send this informations to an external server. -ExternalHttpClient and IS EXTERNAL REQUEST HttpContext Token were added in V6.0.4. -The ABP Http interceptors check the value of the `IS_EXTERNAL_REQUEST` token. If the token is True then ABP-specific headers won't be added to Http Request. -The `ExternalHttpClient` extends from `HTTPClient` and sets the `IS_EXTERNAL_REQUEST` context token to true. -When you are using `ExternalHttpClient` as HttpClient in your components, it does not add ABP-specific headers. - -Note: With `IS_EXTERNAL_REQUEST` or without it, ABP loading service works. - -## See Also - -- [HTTP Error Handling / Customization](./HTTP-Error-Handling) diff --git a/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md b/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md deleted file mode 100644 index c617b89a14..0000000000 --- a/docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md +++ /dev/null @@ -1,271 +0,0 @@ -# How Replaceable Components Work with Extensions - -Additional UI extensibility points ([Entity action extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Entity-Action-Extensions), [data table column extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Data-Table-Column-Extensions), [page toolbar extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Page-Toolbar-Extensions) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. - -Create a new module called `MyRolesModule`: - -```bash -yarn ng generate module my-roles --module app -``` - -Create a new component called `MyRolesComponent`: - -```bash -yarn ng generate component my-roles/my-roles --flat --export -``` - -Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following: - -```js -import { ListService, PagedAndSortedResultRequestDto, PagedResultDto } from '@abp/ng.core'; -import { eIdentityComponents, IdentityRoleDto, IdentityRoleService, RolesComponent } from '@abp/ng.identity'; -import { ePermissionManagementComponents } from '@abp/ng.permission-management'; -import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; -import { - EXTENSIONS_IDENTIFIER, - FormPropData, - generateFormFromProps -} from '@abp/ng.components/extensible'; -import { Component, Injector, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { finalize } from 'rxjs/operators'; - -@Component({ - selector: 'app-my-roles', - templateUrl: './my-roles.component.html', - providers: [ - ListService, - { - provide: EXTENSIONS_IDENTIFIER, - useValue: eIdentityComponents.Roles, - }, - { - provide: RolesComponent, - useExisting: MyRolesComponent - } - ], -}) -export class MyRolesComponent implements OnInit { - data: PagedResultDto = { items: [], totalCount: 0 }; - - form: FormGroup; - - selected: IdentityRoleDto; - - isModalVisible: boolean; - - visiblePermissions = false; - - providerKey: string; - - modalBusy = false; - - permissionManagementKey = ePermissionManagementComponents.PermissionManagement; - - onVisiblePermissionChange = event => { - this.visiblePermissions = event; - }; - - constructor( - public readonly list: ListService, - protected confirmationService: ConfirmationService, - protected injector: Injector, - protected service: IdentityRoleService, - ) {} - - ngOnInit() { - this.hookToQuery(); - } - - buildForm() { - const data = new FormPropData(this.injector, this.selected); - this.form = generateFormFromProps(data); - } - - openModal() { - this.buildForm(); - this.isModalVisible = true; - } - - add() { - this.selected = {} as IdentityRoleDto; - this.openModal(); - } - - edit(id: string) { - this.service.get(id).subscribe(res => { - this.selected = res; - this.openModal(); - }); - } - - save() { - if (!this.form.valid) return; - this.modalBusy = true; - - const { id } = this.selected; - (id - ? this.service.update(id, { ...this.selected, ...this.form.value }) - : this.service.create(this.form.value) - ) - .pipe(finalize(() => (this.modalBusy = false))) - .subscribe(() => { - this.isModalVisible = false; - this.list.get(); - }); - } - - delete(id: string, name: string) { - this.confirmationService - .warn('AbpIdentity::RoleDeletionConfirmationMessage', 'AbpIdentity::AreYouSure', { - messageLocalizationParams: [name], - }) - .subscribe((status: Confirmation.Status) => { - if (status === Confirmation.Status.confirm) { - this.service.delete(id).subscribe(() => this.list.get()); - } - }); - } - - private hookToQuery() { - this.list.hookToQuery(query => this.service.getList(query)).subscribe(res => (this.data = res)); - } - - openPermissionsModal(providerKey: string) { - this.providerKey = providerKey; - setTimeout(() => { - this.visiblePermissions = true; - }, 0); - } - - sort(data) { - const { prop, dir } = data.sorts[0]; - this.list.sortKey = prop; - this.list.sortOrder = dir; - } -} -``` - -```js - { - provide: EXTENSIONS_IDENTIFIER, - useValue: eIdentityComponents.Roles, - }, - { - provide: RolesComponent, - useExisting: MyRolesComponent - } -``` - -The two providers we have defined in `MyRolesComponent` are required for the extension components to work correctly. - -* With the first provider, we defined the extension identifier for using `RolesComponent`'s extension actions in the `MyRolesComponent`. -* With the second provider, we have replaced the `RolesComponent` injection with the `MyRolesComponent`. Default extension actions of the `RolesComponent` try to get `RolesComponent` instance. However, the actions can get the `MyRolesComponent` instance after defining the second provider. - -Open the generated `src/app/my-role/my-role.component.html` file and replace its content with the following: - -```html -
    -
    -
    -
    -
    My Roles
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -

    {%{{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewRole') | abpLocalization }}}%}

    -
    - - -
    - -
    -
    - - - - {%{{{ - 'AbpIdentity::Save' | abpLocalization - }}}%} - -
    - - - -``` - -We have added the `abp-page-toolbar`, `abp-extensible-table`, and `abp-extensible-form` extension components to template of the `MyRolesComponent`. - -You should import the required modules for the `MyRolesComponent` to `MyRolesModule`. Open the `src/my-roles/my-roles.module.ts` file and replace the content with the following: - -```js -import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { MyRolesComponent } from './my-roles.component'; -import { PermissionManagementModule } from '@abp/ng.permission-management'; - -@NgModule({ - declarations: [MyRolesComponent], - imports: [SharedModule, UiExtensionsModule, PermissionManagementModule], - exports: [MyRolesComponent], -}) -export class MyRolesModule {} -``` - -- `UiExtensionsModule` imported to be able to use the extension components in your component. -- `PermissionManagementModule` imported to be able to use the `abp-permission-*management` in your component. - -As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below: - -```js -import { ReplaceableComponentsService } from '@abp/ng.core'; -import { eIdentityComponents } from '@abp/ng.identity'; -import { MyRolesComponent } from './my-roles/my-roles.component'; - -@Component(/* component metadata */) -export class AppComponent { - constructor(private replaceableComponents: ReplaceableComponentsService) { - this.replaceableComponents.add({ component: MyRolesComponent, key: eIdentityComponents.Roles }); - } -} -``` - -After the steps above, the `RolesComponent` has been successfully replaced with the `MyRolesComponent`. When you navigate to the `/identity/roles` URL, you will see the `MyRolesComponent`'s template and see the extension components working correctly. - -![my-roles-component-with-extensions](./images/my-roles-component-with-extensions.jpg) - -![my-roles-component-form-extensions](./images/my-roles-component-form-extensions.jpg) diff --git a/docs/en/UI/Angular/Internet-Connection-Service.md b/docs/en/UI/Angular/Internet-Connection-Service.md deleted file mode 100644 index d9002b660e..0000000000 --- a/docs/en/UI/Angular/Internet-Connection-Service.md +++ /dev/null @@ -1,30 +0,0 @@ -# Internet Connection Service -`InternetConnectionService` is a service which is exposed by the `@abp/ng.core` package. **You can use this service in order to check your internet connection** - -## Getting Started -When you inject the InternetConnectionService you can get the current internet status, and it gets immediately updated if the status changes. - -`InternetConnectionService` provides two choices to catch the network status: -1. Signal (readonly) -2. Observable - - -# How To Use -İt's easy, just inject the service and get the network status. - -**You can get via signal** -```ts -class SomeComponent{ - internetConnectionService = inject(InternetConnectionService); - isOnline = this.internetConnectionService.networkStatus -} -``` -**or you can get the observable** -```ts -class SomeComponent{ - internetConnectionService = inject(InternetConnectionService); - isOnline = this.internetConnectionService.networkStatus$ -} -``` - -To see how we implement to the template, check the `InternetConnectionStatusComponent` diff --git a/docs/en/UI/Angular/Lazy-Load-Service.md b/docs/en/UI/Angular/Lazy-Load-Service.md deleted file mode 100644 index eccea527ab..0000000000 --- a/docs/en/UI/Angular/Lazy-Load-Service.md +++ /dev/null @@ -1,206 +0,0 @@ -# Lazy Loading Scripts & Styles - -You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way. - - - - -## Getting Started - -You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. - -```js -import { LazyLoadService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private lazyLoadService: LazyLoadService) {} -} -``` - - - - -## Usage - -You can use the `load` method of `LazyLoadService` to create a ` -```` - -### External/CDN file Support - -The bundling system automatically recognizes the external/CDN files and adds them to the page without any change. - -#### Using External/CDN files in `AbpBundlingOptions` - -````csharp -Configure(options => -{ - options.StyleBundles - .Add("MyStyleBundle", configuration => - { - configuration - .AddFiles("/styles/my-style1.css") - .AddFiles("/styles/my-style2.css") - .AddFiles("https://cdn.abp.io/bootstrap.css") - .AddFiles("/styles/my-style3.css") - .AddFiles("/styles/my-style4.css"); - }); - - options.ScriptBundles - .Add("MyScriptBundle", configuration => - { - configuration - .AddFiles("/scripts/my-script1.js") - .AddFiles("/scripts/my-script2.js") - .AddFiles("https://cdn.abp.io/bootstrap.js") - .AddFiles("/scripts/my-script3.js") - .AddFiles("/scripts/my-script4.js"); - }); -}); -```` - -**Output HTML:** - -````html - - - - - - - -```` - -#### Using External/CDN files in Tag Helpers. - -````html - - - - - - - - - - - - - - - -```` - -**Output HTML:** - -````html - - - - - - - -```` - -## Themes - -Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. - -## Best Practices & Suggestions - -It's suggested to define multiple bundles for an application, each one is used for different purposes. - -* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. -* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. -* **Module bundles**: For shared resources among the pages of an individual module. -* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. - -Establish a balance between performance, network bandwidth usage and count of many bundles. - -## See Also - -* [Client Side Package Management](Client-Side-Package-Management.md) -* [Theming](Theming.md) diff --git a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md deleted file mode 100644 index dc242e163d..0000000000 --- a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md +++ /dev/null @@ -1,116 +0,0 @@ - -## ASP.NET Core MVC Client Side Package Management - -ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. - -However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. - -Finally, we suggest the [**Yarn**](https://classic.yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. - -### @ABP NPM Packages - -ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. - -One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. - -To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [GitHub repository](https://github.com/volosoft/abp/tree/master/npm/packs). - -The benefit of a **standard package** is: - -* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. -* It contains the mappings copy library resources (js, css, img... files) from the `node_modules` folder to `wwwroot/libs` folder. See the *Mapping The Library Resources* section for more. - -Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: - -```json -{ - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } -} -``` - -It's suggested to depend on a standard package instead of directly depending on a third-party package. - -#### Package Installation - -After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: - -```bash -yarn -``` - -Alternatively, you can use `npm install` but [Yarn](https://classic.yarnpkg.com/) is suggested as mentioned before. - -#### Package Contribution - -If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: - -* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). -* It should be the **latest stable** version of the package. -* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. -* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. -* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. - -See current standard packages for examples. - -### Mapping The Library Resources - -Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. - -Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. - -ABP CLI's `abp install-libs` command **copies resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. - -The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. - -#### Resource Mapping Definition File - -A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: - -```json -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs", - "!@libs/**/foo.txt" - ], - mappings: { - - } -} -``` - -* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. -* **clean** section is a list of folders to clean before copying the files. Glob matching and negation is enabled, so you can fine-tune what to delete and keep. The example above will clean everything inside `./wwwroot/libs`, but keep any `foo.txt` files. -* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. - -An example mapping configuration is shown below: - -```json -mappings: { - "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", - "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/", - "@node_modules/bootstrap-v4-rtl/dist/**/*": "@libs/bootstrap-v4-rtl/dist/" -} -``` - -#### install-libs Command - -Once you properly configure the `abp.resourcemapping.js` file, you can run the following ABP CLI command from the command line: - -````bash -abp install-libs -```` - -When you run this command, all packages will copy their own resources into the `wwwroot/libs` folder. Running `abp install-libs` is only necessary if you make a change in your dependencies in the **package.json** file. - -#### See Also - -* [Bundling & Minification](Bundling-Minification.md) -* [Theming](Theming.md) diff --git a/docs/en/UI/AspNetCore/Customization-User-Interface.md b/docs/en/UI/AspNetCore/Customization-User-Interface.md deleted file mode 100644 index 991f1247b8..0000000000 --- a/docs/en/UI/AspNetCore/Customization-User-Interface.md +++ /dev/null @@ -1,477 +0,0 @@ -# ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide - -This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) or [theme](Theming.md) for ASP.NET Core MVC / Razor Page applications. - -## Overriding a Page - -This section covers the [Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/) development, which is the recommended approach to create server rendered user interface for ASP.NET Core. Pre-built modules typically uses the Razor Pages approach instead of the classic MVC pattern (next sections will cover the MVC pattern too). - -You typically have three kind of override requirement for a page: - -* Overriding **only the Page Model** (C#) side to perform additional logic without changing the page UI. -* Overriding **only the Razor Page** (.chtml file) to change the UI without changing the c# behind the page. -* **Completely overriding** the page. - -### Overriding a Page Model (C#) - -````csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Identity; -using Volo.Abp.Identity.Web.Pages.Identity.Users; - -namespace Acme.BookStore.Web.Pages.Identity.Users -{ - [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(EditModalModel))] - public class MyEditModalModel : EditModalModel - { - public MyEditModalModel( - IIdentityUserAppService identityUserAppService, - IIdentityRoleAppService identityRoleAppService - ) : base( - identityUserAppService, - identityRoleAppService) - { - } - - public async override Task OnPostAsync() - { - //TODO: Additional logic - await base.OnPostAsync(); - //TODO: Additional logic - } - } -} -```` - -* This class inherits from and replaces the `EditModalModel` for the users and overrides the `OnPostAsync` method to perform additional logic before and after the underlying code. -* It uses `ExposeServices` and `Dependency` attributes to replace the class. - -### Overriding a Razor Page (.CSHTML) - -Overriding a `.cshtml` file (razor page, razor view, view component... etc.) is possible through creating the same `.cshtml` file under the same path. - -#### Example - -This example overrides the **login page** UI defined by the [Account Module](../../Modules/Account.md). - -The account module defines a `Login.cshtml` file under the `Pages/Account` folder. So, you can override it by creating a file in the same path: - -![overriding-login-cshtml](../../images/overriding-login-cshtml.png) - -You typically want to copy the original `.cshtml` file of the module, then make the necessary changes. You can find the original file [here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml). Do not copy the `Login.cshtml.cs` file which is the code behind file for the razor page and we don't want to override it yet (see the next section). - -> Don't forget to add [_ViewImports.cshtml](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/layout?view=aspnetcore-7.0#importing-shared-directives) if the page you want to override contains [ABP Tag Helpers](../AspNetCore/Tag-Helpers/Index.md). - -````csharp -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -That's all, you can change the file content however you like. - -### Completely Overriding a Razor Page - -You may want to completely override a page; the razor and the c# file related to the page. - -In such a case; - -1. Override the C# page model class just like described above, but don't replace the existing page model class. -2. Override the Razor Page just described above, but also change the @model directive to point your new page model. - -#### Example - -This example overrides the **login page** defined by the [Account Module](../../Modules/Account.md). - -Create a page model class deriving from the ` LoginModel ` (defined in the ` Volo.Abp.Account.Web.Pages.Account ` namespace): - -````csharp -public class MyLoginModel : LoginModel -{ - public MyLoginModel( - IAuthenticationSchemeProvider schemeProvider, - IOptions accountOptions - ) : base( - schemeProvider, - accountOptions) - { - - } - - public override Task OnPostAsync(string action) - { - //TODO: Add logic - return base.OnPostAsync(action); - } - - //TODO: Add new methods and properties... -} -```` - -You can override any method or add new properties/methods if needed. - -> Notice that we didn't use `[Dependency(ReplaceServices = true)]` or `[ExposeServices(typeof(LoginModel))]` since we don't want to replace the existing class in the dependency injection, we define a new one. - -Copy `Login.cshtml` file into your solution as just described above. Change the **@model** directive to point to the `MyLoginModel`: - -````xml -@page -... -@model Acme.BookStore.Web.Pages.Account.MyLoginModel -... -```` - -That's all! Make any change in the view and run your application. - -#### Replacing Page Model Without Inheritance - -You don't have to inherit from the original page model class (like done in the previous example). Instead, you can completely **re-implement** the page yourself. In this case, just derive from `PageModel`, `AbpPageModel` or any suitable base class you need. - -## Overriding a View Component - -The ABP Framework, pre-built themes and modules define some **re-usable view components**. These view components can be replaced just like a page described above. - -### Example - -The screenshot below was taken from the [Basic Theme](Basic-Theme.md) comes with the application startup template. - -![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) - -The [Basic Theme](Basic-Theme.md) defines some view components for the layout. For example, the highlighted area with the red rectangle above is called **Brand component**. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. - -First, create your logo and place under a folder in your web application. We used `wwwroot/logos/bookstore-logo.png` path. Then copy the Brand component's view ([from here](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml)) from the basic theme files under the `Themes/Basic/Components/Brand` folder. The result should be similar the picture below: - -![bookstore-added-brand-files](../../images/bookstore-added-brand-files.png) - -Then change the `Default.cshtml` as you like. Example content can be like that: - -````xml - - - -```` - -Now, you can run the application to see the result: - -![bookstore-added-logo](../../images/bookstore-added-logo.png) - -If you need, you can also replace [the code behind c# class](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) of the component just using the dependency injection system. - -### Overriding the Theme - -Just as explained above, you can replace any component, layout or c# class of the used theme. See the [theming document](Theming.md) for more information on the theming system. - -## Overriding Static Resources - -Overriding a static embedded resource (like JavaScript, Css or image files) of a module is pretty easy. Just place a file in the same path in your solution and let the [Virtual File System](../../Virtual-File-System.md) to handle it. - -## Manipulating the Bundles - -The [Bundling & Minification](Bundling-Minification.md) system provides an **extensible and dynamic** system to create **script** and **style** bundles. It allows you to extend and manipulate the existing bundles. - -### Example: Add a Global CSS File - -For example, ABP Framework defines a **global style bundle** which is added to every page (actually, added to the layout by the themes). Let's add a **custom style file** to the end of the bundle files, so we can override any global style. - -First, create a CSS file and locate it in a folder inside the `wwwroot`: - -![bookstore-global-css-file](../../images/bookstore-global-css-file.png) - -Define some custom CSS rules inside the file. Example: - -````css -.card-title { - color: orange; - font-size: 2em; - text-decoration: underline; -} - -.btn-primary { - background-color: red; -} -```` - -Then add this file to the standard global style bundle in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md): - -````csharp -Configure(options => -{ - options.StyleBundles.Configure( - StandardBundles.Styles.Global, //The bundle name! - bundleConfiguration => - { - bundleConfiguration.AddFiles("/styles/my-global-styles.css"); - } - ); -}); -```` - -#### The Global Script Bundle - -Just like the `StandardBundles.Styles.Global`, there is a `StandardBundles.Scripts.Global` that you can add files or manipulate the existing ones. - -### Example: Manipulate the Bundle Files - -The example above adds a new file to the bundle. You can do more if you create a **bundle contributor** class. Example: - -````csharp -public class MyGlobalStyleBundleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.Clear(); - context.Files.Add("/styles/my-global-styles.css"); - } -} -```` - -Then you can add the contributor to an existing bundle: - -````csharp -Configure(options => -{ - options.StyleBundles.Configure( - StandardBundles.Styles.Global, - bundleConfiguration => - { - bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); - } - ); -}); -```` - -It is not a good idea to clear all CSS files. In a real world scenario, you can find and replace a specific file with your own file. - -### Example: Add a JavaScript File for a Specific Page - -The examples above works with the global bundle added to the layout. What if you want to add a CSS/JavaScript file (or replace a file) for a specific page defines inside a depended module? - -Assume that you want to run a **JavaScript code** once the user enters to the **Role Management** page of the Identity Module. - -First, create a standard JavaScript file under the `wwwroot`, `Pages` or `Views` folder (ABP support to add static resources inside these folders by default). We prefer the `Pages/Identity/Roles` folder to follow the conventions: - -![bookstore-added-role-js-file](../../images/bookstore-added-role-js-file.png) - -Content of the file is simple: - -````js -$(function() { - abp.log.info('My custom role script file has been loaded!'); -}); -```` - -Then add this file to the bundle of the role management page: - -````csharp -Configure(options => -{ - options.ScriptBundles - .Configure( - typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName, - bundleConfig => - { - bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js"); - }); -}); -```` - -`typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName` is the safe way to get the bundle name for the role management page. - -> Notice that not every page defines such page bundles. They define only if needed. - -In addition to adding new CSS/JavaScript file to a page, you also can replace the existing one (by defining a bundle contributor). - -## Layout Customization - -Layouts are defined by the theme ([see the theming](Theming.md)) by design. They are not included in a downloaded application solution. In this way you can easily **upgrade** the theme and get new features. You can not **directly change** the layout code in your application unless you replace it by your own layout (will be explained in the next sections). - -There are some common ways to **customize the layout** described in the next sections. - -### Menu Contributors - -There are two **standard menus** defined by the ABP Framework: - -![bookstore-menus-highlighted](../../images/bookstore-menus-highlighted.png) - -* `StandardMenus.Main`: The main menu of the application. -* `StandardMenus.User`: The user menu (generally at the top right of the screen). - -Rendering the menus is a responsibility of the theme, but **menu items** are determined by the modules and your application code. Just implement the `IMenuContributor` interface and **manipulate the menu items** in the `ConfigureMenuAsync` method. - -Menu contributors are executed whenever need to render the menu. There is already a menu contributor defined in the **application startup template**, so you can take it as an example and improve if necessary. See the [navigation menu](Navigation-Menu.md) document for more. - -### Toolbar Contributors - -[Toolbar system](Toolbars.md) is used to define **toolbars** on the user interface. Modules (or your application) can add **items** to a toolbar, then the theme renders the toolbar on the **layout**. - -There is only one **standard toolbar** (named "Main" - defined as a constant: `StandardToolbars.Main`). For the basic theme, it is rendered as shown below:![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) - -In the screenshot above, there are two items added to the main toolbar: Language switch component & user menu. You can add your own items here. - -#### Example: Add a Notification Icon - -In this example, we will add a **notification (bell) icon** to the left of the language switch item. A item in the toolbar should be a **view component**. So, first, create a new view component in your project: - -![bookstore-notification-view-component](../../images/bookstore-notification-view-component.png) - -**NotificationViewComponent.cs** - -````csharp -public class NotificationViewComponent : AbpViewComponent -{ - public async Task InvokeAsync() - { - return View("/Pages/Shared/Components/Notification/Default.cshtml"); - } -} -```` - -**Default.cshtml** - -````xml -
    - -
    -```` - -Now, we can create a class implementing the `IToolbarContributor` interface: - -````csharp -public class MyToolbarContributor : IToolbarContributor -{ - public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) - { - if (context.Toolbar.Name == StandardToolbars.Main) - { - context.Toolbar.Items - .Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); - } - - return Task.CompletedTask; - } -} -```` - -This class adds the `NotificationViewComponent` as the first item in the `Main` toolbar. - -Finally, you need to add this contributor to the `AbpToolbarOptions`, in the `ConfigureServices` of your module: - -````csharp -Configure(options => -{ - options.Contributors.Add(new MyToolbarContributor()); -}); -```` - -That's all, you will see the notification icon on the toolbar when you run the application: - -![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) - -`NotificationViewComponent` in this sample simply returns a view without any data. In real life, you probably want to **query database** (or call an HTTP API) to get notifications and pass to the view. If you need, you can add a `JavaScript` or `CSS` file to the global bundle (as described before) for your toolbar item. - -See the [toolbars document](Toolbars.md) for more about the toolbar system. - -### Layout Hooks - -[Layout Hooks](Layout-Hooks.md) system allows you to **add code** at some specific parts of the layout. All layouts of all themes should implement these hooks. Then you can then add a **view component** into a hook point. - -#### Example: Add Google Analytics Script - -Assume that you need to add the Google Analytics script to the layout (that will be available for all the pages). First, **create a view component** in your project: - -![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) - -**GoogleAnalyticsViewComponent.cs** - -````csharp -public class GoogleAnalyticsViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); - } -} -```` - -**Default.cshtml** - -````html - -```` - -Change `UA-xxxxxx-1` with your own code. - -You can then add this component to any of the hook points in the `ConfigureServices` of your module: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, //The hook name - typeof(GoogleAnalyticsViewComponent) //The component to add - ); -}); -```` - -Now, the GA code will be inserted in the `head` of the page as the last item. You (or the modules you are using) can add multiple items to the same hook. All of them will be added to the layout. - -The configuration above adds the `GoogleAnalyticsViewComponent` to all layouts. You may want to only add to a specific layout: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, - typeof(GoogleAnalyticsViewComponent), - layout: StandardLayouts.Application //Set the layout to add - ); -}); -```` - -See the layouts section below to learn more about the layout system. - -### Layouts - -Layout system allows themes to define standard, named layouts and allows any page to select a proper layout for its purpose. There are three pre-defined layouts: - -* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. -* "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. -* "**Empty**": Empty and minimal layout. - -These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are the standard layout names and implemented by all the themes out of the box. - -#### Layout Location - -You can find the layout files [here](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts) for the basic theme. You can take them as references to build your own layouts or you can override them if necessary. - -#### ITheme - -ABP Framework uses the `ITheme` service to get the layout location by the layout name. You can replace this service to dynamically select the layout location. - -#### IThemeManager - -`IThemeManager` is used to obtain the current theme and get the layout path. Any page can determine the layout of its own. Example: - -````html -@using Volo.Abp.AspNetCore.Mvc.UI.Theming -@inject IThemeManager ThemeManager -@{ - Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); -} -```` - -This page will use the empty layout. You use `ThemeManager.CurrentTheme.GetEmptyLayout();` extension method as a shortcut. - -If you want to set the layout for all the pages under a specific folder, then write the code above in a `_ViewStart.cshtml` file under that folder. diff --git a/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md b/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md deleted file mode 100644 index fec0ac71fb..0000000000 --- a/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md +++ /dev/null @@ -1,161 +0,0 @@ -# Data Table Column Extensions for ASP.NET Core UI - -## Introduction - -Data table column extension system allows you to add a **new table column** on the user interface. The example below adds a new column with the "Social security no" title: - -![user-action-extension-click-me](../../images/table-column-extension-example.png) - -You can use the standard column options to fine control the table column. - -> Note that this is a low level API to find control the table column. If you want to show an extension property on the table, see the [module entity extension](../../Module-Entity-Extensions.md) document. - -## How to Set Up - -### Create a JavaScript File - -First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: - -![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) - -Here, the content of this JavaScript file: - -```js -abp.ui.extensions.tableColumns - .get('identity.user') - .addContributor(function (columnList) { - columnList.addTail({ //add as the last column - title: 'Social security no', - data: 'extraProperties.SocialSecurityNumber', - orderable: false, - render: function (data, type, row) { - if (row.extraProperties.SocialSecurityNumber) { - return '' + - row.extraProperties.SocialSecurityNumber + - ''; - } else { - return 'undefined'; - } - } - }); - }); -``` - -This example defines a custom `render` function to return a custom HTML to render in the column. - -### Add the File to the User Management Page - -Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification). - -Write the following code inside the `ConfigureServices` of your module class: - -```csharp -Configure(options => -{ - options.ScriptBundles.Configure( - typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, - bundleConfiguration => - { - bundleConfiguration.AddFiles( - "/Pages/Identity/Users/my-user-extensions.js" - ); - }); -}); -``` - -This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. - -### Rendering the Column - -This example assumes that you've defined a `SocialSecurityNumber` extra property using the [module entity extension](../../Module-Entity-Extensions.md) system. However; - -* You can add a new column that is related to an existing property of the user (that was not added to the table by default). Example: - -````js -abp.ui.extensions.tableColumns - .get('identity.user') - .addContributor(function (columnList) { - columnList.addTail({ - title: 'Phone confirmed?', - data: 'phoneNumberConfirmed', - render: function (data, type, row) { - if (row.phoneNumberConfirmed) { - return 'YES'; - } else { - return 'NO'; - } - } - }); - }); -```` - -* You can add a new custom column that is not related to any entity property, but a completely custom information. Example: - -````js -abp.ui.extensions.tableColumns - .get('identity.user') - .addContributor(function (columnList) { - columnList.addTail({ - title: 'Custom column', - data: {}, - orderable: false, - render: function (data) { - if (data.phoneNumber) { - return "call: " + data.phoneNumber; - } else { - return ''; - } - } - }); - }); -```` - -## API - -This section explains details of the `abp.ui.extensions.tableColumns` JavaScript API. - -### abp.ui.extensions.tableColumns.get(entityName) - -This method is used to access the table columns for an entity of a specific module. It takes one parameter: - -* **entityName**: The name of the entity defined by the related module. - -### abp.ui.extensions.tableColumns.get(entityName).columns - -The `columns` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined columns for a table. All contributors are executed in order to prepare the final column list. This is normally called by the modules to show the columns in the table. However, you can use it if you are building your own extensible UIs. - -### abp.ui.extensions.tableColumns.get(entityName).addContributor(contributeCallback [, order]) - -The `addContributor` method covers all scenarios, e.g. you want to add your column in a different position in the list, change or remove an existing column. `addContributor` has the following parameters: - -* **contributeCallback**: A callback function that is called whenever the column list should be created. You can freely modify the column list inside this callback method. -* **order** (optional): The order of the callback in the callback list. Your callback is added to the end of the list (so, you have opportunity to modify columns added by the previous contributors). You can set it `0` to add your contributor as the first item. - -#### Example - -```js -var myColumnDefinition = { - title: 'Custom column', - data: {}, - orderable: false, - render: function(data) { - if (data.phoneNumber) { - return "call: " + data.phoneNumber; - } else { - return ''; - } - } -}; - -abp.ui.extensions.tableColumns - .get('identity.user') - .addContributor(function (columnList) { - // Remove an item from actionList - columnList.dropHead(); - - // Add a new item to the actionList - columnList.addHead(myColumnDefinition); - }); -``` - -> `columnList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/UI/AspNetCore/Data-Tables.md b/docs/en/UI/AspNetCore/Data-Tables.md deleted file mode 100644 index d5181b2da9..0000000000 --- a/docs/en/UI/AspNetCore/Data-Tables.md +++ /dev/null @@ -1,298 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: Data Tables - -A Data Table (aka Data Grid) is a UI component to show tabular data to the users. There are a lot of Data table components/libraries and **you can use any one you like** with the ABP Framework. However, the startup templates come with the [DataTables.Net](https://datatables.net/) library as **pre-installed and configured**. ABP Framework provides adapters for this library and make it easy to use with the API endpoints. - -An example screenshot from the user management page that shows the user list in a data table: - -![datatables-example](../../images/datatables-example.png) - -## DataTables.Net Integration - -First of all, you can follow the official documentation to understand how the [DataTables.Net](https://datatables.net/) works. This section will focus on the ABP addons & integration points rather than fully covering the usage of this library. - -### A Quick Example - -You can follow the [web application development tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) for a complete example application that uses the DataTables.Net as the Data Table. This section shows a minimalist example. - -You do nothing to add DataTables.Net library to the page since it is already added to the global [bundle](Bundling-Minification.md) by default. - -First, add an `abp-table` as shown below, with an `id`: - -````html - -```` - -> `abp-table` is a [Tag Helper](Tag-Helpers/Index.md) defined by the ABP Framework, but a simple `` tag would also work. - -Then call the `DataTable` plugin on the table selector: - -````js -var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - ///... - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - } - ] - }) -); -```` - -The example code above uses some ABP integration features those will be explained in the next sections. - -### Configuration Normalization - -`abp.libs.datatables.normalizeConfiguration` function takes a DataTables configuration and normalizes to simplify it; - -* Sets `scrollX` option to `true`, if not set. -* Sets `target` index for the column definitions. -* Sets the `language` option to [localize](../../Localization.md) the table in the current language. - -#### Default Configuration - -`normalizeConfiguration` uses the default configuration. You can change the default configuration using the `abp.libs.datatables.defaultConfigurations` object. Example: - -````js -abp.libs.datatables.defaultConfigurations.scrollX = false; -```` - -Here, the all configuration options; - -* `scrollX`: `false` by default. -* `dom`: Default value is `<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto"i><"col"p>>`. -* `language`: A function that returns the localization text using the current language. - -### AJAX Adapter - -DataTables.Net has its own expected data format while getting results of an AJAX call to the server to get the table data. They are especially related how paging and sorting parameters are sent and received. ABP Framework also offers its own conventions for the client-server [AJAX](JavaScript-API/Ajax.md) communication. - -The `abp.libs.datatables.createAjax` method (used in the example above) adapts request and response data format and perfectly works with the [Dynamic JavaScript Client Proxy](Dynamic-JavaScript-Proxies.md) system. - -This works automatically, so most of the times you don't need to know how it works. See the [DTO document](../../Data-Transfer-Objects.md) if you want to learn more about `IPagedAndSortedResultRequest`, `IPagedResult` and other standard interfaces and base DTO classes those are used in client to server communication. - -The `createAjax` also supports you to customize request parameters and handle the responses. - -**Example:** - -````csharp -var inputAction = function (requestData, dataTableSettings) { - return { - id: $('#Id').val(), - name: $('#Name').val(), - }; -}; - -var responseCallback = function(result) { - - // your custom code. - - return { - recordsTotal: result.totalCount, - recordsFiltered: result.totalCount, - data: result.items - }; -}; - -ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList, inputAction, responseCallback) -```` - -If you don't need access or modify the `requestData` or the `dataTableSettings`, you can specify a simple object as the second parameter. - -````js -ajax: abp.libs.datatables.createAjax( - acme.bookStore.books.book.getList, - { id: $('#Id').val(), name: $('#Name').val() } -) -```` - -### Row Actions - -`rowAction` is an option defined by the ABP Framework to the column definitions to show a drop down button to take actions for a row in the table. - -The example screenshot below shows the actions for each user in the user management table: - -![datatables-example](../../images/datatables-row-actions.png) - -`rowAction` is defined as a part of a column definition: - -````csharp -{ - title: l('Actions'), - rowAction: { - //TODO: CONFIGURATION - } -}, -```` - -**Example: Show *Edit* and *Delete* actions for a book row** - -````js -{ - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - //TODO: Open a modal to edit the book - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return "Are you sure to delete the book " + data.record.name; - }, - action: function (data) { - acme.bookStore.books.book - .delete(data.record.id) - .then(function() { - abp.notify.info("Successfully deleted!"); - data.table.ajax.reload(); - }); - } - } - ] - } -}, -```` - -#### Action Items - -`items` is an array of action definitions. An action definition can have the following options; - -* `text`: The text (a `string`) for this action to be shown in the actions drop down. -* `action`: A `function` that is executed when the user clicks to the action. The function takes a `data` argument that has the following fields; - * `data.record`: This is the data object related to the row. You can access the data fields like `data.record.id`, `data.record.name`... etc. - * `data.table`: The DataTables instance. -* `confirmMessage`: A `function` (see the example above) that returns a message (`string`) to show a dialog to get a confirmation from the user before executing the `action`. Example confirmation dialog: - -![datatables-row-actions-confirmation](../../images/datatables-row-actions-confirmation.png) - -You can use the [localization](JavaScript-API/Localization.md) system to show a localized message. - -* `visible`: A `bool` or a `function` that returns a `bool`. If the result is `false`, then the action is not shown in the actions dropdown. This is generally combined by the [authorization](JavaScript-API/Auth.md) system to hide the action if the user has no permission to take this action. Example: - -````js -visible: abp.auth.isGranted('BookStore.Books.Delete'); -```` - -If you define a `function`, then the `function` has two arguments: `record` (the data object of the related row) and the `table` (the DataTable instance). So, you can decide to show/hide the action dynamically, based on the row data and other conditions. - -* `iconClass`: Can be used to show a font-icon, like a [Font-Awesome](https://fontawesome.com/) icon (ex: `fas fa-trash-alt`), near to the action text. Example screenshot: - -![datatables-row-actions-confirmation](../../images/datatables-row-actions-icon.png) - -* `enabled`: A `function` that returns a `bool` to disable the action. The `function` takes a `data` object with two fields: `data.record` is the data object related to the row and `data.table` is the DataTables instance. -* `displayNameHtml`: Set this to `true` is the `text` value contains HTML tags. - -There are some rules with the action items; - -* If none of the action items is visible then the actions column is not rendered. - -### Data Format - -#### The Problem - -See the *Creation Time* column in the example below: - -````js -{ - title: l('CreationTime'), - data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } -} -```` - -The `render` is a standard DataTables option to render the column content by a custom function. This example uses the [luxon](https://moment.github.io/luxon/) library (which is installed by default) to write a human readable value of the `creationTime` in the current user's language. Example output of the column: - -![datatables-custom-render-date](../../images/datatables-custom-render-date.png) - -If you don't define the render option, then the result will be ugly and not user friendly: - -![datatables-custom-render-date](../../images/datatables-default-render-date.png) - -However, rendering a `DateTime` is almost same and repeating the same rendering logic everywhere is against to the DRY (Don't Repeat Yourself!) principle. - -#### dataFormat Option - -`dataFormat` column option specifies the data format that is used to render the column data. The same output could be accomplished using the following column definition: - -````js -{ - title: l('CreationTime'), - data: "creationTime", - dataFormat: 'datetime' -} -```` - -`dataFormat: 'datetime'` specifies the data format for this column. There are a few pre-defined `dataFormat`s: - -* `boolean`: Shows a `check` icon for `true` and `times` icon for `false` value and useful to render `bool` values. -* `date`: Shows date part of a `DateTime` value, formatted based on the current culture. -* `datetime`: Shows date & time (excluding seconds) of a `DateTime` value, formatted based on the current culture. - -### Default Renderers - -`abp.libs.datatables.defaultRenderers` option allows you to define new data formats and set renderers for them. - -**Example: Render male / female icons based on the gender** - -````js -abp.libs.datatables.defaultRenderers['gender'] = function(value) { - if (value === 'f') { - return ''; - } else { - return ''; - } -}; -```` - -Assuming that the possible values for a column data is `f` and `m`, the `gender` data format shows female/male icons instead of `f` and `m` texts. You can now set `dataFormat: 'gender'` for a column definition that has the proper data values. - -> You can write the default renderers in a single JavaScript file and add it to the [Global Script Bundle](Bundling-Minification.md), so you can reuse them in all the pages. - -## Other Data Grids - -You can use any library you like. For example, [see this article](https://community.abp.io/articles/using-devextreme-components-with-the-abp-framework-zb8z7yqv) to learn how to use DevExtreme Data Grid in your applications. diff --git a/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md b/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md deleted file mode 100644 index 31c2dbe7ea..0000000000 --- a/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md +++ /dev/null @@ -1,97 +0,0 @@ -# Dynamic JavaScript API Client Proxies - -It is typical to consume your HTTP APIs from your JavaScript code. To do that, you normally deal with low level AJAX calls, like $.ajax, or better [abp.ajax](JavaScript-API/Ajax.md). ABP Framework provides **a better way** to call your HTTP APIs from your JavaScript code: JavaScript API Client Proxies! - -## Static vs Dynamic JavaScript Client Proxies - -ABP provides **two types** of client proxy generation system. This document explains the **dynamic client proxies**, which generates client-side proxies on runtime. You can also see the [Static JavaScript API Client Proxies](Static-JavaScript-Proxies.md) documentation to learn how to generate proxies on development time. - -Development-time (static) client proxy generation has a **slight performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**. - -## A Quick Example - -Assume that you have an application service defined as shown below: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Authors -{ - public interface IAuthorAppService : IApplicationService - { - Task GetAsync(Guid id); - - Task> GetListAsync(GetAuthorListDto input); - - Task CreateAsync(CreateAuthorDto input); - - Task UpdateAsync(Guid id, UpdateAuthorDto input); - - Task DeleteAsync(Guid id); - } -} -```` - -> You can follow the [web application development tutorial](../../Tutorials/Part-1.md) to learn how to create [application services](../../Application-Services.md), expose them as [HTTP APIs](../../API/Auto-API-Controllers.md) and consume from the JavaScript code as a complete example. - -You can call any of the methods just like calling a JavaScript function. The JavaScript function has the identical function **name**, **parameters** and the **return value** with the C# method. - -**Example: Get the authors list** - -````js -acme.bookStore.authors.author.getList({ - maxResultCount: 10 -}).then(function(result){ - console.log(result.items); -}); -```` - -**Example: Delete an author** - -```js -acme.bookStore.authors.author - .delete('7245a066-5457-4941-8aa7-3004778775f0') //Get id from somewhere! - .then(function() { - abp.notify.info('Successfully deleted!'); - }); -``` - -## AJAX Details - -JavaScript client proxy functions use the [abp.ajax](JavaScript-API/Ajax.md) under the hood. So, you have the same benefits like **automatic error handling**. Also, you can fully control the AJAX call by providing the options. - -### The Return Value - -Every function returns a [Deferred object](https://api.jquery.com/category/deferred-object/). That means you can chain with `then` to get the result, `catch` to handle the error, `always` to perform an action once the operation completes (success or failed). - -### AJAX Options - -Every function gets an additional **last parameter** after your own parameters. The last parameter is called as `ajaxParams`. It is an object that overrides the AJAX options. - -**Example: Set `type` and `dataType` AJAX options** - -````js -acme.bookStore.authors.author - .delete('7245a066-5457-4941-8aa7-3004778775f0', { - type: 'POST', - dataType: 'xml' - }) - .then(function() { - abp.notify.info('Successfully deleted!'); - }); -```` - -See the [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) documentation for all the available options. - -## Service Proxy Script Endpoint - -The magic is done by the `/Abp/ServiceProxyScript` endpoint defined by the ABP Framework and automatically added to the layout. You can visit this endpoint in your application to see the client proxy function definitions. This script file is automatically generated by the ABP Framework based on the server side method definitions and the related HTTP endpoint details. - -## See Also - -* [Static JavaScript API Client Proxies](Static-JavaScript-Proxies.md) -* [Auto API Controllers](../../API/Auto-API-Controllers.md) -* [Web Application Development Tutorial](../../Tutorials/Part-1.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Entity-Action-Extensions.md b/docs/en/UI/AspNetCore/Entity-Action-Extensions.md deleted file mode 100644 index f6e865375d..0000000000 --- a/docs/en/UI/AspNetCore/Entity-Action-Extensions.md +++ /dev/null @@ -1,108 +0,0 @@ -# Entity Action Extensions for ASP.NET Core UI - -## Introduction - -Entity action extension system allows you to add a **new action** to the action menu for an entity. A **Click Me** action was added to the *User Management* page below: - -![user-action-extension-click-me](../../images/user-action-extension-click-me.png) - -You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code. - -## How to Set Up - -In this example, we will add a "Click Me!" action and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). - -### Create a JavaScript File - -First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: - -![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) - -Here, the content of this JavaScript file: - -```js -var clickMeAction = { - text: 'Click Me!', - action: function(data) { - //TODO: Write your custom code - alert(data.record.userName); - } -}; - -abp.ui.extensions.entityActions - .get('identity.user') - .addContributor(function(actionList) { - actionList.addTail(clickMeAction); - }); -``` - -In the `action` function, you can do anything you need. See the API section for a detailed usage. - -### Add the File to the User Management Page - -Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification System](Bundling-Minification.md). - -Write the following code inside the `ConfigureServices` of your module class: - -```csharp -Configure(options => -{ - options.ScriptBundles.Configure( - typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, - bundleConfiguration => - { - bundleConfiguration.AddFiles( - "/Pages/Identity/Users/my-user-extensions.js" - ); - }); -}); -``` - -This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. - -That's all. Run your application to see the result. - -## API - -This section explains details of the `abp.ui.extensions.entityActions` JavaScript API. - -### abp.ui.extensions.entityActions.get(entityName) - -This method is used to access the entity actions of a specific module. It takes one parameter: - -* **entityName**: The name of the entity defined by the related module. - -### abp.ui.extensions.entityActions.get(entityName).actions - -The `actions` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined actions for an entity. All contributors are executed in order to prepare the final actions list. This is normally called by the modules to show the actions in the grid. However, you can use it if you are building your own extensible UIs. - -### abp.ui.extensions.entityActions.get(entityName).addContributor(contributeCallback) - -The `addContributor` method covers all scenarios, e.g. you want to add your action in a different position in the list, change or remove an existing action item. `addContributor` with the following parameter: - -* **contributeCallback**: A callback function that is called whenever the action list should be created. You can freely modify the action list inside this callback method. - -#### Example - -```js -var clickMe2Action = { - text: 'Click Me 2!', - icon: 'fas fa-hand-point-right', - action: function(data) { - //TODO: Write your custom code - alert(data.record.userName); - } -}; - -abp.ui.extensions.entityActions - .get('identity.user') - .addContributor(function(actionList) { - // Remove an item from actionList - actionList.dropHead(); - - // Add the new item to the actionList - actionList.addHead(clickMe2Action); - }); -``` - -> `actionList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/UI/AspNetCore/Forms-Validation.md b/docs/en/UI/AspNetCore/Forms-Validation.md deleted file mode 100644 index 257752a517..0000000000 --- a/docs/en/UI/AspNetCore/Forms-Validation.md +++ /dev/null @@ -1,227 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: Forms & Validation - -ABP Framework provides infrastructure and conventions to make easier to create forms, localize display names for the form elements and handle server & client side validation; - -* [abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper automates **creating a complete form** from a C# model class: Creates the input elements, handles localization and client side validation. -* [ABP Form tag helpers](Tag-Helpers/Form-elements.md) (`abp-input`, `abp-select`, `abp-radio`...) render **a single form element** with handling localization and client side validation. -* ABP Framework automatically **localizes the display name** of a form element without needing to add a `[DisplayName]` attribute. -* **Validation errors** are automatically localized based on the user culture. - -> This document is for the **client side validation** and it doesn't cover the server side validation. Check the [validation document](../../Validation.md) for server side validation infrastructure. - -## The Classic Way - -In a typical Bootstrap based ASP.NET Core MVC / Razor Pages UI, you [need to write](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation) such a boilerplate code to create a simple form element: - -````html -
    - - - -
    -```` - -You can continue to use this approach if you need or prefer it. However, ABP Form tag helpers can produce the same output with a minimal code. - -## ABP Dynamic Forms - -[abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper completely automates the form creation. Take this model class as an example: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace MyProject.Web.Pages -{ - public class MovieViewModel - { - [Required] - [StringLength(256)] - public string Name { get; set; } - - [Required] - [DataType(DataType.Date)] - public DateTime ReleaseDate { get; set; } - - [Required] - [TextArea] - [StringLength(1000)] - public string Description { get; set; } - - public Genre Genre { get; set; } - - public float? Price { get; set; } - - public bool PreOrder { get; set; } - } -} -``` - -It uses the data annotation attributes to define validation rules and UI styles for the properties. `Genre`, is an `enum` in this example: - -````csharp -namespace MyProject.Web.Pages -{ - public enum Genre - { - Classic, - Action, - Fiction, - Fantasy, - Animation - } -} -```` - -In order to create the form in a razor page, create a property in your `PageModel` class: - -```csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MyProject.Web.Pages -{ - public class CreateMovieModel : PageModel - { - [BindProperty] - public MovieViewModel Movie { get; set; } - - public void OnGet() - { - Movie = new MovieViewModel(); - } - - public async Task OnPostAsync() - { - if (ModelState.IsValid) - { - //TODO: Save the Movie - } - } - } -} -``` - -Then you can render the form in the `.cshtml` file: - -```html -@page -@model MyProject.Web.Pages.CreateMovieModel - -

    Create a new Movie

    - - -``` - -The result is shown below: - -![abp-dynamic-form-result](../../images/abp-dynamic-form-result.png) - -See the *Localization & Validation* section below to localize the field display names and see how the validation works. - -> See [its own document](Tag-Helpers/Dynamic-Forms.md) for all options of the `abp-dynamic-form` tag helper. - -## ABP Form Tag Helpers - -`abp-dynamic-form` covers most of the scenarios and allows you to control and customize the form using the attributes. - -However, if you want to **render the form body yourself** (for example, you may want to fully control the **form layout**), you can directly use the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md). The same auto-generated form above can be created using the ABP Form Tag Helpers as shown below: - -```html -@page -@model MyProject.Web.Pages.CreateMovieModel - -

    Create a new Movie

    - -
    - - - - - - - Save - -``` - -> See the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md) document for details of these tag helpers and their options. - -## Validation & Localization - -Both of the Dynamic Form and the Form Tag Helpers **automatically validate** the input based on the data annotation attributes and shows validation error messages on the user interface. Error messages are **automatically localized** based on the current culture. - -**Example: User leaves empty a required string property** - -![abp-form-input-validation-error](../../images/abp-form-input-validation-error.png) - -The error message below is shown if the language is French: - -![abp-form-input-validation-error](../../images/abp-form-input-validation-error-french.png) - -Validation errors are already [translated](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization) a lot of languages. You can [contribute](../../Contribution/Index.md) to the translation for your own language or override the texts for your own application by following the [localization](../../Localization.md) documentation. - -## Display Name Localization - -ABP Framework uses the property name as the field name on the user interface. You typically want to [localize](../../Localization.md) this name based on the current culture. - -ABP Framework can conventionally localize the fields on the UI when you add the localization keys to the localization JSON files. - -Example: French localization for the *Name* property (add into the `fr.json` in the application): - -````js -"Name": "Nom" -```` - -Then the UI will use the given name for French language: - -![abp-form-input-validation-error](../../images/abp-form-input-validation-error-french-name.png) - -### Using the `DisplayName:` Prefix - -Directly using the property name as the localization key may be a problem if you need to use the property name for other purpose, which a different translation value. In this case, use the `DisplayName:` prefix for the localization key: - -````js -"DisplayName:Name": "Nom" -```` - -ABP prefers to use the `DisplayName:Name` key over the `Name` key if it does exists. - -### Using a Custom Localization Key - -If you need, you can use the `[DisplayName]` attribute to specify the localization key for a specific property: - -````csharp -[DisplayName("MyNameKey")] -public string Name { get; set; } -```` - -In this case, you can add an entry to the localization file using the key `MyNameKey`. - -> If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system. - -### Enum Localization - -Enum members are also automatically localized wherever possible. For example, when we added `` to the form (like we did in the *ABP Form Tag Helpers* section), ABP can automatically fill the localized names of Enum members. To enabled it, you should define the localized values in your localization JSON file. Example entries for the `Genre` Enum defined in the *ABP Form Tag Helpers* section: - -````json -"Enum:Genre.0": "Classic movie", -"Enum:Genre.1": "Action movie", -"Enum:Genre.2": "Fiction", -"Enum:Genre.3": "Fantasy", -"Enum:Genre.4": "Animation/Cartoon" -```` - -You can use one of the following syntaxes for the localization keys: - -* `Enum:.` -* `.` - -> Remember that if you don't specify values for your Enum, the values will be ordered, starting from `0`. - -> MVC tag helpers also support using Enum member names instead of values (so, you can define `"Enum:Genre.Action"` instead of `"Enum:Genre.1"`, for example), but it is not suggested. Because, when you serialize Enum properties to JSON and send to clients, default serializer uses Enum values instead of Enum names. So, the Enum name won't be available to clients, and it will be a problem if you want to use the same localization values on the client side. - -## See Also - -* [Server Side Validation](../../Validation.md) diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md b/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md deleted file mode 100644 index c100199d27..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md +++ /dev/null @@ -1,150 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI JavaScript AJAX API - -`abp.ajax` API provides a convenient way of performing AJAX calls to the server. It internally uses JQuery's `$.ajax`, but automates some common tasks for you; - -* Automatically **handles & localize the errors** and informs the user (using the [abp.message](Message.md)). So you typically don't care about errors. -* Automatically adds **anti forgery** token to the HTTP header to satisfy CSRF protection validation on the server side. -* Automatically sets **default options** and allows to configure the defaults in a single place. -* Can **block** a UI part (or the full page) during the AJAX operation. -* Allows to fully customize any AJAX call, by using the standard `$.ajax` **options**. - -> While `abp.ajax` makes the AJAX call pretty easier, you typically will use the [Dynamic JavaScript Client Proxy](../Dynamic-JavaScript-Proxies.md) system to perform calls to your server side HTTP APIs. `abp.ajax` can be used when you need to perform low level AJAX operations. - -## Basic Usage - -`abp.ajax` accepts an options object that is accepted by the standard [$.ajax](https://api.jquery.com/jquery.ajax/#jQuery-ajax-settings). All the standard options are valid. It returns a [promise](https://api.jquery.com/category/deferred-object/) as the return value. - -**Example: Get the list of users** - -````js -abp.ajax({ - type: 'GET', - url: '/api/identity/users' -}).then(function(result){ - console.log(result); -}); -```` - -This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../Authorization.md) for the user management page of the [Identity Module](../../../Modules/Identity.md). - -## Error Handling - -The example AJAX call above shows an **error message** if you haven't login to the application or you don't have the necessary permissions to perform this request: - -![ajax-error](../../../images/ajax-error.png) - -All kinds of errors are automatically handled by `abp.ajax`, unless you want to disable it. - -### Standard Error Response - -`abp.ajax` is compatible with the [exception handling system](../../../Exception-Handling.md) of the ABP Framework and it properly handles the standard error format returned from the server. A typical error message is a JSON as like below: - -````json -{ - "error": { - "code": "App:010042", - "message": "This topic is locked and can not add a new message", - "details": "A more detailed info about the error..." - } -} -```` - -The error message is directly shown to the user, using the `message` and `details` properties. - -### Non-Standard Error Response & HTTP Status Codes - -It also handles errors even if the standard error format was not sent by the server. This can be case if you bypass the ABP exception handling system and manually build the HTTP response on the server. In that case, **HTTP status codes** are considered. - -The following HTTP Status Codes are pre-defined; - -* **401**: Shows an error message like "*You should be authenticated (sign in) in order to perform this operation*". When the users click the OK button, they are redirected to the home page of the application to make them login again. -* **403**: Shows an error message like "*You are not allowed to perform this operation*". -* **404**: Shows an error message like "*The resource requested could not found on the server*". -* **Others**: Shows a generic error message like "*An error has occurred. Error detail not sent by server*". - -All these messages are localized based on the current user's language. - -### Manually Handling the Errors - -Since `abp.ajax` returns a promise, you can always chain a `.cactch(...)` call to register a callback that is executed if the AJAX request fails. - -**Example: Show an alert if the AJAX request fails** - -````js -abp.ajax({ - type: 'GET', - url: '/api/identity/users' -}).then(function(result){ - console.log(result); -}).catch(function(){ - alert("request failed :("); -}); -```` - -While your callback is fired, ABP still handles the error itself. If you want to disable automatic error handling, pass `abpHandleError: false` the the `abp.ajax` options. - -**Example: Disable the auto error handling** - -````js -abp.ajax({ - type: 'GET', - url: '/api/identity/users', - abpHandleError: false //DISABLE AUTO ERROR HANDLING -}).then(function(result){ - console.log(result); -}).catch(function(){ - alert("request failed :("); -}); -```` - -If you set `abpHandleError: false` and don't catch the error yourself, then the error will be hidden and the request silently fails. `abp.ajax` still logs the error to the browser console (see the *Configuration* section to override it). - -## Configuration - -`abp.ajax` has a **global configuration** that you can customize based on your requirements. - -### Default AJAX Options - -`abp.ajax.defaultOpts` object is used to configure default options used while performing an AJAX call, unless you override them. Default value of this object is shown below: - -````js -{ - dataType: 'json', - type: 'POST', - contentType: 'application/json', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } -} -```` - -So, if you want to change the default request type, you can do it as shown below: - -````js -abp.ajax.defaultOpts.type = 'GET'; -```` - -Write this code before all of your JavaScript code. You typically want to place such a configuration into a separate JavaScript file and add it to the layout using the global [bundle](../Bundling-Minification.md). - -### Log/Show Errors - -The following functions can be overridden to customize the logging and showing the error messages: - -* `abp.ajax.logError` function logs errors using the [abp.log.error(...)](Logging.md) by default. -* `abp.ajax.showError` function shows the error message using the [abp.message.error(...)](Message.md) by default. -* `abp.ajax.handleErrorStatusCode` handles different HTTP status codes and shows different messages based on the code. -* `abp.ajax.handleAbpErrorResponse` handles the errors sent with the standard ABP error format. -* `abp.ajax.handleNonAbpErrorResponse` handles the non-standard error responses. -* `abp.ajax.handleUnAuthorizedRequest` handles responses with `401` status code and redirect users to the home page of the application. - -**Example: Override the `logError` function** - -````js -abp.ajax.logError = function(error) { - //... -} -```` - -### Other Options - -* `abp.ajax.ajaxSendHandler` function is used to intercept the AJAX requests and add antiforgery token to the HTTP header. Note that this works for all AJAX requests, even if you don't use the `abp.ajax`. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Auth.md b/docs/en/UI/AspNetCore/JavaScript-API/Auth.md deleted file mode 100644 index b50e077ba6..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Auth.md +++ /dev/null @@ -1,23 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Auth API - -Auth API allows you to check permissions (policies) for the current user in the client side. In this way, you can conditionally show/hide UI parts or perform your client side logic based on the current permissions. - -> This document only explains the JavaScript API. See the [authorization document](../../../Authorization.md) to understand the ABP authorization & permission system. - -## Basic Usage - -`abp.auth.isGranted(...)` function is used to check if a permission/policy has granted or not: - -````js -if (abp.auth.isGranted('DeleteUsers')) { - //TODO: Delete the user -} else { - alert("You don't have permission to delete a user!"); -} -```` - -## Other Fields & Functions - -* ` abp.auth.isAnyGranted(...)`: Gets one or more permission/policy names and returns `true` if at least one of them has granted. -* `abp.auth.areAllGranted(...)`: Gets one or more permission/policy names and returns `true` if all of them of them have granted. -* `abp.auth.grantedPolicies`: This is an object where its keys are the permission/policy names. You can find the granted permission/policy names here. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md b/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md deleted file mode 100644 index 943a651432..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md +++ /dev/null @@ -1,56 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript UI Block/Busy API - -UI Block API disables (blocks) the page or a part of the page. - -## Basic Usage - -**Example: Block (disable) the complete page** - -````js -abp.ui.block(); -```` - -**Example: Block (disable) an HTML element** - -````js -abp.ui.block('#MyContainer'); -```` - -**Example: Enables the previously blocked element or page:** - -````js -abp.ui.unblock(); -```` - -## Options - -`abp.ui.block()` method can get an options object which may contain the following fields: - -* `elm`: An optional selector to find the element to be blocked (e.g. `#MyContainerId`). If not provided, the entire page is blocked. The selector can also be directly passed to the `block()` method as shown above. -* `busy`: Set to `true` to show a progress indicator on the blocked area. -* `promise`: A promise object with `always` or `finally` callbacks. This can be helpful if you want to automatically unblock the blocked area when a deferred operation completes. - -**Example: Block an element with busy indicator** - -````js -abp.ui.block({ - elm: '#MySection', - busy: true -}); -```` - -The resulting UI will look like below: - -![ui-busy](../../../images/ui-busy.png) - -## setBusy - -`abp.ui.setBusy(...)` and `abp.ui.clearBusy()` are shortcut functions if you want to use the block with `busy` option. - -**Example: Block with busy** - -````js -abp.ui.setBusy('#MySection'); -```` - -Then you can use `abp.ui.clearBusy();` to re-enable the busy area/page. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md b/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md deleted file mode 100644 index 038bb9dff1..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md +++ /dev/null @@ -1,49 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript CurrentUser API - -`abp.currentUser` is an object that contains information about the current user of the application. - -> This document only explains the JavaScript API. See the [CurrentUser document](../../../CurrentUser.md) to get information about the current user in the server side. - -## Authenticated User - -If the user was authenticated, this object will be something like below: - -````js -{ - isAuthenticated: true, - id: "34f1f4a7-13cc-4b91-84d1-b91c87afa95f", - tenantId: null, - userName: "john", - name: "John", - surName: "Nash", - email: "john.nash@abp.io", - emailVerified: true, - phoneNumber: null, - phoneNumberVerified: false, - roles: ["moderator","supporter"] -} -```` - -So, `abp.currentUser.userName` returns `john` in this case. - -## Anonymous User - -If the user was not authenticated, this object will be something like below: - -````js -{ - isAuthenticated: false, - id: null, - tenantId: null, - userName: null, - name: null, - surName: null, - email: null, - emailVerified: false, - phoneNumber: null, - phoneNumberVerified: false, - roles: [] -} -```` - -You can check `abp.currentUser.isAuthenticated` to understand if the use was authenticated or not. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/DOM.md b/docs/en/UI/AspNetCore/JavaScript-API/DOM.md deleted file mode 100644 index f1adab3f52..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/DOM.md +++ /dev/null @@ -1,116 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript DOM API - -`abp.dom` (Document Object Model) provides events that you can subscribe to get notified when elements dynamically added to and removed from the page (DOM). - -It is especially helpful if you want to initialize the new loaded elements. This is generally needed when you dynamically add elements to DOM (for example, get some HTML elements via AJAX) after page initialization. - -> ABP uses the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to observe the changes made on the DOM. - -## Node Events - -### onNodeAdded - -This event is triggered when an element is added to the DOM. Example: - -````js -abp.dom.onNodeAdded(function(args){ - console.log(args.$el); -}); -```` - -`args` object has the following fields; - -* `$el`: The JQuery selection to get the new element inserted to the DOM. - -### onNodeRemoved - -This event is triggered when an element is removed from the DOM. Example: - -````js -abp.dom.onNodeRemoved(function(args){ - console.log(args.$el); -}); -```` - -`args` object has the following fields; - -* `$el`: The JQuery selection to get the element removed from the DOM. - -## Pre-Build Initializers - -ABP Framework is using the DOM events to initialize some kind of HTML elements when they are added to the DOM after than the page was already initialized. - -> Note that the same initializers also work if these elements were already included in the initial DOM. So, whether they are initially or lazy loaded, they work as expected. - -### Form Initializer - -The Form initializer (defined as `abp.dom.initializers.initializeForms`) initializes the lazy loaded forms; - -* Automatically enabled the `unobtrusive` validation on the form. -* Can automatically show a confirmation message when you submit the form. To enable this feature, just add `data-confirm` attribute with a message (like `data-confirm="Are you sure?"`) to the `form` element. -* If the `form` element has `data-ajaxForm="true"` attribute, then automatically calls the `.abpAjaxForm()` on the `form` element, to make the form posted via AJAX. - -See the [Forms & Validation](../Forms-Validation.md) document for more. - -### Script Initializer - -Script initializer (`abp.dom.initializers.initializeScript`) can execute a JavaScript code for a DOM element. - -**Example: Lazy load a component and execute some code when the element has loaded** - -Assume that you've a container to load the element inside: - -````html -
    -```` - -And this is the component that will be loaded via AJAX from the server and inserted into the container: - -````html -
    -

    Sample message

    -
    -```` - -`data-script-class="MyCustomClass"` indicates the JavaScript class that will be used to perform some logic on this element: - -`MyCustomClass` is a global object defined as shown below: - -````js -MyCustomClass = function(){ - - function initDom($el){ - $el.css('color', 'red'); - } - - return { - initDom: initDom - } -}; -```` - -`initDom` is the function that is called by the ABP Framework. The `$el` argument is the loaded HTML element as a JQuery selection. - -Finally, you can load the component inside the container after an AJAX call: - -````js -$(function () { - setTimeout(function(){ - $.get('/get-my-element').then(function(response){ - $('#LazyComponent').html(response); - }); - }, 2000); -}); -```` - -Script Initialization system is especially helpful if you don't know how and when the component will be loaded into the DOM. This can be possible if you've developed a reusable UI component in a library and you want the application developer shouldn't care how to initialize the component in different use cases. - -> Script initialization doesn't work if the component was loaded in the initial DOM. In this case, you are responsible to initialize it. - -### Other Initializers - -The following Bootstrap components and libraries are automatically initialized when they are added to the DOM: - -* Tooltip -* Popover -* Timeago diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Features.md b/docs/en/UI/AspNetCore/JavaScript-API/Features.md deleted file mode 100644 index 8d58347f49..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Features.md +++ /dev/null @@ -1,29 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Features API - -`abp.features` API allows you to check features or get the values of the features on the client side. You can read the current value of a feature in the client side only if it is allowed by the feature definition (on the server side). - -> This document only explains the JavaScript API. See the [Features](../../../Features.md) document to understand the ABP Features system. - -## Basic Usage - -````js -//Gets a value as string. -var value = abp.features.get('ExportingToExcel'); - -//Check the feature is enabled -var enabled = abp.features.isEnabled('ExportingToExcel.Enabled'); -```` - -## All Values - -`abp.features.values` can be used to access to the all feature values. - -An example value of this object is shown below: - -````js -{ - Identity.TwoFactor: "Optional", - ExportingToExcel.Enabled: "true", - ... -} -```` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/GlobalFeatures.md b/docs/en/UI/AspNetCore/JavaScript-API/GlobalFeatures.md deleted file mode 100644 index 7b451addcd..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/GlobalFeatures.md +++ /dev/null @@ -1,24 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Global Features API - -`abp.globalFeatures` API allows you to get the enabled features of the [Global Features](../../../Global-Features.md) in the client side. - -> This document only explains the JavaScript API. See the [Global Features](../../../Global-Features.md) document to understand the ABP Global Features system. - -## Usage - -````js -//Gets all enabled global features. -> abp.globalFeatures.enabledFeatures - -[ 'Shopping.Payment', 'Ecommerce.Subscription' ] - - -//Check the global feature is enabled -> abp.globalFeatures.isEnabled('Ecommerce.Subscription') - -true - -> abp.globalFeatures.isEnabled('My.Subscription') - -false -```` diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Index.md b/docs/en/UI/AspNetCore/JavaScript-API/Index.md deleted file mode 100644 index 21a3eeed93..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Index.md +++ /dev/null @@ -1,20 +0,0 @@ -# JavaScript API - -ABP provides a set of JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform common application requirements easily in the client side and integrate to the server side. - -## APIs - -* [AJAX](Ajax.md) -* [Auth](Auth.md) -* [CurrentUser](CurrentUser.md) -* [DOM](DOM.md) -* [Events](Events.md) -* [Features](Features.md) -* [Global Features](GlobalFeatures.md) -* [Localization](Localization.md) -* [Logging](Logging.md) -* [ResourceLoader](ResourceLoader.md) -* [Settings](Settings.md) -* [UI Block/Busy](Block-Busy.md) -* [UI Message](Message.md) -* [UI Notification](Notify.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Localization.md b/docs/en/UI/AspNetCore/JavaScript-API/Localization.md deleted file mode 100644 index 4932dc7696..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Localization.md +++ /dev/null @@ -1,146 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Localization API - -Localization API allows you to reuse the server side localization resources in the client side. - -> This document only explains the JavaScript API. See the [localization document](../../../Localization.md) to understand the ABP localization system. - -## Basic Usage - -`abp.localization.getResource(...)` function is used to get a localization resource: - -````js -var testResource = abp.localization.getResource('Test'); -```` - -Then you can localize a string based on this resource: - -````js -var str = testResource('HelloWorld'); -```` - -`abp.localization.localize(...)` function is a shortcut where you can both specify the text name and the resource name: - -````js -var str = abp.localization.localize('HelloWorld', 'Test'); -```` - -`HelloWorld` is the text to localize, where `Test` is the localization resource name here. - -### Fallback Logic - -If given texts was not localized, localization method returns the given key as the localization result. - -### Default Localization Resource - -If you don't specify the localization resource name, it uses the **default localization resource** defined on the `AbpLocalizationOptions` (see the [localization document](../../../Localization.md)). - -**Example: Using the default localization resource** - -````js -var str = abp.localization.localize('HelloWorld'); //uses the default resource -```` - -### Format Arguments - -If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples: - -````js -var testSource = abp.localization.getResource('Test'); -var str1 = testSource('HelloWelcomeMessage', 'John'); -var str2 = abp.localization.localize('HelloWelcomeMessage', 'Test', 'John'); -```` - -Assuming the `HelloWelcomeMessage` is localized as `Hello {0}, welcome!`, both of the samples above produce the output `Hello John, welcome!`. - -## Other Properties & Methods - -### abp.localization.resources - -`abp.localization.resources` property stores all the localization resources, keys and their values. - -### abp.localization.isLocalized - -Returns a boolean indicating that if the given text was localized or not. - -**Example** - -````js -abp.localization.isLocalized('ProductName', 'MyResource'); -```` - -Returns `true` if the `ProductName` text was localized for the `MyResource` resource. Otherwise, returns `false`. You can leave the resource name empty to use the default localization resource. - -### abp.localization.defaultResourceName - -`abp.localization.defaultResourceName` can be set to change the default localization resource. You normally don't set this since the ABP Framework automatically sets is based on the server side configuration. - -### abp.localization.currentCulture - -`abp.localization.currentCulture` returns an object to get information about the **currently selected language**. - -An example value of this object is shown below: - -````js -{ - "displayName": "English", - "englishName": "English", - "threeLetterIsoLanguageName": "eng", - "twoLetterIsoLanguageName": "en", - "isRightToLeft": false, - "cultureName": "en", - "name": "en", - "nativeName": "English", - "dateTimeFormat": { - "calendarAlgorithmType": "SolarCalendar", - "dateTimeFormatLong": "dddd, MMMM d, yyyy", - "shortDatePattern": "M/d/yyyy", - "fullDateTimePattern": "dddd, MMMM d, yyyy h:mm:ss tt", - "dateSeparator": "/", - "shortTimePattern": "h:mm tt", - "longTimePattern": "h:mm:ss tt" - } -} -```` - -### abp.localization.languages - -Used to get list of all **available languages** in the application. An example value of this object is shown below: - -````js -[ - { - "cultureName": "en", - "uiCultureName": "en", - "displayName": "English", - "flagIcon": null - }, - { - "cultureName": "fr", - "uiCultureName": "fr", - "displayName": "Français", - "flagIcon": null - }, - { - "cultureName": "pt-BR", - "uiCultureName": "pt-BR", - "displayName": "Português", - "flagIcon": null - }, - { - "cultureName": "tr", - "uiCultureName": "tr", - "displayName": "Türkçe", - "flagIcon": null - }, - { - "cultureName": "zh-Hans", - "uiCultureName": "zh-Hans", - "displayName": "简体中文", - "flagIcon": null - } -] -```` - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/localization) diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Logging.md b/docs/en/UI/AspNetCore/JavaScript-API/Logging.md deleted file mode 100644 index a46203b019..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Logging.md +++ /dev/null @@ -1,50 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Logging API - -`abp.log` API is used to write simple logs in the client side. - -> The logs are written to console, using the `console.log`, by default. - -> This document is for simple client side logging. See the [Logging](../../../Logging.md) document for server side logging system. - -## Basic Usage - -Use one of the `abp.log.xxx(...)` methods based on the severity of your log message. - -````js -abp.log.debug("Some debug log here..."); //Logging a simple debug message -abp.log.info({ name: "john", age: 42 }); //Logging an object as an information log -abp.log.warn("A warning message"); //Logging a warning message -abp.log.error('An error happens...'); //Error message -abp.log.fatal('Network connection has gone away!'); //Fatal error -```` - -## Log Levels - -There are 5 levels for a log message: - -* DEBUG = 1 -* INFO = 2 -* WARN = 3 -* ERROR = 4 -* FATAL = 5 - -These are defined in the `abp.log.levels` object (like `abp.log.levels.WARN`). - -### Changing the Current Log Level - -You can control the log level as shown below: - -````js -abp.log.level = abp.log.levels.WARN; -```` - -Default log level is `DEBUG`. - -### Logging with Specifying the Level - -Instead of calling `abp.log.info(...)` function, you can use the `abp.log.log` by specifying the log level as a parameter: - -````js -abp.log.log("log message...", abp.log.levels.INFO); -```` - diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Message.md b/docs/en/UI/AspNetCore/JavaScript-API/Message.md deleted file mode 100644 index 9bfb20f3c2..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Message.md +++ /dev/null @@ -1,128 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Message API - -Message API is used to show nice looking messages to the user as a blocking dialog. Message API is an abstraction provided by the ABP Framework and implemented using the [SweetAlert](https://sweetalert.js.org/) library by default. - -## Quick Example - -Use `abp.message.success(...)` function to show a success message: - -````js -abp.message.success('Your changes have been successfully saved!', 'Congratulations'); -```` - -It will show a dialog on the UI: - -![js-message-success](../../../images/js-message-success.png) - -## Informative Messages - -There are four types of informative message functions: - -* `abp.message.info(...)` -* `abp.message.success(...)` -* `abp.message.warn(...)` -* `abp.message.error(...)` - -All these methods get two parameters: - -* `message`: The message (`string`) to be shown. -* `title`: An optional (`string`) title. - -**Example: Show an error message** - -````js -abp.message.error('Your credit card number is not valid!'); -```` - -![js-message-error](../../../images/js-message-error.png) - -## Confirmation Message - -`abp.message.confirm(...)` function can be used to get a confirmation from the user. - -**Example** - -Use the following code to get a confirmation result from the user: - -````js -abp.message.confirm('Are you sure to delete the "admin" role?') -.then(function(confirmed){ - if(confirmed){ - console.log('TODO: deleting the role...'); - } -}); -```` - -The resulting UI will be like shown below: - -![js-message-confirm](../../../images/js-message-confirm.png) - -If user has clicked the `Yes` button, the `confirmed` argument in the `then` callback function will be `true`. - -> "*Are you sure?*" is the default title (localized based on the current language) and you can override it. - -### The Return Value - -The return value of the `abp.message.confirm(...)` function is a promise, so you can chain a `then` callback as shown above. - -### Parameters - -`abp.message.confirm(...)` function has the following parameters: - -* `message`: A message (string) to show to the user. -* `titleOrCallback` (optional): A title or a callback function. If you supply a string, it is shown as the title. If you supply a callback function (that gets a `bool` parameter) then it's called with the result. -* `callback` (optional): If you've passes a title to the second parameter, you can pass your callback function as the 3rd parameter. - -Passing a callback function is an alternative to the `then` callback shown above. - -**Example: Providing all the parameters and getting result with the callback function** - -````js -abp.message.confirm( - 'Are you sure to delete the "admin" role?', - 'Be careful!', - function(confirmed){ - if(confirmed){ - console.log('TODO: deleting the role...'); - } - }); -```` - -## SweetAlert Configuration - -The Message API is implemented using the [SweetAlert](https://sweetalert.js.org/) library by default. If you want to change its configuration, you can set the options in the `abp.libs.sweetAlert.config` object. The default configuration object is shown below: - -````js -{ - 'default': { - }, - info: { - icon: 'info' - }, - success: { - icon: 'success' - }, - warn: { - icon: 'warning' - }, - error: { - icon: 'error' - }, - confirm: { - icon: 'warning', - title: 'Are you sure?', - buttons: ['Cancel', 'Yes'] - } -} -```` - -> "Are you sure?", "Cancel" and "Yes" texts are automatically localized based on the current language. - -So, if you want to set the `warn` icon, you can set it like: - -````js -abp.libs.sweetAlert.config.warn.icon = 'error'; -```` - -See the [SweetAlert document](https://sweetalert.js.org/) for all the configuration options. - diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Notify.md b/docs/en/UI/AspNetCore/JavaScript-API/Notify.md deleted file mode 100644 index 371aa8d4b9..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Notify.md +++ /dev/null @@ -1,45 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Notify API - -Notify API is used to show toast style, auto disappearing UI notifications to the end user. It is implemented by the [Toastr](https://github.com/CodeSeven/toastr) library by default. - -## Quick Example - -Use `abp.notify.success(...)` function to show a success message: - -````js -abp.notify.success( - 'The product "Acme Atom Re-Arranger" has been successfully deleted.', - 'Deleted the Product' -); -```` - -A notification message is shown at the bottom right of the page: - -![js-message-success](../../../images/js-notify-success.png) - -## Notification Types - -There are four types of pre-defined notifications; - -* `abp.notify.success(...)` -* `abp.notify.info(...)` -* `abp.notify.warn(...)` -* `abp.notify.error(...)` - -All of the methods above gets the following parameters; - -* `message`: A message (`string`) to show to the user. -* `title`: An optional title (`string`). -* `options`: Additional options to be passed to the underlying library, to the Toastr by default. - -## Toastr Configuration - -The notification API is implemented by the [Toastr](https://github.com/CodeSeven/toastr) library by default. You can see its own configuration options. - -**Example: Show toast messages on the top right of the page** - -````js -toastr.options.positionClass = 'toast-top-right'; -```` - -> ABP sets this option to `toast-bottom-right` by default. You can override it just as shown above. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Settings.md b/docs/en/UI/AspNetCore/JavaScript-API/Settings.md deleted file mode 100644 index cf96cecdd0..0000000000 --- a/docs/en/UI/AspNetCore/JavaScript-API/Settings.md +++ /dev/null @@ -1,33 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: JavaScript Setting API - -Localization API allows you to get the values of the settings on the client side. You can read the current value of a setting in the client side only if it is allowed by the setting definition (on the server side). - -> This document only explains the JavaScript API. See the [settings document](../../../Settings.md) to understand the ABP setting system. - -## Basic Usage - -````js -//Gets a value as string. -var language = abp.setting.get('Abp.Localization.DefaultLanguage'); - -//Gets an integer value. -var requiredLength = abp.setting.getInt('Abp.Identity.Password.RequiredLength'); - -//Gets a boolean value. -var requireDigit = abp.setting.getBoolean('Abp.Identity.Password.RequireDigit'); -```` - -## All Values - -`abp.setting.values` can be used to obtain all the setting values as an object where the object properties are setting names and property values are the setting values. - -An example value of this object is shown below: - -````js -{ - Abp.Localization.DefaultLanguage: "en", - Abp.Timing.TimeZone: "UTC", - ... -} -```` - diff --git a/docs/en/UI/AspNetCore/Layout-Hooks.md b/docs/en/UI/AspNetCore/Layout-Hooks.md deleted file mode 100644 index 1c2df5e7be..0000000000 --- a/docs/en/UI/AspNetCore/Layout-Hooks.md +++ /dev/null @@ -1,105 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: Layout Hooks - -ABP Framework theming system places the page layout into the [theme](Theming.md) NuGet packages. That means the final application doesn't include a `Layout.cshtml`, so you can't directly change the layout code to customize it. - -You copy the theme code into your solution. In this case you are completely free to customize it. However, then you won't be able to get automatic updates of the theme (by upgrading the theme NuGet package). - -ABP Framework provides different ways of [customizing the UI](Customization-User-Interface.md). - -The **Layout Hook System** allows you to **add code** at some specific parts of the layout. All layouts of all themes should implement these hooks. Finally, you can add a **view component** into a hook point. - -## Example: Add Google Analytics Script - -Assume that you need to add the Google Analytics script to the layout (that will be available for all the pages). First, **create a view component** in your project: - -![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) - -**NotificationViewComponent.cs** - -````csharp -public class GoogleAnalyticsViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); - } -} -```` - -**Default.cshtml** - -````html - -```` - -Change `UA-xxxxxx-1` with your own code. - -You can then add this component to any of the hook points in the `ConfigureServices` of your module: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, //The hook name - typeof(GoogleAnalyticsViewComponent) //The component to add - ); -}); -```` - -Now, the GA code will be inserted in the `head` of the page as the last item. - -### Specifying the Layout - -The configuration above adds the `GoogleAnalyticsViewComponent` to all layouts. You may want to only add to a specific layout: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, - typeof(GoogleAnalyticsViewComponent), - layout: StandardLayouts.Application //Set the layout to add - ); -}); -```` - -See the *Layouts* section below to learn more about the layout system. - -## Layout Hook Points - -There are some pre-defined layout hook points. The `LayoutHooks.Head.Last` used above was one of them. The standard hook points are; - -* `LayoutHooks.Head.First`: Used to add a component as the first item in the HTML head tag. -* `LayoutHooks.Head.Last`: Used to add a component as the last item in the HTML head tag. -* `LayoutHooks.Body.First`: Used to add a component as the first item in the HTML body tag. -* `LayoutHooks.Body.Last`: Used to add a component as the last item in the HTML body tag. -* `LayoutHooks.PageContent.First`: Used to add a component just before the page content (the `@RenderBody()` in the layout). -* `LayoutHooks.PageContent.Last`: Used to add a component just after the page content (the `@RenderBody()` in the layout). - -> You (or the modules you are using) can add **multiple items to the same hook point**. All of them will be added to the layout by the order they were added. - -## Layouts - -Layout system allows themes to define standard, named layouts and allows any page to select a proper layout for its purpose. There are three pre-defined layouts: - -* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. -* "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. -* "**Empty**": Empty and minimal layout. - -These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are the standard layout names and implemented by all the themes out of the box. - -### Layout Location - -You can find the layout files [here](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts) for the basic theme. You can take them as references to build your own layouts or you can override them if necessary. - -## See Also - -* [Customizing the User Interface](Customization-User-Interface.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Modals.md b/docs/en/UI/AspNetCore/Modals.md deleted file mode 100644 index 95eaf2e404..0000000000 --- a/docs/en/UI/AspNetCore/Modals.md +++ /dev/null @@ -1,484 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: Modals - -While you can continue to use the standard [Bootstrap way](https://getbootstrap.com/docs/4.5/components/modal/) to create, open and manage modals in your applications, ABP Framework provides a **flexible** way to manage modals by **automating common tasks** for you. - -**Example: A modal dialog to create a new role entity** - -![modal-manager-example-modal](../../images/modal-manager-example-modal.png) - -ABP Framework provides the following benefits for such a modal with a form inside it; - -* **Lazy loads** the modal HTML into the page and **removes** it from the DOM once its closed. This makes easy to consume a reusable modal dialog. Also, every time you open the modal, it will be a fresh new modal, so you don't have to deal with resetting the modal content. -* **Auto-focuses** the first input of the form once the modal has been opened. You can also specify it using a `function` or `jquery selector`. -* Automatically determines the **form** inside a modal and posts the form via **AJAX** instead of normal page post. -* Automatically checks if the form inside the modal **has changed, but not saved**. It warns the user in this case. -* Automatically **disables the modal buttons** (save & cancel) until the AJAX operation completes. -* Makes it easy to register a **JavaScript object that is initialized** once the modal has loaded. - -So, it makes you write less code when you deal with the modals, especially the modals with a form inside. - -## Basic Usage - -### Creating a Modal as a Razor Page - -To demonstrate the usage, we are creating a simple Razor Page, named `ProductInfoModal.cshtml`, under the `/Pages/Products` folder: - -![modal-page-on-rider](../../images/modal-page-on-rider.png) - -**ProductInfoModal.cshtml Content:** - -````html -@page -@model MyProject.Web.Pages.Products.ProductInfoModalModel -@{ - Layout = null; -} - - - -

    @Model.ProductName

    -
    - -
    -

    - @Model.ProductDescription -

    -

    - Reference: https://acme.com/catalog/ -

    -
    - -
    -```` - -* This page sets the `Layout` to `null` since we will show this as a modal. So, no need to wrap with a layout. -* It uses [abp-modal tag helper](Tag-Helpers/Modals.md) to simplify creating the modal HTML code. You can use the standard Bootstrap modal code if you prefer it. - -**ProductInfoModalModel.cshtml.cs Content:** - -```csharp -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; - -namespace MyProject.Web.Pages.Products -{ - public class ProductInfoModalModel : AbpPageModel - { - public string ProductName { get; set; } - - public string ProductDescription { get; set; } - - public string ProductImageUrl { get; set; } - - public void OnGet() - { - ProductName = "Acme Indestructo Steel Ball"; - ProductDescription = "The ACME Indestructo Steel Ball is completely indestructible, there is nothing that can destroy it!"; - ProductImageUrl = "https://acme.com/catalog/acmeindestructo.jpg"; - } - } -} -``` - -You can surely get the product info from a database or API. We are setting the properties hard-coded for the sake of simplicity, - -### Defining the Modal Manager - -Once you have a modal, you can open it in any page using some simple **JavaScript** code. - -First, create an `abp.ModalManager` object by setting the `viewUrl`, in the JavaScript file of the page that will use the modal: - -````js -var productInfoModal = new abp.ModalManager({ - viewUrl: '/Products/ProductInfoModal' -}); -```` - -> If you only need to specify the `viewUrl`, you can directly pass it to the `ModalManager` constructor, as a shortcut. Example: `new abp.ModalManager('/Products/ProductInfoModal');` - -### Opening the Modal - -Then open the modal whenever you need: - -````js -productInfoModal.open(); -```` - -You typically want to open the modal when something happens; For example, when the user clicks a button: - -````js -$('#OpenProductInfoModal').click(function(){ - productInfoModal.open(); -}); -```` - -The resulting modal will be like that: - -![modal-example-product-info](../../images/modal-example-product-info.png) - -#### Opening the Modal with Arguments - -When you call the `open()` method, `ModalManager` loads the modal HTML by requesting it from the `viewUrl`. You can pass some **query string parameters** to this URL when you open the modal. - -**Example: Pass the product id while opening the modal** - -````js -productInfoModal.open({ - productId: 42 -}); -```` - -You can add a `productId` parameter to the get method: - -````csharp -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; - -namespace MyProject.Web.Pages.Products -{ - public class ProductInfoModalModel : AbpPageModel - { - //... - - public async Task OnGetAsync(int productId) //Add productId parameter - { - //TODO: Get the product with database with the given productId - //... - } - } -} -```` - -In this way, you can use the `productId` to query the product from a data source. - -## Modals with Forms - -`abp.ModalManager` handles various common tasks (described in the introduction) when you want to use a form inside the modal. - -### Example Modal with a Form - -This section shows an example form to create a new product. - -#### Creating the Razor Page - -For this example, creating a new Razor Page, named `ProductCreateModal.cshtml`, under the `/Pages/Products` folder: - -![product-create-modal-page-on-rider](../../images/product-create-modal-page-on-rider.png) - -**ProductCreateModal.cshtml Content:** - -````html -@page -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model MyProject.Web.Pages.Products.ProductCreateModalModel -@{ - Layout = null; -} -
    - - - - - - - - - -
    -```` - -* The `abp-modal` has been wrapped by the `form`. This is needed to place the `Save` and the `Cancel` buttons into the form. In this way, the `Save` button acts as the `submit` button for the `form`. -* Used the [abp-input tag helpers](Tag-Helpers/Form-elements.md) to simplify to create the form elements. Otherwise, you need to write more HTML. - -**ProductCreateModal.cshtml.cs Content:** - -```csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; - -namespace MyProject.Web.Pages.Products -{ - public class ProductCreateModalModel : AbpPageModel - { - [BindProperty] - public PoductCreationDto Product { get; set; } - - public async Task OnGetAsync() - { - //TODO: Get logic, if available - } - - public async Task OnPostAsync() - { - //TODO: Save the Product... - - return NoContent(); - } - } -} -``` - -* This is a simple `PageModal` class. The `[BindProperty]` make the form binding to the model when you post (submit) the form; The standard ASP.NET Core system. -* `OnPostAsync` returns `NoContent` (this method is defined by the base `AbpPageModel` class). Because we don't need to a return value in the client side, after the form post operation. - -**PoductCreationDto:** - -`ProductCreateModalModel` uses a `PoductCreationDto` class defined as shown below: - -````csharp -using System; -using System.ComponentModel.DataAnnotations; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace MyProject.Web.Pages.Products -{ - public class PoductCreationDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [TextArea(Rows = 4)] - [StringLength(2000)] - public string Description { get; set; } - - [DataType(DataType.Date)] - public DateTime ReleaseDate { get; set; } - } -} -```` - -* `abp-input` Tag Helper can understand the data annotation attributes and uses them to shape and validate the form elements. See the [abp-input tag helpers](Tag-Helpers/Form-elements.md) document to learn more. - -#### Defining the Modal Manager - -Again, create an `abp.ModalManager` object by setting the `viewUrl`, in the JavaScript file of the page that will use the modal: - -````js -var productCreateModal = new abp.ModalManager({ - viewUrl: '/Products/ProductCreateModal' -}); -```` - -#### Opening the Modal - -Then open the modal whenever you need: - -````js -productCreateModal.open(); -```` - -You typically want to open the modal when something happens; For example, when the user clicks a button: - -````js -$('#OpenProductCreateModal').click(function(){ - productCreateModal.open(); -}); -```` - -So, the complete code will be something like that (assuming you have a `button` with `id` is `OpenProductCreateModal` on the view side): - -```js -$(function () { - - var productCreateModal = new abp.ModalManager({ - viewUrl: '/Products/ProductCreateModal' - }); - - $('#OpenProductCreateModal').click(function () { - productCreateModal.open(); - }); - -}); -``` - -The resulting modal will be like that: - -![modal-example-product-create](../../images/modal-example-product-create.png) - -#### Saving the Modal - -When you click to the `Save` button, the form is posted to the server. If the server returns a **success response**, then the `onResult` event is triggered with some arguments including the server response and the modal is automatically closed. - -An example callback that logs the arguments passed to the `onResult` method: - -````js -productCreateModal.onResult(function(){ - console.log(arguments); -}); -```` - -If the server returns a failed response, it shows the error message returned from the server and keeps the modal open. - -> See the *Modal Manager Reference* section below for other modal events. - -#### Canceling the Modal - -If you click to the Cancel button with some changes made but not saved, you get such a warning message: - -![modal-manager-cancel-warning](../../images/modal-manager-cancel-warning.png) - -If you don't want such a check & message, you can add `data-check-form-on-close="false"` attribute to your `form` element. Example: - -````html -
    -```` - -### Form Validation - -`ModalManager` automatically triggers the form validation when you click to the `Save` button or hit the `Enter` key on the form: - -![modal-manager-validation](../../images/modal-manager-validation.png) - -See the [Forms & Validation document](Forms-Validation.md) to learn more about the validation. - -## Modals with Script Files - -You may need to perform some logic for your modal. To do that, create a JavaScript file like below: - -````js -abp.modals.ProductInfo = function () { - - function initModal(modalManager, args) { - var $modal = modalManager.getModal(); - var $form = modalManager.getForm(); - - $modal.find('h3').css('color', 'red'); - - console.log('initialized the modal...'); - }; - - return { - initModal: initModal - }; -}; -```` - -* This code simply adds a `ProductInfo` class into the `abp.modals` namespace. The `ProductInfo` class exposes a single public function: `initModal`. -* `initModal` method is called by the `ModalManager` once the modal HTML is inserted to DOM and ready for the initialization logic. -* `modalManager` parameter is the `ModalManager` object related to this modal instance. So, you can use any function on it in your code. See the *ModalManager Reference* section. - -Then include this file to the page that you use the modal: - -````html - - -```` - -* We've use the `abp-script` Tag Helper here. See the [Bundling & Minification](Bundling-Minification.md) document if you want to understand it. You can use the standard `script` tag. It doesn't matter for this case. - -Finally, set the `modalClass` option while creating the `ModalManager` instance: - -````js -var productInfoModal = new abp.ModalManager({ - viewUrl: '/Products/ProductInfoModal', - modalClass: 'ProductInfo' //Matches to the abp.modals.ProductInfo -}); -```` - -### Lazy Loading the Script File - -Instead of adding the `ProductInfoModal.js` to the page you use the modal, you can configure it to lazy load the script file when the first time the modal is opened. - -Example: - -````js -var productInfoModal = new abp.ModalManager({ - viewUrl: '/Products/ProductInfoModal', - scriptUrl: '/Pages/Products/ProductInfoModal.js', //Lazy Load URL - modalClass: 'ProductInfo' -}); -```` - -* `scriptUrl` is used to set the URL to load the script file of the modal. -* In this case, you no longer need to include the `ProductInfoModal.js` to the page. It will be loaded on demand. - -#### Tip: Bundling & Minification - -While lazy loading seems cool at the beginning, it requires an additional call to the server when you first open the modal. - -Instead, you can use the [Bundling & Minification](Bundling-Minification.md) system to create a bundle (that is a single and minified file on production) for all the used script files for a page: - -````html - - - - -```` - -This is efficient if the script file is not large and frequently opened while users use the page. - -Alternatively, you can define the `abp.modals.ProductInfo` class in the page's main JavaScript file if the modal is only and always used in the same page. In this case, you don't need to another external script file at all. - -## ModalManager Reference - -### Options - -Options can be passed when you create a new `ModalManager` object: - -````js -var productInfoModal = new abp.ModalManager({ - viewUrl: '/Products/ProductInfoModal', - //...other options -}); -```` - -Here, the list of all available options; - -* `viewUrl` (required, `string`): The URL to lazy load the HTML of the modal. -* `scriptUrl` (optional, `string`): A URL to lazy load a JavaScript file. It is loaded only once, when the modal first opened. -* `modalClass` (optional, `string`): A JavaScript class defined in the `abp.modals` namespace that can be used to execute code related to the modal. -* `focusElement` (optional, `function or string`): Specifies the element that gets focus. - -### Functions - -When you create a new `ModalManager` object, you can use its functions to perform operations on the modal. Example: - -````js -var myModal = new abp.ModalManager({ - //...options -}); - -//Open the modal -myModal.open(); - -//Close the modal -myModal.close(); -```` - -Here, the list of all available functions of the `ModalManager` object; - -* `open([args])`: Opens the modal dialog. It can get an `args` object that is converted to query string while getting the `viewUrl` from the server. For example, if `args` is `{ productId: 42 }`, then the `ModalManager` passes `?productId=42` to the end of the `viewUrl` while loading the view from the server. -* `reopen()`: Opens the modal with the latest provided `args` for the `open()` method. So, it is a shortcut if you want to re-open the modal with the same `args`. -* `close()`: Closes the modal. The modal HTML is automatically removed from DOM once it has been closed. -* `getModalId()`: Gets the `id` attribute of the container that contains the view returned from the server. This is a unique id per modal and it doesn't change after you create the `ModalManager`. -* `getModal()`: Returns the modal wrapper DOM element (the HTML element with the `modal` CSS class) as a JQuery selection, so you can perform any JQuery method on it. -* `getForm()`: Returns the `form` HTML element as a JQuery selection, so you can perform any JQuery method on it. It returns `null` if the modal has no form inside it. -* `getArgs()` Gets the latest arguments object provided while opening the modal. -* `getOptions()`: Gets the options object passed to the `ModalManager` constructor. -* `setResult(...)`: Triggers the `onResult` event with the provided arguments. You can pass zero or more arguments those are directly passed to the `onResult` event. This function is generally called by the modal script to notify the page that uses the modal. - -### Events - -When you create a new `ModalManager` object, you can use its functions register to events of the modal. Examples: - -````js -var myModal = new abp.ModalManager({ - //...options -}); - -myModal.onOpen(function () { - console.log('opened the modal...'); -}); - -myModal.onClose(function () { - console.log('closed the modal...'); -}); -```` - -Here, the list of all available functions to register to events of the `ModalManager` object; - -* `onOpen(callback)`: Registers a callback function to get notified once the modal is opened. It is triggered when the modal is completely visible on the UI. -* `onClose(callback)`: Registers a callback function to get notified once the modal is closed. It is triggered when the modal is completely invisible on the UI. -* `onResult(callback)`: Registers a callback function that is triggered when the `setResult(...)` method is called. All the parameters sent to the `setResult` method is passed to the callback. diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md deleted file mode 100644 index 60621f9906..0000000000 --- a/docs/en/UI/AspNetCore/Navigation-Menu.md +++ /dev/null @@ -1,288 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI: Navigation Menu - -Every application has a main menu to allow users to navigate to pages/screens of the application. Some applications may contain more than one menu in different sections of the UI. - -ABP Framework is a [modular](../../Module-Development-Basics.md) application development framework. **Every module may need to add items to the menu**. - -So, ABP Framework **provides a menu infrastructure** where; - -* The application or the modules can add items to a menu, without knowing how the menu is rendered. -* The [theme](Theming.md) properly renders the menu. - -## Adding Menu Items - -In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. - -> The [application startup template](../../Startup-Templates/Application.md) already contains an implementation of the `IMenuContributor`. So, you can add items inside that class instead of creating a new one. - -**Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** - -```csharp -using System.Threading.Tasks; -using MyProject.Localization; -using Volo.Abp.UI.Navigation; - -namespace MyProject.Web.Menus -{ - public class MyProjectMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if (context.Menu.Name == StandardMenus.Main) - { - await ConfigureMainMenuAsync(context); - } - } - - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - var l = context.GetLocalizer(); - - context.Menu.AddItem( - new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) - .AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Customers", - displayName: l["Menu:Customers"], - url: "/crm/customers") - ).AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Orders", - displayName: l["Menu:Orders"], - url: "/crm/orders") - ) - ); - } - } -} -``` - -* This example adds items only to the main menu (`StandardMenus.Main`: see the *Standard Menus* section below). -* It gets a `IStringLocalizer` from `context` to [localize](../../Localization.md) the display names of the menu items. -* Adds the Customers and Orders as children of the CRM menu. - -Once you create a menu contributor, you need to add it to the `AbpNavigationOptions` in the `ConfigureServices` method of your module: - -````csharp -Configure(options => -{ - options.MenuContributors.Add(new MyProjectMenuContributor()); -}); -```` - -This example uses some localization keys as display names those should be defined in the localization file: - -````json -"Menu:CRM": "CRM", -"Menu:Orders": "Orders", -"Menu:Customers": "Customers" -```` - -See the [localization document](../../Localization.md) to learn more about the localization. - -When you run the application, you will see the menu items added to the main menu: - -![nav-main-menu](../../images/nav-main-menu.png) - -> The menu is rendered by the current UI theme. So, the look of the main menu can be completely different based on your theme. - -Here, a few notes on the menu contributors; - -* ABP Framework calls the `ConfigureMenuAsync` method **whenever need to render** the menu. -* Every menu item can have **children**. So, you can add menu items with **unlimited depth** (however, your UI theme may not support unlimited depth). -* Only leaf menu items have `url`s normally. When you click to a parent menu, its sub menu is opened or closed, you don't navigate the `url` of a parent menu item. -* If a menu item has no children and has no `url` defined, then it is not rendered on the UI. This simplifies to authorize the menu items: You only authorize the child items (see the next section). If none of the children are authorized, then the parent automatically disappears. - -### Menu Item Properties - -There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; - -* `name` (`string`, required): The **unique name** of the menu item. -* `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. -* `url` (`string`): The URL of the menu item. -* `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. -* `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. -* `customData` (`Dictionary`): A dictionary that allows storing custom objects that you can associate with the menu item and use it while rendering the menu item. -* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications. -* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. -* `cssClass` (`string`): Additional string classes for the menu item. -* `groupName` (`string`): Can be used to group menu items. - -### Authorization - -As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. - -One use case is the [authorization](../../Authorization.md). You typically want to add menu items by checking a permission. - -**Example: Check if the current user has a permission** - -````csharp -if (await context.IsGrantedAsync("MyPermissionName")) -{ - //...add menu items -} -```` - -For the authorization, you can use `RequirePermissions` extension method as a shortcut. It is also more performant, ABP optimizes the permission check for all the items. - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) - .AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Customers", - displayName: l["Menu:Customers"], - url: "/crm/customers") - .RequirePermissions("MyProject.Crm.Customers") - ).AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Orders", - displayName: l["Menu:Orders"], - url: "/crm/orders") - .RequirePermissions("MyProject.Crm.Orders") - ) -); -```` - -> You can use `context.AuthorizationService` to directly access to the `IAuthorizationService`. - -### Resolving Dependencies - -`context.ServiceProvider` can be used to resolve any service dependency. - -**Example: Get a service** - -````csharp -var myService = context.ServiceProvider.GetRequiredService(); -//...use the service -```` - -> You don't need to care about releasing/disposing services. ABP Framework handles it. - -### The Administration Menu - -There is a special menu item in the menu menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): - -![nav-main-menu-administration](../../images/nav-main-menu-administration.png) - -If you want to add menu items under the *Administration* menu item, you can use the `context.Menu.GetAdministration()` extension method: - -````csharp -context.Menu.GetAdministration().AddItem(...) -```` - -### Manipulating the Existing Menu Items - -ABP Framework executes the menu contributors by the [module dependency order](../../Module-Development-Basics.md). So, you can manipulate the menu items that your application or module (directly or indirectly) depends on. - -**Example: Set an icon for the `Users` menu item added by the [Identity Module](../../Modules/Identity.md)** - -````csharp -var userMenu = context.Menu.FindMenuItem(IdentityMenuNames.Users); -userMenu.Icon = "fa fa-users"; -```` - -> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors. - -### Menu Groups - -You can define groups and associate menu items with a group. - -Example: - -```csharp -using System.Threading.Tasks; -using MyProject.Localization; -using Volo.Abp.UI.Navigation; - -namespace MyProject.Web.Menus -{ - public class MyProjectMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if (context.Menu.Name == StandardMenus.Main) - { - await ConfigureMainMenuAsync(context); - } - } - - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - var l = context.GetLocalizer(); - - context.Menu.AddGroup( - new ApplicationMenuGroup( - name: "Main", - displayName: l["Main"] - ) - ) - context.Menu.AddItem( - new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main") - .AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Customers", - displayName: l["Menu:Customers"], - url: "/crm/customers") - ).AddItem(new ApplicationMenuItem( - name: "MyProject.Crm.Orders", - displayName: l["Menu:Orders"], - url: "/crm/orders") - ) - ); - } - } -} -``` - -> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group. - -## Standard Menus - -A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus: - -* `Main`: The main menu of the application. Contains links to the page of the application. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.Main`. -* `User`: User profile menu. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.User`. - -The `Main` menu already covered above. The `User` menu is available when a user has logged in: - -![user-menu](../../images/user-menu.png) - -You can add items to the `User` menu by checking the `context.Menu.Name` as shown below: - -```csharp -if (context.Menu.Name == StandardMenus.User) -{ - //...add items -} -``` - -## IMenuManager - -`IMenuManager` is generally used by the UI [theme](Theming.md) to render the menu items on the UI. So, **you generally don't need to directly use** the `IMenuManager`. - -**Example: Getting the `Main` menu items** - -```csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Volo.Abp.UI.Navigation; - -namespace MyProject.Web.Pages -{ - public class IndexModel : PageModel - { - private readonly IMenuManager _menuManager; - - public IndexModel(IMenuManager menuManager) - { - _menuManager = menuManager; - } - - public async Task OnGetAsync() - { - var mainMenu = await _menuManager.GetAsync(StandardMenus.Main); - - foreach (var menuItem in mainMenu.Items) - { - //... - } - } - } -} -``` diff --git a/docs/en/UI/AspNetCore/Overall.md b/docs/en/UI/AspNetCore/Overall.md deleted file mode 100644 index 0c7e05c105..0000000000 --- a/docs/en/UI/AspNetCore/Overall.md +++ /dev/null @@ -1,160 +0,0 @@ -# ASP.NET Core MVC / Razor Pages UI - -## Introduction - -ABP Framework provides a convenient and comfortable way of creating web applications using the ASP.NET Core MVC / Razor Pages as the User Interface framework. - -> ABP doesn't offer a new/custom way of UI development. You can continue to use your current skills to create the UI. However, it offers a lot of features to make your development easier and have a more maintainable code base. - -### MVC vs Razor Pages - -ASP.NET Core provides two models for UI development: - -* **[MVC (Model-View-Controller)](https://docs.microsoft.com/en-us/aspnet/core/mvc/)** is the classic way that exists from the version 1.0. This model can be used to create UI pages/components and HTTP APIs. -* **[Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/)** was introduced with the ASP.NET Core 2.0 as a new way to create web pages. - -**ABP Framework supports both** of the MVC and the Razor Pages models. However, it is suggested to create the **UI pages with Razor Pages** approach and use the **MVC model to build HTTP APIs**. So, all the pre-build modules, samples and the documentation is based on the Razor Pages for the UI development, while you can always apply the MVC pattern to create your own pages. - -### Modularity - -[Modularity](../../Module-Development-Basics.md) is one of the key goals of the ABP Framework. It is not different for the UI; It is possible to develop modular applications and reusable application modules with isolated and reusable UI pages and components. - -The [application startup template](../../Startup-Templates/Application.md) comes with some application modules pre-installed. These modules have their own UI pages embedded into their own NuGet packages. You don't see their code in your solution, but they work as expected on runtime. - -## Theme System - -ABP Framework provides a complete [Theming](Theming.md) system with the following goals: - -* Reusable [application modules](../../Modules/Index.md) are developed **theme-independent**, so they can work with any UI theme. -* UI theme is **decided by the final application**. -* The theme is distributed via NuGet/NPM packages, so it is **easily upgradable**. -* The final application can **customize** the selected theme. - -### Current Themes - -Currently, three themes are **officially provided**: - -* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. -* The [Lepton Theme](https://commercial.abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP Commercial](https://commercial.abp.io/) license. -* The [LeptonX Theme](https://x.leptontheme.com/) is a theme that has both [commercial](https://docs.abp.io/en/commercial/latest/themes/lepton-x/mvc) and [lite](../../Themes/LeptonXLite/AspNetCore.md) choices. - -There are also some community-driven themes for the ABP Framework (you can search on the web). - -### Base Libraries - -There are a set of standard JavaScript/CSS libraries that comes pre-installed and supported by all the themes: - -- [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. -- [JQuery](https://jquery.com/) for DOM manipulation. -- [DataTables.Net](https://datatables.net/) for data grids. -- [JQuery Validation](https://jqueryvalidation.org/) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation -- [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. -- [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. -- [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. -- [Lodash](https://lodash.com/) as a utility library. -- [Luxon](https://moment.github.io/luxon/) for date/time operations. -- [JQuery Form](https://github.com/jquery-form/form) for AJAX forms. -- [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers. -- [Select2](https://select2.org/) for better select/combo boxes. -- [Timeago](http://timeago.yarp.com/) to show automatically updating fuzzy timestamps. -- [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars. - -You can use these libraries directly in your applications, without needing to manually import your page. - -### Layouts - -The themes provide the standard layouts. So, you have responsive layouts with the standard features already implemented. The screenshot below has taken from the Application Layout of the [Basic Theme](Basic-Theme.md): - -![basic-theme-application-layout](../../images/basic-theme-application-layout.png) - -See the [Theming](Theming.md) document for more layout options and other details. - -### Layout Parts - -A typical layout consists of multiple parts. The [Theming](Theming.md) system provides [menus](Navigation-Menu.md), [toolbars](Toolbars.md), [layout hooks](Layout-Hooks.md) and more to dynamically control the layout by your application and the modules you are using. - -## Features - -This section highlights some of the features provided by the ABP Framework for the ASP.NET Core MVC / Razor Pages UI. - -### Dynamic JavaScript API Client Proxies - -Dynamic JavaScript API Client Proxy system allows you to consume your server side HTTP APIs from your JavaScript client code, just like calling local functions. - -**Example: Get a list of authors from the server** - -````js -acme.bookStore.authors.author.getList({ - maxResultCount: 10 -}).then(function(result){ - console.log(result.items); -}); -```` - -`acme.bookStore.authors.author.getList` is an auto-generated function that internally makes an AJAX call to the server. - -See the [Dynamic JavaScript API Client Proxies](Dynamic-JavaScript-Proxies.md) document for more. - -### Bootstrap Tag Helpers - -ABP makes it easier & type safe to write Bootstrap HTML. - -**Example: Render a Bootstrap modal** - -````html - - - - Woohoo, you're reading this text in a modal! - - - -```` - -See the [Tag Helpers](Tag-Helpers/Index.md) document for more. - -### Forms & Validation - -ABP provides `abp-dynamic-form` and `abp-input` tag helpers to dramatically simplify to create a fully functional form that automates localization, validation and AJAX submission. - -**Example: Use `abp-dynamic-form` to create a complete form based on a model** - -````html - -```` - -See the [Forms & Validation](Forms-Validation.md) document for details. - -### Bundling & Minification / Client Side Libraries - -ABP provides a flexible and modular Bundling & Minification system to create bundles and minify style/script files on runtime. - -````html - - - - - - -```` - -Also, Client Side Package Management system offers a modular and consistent way of managing 3rd-party library dependencies. - -See the [Bundling & Minification](Bundling-Minification.md) and [Client Side Package Management](Client-Side-Package-Management.md) documents. - -### JavaScript APIs - -[JavaScript APIs](JavaScript-API/Index.md) provides a strong abstractions to the server side localization, settings, permissions, features... etc. They also provide a simple way to show messages and **notifications** to the user. - -### Modals, Alerts, Widgets and More - -ABP Framework provides a lot of built-in solutions to common application requirements; - -* [Widget System](Widgets.md) can be used to create reusable widgets & create dashboards. -* [Page Alerts](Page-Alerts.md) makes it easy to show alerts to the user. -* [Modal Manager](Modals.md) provides a simple way to build and use modals. -* [Data Tables](Data-Tables.md) integration makes straightforward to create data grids. - -## Customization - -There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](Customization-User-Interface.md). diff --git a/docs/en/UI/AspNetCore/Page-Alerts.md b/docs/en/UI/AspNetCore/Page-Alerts.md deleted file mode 100644 index cbf6eb493d..0000000000 --- a/docs/en/UI/AspNetCore/Page-Alerts.md +++ /dev/null @@ -1,84 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: Page Alerts - -It is common to show error, warning or information alerts to inform the user. An example *Service Interruption* alert is shown below: - -![page-alert-example](../../images/page-alert-example.png) - -## Basic Usage - -If you directly or indirectly inherit from `AbpPageModel`, you can use the `Alerts` property to add alerts to be rendered after the request completes. - -**Example: Show a Warning alert** - -```csharp -namespace MyProject.Web.Pages -{ - public class IndexModel : MyProjectPageModel //or inherit from AbpPageModel - { - public void OnGet() - { - Alerts.Warning( - text: "We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!", - title: "Service Interruption" - ); - } - } -} -``` - -This usage renders an alert that was shown above. If you need to localize the messages, you can always use the standard [localization](../../Localization.md) system. - -### Exceptions / Invalid Model States - -It is typical to show alerts when you manually handle exceptions (with try/catch statements) or want to handle `!ModelState.IsValid` case and warn the user. For example, the Account Module shows a warning if user enters an incorrect username or password: - -![page-alert-account-layout](../../images/page-alert-account-layout.png) - -> Note that you generally don't need to manually handle exceptions since ABP Framework provides an automatic [exception handling](../../Exception-Handling.md) system. - -### Alert Types - -`Warning` is used to show a warning alert. Other common methods are `Info`, `Danger` and `Success`. - -Beside the standard methods, you can use the `Alerts.Add` method by passing an `AlertType` `enum` with one of these values: `Default`, `Primary`, `Secondary`, `Success`, `Danger`, `Warning`, `Info`, `Light`, `Dark`. - -### Dismissible - -All alert methods gets an optional `dismissible` parameter. Default value is `true` which makes the alert box dismissible. Set it to `false` to create a sticky alert box. - -## IAlertManager - -If you need to add alert messages from another part of your code, you can inject the `IAlertManager` service and use its `Alerts` list. - -**Example: Inject the `IAlertManager`** - -```csharp -using Volo.Abp.AspNetCore.Mvc.UI.Alerts; -using Volo.Abp.DependencyInjection; - -namespace MyProject.Web.Pages -{ - public class MyService : ITransientDependency - { - private readonly IAlertManager _alertManager; - - public MyService(IAlertManager alertManager) - { - _alertManager = alertManager; - } - - public void Test() - { - _alertManager.Alerts.Add(AlertType.Danger, "Test message!"); - } - } -} -``` - -## Notes - -### AJAX Requests - -Page Alert system was designed to be used in a regular full page request. It is not for AJAX/partial requests. The alerts are rendered in the page layout, so a full page refresh is needed. - -For AJAX requests, it is more proper to throw exceptions (e.g. `UserFriendlyException`). See the [exception handling](../../Exception-Handling.md) document. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Page-Header.md b/docs/en/UI/AspNetCore/Page-Header.md deleted file mode 100644 index e8c045cf08..0000000000 --- a/docs/en/UI/AspNetCore/Page-Header.md +++ /dev/null @@ -1,62 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: Page Header - -`IPageLayout` service can be used to set the page title, selected menu item and the breadcrumb items for a page. It's the [theme](Theming.md)'s responsibility to render these on the page. - -## IPageLayout - -`IPageLayout` can be injected in any page/view to set the page header properties. - -### Page Title - -Page Title can be set as shown in the example below: - -```csharp -@inject IPageLayout PageLayout -@{ - PageLayout.Content.Title = "Book List"; -} -``` - -* The Page Title is set to the HTML `title` tag (in addition to the [brand/application name](Branding.md)). -* The theme may render the Page Title before the Page Content (not implemented by the Basic Theme). - -### Breadcrumb - -> **The [Basic Theme](Basic-Theme.md) currently doesn't implement the breadcrumbs.** -> -> The [LeptonX Lite Theme](../../Themes/LeptonXLite/AspNetCore.md) supports breadcrumbs. - -Breadcrumb items can be added to the `PageLayout.Content.BreadCrumb`. - -**Example: Add Language Management to the breadcrumb items.** - -``` -PageLayout.Content.BreadCrumb.Add("Language Management"); -``` - -The theme then renders the breadcrumb. An example render result can be: - -![breadcrumbs-example](../../images/breadcrumbs-example.png) - -* The Home icon is rendered by default. Set `PageLayout.Content.BreadCrumb.ShowHome` to `false` to hide it. -* Current Page name (got from the `PageLayout.Content.Title`) is added as the last by default. Set `PageLayout.Content.BreadCrumb.ShowCurrent` to `false` to hide it. - -Any item that you add is inserted between Home and Current Page items. You can add as many item as you need. `BreadCrumb.Add(...)` method gets three parameters: - -* `text`: The text to show for the breadcrumb item. -* `url` (optional): A URL to navigate to, if the user clicks to the breadcrumb item. -* `icon` (optional): An icon class (like `fas fa-user-tie` for Font-Awesome) to show with the `text`. - -### The Selected Menu Item - -> **The [Basic Theme](Basic-Theme.md) currently doesn't implement the selected menu item since it is not applicable to the top menu which is the only option for the Basic Theme for now.** -> -> The [LeptonX Lite Theme](../../Themes/LeptonXLite/AspNetCore.md) supports selected menu item. - -You can set the Menu Item name related to this page: - -```csharp -PageLayout.Content.MenuItemName = "BookStore.Books"; -``` - -Menu item name should match a unique menu item name defined using the [Navigation / Menu](Navigation-Menu.md) system. In this case, it is expected from the theme to make the menu item "active" in the main menu. diff --git a/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md b/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md deleted file mode 100644 index db466bebdb..0000000000 --- a/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md +++ /dev/null @@ -1,163 +0,0 @@ -# Page Toolbar Extensions for ASP.NET Core UI - -Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below: - -![page-toolbar-button](../../images/page-toolbar-button.png) - -You can add any type of view component item to the page toolbar or modify existing items. - -## How to Set Up - -In this example, we will add an "Import users from excel" button and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). - -### Add a New Button to the User Management Page - -Write the following code inside the `ConfigureServices` of your web module class: - -````csharp -Configure(options => -{ - options.Configure(toolbar => - { - toolbar.AddButton( - LocalizableString.Create("ImportFromExcel"), - icon: "file-import", - id: "ImportUsersFromExcel", - type: AbpButtonType.Secondary - ); - }); -}); -```` - -`AddButton` is a shortcut to simply add a button component. Note that you need to add the `ImportFromExcel` to your localization dictionary (json file) to localize the text. - -When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `order` to set the order of the button component relative to the other components). - -### Create a JavaScript File - -Now, we can go to the client side to handle click event of the new button. First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: - -![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) - -Here, the content of this JavaScript file: - -````js -$(function () { - $('#ImportUsersFromExcel').click(function (e) { - e.preventDefault(); - alert('TODO: import users from excel'); - }); -}); -```` - -In the `click` event, you can do anything you need to do. - -### Add the File to the User Management Page - -Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](Bundling-Minification.md). - -Write the following code inside the `ConfigureServices` of your module class: - -````csharp -Configure(options => -{ - options.ScriptBundles.Configure( - typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, - bundleConfiguration => - { - bundleConfiguration.AddFiles( - "/Pages/Identity/Users/my-user-extensions.js" - ); - }); -}); -```` - -This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. - -## Advanced Use Cases - -While you typically want to add a button action to the page toolbar, it is possible to add any type of component. - -### Add View Component to a Page Toolbar - -First, create a new view component in your project: - -![page-toolbar-custom-component](../../images/page-toolbar-custom-component.png) - -For this example, we've created a `MyToolbarItem` view component under the `/Pages/Identity/Users/MyToolbarItem` folder. - -`MyToolbarItemViewComponent.cs` content: - -````csharp -public class MyToolbarItemViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("~/Pages/Identity/Users/MyToolbarItem/Default.cshtml"); - } -} -```` - -`Default.cshtml` content: - -````xml - - - -```` - -* `.cshtml` file can contain any type of component(s). It is a typical view component. -* `MyToolbarItemViewComponent` can inject and use any service if you need. - -Then you can add the `MyToolbarItemViewComponent` to the user management page: - -````csharp -Configure(options => -{ - options.Configure( - toolbar => - { - toolbar.AddComponent(); - } - ); -}); -```` - -* If your component accepts arguments (in the `Invoke`/`InvokeAsync` method), you can pass them to the `AddComponent` method as an anonymous object. - -#### Permissions - -If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. - -### Add a Page Toolbar Contributor - -If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class: - -````csharp -public class MyToolbarContributor : PageToolbarContributor -{ - public override Task ContributeAsync(PageToolbarContributionContext context) - { - context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarItemViewComponent))); - - return Task.CompletedTask; - } -} -```` - -* You can use `context.ServiceProvider` to resolve dependencies if you need. - -Then add your class to the `Contributors` list: - -````csharp -Configure(options => -{ - options.Configure( - toolbar => - { - toolbar.Contributors.Add(new MyToolbarContributor()); - } - ); -}); -```` - diff --git a/docs/en/UI/AspNetCore/Security-Headers.md b/docs/en/UI/AspNetCore/Security-Headers.md deleted file mode 100644 index c66f28aee1..0000000000 --- a/docs/en/UI/AspNetCore/Security-Headers.md +++ /dev/null @@ -1,99 +0,0 @@ -# Security Headers - -ABP Framework allows you to add frequently used security headers into your application. The following security headers will be added as response headers to your application if you use the `UseAbpSecurityHeaders` middleware: - -* `X-Content-Type-Options`: Tells the browser to not try and guess what a mime-type of a resource might be, and to just take what mime-type the server has returned. -* `X-XSS-Protection`: This is a feature of Internet Explorer, Chrome, and Safari that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks. -* `X-Frame-Options`: This header can be used to indicate whether or not a browser should be allowed to render a page in a ` + +{{else if UI=="Blazor" && DB=="EF"}} + + + +{{else if UI=="BlazorServer" && DB=="EF"}} + + + +{{else if UI=="NG" && DB=="EF"}} + + + +{{else if UI=="MVC" && DB=="Mongo"}} + + + +{{else if UI=="BlazorServer" && DB=="Mongo"}} + + + +{{else if UI=="Blazor" && DB=="Mongo"}} + + + +{{else if UI=="NG" && DB=="Mongo"}} + + + +{{end}} + +## Pre-Requirements + +* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. +* [Node v16.x](https://nodejs.org/) + +{{if DB=="Mongo"}} + +* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) + +{{end}} + +## Install ABP CLI Tool + +We will use the [ABP CLI](../../../cli/index.md) to create new ABP solutions. You can run the following command on a terminal window to install this dotnet tool: + +``` +dotnet tool install -g Volo.Abp.Studio.Cli +``` + +## Create Your ABP Solution + +Create an empty folder, open a command-line terminal and execute the following command in the terminal: + +``` +abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} +``` + +{{if UI=="NG"}} + +This will create a new solution, named *TodoApp* with `angular` and `aspnet-core` folders. Once the solution is ready, open the ASP.NET Core solution in your favorite IDE. + +{{else}} + +This will create a new solution, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. + +{{end}} + +### Create the Database + +If you are using Visual Studio, right click on the `TodoApp.DbMigrator` project, select *Set as StartUp Project*, then hit *Ctrl+F5* to run it without debugging. It will create the initial database and seed the initial data. + +{{if DB=="EF"}} + +> Some IDEs (e.g. Rider) may have problems for the first run since *DbMigrator* adds the initial migration and re-compiles the project. In this case, open a command-line terminal in the folder of the `.DbMigrator` project and execute the `dotnet run` command. + +{{end}} + +### Before Running the Application + +#### Installing the Client-Side Packages + +[ABP CLI](../../../cli/index.md) runs the `abp install-libs` command behind the scenes to install the required NPM packages for your solution while creating the application. + +However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from *node_modules* folder didn't copy to *wwwroot/libs* folder, or if you have added a new client-side package dependency to your solution. + +For such cases, run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: + +``` +abp install-libs +``` + +> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. + +{{if UI=="Blazor" || UI=="BlazorServer"}} + +#### Bundling and Minification + +`abp bundle` command offers bundling and minification support for client-side resources (JavaScript and CSS files) for Blazor projects. This command automatically run when you create a new solution with the [ABP CLI](../../../cli/index.md). + +However, sometimes you might need to run this command manually. To update script & style references without worrying about dependencies, ordering, etc. in a project, you can run this command in the directory of your blazor application: + +``` +abp bundle +``` + +> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../../../framework/ui/blazor/global-scripts-styles.md). + +{{end}} + +### Run the Application + +{{if UI=="MVC" || UI=="BlazorServer"}} + +It is good to run the application before starting the development. Ensure the {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the initial UI: + +{{else if UI=="Blazor"}} + +It is good to run the application before starting the development. The solution has two main applications; + +* `TodoApp.HttpApi.Host` hosts the server-side HTTP API. +* `TodoApp.Blazor` is the client-side Blazor WebAssembly application. + +Ensure the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): + +![todo-swagger-ui-initial](../images/todo-swagger-ui-initial.png) + +You can explore and test your HTTP API with this UI. Now, we can set the `TodoApp.Blazor` as the startup project and run it to open the actual Blazor application UI: + +{{else if UI=="NG"}} + +It is good to run the application before starting the development. The solution has two main applications: + +* `TodoApp.HttpApi.Host` (in the .NET solution) host the server-side HTTP API. +* `angular` folder contains the Angular application. + +Ensure that the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): + +![todo-swagger-ui-initial](../images/todo-swagger-ui-initial.png) + +You can explore and test your HTTP API with this UI. If it works, we can run the Angular client application. + +You can run the application using the following command: + +``` +npm start +``` + +This command takes time, but eventually runs and opens the application in your default browser: + +{{end}} + +![todo-ui-initial](../images/todo-ui-initial.png) + +You can click on the *Login* button, use `admin` as the username and `1q2w3E*` as the password to login to the application. + +All ready. We can start coding! + +## Domain Layer + +This application has a single [entity](../../../framework/architecture/domain-driven-design/entities.md) and we'll start by creating it. Create a new `TodoItem` class inside the *TodoApp.Domain* project: + +```csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace TodoApp +{ + public class TodoItem : BasicAggregateRoot + { + public string Text { get; set; } = string.Empty; + } +} +``` + +`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. + +## Database Integration + +{{if DB=="EF"}} + +Next step is to setup the [Entity Framework Core](../../../framework/data/entity-framework-core/index.md) configuration. + +### Mapping Configuration + +Open the `TodoAppDbContext` class in the `EntityFrameworkCore` folder of the *TodoApp.EntityFrameworkCore* project and add a new `DbSet` property to this class: + +```csharp +public DbSet TodoItems { get; set; } +``` + +Then navigate to the `OnModelCreating` method in the `TodoAppDbContext` class and add the mapping code for the `TodoItem ` entity: + +```csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + ... + + /* Configure your own tables/entities inside here */ + builder.Entity(b => + { + b.ToTable("TodoItems"); + }); +} +``` + +We've mapped the `TodoItem` entity to the `TodoItems` table in the database. + +### Code First Migrations + +The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. + +Open a command-line terminal in the directory of the *TodoApp.EntityFrameworkCore* project and type the following command: + +``` +dotnet ef migrations add Added_TodoItem +``` + +This will add a new migration class to the project: + +![todo-efcore-migration](../images/todo-efcore-migration.png) + +You can apply changes to the database using the following command, in the same command-line terminal: + +``` +dotnet ef database update +``` + +> If you are using Visual Studio, you may want to use the `Add-Migration Added_TodoItem` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} is the startup project and `TodoApp.EntityFrameworkCore` is the *Default Project* in PMC. + +{{else if DB=="Mongo"}} + +Next step is to setup the [MongoDB](../../../framework/data/mongodb/index.md) configuration. Open the `TodoAppMongoDbContext` class in the `MongoDb` folder of the *TodoApp.MongoDB* project and make the following changes: + +1. Add a new property to the class: + +```csharp +public IMongoCollection TodoItems => Collection(); +``` + +2. Add the following code inside the `CreateModel` method: + +```csharp +modelBuilder.Entity(b => +{ + b.CollectionName = "TodoItems"; +}); +``` + +{{end}} + +Now, we can use the ABP repositories to save and retrieve the todo items, as we'll do in the next section. + +## Application Layer + +An [Application Service](../../../framework/architecture/domain-driven-design/application-services.md) is used to perform the use cases of the application. We need to perform the following use cases: + +* Get the list of the todo items +* Create a new todo item +* Delete an existing todo item + +### Application Service Interface + +We can start by defining an interface for the application service. Create a new `ITodoAppService` interface in the *TodoApp.Application.Contracts* project, as shown below: + +```csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace TodoApp +{ + public interface ITodoAppService : IApplicationService + { + Task> GetListAsync(); + Task CreateAsync(string text); + Task DeleteAsync(Guid id); + } +} +``` + +### Data Transfer Object + +`GetListAsync` and `CreateAsync` methods return `TodoItemDto`. `ApplicationService` typically gets and returns DTOs ([Data Transfer Objects](../../../framework/architecture/domain-driven-design/data-transfer-objects.md)) instead of entities. So, we should define the DTO class here. Create a new `TodoItemDto` class inside the *TodoApp.Application.Contracts* project: + +```csharp +using System; + +namespace TodoApp +{ + public class TodoItemDto + { + public Guid Id { get; set; } + public string Text { get; set; } = string.Empty; + } +} +``` + +This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`. + +### Application Service Implementation + +Create a `TodoAppService` class inside the *TodoApp.Application* project, as shown below: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace TodoApp +{ + public class TodoAppService : ApplicationService, ITodoAppService + { + private readonly IRepository _todoItemRepository; + + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; + } + + // TODO: Implement the methods here... + } +} +``` + +This class inherits from the `ApplicationService` class of the ABP and implements the `ITodoAppService` that was defined before. ABP provides default generic [repositories](../../../framework/architecture/domain-driven-design/entities.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../../framework/fundamentals/dependency-injection.md) `IRepository`, which is the default repository for the `TodoItem` entity. We will use it to implement the use cases described before. + +#### Getting Todo Items + +Let's start by implementing the `GetListAsync` method: + +```csharp +public async Task> GetListAsync() +{ + var items = await _todoItemRepository.GetListAsync(); + return items + .Select(item => new TodoItemDto + { + Id = item.Id, + Text = item.Text + }).ToList(); +} +``` + +We are simply getting the complete `TodoItem` list from the database, mapping them to `TodoItemDto` objects and returning as the result. + +#### Creating a New Todo Item + +Next method is `CreateAsync` and we can implement it as shown below: + +```csharp +public async Task CreateAsync(string text) +{ + var todoItem = await _todoItemRepository.InsertAsync( + new TodoItem {Text = text} + ); + + return new TodoItemDto + { + Id = todoItem.Id, + Text = todoItem.Text + }; +} +``` + +The repository's `InsertAsync` method inserts the given `TodoItem` to the database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. + +#### Deleting a Todo Item + +Finally, we can implement the `DeleteAsync` as the following code block: + +```csharp +public async Task DeleteAsync(Guid id) +{ + await _todoItemRepository.DeleteAsync(id); +} +``` + +The application service is ready to be used from the UI layer. + +## User Interface Layer + +It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI: + +![todo-list](../images/todo-list.png) + +> **We will keep the UI side minimal for this tutorial to make the tutorial simple and focused. See the [web application development tutorial](../../book-store/part-01.md) to build real-life pages with all aspects.** + +{{if UI=="MVC"}} + +### Index.cshtml.cs + +Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* project and replace the content with the following code block: + +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TodoApp.Web.Pages +{ + public class IndexModel : TodoAppPageModel + { + public List TodoItems { get; set; } + + private readonly ITodoAppService _todoAppService; + + public IndexModel(ITodoAppService todoAppService) + { + _todoAppService = todoAppService; + } + + public async Task OnGetAsync() + { + TodoItems = await _todoAppService.GetListAsync(); + } + } +} +``` + +This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. + +### Index.cshtml + +Open the `Index.cshtml` file in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: + +```xml +@page +@model TodoApp.Web.Pages.IndexModel +@section styles { + +} +@section scripts { + +} +
    + + + + TODO LIST + + + + + +
    +
    + +
    +
    +
    + +
    + + +
      + @foreach (var todoItem in Model.TodoItems) + { +
    • + @todoItem.Text +
    • + } +
    +
    +
    +
    +``` + +We are using ABP's [card tag helper](../../../framework/ui/mvc-razor-pages/tag-helpers/cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers](../../../framework/ui/mvc-razor-pages/tag-helpers) make it much easier and type safe. + +This page imports a CSS and a JavaScript file, so we should also create them. + +### Index.js + +Create a file named `Index.js` in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: + +```js +$(function () { + + // DELETING ITEMS ///////////////////////////////////////// + $('#TodoList').on('click', 'li i', function(){ + var $li = $(this).parent(); + var id = $li.attr('data-id'); + + todoApp.todo.delete(id).then(function(){ + $li.remove(); + abp.notify.info('Deleted the todo item.'); + }); + }); + + // CREATING NEW ITEMS ///////////////////////////////////// + $('#NewItemForm').submit(function(e){ + e.preventDefault(); + + var todoText = $('#NewItemText').val(); + todoApp.todo.create(todoText).then(function(result){ + $('
  • ') + .html(' ' + result.text) + .appendTo($('#TodoList')); + $('#NewItemText').val(''); + }); + }); +}); +``` + +In the first part, we are subscribing to the click events of the trash icons near the todo items, deleting the related item on the server and showing a notification on the UI. Also, we are removing the deleted item from the DOM, so we don't need to refresh the page. + +In the second part, we are creating a new todo item on the server. If it succeeds, we are then manipulating the DOM to insert a new `
  • ` element to the todo list. This way we don't need to refresh the whole page after creating a new todo item. + +The interesting part here is how we communicate with the server. See the *Dynamic JavaScript Proxies & Auto API Controllers* section to understand how it works. But now, let's continue and complete the application. + +### Index.css + +As the final touch, Create a file named `Index.css` in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: + +```css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +``` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again and see the result. + +### Dynamic JavaScript Proxies & Auto API Controllers + +In the `Index.js` file, we've used the `todoApp.todo.delete(...)` and `todoApp.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP, thanks to the [Dynamic JavaScript Client Proxy](../../../framework/ui/mvc-razor-pages/dynamic-javascript-proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. + +However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../../framework/api-development/auto-controllers.md) feature of the ABP. It automatically converts the application services to API Controllers by convention. + +If you open the [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: + +![todo-api](../images/todo-api.png) + +{{else if UI=="Blazor" || UI=="BlazorServer"}} + +### Index.razor.cs + +Open the `Index.razor.cs` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: + +```csharp +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TodoApp.Blazor.Pages +{ + public partial class Index + { + [Inject] + private ITodoAppService TodoAppService { get; set; } + + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } = string.Empty; + + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } + + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); + } + } +} +``` + +This class uses `ITodoAppService` to perform operations for the todo items. It manipulates the `TodoItems` list after create and delete operations. This way, we don't need to refresh the whole todo list from the server. + +{{if UI=="Blazor"}} + +See the *Dynamic C# Proxies & Auto API Controllers* section below to learn how we could inject and use the application service interface from the Blazor application which is running on the browser! But now, let's continue and complete the application. + +{{end # Blazor}} + +### Index.razor + +Open the `Index.razor` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: + +```xml +@page "/" +@inherits TodoAppComponentBase +
    + + + + TODO LIST + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      + @foreach (var todoItem in TodoItems) + { +
    • + @todoItem.Text +
    • + } +
    +
    +
    +
    +``` + +### Index.razor.css + +As the final touch, open the `Index.razor.css` file in the `Pages` folder of the *TodoApp.Blazor* project and replace it with the following content: + +```css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +``` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again to see the result. + +{{if UI=="Blazor"}} + +### Dynamic C# Proxies & Auto API Controllers + +In the `Index.razor.cs` file, we've injected (with the `[Inject]` attribute) and used the `ITodoAppService` just like using a local service. Remember that the Blazor application is running on the browser while the implementation of this application service is running on the server. + +The magic is done by the ABP's [Dynamic C# Client Proxy](../../../framework/api-development/dynamic-csharp-clients.md) system. It uses the standard `HttpClient` and performs HTTP API requests to the remote server. It also handles all the standard tasks for us, including authorization, JSON serialization and exception handling. + +However, you may ask that we haven't created any API Controller, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../../framework/api-development/auto-controllers.md) feature of the ABP. It automatically converts the application services to API Controllers by convention. + +If you run the `TodoApp.HttpApi.Host` application, you can see the Todo API: + +![todo-api](../images/todo-api.png) + +{{end # Blazor}} + +{{else if UI=="NG"}} + +### Service Proxy Generation + +ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. + +You first need to run the `TodoApp.HttpApi.Host` project since the proxy generator reads API definitions from the server application. + +> **Warning**: There is an issue with IIS Express: it doesn't allow connecting to the application from another process. If you are using Visual Studio, select the `TodoApp.HttpApi.Host` instead of IIS Express in the run button drop-down list, as shown in the figure below: + +![run-without-iisexpress](../images/run-without-iisexpress.png) + +Once you run the `TodoApp.HttpApi.Host` project, open a command-line terminal in the `angular` folder and type the following command: + +``` +abp generate-proxy -t ng +``` + +If everything goes well, it should generate an output as shown below: + +``` +CREATE src/app/proxy/generate-proxy.json (170978 bytes) +CREATE src/app/proxy/README.md (1000 bytes) +CREATE src/app/proxy/todo.service.ts (794 bytes) +CREATE src/app/proxy/models.ts (66 bytes) +CREATE src/app/proxy/index.ts (58 bytes) +``` + +We can then use `todoService` to use the server-side HTTP APIs, as we'll do in the next section. + +### home.component.ts + +Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: + +```js +import { ToasterService } from '@abp/ng.theme.shared'; +import { Component, OnInit } from '@angular/core'; +import { TodoItemDto, TodoService } from '@proxy'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit { + + todoItems: TodoItemDto[]; + newTodoText: string; + + constructor( + private todoService: TodoService, + private toasterService: ToasterService) + { } + + ngOnInit(): void { + this.todoService.getList().subscribe(response => { + this.todoItems = response; + }); + } + + create(): void{ + this.todoService.create(this.newTodoText).subscribe((result) => { + this.todoItems = this.todoItems.concat(result); + this.newTodoText = null; + }); + } + + delete(id: string): void { + this.todoService.delete(id).subscribe(() => { + this.todoItems = this.todoItems.filter(item => item.id !== id); + this.toasterService.info('Deleted the todo item.'); + }); + } +} + +``` + +We've used `todoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side. + +### home.component.html + +Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: + +```html +
    +
    +
    +
    TODO LIST
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      +
    • + {%{{{ todoItem.text }}}%} +
    • +
    +
    +
    +
    +``` + +### home.component.scss + +As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: + +```css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +``` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again to see the result. + +{{end}} + +## Conclusion + +In this tutorial, we've built a very simple application to warm up for the ABP. If you are looking to build a serious application, please check the [web application development tutorial](../../book-store/part-01.md) which covers all the aspects of real-life web application development. + +## Source Code + +You can find source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). + +## See Also + +* [Web Application Development Tutorial](../../book-store/part-01.md) diff --git a/docs/en/tutorials/todo/single-layer/index.md b/docs/en/tutorials/todo/single-layer/index.md new file mode 100644 index 0000000000..8ed78c1d27 --- /dev/null +++ b/docs/en/tutorials/todo/single-layer/index.md @@ -0,0 +1,865 @@ +# TODO Application Tutorial with Single-Layer Solution + +````json +//[doc-params] +{ + "UI": ["MVC", "Blazor", "BlazorServer", "NG"], + "DB": ["EF", "Mongo"] +} +```` + +This is a single-part quick-start tutorial to build a simple todo application with the ABP. Here's a screenshot from the final application: + +![todo-list](../images/todo-list.png) + +You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). + +{{if UI=="Blazor"}} +We are currently preparing a video tutorial for Blazor UI. You can watch other tutorials for the three UI types from [here](https://www.youtube.com/playlist?list=PLsNclT2aHJcPqZxk7D4tU8LtTeCFcN_ci). +{{else}} +This documentation has a video tutorial on **YouTube**!! You can watch it here: +{{end}} + +{{if UI=="MVC" && DB =="EF"}} + + + +{{else if UI=="BlazorServer" && DB=="EF"}} + + + +{{else if UI=="NG" && DB=="EF"}} + + + +{{else if UI=="MVC" && DB=="Mongo"}} + + + +{{else if UI=="BlazorServer" && DB=="Mongo"}} + + + +{{else if UI=="NG" && DB=="Mongo"}} + + + +{{end}} + +## Pre-Requirements + +* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) development. +* [Node v16.x](https://nodejs.org/) + +{{if DB=="Mongo"}} + +* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) + +{{end}} + +## Creating a New Solution + +In this tutorial, we will use the [ABP CLI](../../../cli/index.md) to create the sample application with the ABP. You can run the following command in a command-line terminal to install the **ABP CLI**, if you haven't installed it yet: + +````bash +dotnet tool install -g Volo.Abp.Studio.Cli +```` + +Then create an empty folder, open a command-line terminal and execute the following command in the terminal: + +````bash +abp new TodoApp -t app-nolayers{{if UI=="BlazorServer"}} -u blazor-server{{else if UI=="Blazor"}} -u blazor{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} +```` + +{{if UI=="NG"}} + +This will create a new solution, named *TodoApp*, with `angular` and `aspnet-core` folders. Once the solution is ready, open the solution (in the `aspnet-core` folder) with your favorite IDE. + +{{else if UI=="Blazor"}} + +This will create a new solution with three projects: + +* A `blazor` application that contains the Blazor code, the client-side. +* A `host` application, hosts and serves the `blazor` application. +* A `contracts` project, shared library between these two projects. + +Once the solution is ready, open it in your favorite IDE. + +{{else}} + +This will create a new solution with a single project, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. + +{{end}} + +### Before Running the Application + +You can skip to the *Run the Application* section if you have created the solution using the ABP CLI. It automatically performs the following steps. However, sometimes these steps might need to be manually done. For example, you need to perform them if you have cloned the application code from a source control system. + +#### Creating the Database + +You can run the following command in the {{if UI=="Blazor"}} directory of your `TodoApp.Host` project {{else}}root directory of your project (in the same folder of the `.csproj` file){{end}} to create the database and seed the initial data: + +```bash +dotnet run --migrate-database +``` + +This command will create the database and seed the initial data for you. You could also execute the `migrate-database.ps1` script that is included in the root folder of the solution. + +#### Installing the Client-Side Packages + +Run the `abp install-libs` command on the root directory of your solution to install all required NPM packages: + +```bash +abp install-libs +``` + +> We suggest you install [Yarn](https://classic.yarnpkg.com/) to prevent possible package inconsistencies, if you haven't installed it yet. + +{{if UI=="Blazor" || UI=="BlazorServer"}} + +#### Bundling and Minification + +Run the following command in the directory of your blazor application: + +```bash +abp bundle +``` + +> For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../../../framework/ui/blazor/global-scripts-styles.md). + +{{end}} + +### Run the Application + +{{if UI=="MVC" || UI=="BlazorServer"}} + +It is good to run the application before starting the development. Running the application is pretty straight-forward, you can run the application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project: + +{{else if UI=="Blazor"}} + +It is good to run the application before starting the development. Running the application is pretty straight-forward, you just need to run the `TodoApp.Host` application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project. + +> **Note:** The `host` application hosts and serves the `blazor` application. Therefore, you should run the `host` application only. + +After the application runs, open the application in your default browser: + +{{else if UI=="NG"}} + +It is good to run the application before starting the development. The solution has two main applications: + +* `TodoApp` (in the .NET solution) hosts the server-side HTTP API, so the Angular application can consume it. (server-side application) +* `angular` folder contains the Angular application. (client-side application) + +Firstly, run the `TodoApp` project in your favorite IDE (or run the `dotnet run` CLI command on your project directory) to see the server-side HTTP API on [Swagger UI](https://swagger.io/tools/swagger-ui/): + +![todo-swagger-ui-initial](../images/todo-swagger-ui-initial.png) + +You can explore and test your HTTP API with this UI. If it works, then we can run the Angular client application. + +You can run the application using the following (or `yarn start`) command: + +````bash +npm start +```` + +This command takes time, but eventually runs and opens the application in your default browser: + +{{end}} + +![todo-ui-initial](../images/todo-ui-initial.png) + +You can click on the *Login* button and use `admin` as the username and `1q2w3E*` as the password to login to the application. + +All right. We can start coding! + +## Defining the Entity + +This application will have a single [entity](../../../framework/architecture/domain-driven-design/entities.md) and we can start by creating it. So, create a new `TodoItem` class under the `Entities` folder of {{if UI=="Blazor"}}the `TodoApp.Host` project{{else}}the project{{end}}: + +````csharp +using Volo.Abp.Domain.Entities; + +namespace TodoApp.Entities; + +public class TodoItem : BasicAggregateRoot +{ + public string Text { get; set; } +} +```` + +`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. + +## Database Integration + +{{if DB=="EF"}} + +Next step is to setup the [Entity Framework Core](../../../framework/data/entity-framework-core) configuration. + +### Mapping Configuration + +Open the `TodoAppDbContext` class (in the `Data` folder) and add a new `DbSet` property to this class: + +````csharp +public DbSet TodoItems { get; set; } +```` + +Then navigate to the `OnModelCreating` method in the same class and add the following mapping code for the `TodoItem ` entity: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + ... + + /* Configure your own tables/entities inside here */ + builder.Entity(b => + { + b.ToTable("TodoItems"); + }); +} +```` + +We've mapped the `TodoItem` entity to the `TodoItems` table in the database. The next step is to create a migration and apply the changes to the database. + +### Code First Migrations + +The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. + +Open a command-line terminal in the {{if UI=="Blazor"}} directory of your `TodoApp.Host` project {{else}}root directory of your project (in the same folder of the `.csproj` file){{end}} and type the following command: + +````bash +dotnet ef migrations add Added_TodoItem +```` + +This will add a new migration class to the project. You should see the new migration in the `Migrations` folder: + +![todo-efcore-migration](todo-efcore-migration-single-layer.png) + +Then, you can apply changes to the database using the following command, in the same command-line terminal: + +````bash +dotnet ef database update +```` + +{{else if DB=="Mongo"}} + +The next step is to setup the [MongoDB](../../../framework/data/mongodb) configuration. Open the `TodoAppDbContext` class (under the **Data** folder) in your project and make the following changes: + +1. Add a new property to the class: + +````csharp +public IMongoCollection TodoItems => Collection(); +```` + +2. Add the following code inside the `CreateModel` method: + +````csharp +modelBuilder.Entity(b => +{ + b.CollectionName = "TodoItems"; +}); +```` + +{{end}} + +After the database integrations, now we can start to create application service methods and implement our use-cases. + +## Creating the Application Service + +An [application service](../../../framework/architecture/domain-driven-design/application-services.md) is used to perform the use cases of the application. We need to perform the following use cases in this application: + +* Get the list of the todo items +* Create a new todo item +* Delete an existing todo item + +Before starting to implement these use cases, first we need to create a DTO class that will be used in the application service. + +### Creating the Data Transfer Object (DTO) + +[Application services](../../../framework/architecture/domain-driven-design/application-services.md) typically get and return DTOs ([Data Transfer Objects](../../../framework/architecture/domain-driven-design/data-transfer-objects.md)) instead of entities. So, create a new `TodoItemDto` class under the `Services/Dtos` folder{{if UI=="Blazor"}} of your `TodoApp.Contracts` project{{end}}: + +```csharp +namespace TodoApp.Services.Dtos; + +public class TodoItemDto +{ + public Guid Id { get; set; } + public string Text { get; set; } +} +``` + +This is a very simple DTO class that has the same properties as the `TodoItem` entity. Now, we are ready to implement our use-cases. + +{{if UI=="Blazor"}} + +### The Application Service Interface + +Create a `ITodoAppService` interface under the `Services` folder of the `TodoApp.Contracts` project, as shown below: + +```csharp +using TodoApp.Services.Dtos; +using Volo.Abp.Application.Services; + +namespace TodoApp.Services; + +public interface ITodoAppService : IApplicationService +{ + Task> GetListAsync(); + + Task CreateAsync(string text); + + Task DeleteAsync(Guid id); +} +``` + +{{end}} + +### The Application Service Implementation + +Create a `TodoAppService` class under the `Services` folder of {{if UI=="Blazor"}}your `TodoApp.Host` project{{else}}your project{{end}}, as shown below: + +```csharp +using TodoApp.Services; +using TodoApp.Services.Dtos; +using TodoApp.Entities; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace TodoApp.Services; + +public class TodoAppService : TodoAppAppService{{if UI=="Blazor"}}, ITodoAppService{{end}} +{ + private readonly IRepository _todoItemRepository; + + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; + } + + // TODO: Implement the methods here... +} +``` + +This class inherits from the `TodoAppAppService`, which inherits from the `ApplicationService` class of the ABP and implements our use-cases. ABP provides default generic [repositories](../../../framework/architecture/domain-driven-design/repositories.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../../framework/fundamentals/dependency-injection.md) `IRepository`, which is the default repository for the `TodoItem` entity. We will use it to implement our use cases. + +#### Getting the Todo Items + +Let's start by implementing the `GetListAsync` method, which is used to get a list of todo items: + +````csharp +public async Task> GetListAsync() +{ + var items = await _todoItemRepository.GetListAsync(); + return items + .Select(item => new TodoItemDto + { + Id = item.Id, + Text = item.Text + }).ToList(); +} +```` + +We are simply getting the `TodoItem` list from the repository, mapping them to the `TodoItemDto` objects and returning as the result. + +#### Creating a New Todo Item + +The next method is `CreateAsync` and we can implement it as shown below: + +````csharp +public async Task CreateAsync(string text) +{ + var todoItem = await _todoItemRepository.InsertAsync( + new TodoItem {Text = text} + ); + + return new TodoItemDto + { + Id = todoItem.Id, + Text = todoItem.Text + }; +} +```` + +The repository's `InsertAsync` method inserts the given `TodoItem` to the database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. + +#### Deleting a Todo Item + +Finally, we can implement the `DeleteAsync` as the following code block: + +````csharp +public async Task DeleteAsync(Guid id) +{ + await _todoItemRepository.DeleteAsync(id); +} +```` + +The application service is ready to be used from the UI layer. So, let's implement it. + +## User Interface + +It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI: + +![todo-list](../images/todo-list.png) + +{{if UI=="MVC"}} + +### Index.cshtml.cs + +Open the `Index.cshtml.cs` file in the `Pages` folder and replace the content with the following code block: + +```csharp +using TodoApp.Services; +using TodoApp.Services.Dtos; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace TodoApp.Pages; + +public class IndexModel : AbpPageModel +{ + public List TodoItems { get; set; } + + private readonly TodoAppService _todoAppService; + + public IndexModel(TodoAppService todoAppService) + { + _todoAppService = todoAppService; + } + + public async Task OnGetAsync() + { + TodoItems = await _todoAppService.GetListAsync(); + } +} +``` + +This class uses `TodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. + +### Index.cshtml + +Open the `Index.cshtml` file in the `Pages` folder and replace it with the following content: + +```xml +@page +@model TodoApp.Pages.IndexModel + +@section styles { + +} +@section scripts { + +} + +
    + + + + TODO LIST + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      + @foreach (var todoItem in Model.TodoItems) + { +
    • + @todoItem.Text +
    • + } +
    +
    +
    +
    +``` + +We are using ABP's [card tag helper](../../../framework/ui/mvc-razor-pages/tag-helpers/cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers](../../../framework/ui/mvc-razor-pages/tag-helpers) make it much easier and type safe. + +This page imports a CSS and a JavaScript file, so we should also create them. + +### Index.cshtml.js + +Open the `Index.cshtml.js` file in the `Pages` folder and replace with the following content: + +````js +$(function () { + + // DELETING ITEMS ///////////////////////////////////////// + $('#TodoList').on('click', 'li i', function(){ + var $li = $(this).parent(); + var id = $li.attr('data-id'); + + todoApp.services.todo.delete(id).then(function(){ + $li.remove(); + abp.notify.info('Deleted the todo item.'); + }); + }); + + // CREATING NEW ITEMS ///////////////////////////////////// + $('#NewItemForm').submit(function(e){ + e.preventDefault(); + + var todoText = $('#NewItemText').val(); + todoApp.services.todo.create(todoText).then(function(result){ + $('
  • ') + .html(' ' + result.text) + .appendTo($('#TodoList')); + $('#NewItemText').val(''); + }); + }); +}); +```` + +In the first part, we subscribed to the click events of the trash icons near the todo items, deleted the related item on the server and showed a notification on the UI. Also, we removed the deleted item from the DOM, so we wouldn't need to refresh the page. + +In the second part, we created a new todo item on the server. If it succeeded, we would then manipulate the DOM to insert a new `
  • ` element to the todo list. This way, we wouldn't need to refresh the whole page after creating a new todo item. + +The interesting part here is how we communicate with the server. See the *Dynamic JavaScript Proxies & Auto API Controllers* section to understand how it works. But now, let's continue and complete the application. + +### Index.cshtml.css + +As for the final touch, open the `Index.cshtml.css` file in the `Pages` folder and replace with the following content: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again and see the result. + +### Dynamic JavaScript Proxies & Auto API Controllers + +In the `Index.cshtml.js` file, we've used the `todoApp.services.todo.delete(...)` and `todoApp.services.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP, thanks to the [Dynamic JavaScript Client Proxy](../../../framework/ui/mvc-razor-pages/dynamic-javascript-proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. + +> `services` keyword comes from the namespace (`namespace TodoApp.Services;`). It's a naming convention. + +However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../../framework/api-development/auto-controllers.md) feature of the ABP. It automatically converts the application services to **API Controllers** by convention. + +If you open [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: + +![todo-api](../images/todo-api.png) + +{{else if UI=="Blazor" || UI=="BlazorServer"}} + +### Index.razor.cs + +Open the `Index.razor.cs` file in the `Pages` folder{{if UI=="Blazor"}} in your `Todo.Blazor` project{{end}} and replace the content with the following code block: + +```csharp +{{if UI=="Blazor"}} +using Microsoft.AspNetCore.Components; +using TodoApp.Services; +using TodoApp.Services.Dtos; +{{else}} +using Microsoft.AspNetCore.Components; +using TodoApp.Services; +using TodoApp.Services.Dtos; +{{end}} + +namespace TodoApp.Pages; + +public partial class Index +{ + [Inject] + private {{if UI=="Blazor"}}ITodoAppService{{else}}TodoAppService{{end}} TodoAppService { get; set; } + + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } + + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } + + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); + } +} +``` + +This class uses the {{if UI=="Blazor"}}`ITodoAppService`{{else}}`TodoAppService`{{end}} to get the list of todo items. It manipulates the `TodoItems` list after create and delete operations. This way, we don't need to refresh the whole todo list from the server. + +### Index.razor + +Open the `Index.razor` file in the `Pages` folder and replace the content with the following code block: + +```xml +@page "/" +@inherits TodoAppComponentBase + +
    + + + + TODO LIST + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      + @foreach (var todoItem in TodoItems) + { +
    • + + @todoItem.Text +
    • + } +
    +
    +
    +
    +``` + +### Index.razor.css + +As the final touch, open the `Index.razor.css` file in the `Pages` folder and replace it with the following content: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the {{if UI=="Blazor"}}`TodoApp.Host` project{{else}}application{{end}} again to see the result. + +{{else if UI=="NG"}} + +### Service Proxy Generation + +ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. + +You first need to run the `TodoApp` project since the proxy generator reads API definitions from the server application. + +Once you run the `TodoApp` project (**Swagger API Definition** will be shown), open a command-line terminal in the directory of `angular` folder and run the following command: + +```bash +abp generate-proxy -t ng +``` + +If everything goes well, it should generate an output as shown below: + +```bash +CREATE src/app/proxy/generate-proxy.json (182755 bytes) +CREATE src/app/proxy/README.md (1000 bytes) +CREATE src/app/proxy/services/todo.service.ts (833 bytes) +CREATE src/app/proxy/services/dtos/models.ts (71 bytes) +CREATE src/app/proxy/services/dtos/index.ts (26 bytes) +CREATE src/app/proxy/services/index.ts (81 bytes) +CREATE src/app/proxy/index.ts (61 bytes) +``` + +Then, we can use the `TodoService` to use the server-side HTTP APIs, as we'll do in the next section. + +### home.component.ts + +Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: + +```ts +import { ToasterService } from "@abp/ng.theme.shared"; +import { Component, OnInit } from '@angular/core'; +import { TodoItemDto } from "@proxy/services/dtos"; +import { TodoService } from "@proxy/services"; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], +}) + +export class HomeComponent implements OnInit { + + todoItems: TodoItemDto[]; + newTodoText: string; + + constructor( + private todoService: TodoService, + private toasterService: ToasterService) + { } + + ngOnInit(): void { + this.todoService.getList().subscribe(response => { + this.todoItems = response; + }); + } + + create(): void{ + this.todoService.create(this.newTodoText).subscribe((result) => { + this.todoItems = this.todoItems.concat(result); + this.newTodoText = null; + }); + } + + delete(id: string): void { + this.todoService.delete(id).subscribe(() => { + this.todoItems = this.todoItems.filter(item => item.id !== id); + this.toasterService.info('Deleted the todo item.'); + }); + } +} +``` + +We've used `TodoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side. + +### home.component.html + +Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: + +````html +
    +
    +
    +
    TODO LIST
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      +
    • + {%{{{ todoItem.text }}}%} +
    • +
    +
    +
    +
    +```` + +### home.component.scss + +As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again to see the result. + +{{end}} + +## Conclusion + +In this tutorial, we've built a very simple application to warm up with the ABP. + +## Source Code + +You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). + +## See Also + +* Check the [Web Application Development Tutorial](../../book-store/part-01.md) to see a real-life web application development in a layered architecture using the [Layered Application Startup Template](../../../solution-templates/layered-web-application). diff --git a/docs/en/Tutorials/Todo/Single-Layer/todo-efcore-migration-single-layer.png b/docs/en/tutorials/todo/single-layer/todo-efcore-migration-single-layer.png similarity index 100% rename from docs/en/Tutorials/Todo/Single-Layer/todo-efcore-migration-single-layer.png rename to docs/en/tutorials/todo/single-layer/todo-efcore-migration-single-layer.png diff --git a/docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png b/docs/en/tutorials/todo/single-layer/todo-single-layer-ui-initial.png similarity index 100% rename from docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png rename to docs/en/tutorials/todo/single-layer/todo-single-layer-ui-initial.png diff --git a/docs/en/ui-themes/index.md b/docs/en/ui-themes/index.md new file mode 100644 index 0000000000..8c16677e47 --- /dev/null +++ b/docs/en/ui-themes/index.md @@ -0,0 +1,33 @@ +# The Official Themes +ABP provides a complete UI theming system. While you can build your own themes, you can directly use the following pre-built themes in your applications. + +## The LeptonX Theme + +[LeptonX Theme](https://x.leptontheme.com/) is the official and default theme when you create a new ABP solution. A screenshot from the LeptonX theme: + +![LeptonX Lite application layout](../images/leptonx-theme-users-page.png) + +LeptonX theme has two versions: + +* [LeptonX Theme](lepton-x/index.md) is included in commercial licenses. +* [LeptonX Lite Theme](lepton-x-lite/index.md) is provided as free. + +> If you are looking for the legacy Lepton Theme documentation, please [click here](lepton/index.md). + +## The Basic Theme + +The Basic Theme is a minimalist theme that doesn't add any styling on top of the plain [Bootstrap](https://getbootstrap.com/) styles. You can take the Basic Theme as the base theme and build your own theme or styling on top of it. Here, a screenshot from the theme: + +![basic-theme-application-layout](../images/basic-theme-application-layout.png) + +See the following documents based on the UI type you are using: + +- [Basic Theme - MVC UI](../framework/ui/mvc-razor-pages/basic-theme.md) +- [Basic Theme - Blazor UI](../framework/ui/blazor/basic-theme.md) +- [Basic Theme - Angular UI](../framework/ui/angular/basic-theme.md) + +## See Also + +* [Theming - MVC UI](../framework/ui/mvc-razor-pages/theming.md) +* [Theming - Blazor UI](../framework/ui/blazor/theming.md) +* [Theming - Angular UI](../framework/ui/angular/theming.md) \ No newline at end of file diff --git a/docs/en/ui-themes/lepton-x-lite/angular.md b/docs/en/ui-themes/lepton-x-lite/angular.md new file mode 100644 index 0000000000..430bc2ee73 --- /dev/null +++ b/docs/en/ui-themes/lepton-x-lite/angular.md @@ -0,0 +1,305 @@ +# LeptonX Lite Angular UI + +LeptonX Lite has implementation for the ABP Angular Client. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP](https://abp.io/). + +> See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to learn about themes. + +## Installation + +This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: + +To add `LeptonX-lite` into your project, + +- Install `@abp/ng.theme.lepton-x` + +```bash +yarn add @abp/ng.theme.lepton-x +``` + +- Install `bootstrap-icons` + +```bash +yarn add bootstrap-icons +``` + +- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one in the following link : + +* [Styles - Angular UI](../../framework/ui/angular/theme-configurations.md) + +Note: You should remove the old theme styles from "angular.json" if you are switching from "ThemeBasic" or "Lepton." +Look at the [Theme Configurations](../../framework/ui/angular/theme-configurations.md) list of styles. Depending on your theme, you can alter your styles in angular.json. + +- Finally, remove `ThemeBasicModule`, `provideThemeBasicConfig` from `app.module.ts`, and import the related modules in `app.module.ts` + +```js +import { ThemeLeptonXModule } from "@abp/ng.theme.lepton-x"; + +@NgModule({ + imports: [ + // ... + // do not forget to remove ThemeBasicModule or other old theme module + // ThemeBasicModule + ThemeLeptonXModule.forRoot() + ], + providers: [ + // do not forget to remove provideThemeBasicConfig or other old theme providers + // provideThemeBasicConfig + ], + // ... +}) +export class AppModule {} +``` + +Note: If you employ [Resource Owner Password Flow](../../framework/ui/angular/authorization.md#resource-owner-password-flow) for authorization, you should import the following module as well: + +```js +import { AccountLayoutModule } from "@abp/ng.theme.lepton-x/account"; + +@NgModule({ + // ... + imports: [ + // ... + AccountLayoutModule.forRoot(), + // ... + ], + // ... +}) +export class AppModule {} +``` + +To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss` + +```css +:root { + --lpx-logo: url("/assets/images/logo.png"); + --lpx-logo-icon: url("/assets/images/logo-icon.png"); + --lpx-brand: #edae53; +} +``` + +- `--lpx-logo` is used to place the logo in the menu. +- `--lpx-logo-icon` is a square icon used when the menu is collapsed. +- `--lpx-brand` is a color used throughout the application, especially on active elements. + +### Server Side + +In order to migrate to LeptonX on your server side projects (Host and/or AuthServer projects), please follow the [Server Side Migration](asp-net-core.md) document. + +## Customization + +### Layouts + +The Angular version of LeptonX Lite provides **layout components** for your **user interface** on [ABP Theming](../../framework/ui/angular/theming.md). You can use the layouts to **organize your user interface**. You can replace the **layout components** and some parts of the **layout components** with the [ABP replaceable component system](../../framework/ui/angular/component-replacement.md). + +The main responsibility of a theme is to **provide** the layouts. There are **three pre-defined layouts that must be implemented by all the themes:** + +- **ApplicationLayoutComponent:** The **default** layout which is used by the **main** application pages. +- **AccountLayoutComponent:** Mostly used by the **account module** for **login**, **register**, **forgot password**... pages. +- **EmptyLayoutComponent:** The **Minimal** layout that **has no layout components** at all. + +The **Layout components** and all the replacable components are predefined in `eThemeLeptonXComponents` as enum. + +### How to replace a component + +```js +import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService +import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum +import { eThemeLeptonXComponents } from '@abp/ng.theme.lepton-x'; // imported eThemeLeptonXComponents enum + +//... + +@Component(/* component metadata */) +export class AppComponent { + constructor( + private replaceableComponents: ReplaceableComponentsService, // injected the service + ) { + this.replaceableComponents.add({ + component: YourNewApplicationLayoutComponent, + key: eThemeLeptonXComponents.ApplicationLayout, + }); + } +} +``` + +See the [Component Replacement](../../framework/ui/angular/component-replacement.md) documentation for more information on how to replace components. + +### Brand Component + +The **brand component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. You can change the logo via css but if you want to change logo component, the key is `eThemeLeptonXComponents.Logo` + +```js +///... +this.replaceableComponents.add({ + component: YourNewLogoComponent, + key: eThemeLeptonXComponents.Logo, +}); +///... +``` + +![Brand component](../../images/leptonxlite-brand-component.png) + +## Breadcrumb Component + +On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. + +```js +///... +this.replaceableComponents.add({ + component: YourNewSidebarComponent, + key: eThemeLeptonXComponents.Breadcrumb, +}); +///... +``` + +![Breadcrumb component](../../images/leptonxlite-breadcrumb-component.png) + +## Navbar Component + +Sidebar menus have been used as a **directory for Related Pages** to a **Service** offering, **Navigation** items to a **specific service** or topic and even just as **Links** the user may be interested in. + +```js +///... +this.replaceableComponents.add({ + component: YourNewSidebarComponent, + key: eThemeLeptonXComponents.Navbar, +}); +///... +``` + +![Sidebar menu component](../../images/leptonxlite-sidebar-menu-component.png) + +## Page Alerts Component + +Provides contextual **feedback messages** for typical user actions with a handful of **available** and **flexible** **alert messages**. Alerts are available for any length of text, as well as an **optional dismiss button**. + +![Page alerts component](../../images/leptonxlite-page-alerts-component.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewPageAlertContainerComponent, + key: eThemeLeptonXComponents.PageAlertContainer, +}); +///... +``` + +## Toolbar Component + +![Breadcrumb component](../../images/leptonxlite-toolbar-component.png) + +Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. + +```js +///... +this.replaceableComponents.add({ + component: YourNewNavItemsComponent, + key: eThemeLeptonXComponents.NavItems, +}); +///... +``` + +## Toolbar Items + +There are two parts to the toolbar. The first is Language-Switch. The second is the User-Profile element. You can swap out each of these parts individually. + +## Language Switch Component + +Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** + +![Language switch component](../../images/leptonxlite-language-switch-component.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewLanguagesComponent, + key: eThemeLeptonXComponents.Languages, +}); +///... +``` + +## User Menu Component + +The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. + +![User menu component](../../images/leptonxlite-user-menu-component.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewCurrentUserComponent, + key: eThemeLeptonXComponents.CurrentUser, +}); +///... +``` + +Note: The language selection component in the Volo app is not replaceable. It is part of the settings menu. + +## Mobile Navbar Component + +The **mobile navbar component** is used to display the **navbar menu on mobile devices**. The mobile navbar component is a **dropdown menu** that contains language selection and user menu. + +![Mobile user menu component](../../images/leptonxlite-mobile-user-menu-component.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewMobileNavbarComponent, + key: eThemeLeptonXComponents.MobileNavbar, +}); +///... +``` + +## Mobile Navbar Items. + +There are two parts of the mobile navbar. The mobile navbar has Language-Switch and User-Profile. You can swap out each of these parts individually. + +The Mobile language-Selection component key is `eThemeLeptonXComponents.MobileLanguageSelection`. + +The Mobile User-Profile component key is `eThemeLeptonXComponents.MobileUserProfile`. + +## Footer Component + +![Angular Footer Component](../../images/angular-footer.png) + +The Footer is the section of content at the very bottom of the site. This section of the content can be modified. +Inject **FooterLinksService** and use the **setFooterInfo** method of **FooterLinksService** +to assign path or link and description. +**descUrl** and **footerLinks** are nullable. Constant **footerLinks** are on the right side of footer. + +```js +///... + +const footerLinks = [ + { + link: "/components/bootstrap/badge", + text: "Manage Your Profile", + }, + { + link: "/components/bootstrap/border", + text: "My Security Logs", + }, +]; + +const footerInfo: FooterNav = { + desc: "Home", + descUrl: "/components/home", + footerLinks: footerLinks, +}; + +this.footerLinksService.setFooterInfo(footerInfo); + +///... +``` + +If you want to change the footer component, the key is `eThemeLeptonXComponents.Footer` + +```js +///... +this.replaceableComponents.add({ + component: YourNewFooterComponent, + key: eThemeLeptonXComponents.Footer, +}); +///... +``` diff --git a/docs/en/ui-themes/lepton-x-lite/asp-net-core.md b/docs/en/ui-themes/lepton-x-lite/asp-net-core.md new file mode 100644 index 0000000000..dfe431176b --- /dev/null +++ b/docs/en/ui-themes/lepton-x-lite/asp-net-core.md @@ -0,0 +1,221 @@ +# LeptonX Lite MVC UI +LeptonX Lite has implementation for the ABP Razor Pages. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP](https://abp.io/). + +> See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to learn about themes. + +## Installation + +This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: + +- Add the **Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite** package to your **Web** application. + +```bash +dotnet add package Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite --prerelease +``` + +- Remove the **Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic** reference from the project since it's not necessary after switching to LeptonX Lite. + +- Make sure the old theme is removed and LeptonX is added in your Module class. + +```diff +[DependsOn( + // Remove the BasicTheme module from DependsOn attribute +- typeof(AbpAspNetCoreMvcUiBasicThemeModule), + + // Add the LeptonX Lite module to DependsOn attribute ++ typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), +)] +``` + +- Replace `BasicThemeBundles` with `LeptonXLiteThemeBundles` in `AbpBundlingOptions`: + +```diff +Configure(options => +{ + options.StyleBundles.Configure( + // Remove the following line +- BasicThemeBundles.Styles.Global, + // Add the following line instead ++ LeptonXLiteThemeBundles.Styles.Global, + bundle => + { + bundle.AddFiles("/global-styles.css"); + } + ); +}); +``` + +## Customization + +### Layouts + +LeptonX Lite MVC provides **layouts** for your **user interface** based [ABP Theming](../../framework/ui/mvc-razor-pages/theming.md). You can use **layouts** to **organize your user interface**. + +The main responsibility of a theme is to **provide** the layouts. There are **three pre-defined layouts that must be implemented by all the themes:** + +* **Application:** The **default** layout which is used by the **main** application pages. + +* **Account:** Mostly used by the **account module** for **login**, **register**, **forgot password**... pages. + +* **Empty:** The **Minimal** layout that **has no layout components** at all. + +**Layout names** are **constants** defined in the `LeptonXLiteTheme` class in the **MVC** project **root**. + +> The layout pages define under the `Themes/LeptonXLite/Layouts` folder and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Toolbars + +LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. + +- `LeptonXLiteToolbars.Main` +- `LeptonXLiteToolbars.MainMobile` + +```csharp +public class MyProjectNameMainToolbarContributor : IToolbarContributor +{ + public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == LeptonXLiteToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); + } + + if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); + } + } +} +``` + +# LeptonX Lite MVC Components + +ABP **helps** you make **highly customizable UI**. You can easily **customize** your themes to fit your needs. **The Virtual File System** makes it possible to **manage files** that **do not physically** exist on the **file system** (disk). It's mainly used to embed **(js, css, image..)** files into assemblies and **use them like** physical files at runtime. An application (or another module) can **override** a **virtual file of a module** just like placing a file with the **same name** and **extension** into the **same folder** of the **virtual file**. + +LeptonX Lite is built on the [ABP](https://abp.io/), so you can **easily** customize your Asp.Net Core MVC user interface by following [Abp Mvc UI Customization](../../framework/ui/mvc-razor-pages/customization-user-interface.md). + +## Branding Component + +The **branding component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. + +![Brand component](../../images/leptonxlite-brand-component.png) + +### How to override the Branding Component in LeptonX Lite MVC + +* The **branding component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Brand/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **branding component (C# file)** is defined in the `Themes/LeptonXLite/Components/Brand/MainNavbarBrandViewComponent.cs` file and you can **override it** by creating a file with the **same name** and under the **same folder**. + +### How to override the favicon in LeptonX Lite MVC + +You can add a new favicon to the `~/wwwroot/favicon.svg` and `~/wwwroot/favicon.ico` paths to override the current favicon. + +## Breadcrumb Component + +On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. + +![Breadcrumb component](../../images/leptonxlite-breadcrumb-component.png) + +### How to override the Breadcrumb Component in LeptonX Lite MVC + +* The **breadcrumb component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Breadcrumbs/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **breadcrumb component (C# file)** is defined in the `Themes/LeptonXLite/Components/Breadcrumbs/BreadcrumbsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Sidebar Menu Component + +Sidebar menus have been used as **a directory for Related Pages** to a **Service** offering, **Navigation** items to a **specific service** or topic and even just as **Links** the user may be interested in. + +![Sidebar menu component](../../images/leptonxlite-sidebar-menu-component.png) + +### How to override the Sidebar Menu Component in LeptonX Lite MVC + +* **Sidebar menu page (.cshtml)** is defined in the `Themes/LeptonXLite/Components/Menu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* If you want to **override the menu component (C#)** you can override the `Themes/LeptonXLite/Components/Menu/MainMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +> The **sidebar menu** renders menu items **dynamically**. The **menu item** is a **partial view** and is defined in the `Themes/LeptonXLite/Components/Menu/_MenuItem.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Page Alerts Component + +Provides contextual **feedback messages** for typical user actions with the handful of **available** and **flexible** **alert messages**. Alerts are available for any length of text, as well as an **optional dismiss button**. + +![Page alerts component](../../images/leptonxlite-page-alerts-component.png) + +### How to override the Page Alerts Component in LeptonX Lite MVC + +* The **page alerts component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/PageAlerts/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **page alerts component (C#)** is defined in the `Themes/LeptonXLite/Components/PageAlerts/PageAlertsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Toolbar Component + +Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. + +### How to override the Toolbar Component in LeptonX Lite MVC + +* The **toolbar component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/Toolbar/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **toolbar component (C#)** is defined in the `Themes/LeptonXLite/Components/Toolbar/ToolbarViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Toolbar Item Component + +The toolbar item is a **single item** that **contains** a **link**, an **icon**, a **label** etc.. + +### How to override the Toolbar Item Component in LeptonX Lite MVC + +* The **toolbar item component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/ToolbarItems/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **toolbar item component (C#)** is defined in the `Themes/LeptonXLite/Components/ToolbarItems/ToolbarItemsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +You can find the toolbar components below: + +## Language Switch Component + +Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** + +![Language switch component](../../images/leptonxlite-language-switch-component.png) + +### How to override the Language Switch Component in LeptonX Lite MVC + +* The **language switch component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/LanguageSwitch/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **language switch component (C#)** is defined in the `Themes/LeptonXLite/Components/LanguageSwitch/LanguageSwitchViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Mobile Language Switch Component + +The **mobile** **language switch component** is used to switch the language of the website **on mobile devices**. The mobile language switch component is a **dropdown menu** that **contains all the languages** of the website. + +![Mobil language switch component](../../images/leptonxlite-mobile-language-switch-component.png) + +### How to override the Mobile Language Switch Component in LeptonX Lite MVC + +* The **mobile language switch component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/MobileLanguageSwitch/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **mobile language switch component (C#)** is defined in the `Themes/LeptonXLite/Components/MobileLanguageSwitch/MobileLanguageSwitchViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## User Menu Component + +The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. + +![User menu component](../../images/leptonxlite-user-menu-component.png) + +### How to override the User Menu Component in LeptonX Lite MVC + +* The **user menu component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/UserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **user menu component (C#)** is defined in the `Themes/LeptonXLite/Components/UserMenu/UserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +## Mobile User Menu Component + +The **mobile user menu component** is used to display the **user menu on mobile devices**. The mobile user menu component is a **dropdown menu** that contains all the **options** of the **user menu**. + +![Mobile user menu component](../../images/leptonxlite-mobile-user-menu-component.png) + +### How to override the Mobile User Menu Component in LeptonX Lite MVC + +* The **mobile user menu component page (.cshtml file)** is defined in the `Themes/LeptonXLite/Components/MobileUserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **mobile user menu component (C#)** is defined in the `Themes/LeptonLite/Components/MobileUserMenu/MobileUserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. diff --git a/docs/en/ui-themes/lepton-x-lite/blazor.md b/docs/en/ui-themes/lepton-x-lite/blazor.md new file mode 100644 index 0000000000..3a9051b760 --- /dev/null +++ b/docs/en/ui-themes/lepton-x-lite/blazor.md @@ -0,0 +1,528 @@ +# LeptonX Lite Blazor UI + +````json +//[doc-params] +{ + "UI": ["Blazor", "BlazorServer"] +} +```` + +LeptonX Lite has implementation for the ABP Blazor WebAssembly & Blazor Server. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP](https://abp.io/). + +> See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to learn about themes. + +## Installation + +This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: + +{{if UI == "Blazor"}} +- Complete the [MVC Razor Pages Installation](asp-net-core.md#installation) for the **HttpApi.Host** application first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and if Auth Server is separated, install to the **OpenIddict**_. + + +- Add **Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme** package to your **Blazor WebAssembly** application with the following command: + + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme --prerelease + ``` + +- Remove **Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme** reference from the project since it's not necessary after switching to LeptonX Lite. + +- Remove the old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule** type to the **DependsOn** attribute. + +```diff +[DependsOn( + // Remove BasicTheme module from DependsOn attribute +- typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeModule), + + // Add LeptonX Lite module to DependsOn attribute ++ typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule), +)] +``` + +- Change startup App component with the LeptonX one. + +```csharp +// Make sure the 'App' comes from 'Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite' namespace. +builder.RootComponents.Add("#ApplicationContainer"); +``` + +- Run the `abp bundle` command in your **Blazor** application folder. + +{{end}} + + +{{if UI == "BlazorServer"}} + +- Complete the [MVC Razor Pages Installation](asp-net-core.md#installation) first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and **AuthServer**_. + +- Add **Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme** package to your **Blazor server** application with the following command: + + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme --prerelease + ``` + +- Remove **Volo.Abp.AspNetCore.Components.Server.BasicTheme** reference from the project since it's not necessary after switching to LeptonX Lite. + + +- Remove old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsServerLeptonXLiteThemeModule** type to the **DependsOn** attribute. + + ```diff + [DependsOn( + // Remove BasicTheme module from DependsOn attribute + - typeof(AbpAspNetCoreComponentsServerBasicThemeModule), + + // Add LeptonX Lite module to DependsOn attribute + + typeof(AbpAspNetCoreComponentsServerLeptonXLiteThemeModule) + )] + ``` + +- Replace `BlazorBasicThemeBundles` with `BlazorLeptonXLiteThemeBundles` in `AbpBundlingOptions`: + ```diff + options.StyleBundles.Configure( + // Remove following line + - BlazorBasicThemeBundles.Styles.Global, + // Add following line instead + + BlazorLeptonXLiteThemeBundles.Styles.Global, + bundle => + { + bundle.AddFiles("/blazor-global-styles.css"); + //You can remove the following line if you don't use Blazor CSS isolation for components + bundle.AddFiles("/MyProjectName.Blazor.styles.css"); + }); + ``` + +- Update `_Host.cshtml` file. _(located under **Pages** folder by default.)_ + + - Add following usings to Locate **App** and **BlazorLeptonXLiteThemeBundles** classes. + ```csharp + @using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite + @using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Bundling + ``` + - Then replace script & style bundles as following: + ```diff + // Remove following line + - + // Add following line instead + + + ``` + + ```diff + // Remove following line + - + // Add following line instead + + + ``` + +{{end}} + + +--- + +## Customization + +### Layout + +* Create a razor page, like `MyMainLayout.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +@using Volo.Abp.DependencyInjection + +@inherits MainLayout +@attribute [ExposeServices(typeof(MainLayout))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Main Layout"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainLayout.razor.cs`, in your blazor application as shown below: + +```csharp +[ExposeServices(typeof(MainLayout))] +[Dependency(ReplaceServices = true)] +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + public partial class MyMainLayout + { + public string Name = "My Main Layout"; + } +} +``` + +> Don't forget to remove the repeated attributes from the razor page! +> Don't forget to remove the `@code` section from the razor page! + +### Toolbars + +LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. + +- `LeptonXLiteToolbars.Main` +- `LeptonXLiteToolbars.MainMobile` + +```csharp +public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) +{ + if (context.Toolbar.Name == LeptonXLiteToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); + } + + if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); + } + + return Task.CompletedTask; +} +``` + +{{if UI == "BlazorServer"}} + +> _You can visit the [Toolbars Documentation](../../framework/ui/blazor/toolbars.md) for better understanding._ + +{{end}} + +## Components + +LeptonX Blazor is built on the basis of components. You can use the components in your application as you wish, or you can customize the components by overriding them. If you want to override a component please follow the steps. + +### Branding Component + +The **branding component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. + +![](../../images/leptonxlite-brand-component.png) + +#### How to Override Branding Component + +* Create a razor page, like `MyBrandingComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +@using Volo.Abp.DependencyInjection + +@inherits Branding +@attribute [ExposeServices(typeof(Branding))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Branding Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyBrandingComponent.razor.cs`, in your blazor application as shown below: + +```csharp +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + public partial class MyBrandingComponent + { + public string Name = "My Branding Component"; + } +} +``` + +### How to override the favicon + +Startup templates contain `favicon.ico` files under the `wwwroot` folder of the Blazor application. You can change this file to override the current favicon. + +### Breadcrumb Component + +On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, breadcrumbs reduce the number of actions a website **visitor** needs to take in order to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. + +![](../../images/leptonxlite-breadcrumb-component.png) + +#### How to Override the BreadCrumb Component + +* Create a razor page, like `MyBreadcrumbsComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +@using Volo.Abp.DependencyInjection + +@inherits Breadcrumbs +@attribute [ExposeServices(typeof(Breadcrumbs))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Breadcrumbs Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyBreadcrumbsComponent.razor.cs`, in your blazor application as shown below: +* +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(Breadcrumbs))] + [Dependency(ReplaceServices = true)] + public partial class MyBreadcrumbsComponent + { + public string Name = "My Breadcrumbs Component"; + } +} +``` + +### Main Menu Component + +Sidebar menus have been used as **a directory for Related Pages** for a **Service** offering, **Navigation** items for a **specific service** or topic and even just as **Links** the user may be interested in. + +![](../../images/leptonxlite-sidebar-menu-component.png) + +#### How to Override the Main Menu Component + +* Create a razor page, like `MyMainMenuComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MainMenu +@attribute [ExposeServices(typeof(MainMenu))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Main Menu Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenu.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainMenu))] + [Dependency(ReplaceServices = true)] + public partial class MainMenu + { + public string Name = "My Main Menu Component"; + } +} +``` + +> The **main menu** renders the menu items **dynamically**. The **menu item** is a **razor component** named `MainMenuItem.razor.cs` in the same namespace with **main menu** and you can **override it** like the main menu. + +### Toolbar Items Component + +Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. + +#### How to Override the Toolbar Items Component + +* Create a razor page, like `MyToolbarItemsComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +@using Volo.Abp.DependencyInjection + +@inherits ToolbarItemsComponent +@attribute [ExposeServices(typeof(ToolbarItemsComponent))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Toolbar Items Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyToolbarItemsComponent.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(ToolbarItemsComponent))] + [Dependency(ReplaceServices = true)] + public partial class MyToolbarItemsComponent + { + public string Name = "My Toolbar Items Component"; + } +} +``` + +### Language Switch Component + +Think about a **multi-lingual** website and the first thing that could **hit your mind** is **the language switch component**. A **navigation bar** is a **great place** to **embed a language switch**. By embedding the language switch in the navigation bar of your website, you would **make it simpler** for users to **find it** and **easily** switch the **language** **without trying to locate it across the website.** + +![](../../images/leptonxlite-language-switch-component.png) + +#### How to Override the Language Switch Component + +* Create a razor page, like `MyLanguageSwitchComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +@using Volo.Abp.DependencyInjection + +@inherits LanguageSwitchComponent +@attribute [ExposeServices(typeof(LanguageSwitchComponent))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Language Switch Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyLanguageSwitchComponent.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(LanguageSwitchComponent))] + [Dependency(ReplaceServices = true)] + public partial class MyLanguageSwitchComponent + { + public string Name = "My Language Switch Component"; + } +} +``` + +### Mobile Language Switch Component + +The **mobile** **language switch component** is used to switch the language of the website **on mobile devices**. The mobile language switch component is a **dropdown menu** that **contains all the languages** of the website. + +![](../../images/leptonxlite-mobile-language-switch-component.png) + +#### How to Override the Mobile Language Switch Component + +* Create a razor page, like `MyMobilLanguageSwitchComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +@using Volo.Abp.DependencyInjection + +@inherits MobilLanguageSwitchComponent +@attribute [ExposeServices(typeof(MobilLanguageSwitchComponent))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Mobile Language Switch Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobilLanguageSwitchComponent.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MobilLanguageSwitchComponent))] + [Dependency(ReplaceServices = true)] + public partial class MyMobilLanguageSwitchComponent + { + public string Name = "My Mobile Language Switch Component"; + } +} +``` + +### User Menu Component + +The **User Menu** is the **menu** that **drops down** when you **click your name** or **profile picture** in the **upper right corner** of your page (**in the toolbar**). It drops down options such as **Settings**, **Logout**, etc. + +![](../../images/leptonxlite-user-menu-component.png) + +#### How to Override the User Menu Component + +* Create a razor page, like `MyUserMenuComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +@using Volo.Abp.DependencyInjection + +@inherits MobilLanguageSwitchComponent +@attribute [ExposeServices(typeof(MobilLanguageSwitchComponent))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My User Menu Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyUserMenuComponent.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(UserMenuComponent))] + [Dependency(ReplaceServices = true)] + public partial class MyUserMenuComponent + { + public string Name = "My User Menu Component"; + } +} +``` + +### Mobile User Menu Component + +The **mobile user menu component** is used to display the **user menu on mobile devices**. The mobile user menu component is a **dropdown menu** that contains all the **options** of the **user menu**. + +![](../../images/leptonxlite-mobile-user-menu-component.png) + +#### How to override the Mobile User Menu Component + +* Create a razor page, like `MyMobileUserMenuComponent.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +@using Volo.Abp.DependencyInjection + +@inherits MobilUserMenuComponent +@attribute [ExposeServices(typeof(MobilUserMenuComponent))] +@attribute [Dependency(ReplaceServices = true)] + +@Name + +@code { + string Name = "My Mobile User Menu Component"; +} +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobileUserMenuComponent.razor.cs`, in your blazor application as shown below: + +```csharp +using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Themes.LeptonXLite.Toolbar; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MobileUserMenuComponent))] + [Dependency(ReplaceServices = true)] + public partial class MyMobileUserMenuComponent + { + public string Name = "My Mobile User Menu Component"; + } +} +``` diff --git a/docs/en/ui-themes/lepton-x-lite/index.md b/docs/en/ui-themes/lepton-x-lite/index.md new file mode 100644 index 0000000000..2fc30ca7ec --- /dev/null +++ b/docs/en/ui-themes/lepton-x-lite/index.md @@ -0,0 +1,7 @@ +# LeptonX Lite Theme + +This is the official and default UI theme when you create a new free ABP solution. It is a lite version of the [LeptonX Theme](../lepton-x). See the following documents based on the UI type you are using: + +- [MVC UI](asp-net-core.md) +- [Blazor UI](blazor.md) +- [Angular UI](angular.md) diff --git a/docs/en/ui-themes/lepton-x/angular-customization.md b/docs/en/ui-themes/lepton-x/angular-customization.md new file mode 100644 index 0000000000..5c570478c2 --- /dev/null +++ b/docs/en/ui-themes/lepton-x/angular-customization.md @@ -0,0 +1,228 @@ +## Customization + +### Layouts + +The Angular version of LeptonX provides **layout components** for your **user interface** on [ABP Theming](../../framework/ui/angular/theming.md). +You can use layouts to **organize your user interface**. You can replace the **layout components** and some parts of the **layout components** with the [ABP replaceable component system](../../framework/ui/angular/component-replacement.md). + +The main responsibility of a theme is to **provide** the layouts. There are **three pre-defined layouts that must be implemented by all the themes:** + +- **ApplicationLayoutComponent:** The **default** layout which is used by the **main** application pages. + +- **AccountLayoutComponent:** Mostly used by the **account module** for **login**, **register**, **forgot password**... pages. + +- **EmptyLayoutComponent:** The **Minimal** layout that **has no layout components** at all. + +The **Layout components** and all the replacable components are predefined in `eThemeLeptonXComponents` as enum. + +### How to replace a component + +```js +import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService +import {eThemeLeptonXComponents} from "@volosoft/abp.ng.theme.lepton-x"; // imported eThemeLeptonXComponents enum +//... +@Component(/* component metadata */) +export class AppComponent { + constructor( + private replaceableComponents: ReplaceableComponentsService, // injected the service + ) { + this.replaceableComponents.add({ + component: YourNewApplicationLayoutComponent, + key: eThemeLeptonXComponents.ApplicationLayout, + }); + } +} +``` + +See the [Component Replacement]() documentation for more information on how to replace components. + +### Brand Component + +The **brand component** is a simple component that can be used to display your brand. It contains a **logo** and a **company name**. +You can change the logo with css but if you want to change logo component, the key is `eThemeLeptonXComponents.Logo` + +```js +///... +this.replaceableComponents.add({ + component: YourNewLogoComponent, + key: eThemeLeptonXComponents.Logo, +}); +///... +``` + +![Brand component](images/leptonx-main-header-branding.png) + +## Breadcrumb Component + +On websites that have a lot of pages, **breadcrumb navigation** can greatly **enhance the way users find their way** around. In terms of **usability**, +breadcrumbs reduce the number of actions a website **visitor** needs to take in order +to get to a **higher-level page**, and they **improve** the **findability** of **website sections** and **pages**. + +```js +///... +this.replaceableComponents.add({ + component: YourNewBreadCrumbComponent, + key: eThemeLeptonXComponents.Breadcrumb, +}); +///... +``` + +![Breadcrumb component](images/leptonx-breadcrumb.png) + +## Page Alerts Component + +Provides contextual **feedback messages** for typical user actions with a handful of **available** and **flexible** **alert messages**. Alerts are available for any length of text, as well as an **optional dismiss button**. + +![Page alerts component](images/leptonx-page-alerts.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewPageAlertContainerComponent, + key: eThemeLeptonXComponents.PageAlertContainerComponent, +}); +///... +``` + +## Toolbar Component + +Toolbar items are used to add **extra functionality to the toolbar**. The toolbar is a **horizontal bar** that **contains** a group of **toolbar items**. + +![Toolbar component](images/angular-toolbar.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewToolbarComponent, + key: eThemeLeptonXComponents.Toolbar, +}); +///... +``` + +## Navbar Component + +Navbar items are used to add **extra functionality to the Sidebar**. The toolbar is a **vertical bar** that **contains** a group of **Navbar routes**. + +![Navbar component](images/angular-navbar.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewNavbarComponent, + key: eThemeLeptonXComponents.Navbar, +}); +///... +``` + +## Routes Component + +The routes component contains routes in the navbar. + +![Routes component](images/angular-navbar-routes.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewRoutesComponent, + key: eThemeLeptonXComponents.Routes, +}); +///... +``` + +## General Settings + +- The **general settings component** is used to change your **Appearance** , **Container width** and **Language**. At the **bottom right** is the section that contains the settings. + +![General settings](images/leptonx-general-settings.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewGeneralSettingsComponent, + key: eThemeLeptonXComponents.Settings, +}); +///... +``` + +## Mobile Navbar Component + +The **mobile navbar component** is used to display the **navbar menu on mobile devices**. The mobile navbar component is a **dropdown menu** that contains language selection and user menu. + +![Mobile navbar](images/leptonx-sidemenu-mobile-navbar.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewMobileNavbarComponent, + key: eThemeLeptonXComponents.NavbarMobile, +}); +///... +``` + +## Avatar Component + +The **Avatar Component** is your user image that displays on the **Toolbar top right**. + +![Avatar Component](images/current-user-image.png) + +```js +///... +this.replaceableComponents.add({ + component: YourNewAvatarComponent, + key: eThemeLeptonXComponents.CurrentUserImage, +}); +///... +``` + +## Footer Component + +![Angular Footer Component](images/angular-footer.png) + +The Footer is the section of content at the very bottom of the site. This section of the content can be modified. +Inject **FooterLinksService** and use the **setFooterInfo** method of **FooterLinksService** +to assign path or link and description. +**descUrl** and **footerLinks** are nullable. Constant **footerLinks** are on the right side of the footer. + +```js +///... +const footerLinks = [ + { + link: "/account/manage", + text: "Manage Your Profile", + }, + { + link: "/identity/security-logs", + text: "My Security Logs", + }, +]; +const footerInfo: FooterNav = { + desc: "Home", + descUrl: "/home", + footerLinks: footerLinks, +}; +this.footerLinksService.setFooterInfo(footerInfo); +///... +``` + +If you want to change the footer component, the key is `eThemeLeptonXComponents.Footer` + +```js +///... +this.replaceableComponents.add({ + component: YourNewFooterComponent, + key: eThemeLeptonXComponents.Footer, +}); +///... +``` + +## Custom Layout + +If you want to create your own layout, please refer to the [Custom layout usage with Lepton X components](./how-to-use-lepton-x-components-with-angular-custom-layout.md) documentation. + +## Create a New Appearance + +If you want to create your own LeptonX appearance, [see the docs](./angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md) + +## Change the Default Appearance + +The default appearance is "system" but if you want to define another value, [see the docs](./angular/how-to-change-default-theme-option.md) diff --git a/docs/en/ui-themes/lepton-x/angular.md b/docs/en/ui-themes/lepton-x/angular.md new file mode 100644 index 0000000000..60ad62b5d9 --- /dev/null +++ b/docs/en/ui-themes/lepton-x/angular.md @@ -0,0 +1,87 @@ +# LeptonX Angular UI + +To add `LeptonX` into your existing projects, follow the steps below. + +- Firstly, install `@volosoft/abp.ng.theme.lepton-x` using the command below. + `yarn add @volosoft/abp.ng.theme.lepton-x` + +* Then, edit `angular.json` as follows: + +Add theme-specific styles into the `styles` array of the file. Check the [Theme Configurations](../../framework/ui/angular/theme-configurations.md#lepton-x-commercial) documentation for more information. + +Importing a CSS file as an ECMA module is not supported in Angular 14. Therefore, we need to add the styles in the angular.json file. + +- At last, remove `ThemeLeptonModule` from `app.module.ts` and `shared.module.ts`, and import the following modules in `app.module.ts` + +```ts +import { + HttpErrorComponent, + ThemeLeptonXModule, +} from "@volosoft/abp.ng.theme.lepton-x"; +import { SideMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts"; + +@NgModule({ + // ... + imports: [ + // ... + // ThemeLeptonModule.forRoot(), -> remove this line. + ThemeLeptonXModule.forRoot(), + SideMenuLayoutModule.forRoot(), // depends on which layout you choose + // ... + ], + // ... +}) +export class AppModule {} +``` + +If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add TopMenuLayoutModule as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial) + +```ts +import { + HttpErrorComponent, + ThemeLeptonXModule, +} from "@volosoft/abp.ng.theme.lepton-x"; +import { TopMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts"; + +@NgModule({ + // ... + imports: [ + // ... + // ThemeLeptonModule.forRoot(), -> remove this line. + ThemeLeptonXModule.forRoot(), + TopMenuLayoutModule.forRoot(), + ], + // ... +}) +export class AppModule {} +``` + +- At this point, `LeptonX` theme should be up and running within your application. However, you may need to overwrite some css variables based your needs for every theme available as follows: + +```scss +:root { + .lpx-theme-dark { + --lpx-logo: url("/assets/images/logo/logo-light.svg"); + --lpx-logo-icon: url("/assets/images/logo/logo-light-icon.svg"); + --lpx-brand: #edae53; + } + + .lpx-theme-dim { + --lpx-logo: url("/assets/images/logo/logo-light.svg"); + --lpx-logo-icon: url("/assets/images/logo/logo-light-icon.svg"); + --lpx-brand: #f15835; + } + + .lpx-theme-light { + --lpx-logo: url("/assets/images/logo/logo-dark.svg"); + --lpx-logo-icon: url("/assets/images/logo/logo-dark-icon.svg"); + --lpx-brand: #69aada; + } +} +``` + +If everything is ok, you can remove the `@volo/abp.ng.theme.lepton` in package.json + +## Server Side + +In order to migrate to LeptonX on your server side projects (Host and/or IdentityServer projects), please follow [Server Side Migration](https://docs.abp.io/en/commercial/latest/themes/lepton-x/mvc) document. diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md new file mode 100644 index 0000000000..01ea39c5ec --- /dev/null +++ b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md @@ -0,0 +1,86 @@ +## How to Add a New Appearance to LeptonX for Angular +LeptonX has four appearances: light, dim, dark, and system. You can add a new appearance to LeptonX for Angular. In this document, we will show you how to add a new appearance to LeptonX for Angular. + +### Step 1: Creating the New Theme Definition +To add a new theme appearance to LeptonX, we need to define its configuration. +In your Angular project, open the file where you store your theme-related code (e.g., new-theme.provider.ts), and add the following code snippet: + +```js +import { + LPX_THEMES, + LPX_THEME_STYLES_DEFAULTS, + LpxTheme, +} from '@volosoft/ngx-lepton-x'; + +const myNewThemeDefinition = new LpxTheme({ + bundles: [ + { + bundleName: "bootstrap-custom", // it is optional. You can add one or more bundles. + }, + { + bundleName: "custom-theme",// bundle names will be used in angular.json + }, + ], + styleName: 'myNewTheme', + label: 'My New Theme', + icon: 'bi bi-filetype-css', // or what you want +}); + +export const NEW_THEME_PROVIDER = [ + { + provide: LPX_THEMES, + useValue: [...LPX_THEME_STYLES_DEFAULTS, myNewThemeDefinition], + }, +]; +``` + +In this code snippet, we create a new instance of the LpxTheme class called myNewThemeDefinition. We specify the bundles that make up the theme (e.g., "bootstrap-custom" and "custom-theme"), the style name for the theme, a label to display in the UI, and an icon (using Bootstrap icons in this example). + +### Step 2: Registering the New Theme Provider +Now that we have defined the new theme appearance, we need to register it as a provider in our Angular project. Open the app.module.ts file (or the module where LeptonX is configured), and add the following code: + +```js +import { NEW_THEME_PROVIDER } from './new-theme.provider.ts'; + +@NgModule({ + imports: [ + // ... + ], + providers: [ + // ... + NEW_THEME_PROVIDER, + ], + // ... +}) +export class AppModule { } + +``` + +By importing the `NEW_THEME_PROVIDER` from the file where we defined our theme, we can add it to the providers array of our Angular module. This makes the new theme appearance available throughout the application. + +### Step 3: Adding the Styles Path to angular.json + +To apply the styles for your new theme appearance, you need to add the path of your CSS file to the angular.json file of your Angular project. + +Locate the angular.json file in the root directory of your project. +In the "styles" section, add a new object with the following structure: + +```json +"styles": [ + // ... + { + "input": "src/assets/css/my-css-file.min.css", + "inject": false, + "bundleName": "custom-theme" + } + // ... +] +``` + +In the above code, make sure to replace the "src/assets/css/my-css-file.min.css" directory with the actual path to your CSS file. The "inject" property is set to false to prevent automatic injection of the styles into the application. The "bundleName" property should match the bundle name specified in the theme definition. + +By adding this configuration, Angular will include the specified CSS file when building the application, making the styles available for your new theme appearance. + +![new appearance of LeptonX](../../../images/new-lepton-x-variant.png) + +If you want to learn how to build and customize LeptonX using scss files, see [the link](../source-files). diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md new file mode 100644 index 0000000000..961cbc461e --- /dev/null +++ b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md @@ -0,0 +1,27 @@ +## Configuring the Default Theme for LeptonX +The LeptonX theme offers multiple appearances to suit your application's visual style. You can easily configure the default theme for your application using the ThemeLeptonXModule provided by LeptonX. + +### Configuration Code +To set the default theme, you need to configure the ThemeLeptonXModule using the forRoot() function in your application's main module (often referred to as AppModule). Here's an example: +```js +import { ThemeLeptonXModule } from 'leptonx'; // Import the LeptonX theme module + +@NgModule({ + // ... Other module configurations + imports: [ + // ... Other imported modules + ThemeLeptonXModule.forRoot({ + defaultTheme: 'light', // Set the default theme to 'light' + }), + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + + +In the example above, we've imported the ThemeLeptonXModule and configured it using the forRoot() function. By providing the defaultTheme parameter and setting its value to 'light', + +If you delete the defaultTheme parameter in the configuration object, the LeptonX theme will use the default value of "System" as the default theme appearance. + +You can customize the value of the defaultTheme parameter to align with various available theme appearances, including 'dim', 'dark', 'light', or any personally crafted custom themes. diff --git a/docs/en/ui-themes/lepton-x/blazor.md b/docs/en/ui-themes/lepton-x/blazor.md new file mode 100644 index 0000000000..ef7ad683fb --- /dev/null +++ b/docs/en/ui-themes/lepton-x/blazor.md @@ -0,0 +1,806 @@ +# LeptonX Blazor UI + +````json +//[doc-params] +{ + "UI": ["Blazor", "BlazorServer"] +} +```` + +LeptonX theme is implemented and ready to use with ABP. No custom implementation is needed for Blazor Server & WebAssembly. + +## Installation + +{{if UI == "Blazor"}} + +- Add **Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme** package to your **Blazor WASM** application. + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme + ``` + +- Remove old theme from **DependsOn** attribute in your module class and add **AbpAspNetCoreComponentsWebAssemblyLeptonXThemeModule** type to **DependsOn** attribute. + +```diff +[DependsOn( +- typeof(LeptonThemeManagementBlazorModule), +- typeof(AbpAspNetCoreComponentsWebAssemblyLeptonThemeModule), ++ typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXThemeModule) +)] +``` + +- Change startup `App` component with the LeptonX one. + - Add following using declaration and remove your old theme using declaration. + ```csharp + using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components; + ``` + + - Make sure `App` component in following block is `Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.App` + ```csharp + // Make sure the 'App' comes from 'Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components' namespace. + builder.RootComponents.Add("#ApplicationContainer"); + ``` + - If you can't remove or not sure which one is the old theme's using statements, you can use full name of that class: + ```csharp + builder.RootComponents.Add("#ApplicationContainer"); + ``` + +{{end}} + + +{{if UI == "BlazorServer"}} + +- Complete [MVC Installation steps](mvc.md#installation) first. + +- Add **Volo.Abp.AspNetCore.Components.Server.LeptonXTheme** package to your **Blazor Server** application. + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.Server.LeptonXTheme + ``` +- Remove old theme from **DependsOn** attribute in your module class and add **AbpAspNetCoreComponentsServerLeptonXThemeModule** type to **DependsOn** attribute. + + ```diff + [DependsOn( + - typeof(LeptonThemeManagementBlazorModule), + - typeof(AbpAspNetCoreComponentsServerLeptonThemeModule), + + typeof(AbpAspNetCoreComponentsServerLeptonXThemeModule) + )] + ``` + +- Update AbpBundlingOptions + ```diff + options.StyleBundles.Configure( + - BlazorLeptonThemeBundles.Styles.Global, + + BlazorLeptonXThemeBundles.Styles.Global, + bundle => + { + bundle.AddFiles("/blazor-global-styles.css"); + //You can remove the following line if you don't use Blazor CSS isolation for components + bundle.AddFiles("/MyProjectName.Blazor.styles.css"); + }); + ``` + +- Update `_Host.cshtml` file. _(located under **Pages** folder by default.)_ + - Add following usings to Locate **App** and **BlazorLeptonXThemeBundles** classes. + ```csharp + @using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components + @using Volo.Abp.AspNetCore.Components.Server.LeptonXTheme.Bundling + ``` + - Then replace script & style bunles as following + ```diff + - + + + ``` + + ```diff + - + + + ``` +{{end}} + +--- + +## Source-Code +You can download the source-code of the LeptonX Theme according to you your ABP License. + +Visit the [LeptonX Source Code](index#source-code) section to download the source-code. + +## Customization +Before starting to customize the theme, you can consider downloading the source code of the theme. You can find the original codes of related components below in the source code. + +### Themes +You can set default theme or add or remove themes via using **LeptonXThemeOptions**. + +- `DefaultStyle`: Defines deffault fallback theme. Default value is **Dim** + ```csharp + Configure(options => + { + options.DefaultStyle = LeptonXStyleNames.Dark; + }); + ``` + +- `Styles`: Defines selectable themes from UI. + + ![lepton-x-selectable-themes](images/selectable-themes.png) + + ```csharp + Configure(options => + { + // Removing existing themes + options.Styles.Remove(LeptonXStyleNames.Light); + + // Adding a new theme + options.Styles.Add("red", + new LeptonXThemeStyle( + LocalizableString.Create("Theme:Red"), + "bi bi-circle-fill")); + }); + + ``` + + > `red.css` and `bootstrap-red.css` have to be added under the `wwwroot/_content/Volo.Abp.AspNetCore.Components.Web.LeptonXTheme/side-menu/css/` folder to switch to your custom theme properly when selected. + > + >_If your layout is **TopMenu**, then you have to add them under the `wwwroot/_content/Volo.Abp.AspNetCore.Components.Web.LeptonXTheme/top-menu/css/` folder._ + + ![leptonx-red](https://user-images.githubusercontent.com/23705418/188801370-5aa646cf-72fd-4365-aa49-aab1152b5b86.gif) + +### LeptonXThemeBlazorOptions +Layout options of Blazor UI can be manageable via using **LeptonXThemeMvcOptions**. + +- `Layout`: Layout of main application. Default value is `LeptonXMvcLayouts.SideMenu` + + ```csharp + Configure(options => + { + options.Layout = LeptonXBlazorLayouts.SideMenu; + // Or your custom implemented layout: + options.Layout = typeof(MyCustomLayoutComponent); + }); + ``` + +- `MobileMenuSelector`: Defines items to be displayed at mobile menu. Default value is first 2 items from main menu items. + + ![leptonx-mobile-menu-preview](images/mobile-menu-preview.png) + + ```csharp + Configure(options => + { + options.MobileMenuSelector = items => items.Where(x => x.MenuItem.Name == "MyProjectName.Home" || x.MenuItem.Name == "MyProjectName.Dashboard"); + }); + ``` + + +{{if UI == "Blazor"}} + +#### Updating Bundles on Layout Changes +Layout changes requires bundling and restarting the application. Before bundling, you have to add your layout to `appsettings.json`. Make sure `AbpCli:Bundle:Paramters` has `LeptonXTheme.Layout` key with your layout name. Available values are `side-menu` & `top-menu`. + + +_You can add the following section to root level of your appsettings.json file if not added._ +```json + "AbpCli": { + "Bundle": { + "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ + "Name": "global", + "Parameters": { + "LeptonXTheme.Layout": "top-menu" /* Options: side-menu, top-menu */ + } + } + } +``` + +Then you can run bundling command with ABP Cli + +```bash +abp bundle +``` + +{{end}} + + +## Layouts + +**LeptonX** offers two **ready-made layouts** for your web application. One of them is **placed** with the **menu items** on the **top** and the other with the **menu items** on the **sides**. + +### Top Menu Layout + +![Top menu layout](images/leptonx-top-menu-layout.png) + +### Side Menu Layout + +![Side menu layout](images/leptonx-side-menu-layout.png) + +You can override layouts by following the steps below: + +* Create a razor page, like `MySideMenuLayout.razor`, in your blazor application as shown below: + +```html +@using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout; +@using Volo.Abp.DependencyInjection + +@inherits SideMenuLayout +@attribute [ExposeServices(typeof(SideMenuLayout))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MySideMenuLayout.razor.cs`, in your blazor application as shown below: + +```csharp + +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(SideMenuLayout))] + [Dependency(ReplaceServices = true)] + public partial class MySideMenuLayout + { + public string Name = "My Top Layout"; + } +} +``` + +> Don't forget to remove repeated attributes from the razor page! + +--- + + + +## Common Components + +Commonly used components in all layouts. + +### Breadcrumb + +![Breadcrumb](images/leptonx-breadcrumb.png) + +Breadcrumbs can be customized by using the `PageLayout` service. See the [PageLayout - BreadCrumb](../../framework/ui/blazor/page-layout.md#breadcrumbs) section for more information. + +If you need to replace the component, you can follow the steps below. + +* Create a razor page, like `MyBreadcrumbs.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +@using Volo.Abp.DependencyInjection + +@inherits Breadcrumbs +@attribute [ExposeServices(typeof(Breadcrumbs))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyBreadcrumbs.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(Breadcrumbs))] + [Dependency(ReplaceServices = true)] + public partial class MyBreadcrumbsComponent + { + public string Name = "My Breadcrumbs"; + } +} +``` + +### Content Toolbar + +![Content toolbar](images/leptonx-blazor-content-toolbar.png) + +* Create a razor page, like `MyContentToolbar.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +@using Volo.Abp.DependencyInjection + +@inherits ContentToolbar +@attribute [ExposeServices(typeof(ContentToolbar))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyContentToolbar.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(ContentToolbar))] + [Dependency(ReplaceServices = true)] + public partial class MyContentToolbarComponent + { + public string Name = "My Content Toolbar"; + } +} +``` + +### General Settings + +![General settings](images/leptonx-general-settings.png) + +* Create a razor page, like `MyGeneralSettings.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +@using Volo.Abp.DependencyInjection + +@inherits GeneralSettings +@attribute [ExposeServices(typeof(GeneralSettings))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyGeneralSettings.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(GeneralSettings))] + [Dependency(ReplaceServices = true)] + public partial class MyGeneralSettings + { + public string Name = "My General Settings"; + } +} +``` + +### Mobile General Settings + +![Mobile general settings](images/leptonx-mobile-general-settings.png) + +* Create a razor page, like `MyMobileGeneralSettings.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +@using Volo.Abp.DependencyInjection + +@inherits MobileGeneralSettings +@attribute [ExposeServices(typeof(MobileGeneralSettings))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobileGeneralSettings.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MobileGeneralSettings))] + [Dependency(ReplaceServices = true)] + public partial class MyMobileGeneralSettings + { + public string Name = "My Mobile General Settings"; + } +} +``` + +--- + +## Side Menu Components + +Components used in the side menu layout. + +### Main Menu + +![Sidebar Main menu](images/leptonx-blazor-sidemenu-main-menu.png) + +* Create a razor page, like `MyMainMenu.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MainMenu +@attribute [ExposeServices(typeof(MainMenu))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenu.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainMenu))] + [Dependency(ReplaceServices = true)] + public partial class MyMainMenuComponent + { + public string Name = "My Main Menu"; + } +} +``` + +### Main Menu Item + +* Create a razor page, like `MyMainMenuItem.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MainMenuItem +@attribute [ExposeServices(typeof(MainMenuItem))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenuItem.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainMenu))] + [Dependency(ReplaceServices = true)] + public partial class MyMainMenuItemComponent + { + public string Name = "My Main Menu Item"; + } +} +``` + +### Mobile Navbar + +![Mobile navbar](images/leptonx-sidemenu-mobile-navbar.png) + +* Create a razor page, like `MyMobileNavbar.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MobileNavbar +@attribute [ExposeServices(typeof(MobileNavbar))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobileNavbar.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MobileNavbar))] + [Dependency(ReplaceServices = true)] + public partial class MyMobileNavbar + { + public string Name = "My Mobile Navbar"; + } +} +``` + +### Main Header + +![Main Header](images/leptonx-blazor-sidemenu-main-header.png) + +* Create a razor page, like `MyMainHeader.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeader +@attribute [ExposeServices(typeof(MainHeader))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeader.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeader))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeader + { + public string Name = "My Main Header"; + } +} +``` + +### Main Header Branding + +![Main Header Branding](images/leptonx-blazor-side-menu-main-header-branding.png) + +Application branding can be customized with the `IBrandingProvider`. See the [Branding](../../framework/ui/blazor/branding.md) section for more information. + +If you need to replace the component, you can follow the steps below. + +* Create a razor page, like `MyMainHeaderBranding.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeaderBranding +@attribute [ExposeServices(typeof(MainHeaderBranding))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeaderBranding.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeaderBranding))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeaderBranding + { + public string Name = "My Main Header Branding"; + } +} +``` + +### Main Header Toolbar + +![Main Header Toolbar](images/leptonx-blazor-sidemenu-main-toolbar.png) + +The main toolbar can be managed by using the `ToolbarContributor` classes. See the [Toolbars](../../framework/ui/blazor/toolbars.md) section for more information. + +If you need to replace the component, you can follow the steps below. + +* Create a razor page, like `MyMainHeaderToolbar.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeaderToolbar +@attribute [ExposeServices(typeof(MainHeaderToolbar))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeaderToolbar.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeaderToolbar))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeaderToolbar + { + public string Name = "My Main Header Toolbar"; + } +} +``` + +--- + +## Top Menu Components + +Components used in the top menu layout. + +### Main Menu + +![Sidebar Main menu](images/leptonx-blazor-topmenu-main-menu.png) + +* Create a razor page, like `MyMainMenu.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MainMenu +@attribute [ExposeServices(typeof(MainMenu))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenu.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainMenu))] + [Dependency(ReplaceServices = true)] + public partial class MyMainMenuComponent + { + public string Name = "My Main Menu"; + } +} +``` + +### Main Menu Item + +* Create a razor page, like `MyMainMenuItem.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MainMenuItem +@attribute [ExposeServices(typeof(MainMenuItem))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainMenuItem.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainMenu))] + [Dependency(ReplaceServices = true)] + public partial class MyMainMenuItemComponent + { + public string Name = "My Main Menu Item"; + } +} +``` + +### Mobile Navbar + +![Mobile navbar](images/leptonx-sidemenu-mobile-navbar.png) + +* Create a razor page, like `MyMobileNavbar.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +@using Volo.Abp.DependencyInjection + +@inherits MobileNavbar +@attribute [ExposeServices(typeof(MobileNavbar))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMobileNavbar.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.Navigation; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MobileNavbar))] + [Dependency(ReplaceServices = true)] + public partial class MyMobileNavbar + { + public string Name = "My Mobile Navbar"; + } +} +``` + +### Main Header + +![Main Header](images/leptonx-blazor-topmenu-main-header.png) + +* Create a razor page, like `MyMainHeader.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeader +@attribute [ExposeServices(typeof(MainHeader))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeader.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeader))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeader + { + public string Name = "My Main Header"; + } +} +``` + +### Main Header Branding + +![Main Header Branding](images/leptonx-blazor-topmenu-main-header-branding.png) + +Application branding can be customized with the `IBrandingProvider`. See the [Branding](../../framework/ui/blazor/branding.md) section for more information. + +* Create a razor page, like `MyMainHeaderBranding.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeaderBranding +@attribute [ExposeServices(typeof(MainHeaderBranding))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeaderBranding.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeaderBranding))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeaderBranding + { + public string Name = "My Main Header Branding"; + } +} +``` + +### Main Header Toolbar + +![Main Header Toolbar](images/leptonx-blazor-topmenu-main-header-toolbar.png) + +The main toolbar can be managed by using the `ToolbarContributor` classes. See the [Toolbars](../../framework/ui/blazor/toolbars.md) section for more information. + +If you need to replace the component, you can follow the steps below. + +* Create a razor page, like `MyMainHeaderToolbar.razor`, in your blazor application as shown below: + +```html +@Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader +@using Volo.Abp.DependencyInjection + +@inherits MainHeaderToolbar +@attribute [ExposeServices(typeof(MainHeaderToolbar))] +@attribute [Dependency(ReplaceServices = true)] + +@Name +``` + +* If you prefer to use a code-behind file for the C# code of your component, create a razor component, like `MyMainHeaderToolbar.razor.cs`, in your blazor application as shown below: +```csharp +using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.TopMenu.MainHeader; +using Volo.Abp.DependencyInjection; + +namespace LeptonXLite.DemoApp.Blazor.MyComponents +{ + [ExposeServices(typeof(MainHeaderToolbar))] + [Dependency(ReplaceServices = true)] + public partial class MyMainHeaderToolbar + { + public string Name = "My Main Header Toolbar"; + } +} +``` diff --git a/docs/en/ui-themes/lepton-x/comparison-obsolete.md b/docs/en/ui-themes/lepton-x/comparison-obsolete.md new file mode 100644 index 0000000000..2870be0a8e --- /dev/null +++ b/docs/en/ui-themes/lepton-x/comparison-obsolete.md @@ -0,0 +1,31 @@ +## Feature Set + +### Frontend Choices + +LeptonX comes in all shapes and sizes. You can use it for free or commercially with or without the ABP. + +| Type | Standalone (Lite) | Standalone (Commercial) | ABP (Open-Source) | ABP (Commercial) | +| ------- | ----------------- | ----------------------- | ----------------- | ---------------- | +| HTML | ✅ | ✅ | ✅ (as MVC) | ✅ (as MVC) | +| Angular | ✅ | ✅ | ✅ | ✅ | +| Blazor | ❌ | ❌ | ✅ | ✅ | +| MVC | ❌ | ❌ | ✅ | ✅ | + + +### Angular And HTML +| Features | Lite | Commercial | +| ------------------- | ---- | ---------- | +| Context Menu | ❌ | ✅ | +| Theme Builder | ❌ | ✅ | +| Mobile Nav Menu | ❌ | ✅ | +| Toolbar | ❌ | ✅ | +| [Sidebar](#sidebar) | ✅ | ✅ | +| Breadcrumb | ✅ | ✅ | + + + + +| Features | Lite | Commercial | +| ---------------- | ---- | ---------- | +| Filter Menu | ❌ | ✅ | +| Custom Scrollbar | ❌ | ✅ | diff --git a/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md new file mode 100644 index 0000000000..f68f3bd90f --- /dev/null +++ b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md @@ -0,0 +1,83 @@ +# Custom layout usage with Lepton X components + + +First, The custom layout component should be created and implemented for the Angular application. +Related content can be found in the [Component Replacement Document](../../framework/ui/angular/component-replacement.md#how-to-replace-a-layout) + + + +After creating a custom layout, these imports should be imported in the `app.module.ts` file because the modules contain definitions of the Lepton X components. + + +```javascript +// app.module.ts +import { LpxSideMenuLayoutModule } from '@volosoft/ngx-lepton-x/layouts'; +import { LpxResponsiveModule } from '@volo/ngx-lepton-x.core';// optional. Only, if you are using lpxResponsive directive + + @NgModule({ + //... removed for clearity + imports: [ + //... removed for clearity + LpxSideMenuLayoutModule, + LpxResponsiveModule // <-- Optional + ] +}) +export class AppModule {} + +``` + +Here is the simplified version of the `side-menu-layout.ts` file. Only the ABP Component Replacement code has been removed. + + +```html + + + + +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + + +
    + + + +
    +
    +
    + +``` + +Add this code to your application template and customize it as desired. diff --git a/docs/en/ui-themes/lepton-x/images/angular-footer.png b/docs/en/ui-themes/lepton-x/images/angular-footer.png new file mode 100644 index 0000000000..a16c733961 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/angular-footer.png differ diff --git a/docs/en/ui-themes/lepton-x/images/angular-navbar-routes.png b/docs/en/ui-themes/lepton-x/images/angular-navbar-routes.png new file mode 100644 index 0000000000..0a2440f57e Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/angular-navbar-routes.png differ diff --git a/docs/en/ui-themes/lepton-x/images/angular-navbar.png b/docs/en/ui-themes/lepton-x/images/angular-navbar.png new file mode 100644 index 0000000000..8fe99b16d3 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/angular-navbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/angular-toolbar.png b/docs/en/ui-themes/lepton-x/images/angular-toolbar.png new file mode 100644 index 0000000000..91250d05d7 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/angular-toolbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/current-user-image.png b/docs/en/ui-themes/lepton-x/images/current-user-image.png new file mode 100644 index 0000000000..19166953e4 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/current-user-image.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-account-layout-new.png b/docs/en/ui-themes/lepton-x/images/leptonx-account-layout-new.png new file mode 100644 index 0000000000..90ae483bec Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-account-layout-new.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-account-layout.png b/docs/en/ui-themes/lepton-x/images/leptonx-account-layout.png new file mode 100644 index 0000000000..d84648c58a Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-account-layout.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-content-toolbar.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-content-toolbar.png new file mode 100644 index 0000000000..4307786499 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-content-toolbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-side-menu-main-header-branding.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-side-menu-main-header-branding.png new file mode 100644 index 0000000000..7b2b048470 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-side-menu-main-header-branding.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-header.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-header.png new file mode 100644 index 0000000000..113cbfbb10 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-header.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-menu.png new file mode 100644 index 0000000000..499aa3276f Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-toolbar.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-toolbar.png new file mode 100644 index 0000000000..146d71e5b3 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-sidemenu-main-toolbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-branding.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-branding.png new file mode 100644 index 0000000000..9b27e73c9f Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-branding.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-toolbar.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-toolbar.png new file mode 100644 index 0000000000..10956ce6b8 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header-toolbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header.png new file mode 100644 index 0000000000..2319ea0163 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-header.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-menu.png new file mode 100644 index 0000000000..7071d51174 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-blazor-topmenu-main-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-breadcrumb.png b/docs/en/ui-themes/lepton-x/images/leptonx-breadcrumb.png new file mode 100644 index 0000000000..94be797c60 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-breadcrumb.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-content-title.png b/docs/en/ui-themes/lepton-x/images/leptonx-content-title.png new file mode 100644 index 0000000000..35cbe1f73f Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-content-title.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-general-settings.png b/docs/en/ui-themes/lepton-x/images/leptonx-general-settings.png new file mode 100644 index 0000000000..5234a3c6ba Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-general-settings.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-main-header-branding.png b/docs/en/ui-themes/lepton-x/images/leptonx-main-header-branding.png new file mode 100644 index 0000000000..6128cf3b73 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-main-header-branding.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-mobile-general-settings.png b/docs/en/ui-themes/lepton-x/images/leptonx-mobile-general-settings.png new file mode 100644 index 0000000000..83079bc5d0 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-mobile-general-settings.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-page-alerts.png b/docs/en/ui-themes/lepton-x/images/leptonx-page-alerts.png new file mode 100644 index 0000000000..94fa75f43b Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-page-alerts.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-side-menu-layout.png b/docs/en/ui-themes/lepton-x/images/leptonx-side-menu-layout.png new file mode 100644 index 0000000000..f4742fa07a Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-side-menu-layout.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-sidebar-main-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-sidebar-main-menu.png new file mode 100644 index 0000000000..86754badcc Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-sidebar-main-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-language-switch.png b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-language-switch.png new file mode 100644 index 0000000000..92313caf09 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-language-switch.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-mobile-navbar.png b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-mobile-navbar.png new file mode 100644 index 0000000000..f7d1e9069f Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-mobile-navbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-user-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-user-menu.png new file mode 100644 index 0000000000..fffb9e2e17 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-sidemenu-user-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-top-menu-layout.png b/docs/en/ui-themes/lepton-x/images/leptonx-top-menu-layout.png new file mode 100644 index 0000000000..b8c185f367 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-top-menu-layout.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header-toolbar.png b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header-toolbar.png new file mode 100644 index 0000000000..064de5fc9b Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header-toolbar.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header.png b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header.png new file mode 100644 index 0000000000..3a4ab770d5 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-header.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-menu.png new file mode 100644 index 0000000000..a6d9b7bdca Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-main-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-user-menu.png b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-user-menu.png new file mode 100644 index 0000000000..2a09812003 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/leptonx-topmenu-user-menu.png differ diff --git a/docs/en/ui-themes/lepton-x/images/mobile-menu-preview.png b/docs/en/ui-themes/lepton-x/images/mobile-menu-preview.png new file mode 100644 index 0000000000..4fa31b2310 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/mobile-menu-preview.png differ diff --git a/docs/en/ui-themes/lepton-x/images/selectable-themes.png b/docs/en/ui-themes/lepton-x/images/selectable-themes.png new file mode 100644 index 0000000000..a0ac4319e0 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/selectable-themes.png differ diff --git a/docs/en/ui-themes/lepton-x/index.md b/docs/en/ui-themes/lepton-x/index.md new file mode 100644 index 0000000000..5fc1ee4fb1 --- /dev/null +++ b/docs/en/ui-themes/lepton-x/index.md @@ -0,0 +1,109 @@ +# LeptonX Theme Module + +> You must have an ABP Team or a higher license to use this theme. + +The LeptonX Theme is a professional theme for the ABP. + +* Check out the website of LeptonX on https://leptontheme.com/. +* Check out the live demo to see it in action. https://x.leptontheme.com/. + +## Highlights + +* Built on the [Bootstrap 5](https://getbootstrap.com) library. +* 100% compatible with [Bootstrap 5](https://getbootstrap.com) HTML structure and CSS classes. +* Responsive & mobile-compatible. +* Provides different style like Dim, Dark and Light. + +A screenshot from the light style of the theme: + +![lepton-theme-light](../../images/lepton-x-theme-light.png) + +> [See all the theme styles and create a demo to see it in action](https://abp.io/themes). + +## How to Install + +LeptonX Theme module is pre-installed in [the startup templates](../../get-started). So, no need to manually install it. + +## Packages + +This module follows the [module development best practices guide](../../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. + +### NuGet Packages + +* Volo.Abp.AspNetCore.Components.Server.LeptonXTheme +* Volo.Abp.AspNetCore.Components.Web.LeptonXTheme +* Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme +* Volo.Abp.AspNetCore.LeptonX.Shared +* Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX + +### NPM Packages + +* @volo/abp.aspnetcore.mvc.ui.theme.leptonx +* @volo/aspnetcore.components.server.leptonxtheme + +#### Angular + +* @volo/abp.ng.lepton-x.core +* @volo/ngx-lepton-x.core +* @volosoft/abp.ng.theme.lepton-x +* @volosoft/ngx-lepton-x + +## User Interface + +LeptonX Theme module doesn't provide any UI pages. It just changes the existing UI pages of an application. Here are some sample pages: + +#### Login page + +![lepton-theme-module-login-page](../../images/lepton-x-theme-module-login-page.png) + +#### Languages Page + +![lepton-theme-module-languages-page](../../images/lepton-x-theme-module-languages-page.png) + +### Pages + +This module doesn't define any pages. + +#### Identity Module Settings UI + +LeptonX Theme module adds a new tab to the Settings page to customize the behavior on runtime. + +![lepton-theme-module-settings-page](../../images/lepton-x-theme-module-settings-page.png) + +## Internals + +### Settings + +LeptonX Module doesn't define any settings. + +### Permissions + +LeptonX Module doesn't define any permissions. + +### Source code + +You can use the following CLI command to download the source-code: + +```bash +abp get-source Volo.Abp.LeptonXTheme.Pro +``` + +If you want to download the source code of the preview version, you can use the following command: + +```bash +abp get-source Volo.Abp.LeptonXTheme.Pro --preview +``` + +> You can download the source code of a certain version by using the `--version` parameter. See the [ABP CLI documentation](../../cli/index#get-source) for other possible options. + +ABP customers can also download the source code of the [https://x.leptontheme.com/](https://x.leptontheme.com/) from [https://abp.io/api/download/samples/leptonx-demo](https://abp.io/api/download/samples/leptonx-demo). + +In order to understand structure of LeptonX's source code and build it from its source code, you can check [LeptonX source code documentation](source-files.md). + +## LeptonX Theme Customization + +You can use the following links to see the customizations for different UI types: + +* [LeptonX Theme: MVC UI](mvc.md) +* [LeptonX Theme: Angular UI](angular.md) +* [LeptonX Theme: Blazor UI](blazor.md) diff --git a/docs/en/ui-themes/lepton-x/mvc.md b/docs/en/ui-themes/lepton-x/mvc.md new file mode 100644 index 0000000000..5e7aab2b7f --- /dev/null +++ b/docs/en/ui-themes/lepton-x/mvc.md @@ -0,0 +1,300 @@ +# LeptonX MVC UI +LeptonX theme is implemented and ready to use with ABP. No custom implementation is needed for Razor Pages. + +## Installation + +- Install the package to your **Web** project with the CLI. +```bash +abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX +``` +- Remove the `Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton` and the `Volo.Abp.LeptonTheme.Management.Web` references from the project since it's not necessary after switching to LeptonX. + +- Make sure the old theme is removed and LeptonX is added in your Module class. + +```diff +[DependsOn( + // ... + // remove the following lines +- typeof(LeptonThemeManagementWebModule), +- typeof(AbpAspNetCoreMvcUiLeptonThemeModule), + + // add the line below ++ typeof(AbpAspNetCoreMvcUiLeptonXThemeModule), +)] +``` + +- Replace `LeptonThemeBundles` with `LeptonXThemeBundles` in AbpBundlingOptions. + +```csharp + options.StyleBundles.Configure( + LeptonXThemeBundles.Styles.Global, // 👈 Here + bundle => + { + bundle.AddFiles("/global-styles.css"); + } + ); +``` + +## Source-Code +You can download the source code of the LeptonX Theme according to your ABP License. + +Visit the [LeptonX Source Code](index#source-code) section to download the source-code. + +## Customization + +Before starting to customize the theme, you can consider downloading the source code of the theme. You can find the original codes of the related components below in the source code. + +--- + +### Appearance +You can set a default theme, add or remove appearance styles by using **LeptonXThemeOptions**. + +- `DefaultStyle`: Defines the default fallback theme. The default value is **Dim** + +```csharp +Configure(options => +{ + options.DefaultStyle = LeptonXStyleNames.Dark; +}); +``` + +- `Styles`: Defines the selectable appearances from UI. + +![lepton-x-selectable-themes](images/selectable-themes.png) + +```csharp +Configure(options => +{ + // Removing the existing styles + options.Styles.Remove(LeptonXStyleNames.Light); + + // Adding a new style + options.Styles.Add("red", + new LeptonXThemeStyle( + LocalizableString.Create("Theme:Red"), + "bi bi-circle-fill")); +}); + +``` + +> `red.css` and `bootstrap-red.css` have to be added under **wwwroot/Themes/LeptonX/Global/side-menu/css/** folder for switching to your custom theme properly when selected. +> +> If your layout is **TopMenu**, then you have to add them under the **wwwroot/Themes/LeptonX/Global/top-menu/css/** folder. + +--- + +### LeptonXThemeMvcOptions +Layout options of the MVC Razor Pages UI can be manageable by using **LeptonXThemeMvcOptions**. + +- `ApplicationLayout`: Layout of the main application. The default value is `LeptonXMvcLayouts.SideMenu` + + ```csharp + Configure(options => + { + options.ApplicationLayout = LeptonXMvcLayouts.SideMenu; + // Or your custom implemented layout: + options.ApplicationLayout = "~/Shared/_Layout.cshtml"; + }); + ``` + +- `MobileMenuSelector`: Defines items to be displayed at the mobile menu. The default value is the first 2 items from the main menu items. + + ![leptonx-mobile-menu-preview](images/mobile-menu-preview.png) + + ```csharp + Configure(options => + { + options.MobileMenuSelector = items => items.Where(x => x.MenuItem.Name == "MyProjectName.Home" || x.MenuItem.Name == "MyProjectName.Dashboard"); + }); + ``` + +### Layouts + +**LeptonX** offers two **ready-made layouts** for your web application. One of them is **placed** with the **menu items** on the **top** and the other with the **menu items** on the **sides**. + +### Top Menu Layout + +![Top menu layout](images/leptonx-top-menu-layout.png) + +### Side Menu Layout + +![Side menu layout](images/leptonx-side-menu-layout.png) + +> The layouts are defined in the **Themes/LeptonX/Layouts/Application** folder and you can **override it** by creating the file (**SideMenuLayout.cshtml** or **TopMenuLayout.cshtml**) with the **same name** and under the **same folder**. + +> There are **partial views** that are defined in the **Themes/LeptonX/Layouts/Application** folder. You can **override** them by creating the file with the **same name** and under the **same folder**. + +> - **_Footer.cshtml** +> - **_Sidebar.cshtml** +> - **_Toolbar.cshtml** + +### Account Layout + +![Account layout](images/leptonx-account-layout-new.png) + +> The **account layout** is defined in the **Themes/LeptonX/Layouts/Account** folder and you can **override** it by creating the file (**Default.cshtml**) with the **same name** and under the **same folder**. There is a **partial view**, its name is **Footer.cshtml**. You can **override** it by **following** the **same way**. + +--- + +## Components + +Abp **helps** you make **highly customizable UI**. You can easily **customize** your themes to fit your needs. **The Virtual File System** makes it possible to **manage files** that **do not physically** exist on the **file system** (disk). It's mainly used to embed **(js, css, image..)** files into assemblies and **use them like** physical files at runtime. + +An application (or another module) can **override** a **virtual file of a module** just like placing a file with the **same name** and **extension** into the **same folder** of the **virtual file**. + +LeptonX is built on the [ABP](https://abp.io/), so you can **easily** customize your Asp.Net Core Mvc user interface by following [Abp Mvc UI Customization](../../framework/ui/mvc-razor-pages/customization-user-interface.md). + +## Common Components + +Commonly used components in all layouts. + +### Breadcrumb + +![Breadcrumb](images/leptonx-breadcrumb.png) + +Breadcrumbs can be customized by using the `IPageLayout` service. See [PageLayout - Breadcrumb](https://docs.abp.io/en/abp/7.2/UI%2FAspNetCore%2FPage-Header#breadcrumb) for more information. + +If you need to replace the component, you can follow the steps below. + +- The **breadcrumb component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/Common/BreadCrumb/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +- The **breadcrumb component (C# file)** is defined in the `Themes/LeptonX/Components/Common/BreadCrumb/ContentBreadCrumbViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + + + +### Content Title + +![Content title](images/leptonx-content-title.png) + +Page Titles can be customized by using the `IPageLayout` service. See [PageLayout - Page Title](https://docs.abp.io/en/abp/7.2/UI%2FAspNetCore%2FPage-Header#page-title) for more information. + +If you need to replace the component, you can follow the steps below. + +* The **content title component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/Common/ContentTitle/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **content title component (C# file)** is defined in the `Themes/LeptonX/Components/Common/ContentTitle/ContentTitleViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + + +### General Settings + +![General settings](images/leptonx-general-settings.png) + +General Settings can be replaced with following files. + +* The **general settings component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/Common/GeneralSettings/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **general settings component (C# file)** is defined in the `Themes/LeptonX/Components/Common/GeneralSettings/GeneralSettingsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + + +### Main Header Branding + +![Main header branding](images/leptonx-main-header-branding.png) + +Application name and logo can be customized by using the `IBrandingProvider` service. See [Razor Pages: Branding](../../framework/ui/mvc-razor-pages/branding.md) for more information. + +If you need to replace the component, you can follow the steps below. + +* The **main header branding component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/Common/MainHeaderBranding/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **main header branding component (C# file)** is defined in the `Themes/LeptonX/Components/Common/MainHeaderBranding/MainHeaderBrandingViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Mobile General Settings + +![Mobile general settings](images/leptonx-mobile-general-settings.png) + +* The **mobile general settings component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/Common/MobileGeneralSettings/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **mobile general settings component (C# file)** is defined in the `Themes/LeptonX/Components/Common/MobileGeneralSettings/MobileGeneralSettingsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Page Alerts + +![Page alerts](images/leptonx-page-alerts.png) + +* The **page alerts component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/PageAlerts/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **page alerts component (C# file)** is defined in the `Themes/LeptonX/Components/PageAlerts/PageAlertsViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +--- + +## Side Menu Components + +Components used in the side menu layout. + +### Main Menu + +![Sidebar Main menu](images/leptonx-sidebar-main-menu.png) + +* The **main menu component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/SideMenu/MainMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **main menu component (C# file)** is defined in the `Themes/LeptonX/Components/SideMenu/MainMenu/MainMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +> The **main menu component** uses **partial view** to render the **menu items**. The **partial view** is defined in the `Themes/LeptonX/Components/SideMenu/MainMenu/_MenuItem.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Mobile Navbar + +![Mobile navbar](images/leptonx-sidemenu-mobile-navbar.png) + +* The **mobile navbar component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/SideMenu/MobileNavbar/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **mobile navbar component (C# file)** is defined in the `Themes/LeptonX/Components/SideMenu/MobileNavbar/MobileNavbarViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Language Switch + +![Language switch](images/leptonx-sidemenu-language-switch.png) + +* The **language switch component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/SideMenu/Toolbar/LanguageSwitch/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **language switch component (C# file)** is defined in the `Themes/LeptonX/Components/SideMenu/Toolbar/LanguageSwitch/LanguageSwitchViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### User Menu + +![User menu](images/leptonx-sidemenu-user-menu.png) + +* The **user menu component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/SideMenu/Toolbar/UserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **user menu component (C# file)** is defined in the `Themes/LeptonX/Components/SideMenu/Toolbar/UserMenu/UserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +> LeptonX implements the ABP Menu [ABP Naviation/Menus](../../framework/ui/mvc-razor-pages/navigation-menu.md#standard-menus). So you can check it out to learn how to add/remove menu items to the user menu. + +--- + +## Top Menu Components + +Components used in the top menu layout. + +### Main Header + +![Main header](images/leptonx-topmenu-main-header.png) + +* The **main header component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainHeader/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **main header component (C# file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainHeader/MainHeaderViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### Main Header Toolbar + +![Main header toolbar](images/leptonx-topmenu-main-header-toolbar.png) + +* The **main header toolbar component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainHeaderToolbar/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **main header toolbar component (C# file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainHeaderToolbar/MainHeaderToolbarViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + + +> Before replacing the main header toolbar component, make a visit to the [ABP Toolbars](../../framework/ui/mvc-razor-pages/toolbars.md) documentation to learn how to add/remove menu items to the main header toolbar. + +### Main Menu + +![Main menu](images/leptonx-topmenu-main-menu.png) + +* The **main menu component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **main menu component (C# file)** is defined in the `Themes/LeptonX/Components/TopMenu/MainMenu/MainMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +### User Menu + +![User menu](images/leptonx-topmenu-user-menu.png) + +* The **user menu component page (.cshtml file)** is defined in the `Themes/LeptonX/Components/TopMenu/UserMenu/Default.cshtml` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +* The **user menu component (C# file)** is defined in the `Themes/LeptonX/Components/TopMenu/UserMenu/UserMenuViewComponent.cs` file and you can **override it** by creating a file with the **same name** and **under** the **same folder**. + +> Before replacing the user menu component, make a visit to the [ABP Naviation/Menus](../../framework/ui/mvc-razor-pages/navigation-menu.md#standard-menus) documentation to learn how to add/remove menu items to the user menu. diff --git a/docs/en/ui-themes/lepton-x/source-files.md b/docs/en/ui-themes/lepton-x/source-files.md new file mode 100644 index 0000000000..cd980cb343 --- /dev/null +++ b/docs/en/ui-themes/lepton-x/source-files.md @@ -0,0 +1,117 @@ +# Lepton X SCSS Files + +The LeptonX scss file structure is divided into different bundles. The purpose of this is to exclude unnecessary styles (for example, the top menu layer styles when using side menu layout) in the project. To reload only relevant files on theme changes. + +## Bundle Files + +Folders containing bundle files in source files do not contain underscores. Those containing underscores are files used by the bundle files. + +**Theme bundle files**: dim.scss dark.scss light.scss placed under themes directory. + +**Bootstrap bundle files**: dark/booststrap-dark.scss, light/booststrap-light.scss and dim/booststrap-dim.scss placed under the frameworks/bootstrap directory. + +**Layout bundle files**: side-menu/layout-bundle.scss and top-menu/layout-bundle.scss placed under the pro directory + +**Ui spescific bundles**: The Angular UI bundle is pro/libraries/ng-bundle.scss, the Blazor UI bundle is pro/libraries/blazor-bundle.scss, the MVC UI bundle is pro/libraries/js-bundle.scss + +**ABP bundle file**: Styles of ABP UI elements is pro/abp/abp-bundle.scss + +**Font bundle**: pro/libraries/font-bundle.scss + +## Theme Map + +Theme maps are defined in theme color files. The position of the files is listed as following: +dark: \_colors/dark/colors.scss +light: \_colors/light/colors.scss +dim: \_colors/dim/colors.scss + +Possible properties are listed below + +``` +border-color, +brand, +brand-text, +card-bg, +card-title-text-color, +container-active-text, +content-bg, +content-text, +danger, +dark +info, +light +logo, +logo-icon, +logo-reverse, +navbar-active-bg-color +navbar-active-text-color +navbar-color +navbar-text-color +primary, +radius +secondary, +shadow, +success, +text-white, +warning +``` + +## Theme Builder + +The build-theme mixin reads the theme-map and writes its values to the :root selector as CSS variables by using defined builder functions. + +The global-theme-builder converts the rgb colors of specific property values in the theme map + +## Compiling to CSS + +> Please make sure that the dependencies are installed. You can install with the `yarn install` or `npm install` command. + +For building source files please run the command below + +```bash +yarn build +``` + +CSS files will be created in the built folder. + +## Adding new theme bundle to source file + +Create a new file under \_colors/your-theme/colors.scss and replace the content below + +```scss +$theme-map: ( + light: #f0f4f7, + dark: #062a44, + navbar-color: #fff, + navbar-text-color: #445f72, + navbar-active-text-color: #124163, + navbar-active-bg-color: #f3f6f9, +); +``` + +Create a new file \_colors/your-theme/index.scss and paste content below in it + +```scss +@import "colors"; +@import "../common"; +``` + +Create a new file frameworks/bootstrap/your-theme/bootstrap-your-theme.scss and paste content below in it + +```scss +@import "_colors/your-theme"; +@import "../common"; +``` + +Finally create a new file themes/your-theme.scss and paste content below in it + +```scss +@import "_colors/your-theme"; +@import "builders/_builder"; +``` + +## Other Files + +- build.js: Builds scss bundle files to css files.Also creates rtl.css files. +- package.json: Includes dependencies and build command +- postcss.config.js: Used by postcss, needed for ltr to rtl diff --git a/docs/en/ui-themes/lepton/customizing-lepton-theme.md b/docs/en/ui-themes/lepton/customizing-lepton-theme.md new file mode 100644 index 0000000000..c94ccaafce --- /dev/null +++ b/docs/en/ui-themes/lepton/customizing-lepton-theme.md @@ -0,0 +1,247 @@ +# Customize Lepton Theme with Custom CSS + +````json +//[doc-params] +{ + "UI": ["MVC", "Blazor", "BlazorServer", "NG", "MAUIBlazor"] +} +```` +You may want to change certain aspects of your website’s appearance with a custom CSS (Cascading Style Sheet) file. In this document we will show you how to add a custom CSS to your ABP {{UI_Value}} solution to replace the existing Lepton Theme CSS. By adding your custom CSS, you will modify your website’s overall look. + +{{if UI=="NG"}} + +## Adding Custom Style + +There is a `customStyle` boolean configuration in `ThemeLeptonModule`'s `forRoot` method. If this configuration is true, the style selection box is not included in the theme settings form and `ThemeLeptonModule` does not load its own styles. In this case, a custom style file must be added to the styles array in `angular.json` or must be imported by `style.scss`. + +> Only angular project styles can be changed in this way. If the authorization flow is authorization code flow, MVC pages (login, profile, etc) are not affected by this change. + +Custom style implementation can be done with the following steps + +Set `customStyle` property to `true` where is `ThemeLeptonModule` imported with `forRoot` method. + +```javascript +// app.module.ts +ThemeLeptonModule.forRoot({ + customStyle: true +}) +``` + +Import your style file to `src/style.scss` + +```css +/* style.scss */ +import 'your-custom-style'; +``` + +Or add your style file to the `styles` arrays which in `angular.json` file + +```json +// angular.json +{ + // other configurations + "projects": { + "YourProject": { + // other configurations + "architect": { + "build": { + "styles": [ + // other styles + "your-custom-style-file" + ], + }, + }, + "test": { + "options": { + "styles": [ + // other styles + "your-custom-style-file" + ], + } + }, + } + } + } +} +``` + + +## Inserting Custom Content To Lepton Menu + +Lepton menu can take custom content both before and after the menu items displayed. In order to achieve this, pass a component as content through the parameters of `ThemeLeptonModule.forRoot` when you import the module in your root module, i.e. `AppModule`. Let's take a look at some examples. + + +### Placing Custom Content Before & After Menu Items + +First step is to create a component which will serve as the custom content. + +```js +@Component({ + template: ` + + Support Issues +
    + {%{{{ issueCount$ | async }}}%} +
    +
    `, +}) +export class SupportLinkComponent { + issueCount$ = of(26); // dummy count, replace this with an actual service +} + +@NgModule({ + declarations: [SupportLinkComponent], + imports: [CommonModule], +}) +export class SupportLinkModule {} +``` + +Now, pass this component as `contentAfterRoutes` option to `ThemeLeptonModule`. + +```js +@NgModule({ + imports: [ + // other imports are removed for sake of brevity + SupportLinkModule, + ThemeLeptonModule.forRoot({ + contentAfterRoutes: [SupportLinkComponent], + }) + ], +}) +export class AppModule {} +``` + +If you start the dev server, you must see the inserted content as follows: + + + + +Placing the content before menu items is straightforward: Just replace `contentAfterRoutes` with `contentBeforeRoutes`. + + + + + +### Placing a Search Input Before Menu Items + +The Lepton package has a search component designed to work with the routes in the menu. You can simply import the module and pass the component as `contentBeforeRoutes` option to `ThemeLeptonModule`. + +```js +import { MenuSearchComponent, MenuSearchModule } from '@volo/abp.ng.theme.lepton/extensions'; + +@NgModule({ + imports: [ + // other imports are removed for sake of brevity + + MenuSearchModule.forRoot({ + limit: 3 // search result limit (default: Infinity) + }), + ThemeLeptonModule.forRoot({ + contentBeforeRoutes: [MenuSearchComponent], + }) + ], +}) +export class AppModule {} +``` + +Here is how the search input works: + + + +Please note that the **search component** filters only the routes and cannot hide custom + +{{else}} + +> Be aware that custom CSS must contain all the styles that Lepton Theme uses. If you just want to overwrite some styles, you can use {{if UI== "Blazor"}}`global.css`{{else}}`global-styles.css`{{end}} in the `wwwroot` folder of your web project. + + + +## How to Add a Custom CSS? + +### Download Lepton Theme Source-Code + +Download the Lepton Theme source-code to retrieve the existing CSS files. To do this create a new folder named as `LeptonTheme` and run the following ABP CLI command inside the `LeptonTheme` folder: + +```bash +abp get-source Volo.LeptonTheme +``` + + + +### Copy to Your Project + +In the following folder, you will find all the Lepton CSS files. + +```bash +src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton\Themes\Lepton\Global\styles +``` + +The default theme uses `Lepton6.css`. +Create a folder named `styles` under the `wwwroot`folder of your web project. +Copy `Lepton6.css` to the `wwwroot/styles` folder and rename it to `custom.css`. + +### Customize the CSS + +Open `custom.css` and find the `.lp-opened-sidebar` style. +Comment out the existing background style and add the following background style which changes the main menu's background color. + +```css + background: linear-gradient(7deg, #9f3049, #84a21f); +``` + +![lepton-custom-css-result](../../images/lepton-custom-css-customize.png) + + + +### Set the Style Path + +Add the following code to the {{if UI== "MVC"}} `ConfigureServices` in the `YourProjectWebModule.cs` {{end}}{{if UI== "BlazorServer"}} `ConfigureServices` in the `YourProjectBlazorModule` {{end}}{{if UI== "Blazor"}}`wwwroot/appsettings.json` file{{end}}. + +{{if UI== "MVC"}} +```csharp +Configure(options => +{ + options.StylePath = "/styles/custom.css"; +}); +``` +{{end}} + +{{if UI== "BlazorServer"}} +```csharp +Configure<(options => +{ + options.StylePath = "/styles/custom.css"; +}); +``` +{{end}} + +{{if UI== "Blazor"}} + +```json + "AbpCli": { + "Bundle": { + "Mode": "BundleAndMinify", + "Name": "global", + "Parameters": { + "LeptonTheme.StylePath": "styles/custom.css" + } + } + } +``` + +Run the following ABP CLI command to bundle the `custom.css` + +```bash +abp bundle +``` + +{{end}} + + +### Run the Web Project + +The main menu's background color changed. With this approach, you can change your website’s overall look. + +![lepton-custom-css-result](../../images/lepton-custom-css-result.png) + +{{end}} \ No newline at end of file diff --git a/docs/en/ui-themes/lepton/index.md b/docs/en/ui-themes/lepton/index.md new file mode 100644 index 0000000000..8861595d45 --- /dev/null +++ b/docs/en/ui-themes/lepton/index.md @@ -0,0 +1,89 @@ +# Lepton Theme Module + +> You must have an ABP Team or a higher license to use this theme. + +The Lepton Theme is a professional theme for the ABP. + +* Built on the [Bootstrap 5](https://getbootstrap.com) library. +* 100% compatible with [Bootstrap 5](https://getbootstrap.com) HTML structure and CSS classes +* Responsive & mobile-compatible. +* Provides different style like Material, Dark, Light. +* Provides styles for [Datatables](https://datatables.net). + +A screenshot from the light style of the theme: + +![lepton-theme-light](../../images/lepton-theme-light.png) + +> [See all the theme styles and create a demo to see it in action](https://abp.io/themes). + +## How to Install + +Lepton Theme module is pre-installed in [the startup templates](../../get-started). So, no need to manually install it. + +## Packages + +This module follows the [module development best practices guide](https://docs.abp.io/en/abp/latest/Best-Practices/Index) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. + +### NuGet Packages + +* Volo.Abp.LeptonTheme.Management.Domain.Shared +* Volo.Abp.LeptonTheme.Management.Domain +* Volo.Abp.LeptonTheme.Management.Application.Contracts +* Volo.Abp.LeptonTheme.Management.Application +* Volo.Abp.LeptonTheme.Management.HttpApi +* Volo.Abp.LeptonTheme.Management.HttpApi.Client +* Volo.Abp.LeptonTheme.Management.Web +* Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton + +### NPM Packages + +* @volo/abp.ng.theme.lepton + +## User Interface + +Lepton Theme module doesn't provide any UI pages. It just changes the existing UI pages of an application. Here are some sample pages: + +#### Login page + +![lepton-theme-module-login-page](../../images/lepton-theme-module-login-page.png) + +#### Languages Page + +![lepton-theme-module-languages-page](../../images/lepton-theme-module-languages-page.png) + +### Pages + +This module doesn't define any pages. + +#### Identity Module Settings UI + +Lepton Theme module adds a new tab to the Settings page to customize the behavior on runtime. + +![lepton-theme-module-settings-page](../../images/lepton-theme-module-settings-page.png) + +## Internals + +### Settings + +See the `LeptonThemeSettingNames` class members for all settings defined for this module. + +### Permissions + +See the `LeptonThemeManagementPermissions` class members for all permissions defined for this module. + +### Source code + +To download the source-code of the Lepton Theme use the following CLI command + +``` +abp get-source Volo.LeptonTheme +``` + +## Lepton Theme Customization + +Checkout how to customize the Lepton Theme to change your website’s overall look. + +* [Customizing Lepton Theme for Angular](customizing-lepton-theme.md?UI=NG) +* [Customizing Lepton Theme for MVC](customizing-lepton-theme.md?UI=MVC) +* [Customizing Lepton Theme for Blazor](customizing-lepton-theme.md?UI=Blazor) +* [Customizing Lepton Theme for Blazor Server](customizing-lepton-theme.md?UI=BlazorServer) \ No newline at end of file diff --git a/docs/es/Getting-Started-AspNetCore-Application.md b/docs/es/Getting-Started-AspNetCore-Application.md deleted file mode 100644 index c4d124d827..0000000000 --- a/docs/es/Getting-Started-AspNetCore-Application.md +++ /dev/null @@ -1,140 +0,0 @@ -# Empezando con ABP y una Aplicacion AspNet Core MVC Web - -Este tutorial explica como empezar una aplicacion ABP desde cero usando las dependencias minimas. Uno generalmente desea -empezar con la **[plantilla de inicio](Getting-Started-AspNetCore-MVC-Template.md)**. - -## Crea un Proyecto Nuevo - -1. Crea una Aplicacion Web AspNet Core nueva usando Visual Studio 2022 (17.0.0+): - -![](images/create-new-aspnet-core-application-v2.png) - -2. Configura el nuevo proyecto: - -![](images/select-empty-web-application-v2.png) - -3. Presione el boton Create: - -![create-aspnet-core-application](images/create-aspnet-core-application.png) - -## Instale el paquete Volo.Abp.AspNetCore.Mvc - -Volo.Abp.AspNetCore.Mvc es el paquete de integracion con AspNet Core MVC para ABP. Siendo asi, instalalo en su proyecto: - -```` -Install-Package Volo.Abp.AspNetCore.Mvc -```` - -## Crea el primer modulo ABP - -ABP es un marco de referencia modular y require una clase de **inicio (raíz) tipo modulo** derivada de ``AbpModule``: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - // Configura la canalización de peticiones HTTP. - if (env.IsDevelopment()) - { - app.UseExceptionHandler("/Error"); - // El valor por defecto de HSTS es 30 dias. Debes cambiar esto en ambientes productivos. Referencia https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseConfiguredEndpoints(); - } - } -} -```` - -``AppModule`` es un buen nombre para el modulo de inicio de una aplicacion. - -Los paquetes de ABP definen clases de tipo modulo y cada modulo puede depender de otro. -En el codigo anterior, el ``AppModule`` depende de el modulo ``AbpAspNetCoreMvcModule`` (definido por el paquete [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)). Es comun agregar el atributo ``DependsOn`` despues de instalar un paquete ABP nuevo. - -En vez de la clase de inicion Startup, estamos configurando una canalizacion de ASP.NET Core en este modulo. - -## La clase Program - -El proximo paso es modificar la clase Program para integrate el sistema de modulos ABP: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -await builder.Services.AddApplicationAsync(); - -var app = builder.Build(); - -await app.InitializeApplicationAsync(); -await app.RunAsync(); -```` - -``builder.Services.AddApplicationAsync();`` Agrega todos los servicios definidos en todos los modulos empezando desde ``AppModule``. - -``app.InitializeApplicationAsync()`` inicializa y empieza la aplicacion. - -## Ejecutar la Aplicación - -Es todo! Ejecuta la aplicación, debe funcionar como esperado. - -## Uso de Autofac como Marco de Inyección de Dependencia - -Mientras el sistema de Inyección de Dependencia de ASP.NET Core es suficiente para requerimientos basico, [Autofac](https://autofac.org/) proporciona características avanzadas como Inyección de Propiedades e Intercepcion de Metodos, los cuales son necesarios para que ABP pueda llevar a cabo funciones avanzadas. - -El acto de remplazar el sistema DI de ASP.NET Core por Autofac e integrarlo con ABP es facil. - -1. Instala el paquete [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) - -```` -Install-Package Volo.Abp.Autofac -```` - -2. Agrega la dependencia sobre el modulo ``AbpAutofacModule`` - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] //Agrega la dependencia sobre el modulo ABP Autofac -public class AppModule : AbpModule -{ - ... -} -```` - -3. Actualiza `Program.cs` para que use Autofac: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -builder.Host.UseAutofac(); //Agrega esta linea - -await builder.Services.AddApplicationAsync(); - -var app = builder.Build(); - -await app.InitializeApplicationAsync(); -await app.RunAsync(); -```` - -## Codigo fuente - - Obten el codigo fuente del ejemplo creado en este tutorial de [aqui](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). diff --git a/docs/es/Index.md b/docs/es/Index.md deleted file mode 100644 index d05a4032da..0000000000 --- a/docs/es/Index.md +++ /dev/null @@ -1,31 +0,0 @@ -# Documentación de ABP - -ABP es un **marco de desarrollo de código abierto** enfocado en el desarrollo de aplicaciones web basadas en ASP.NET Core, pero tambien soporta el desarrollo de otro tipo de aplicaciones. - -Explora el menú de navegación de la izquierda para profundizar en la documentación. - -## Estado del proyecto - -ABP es la **próxima generación** del marco de desarrollo de código abierto [ASP.NET Boilerplate](https://aspnetboilerplate.com/). Actualmente se encuentra en una fase preliminar y no está listo para su uso en producción. La documentación todavía está en progreso y se encuentra lejos de estar completa. - -Para aplicaciones en fase de producción o que lo vayan a estar en el corto plazo, se sugiere usar el marco de desarrollo [ASP.NET Boilerplate](https://aspnetboilerplate.com/) el cual tiene un completo conjunto de funciones, es maduro, mantenido y actualizado activamente. - -## Primeros pasos - -La manera más facil para iniciar un proyecto con ABP es usar las plantillas de inicio: - -* [Plantilla ASP.NET Core MVC (Razor Pages) UI](Getting-Started-AspNetCore-MVC-Template.md) -* [Plantilla Angular UI](Getting-Started-Angular-Template.md) - -Si quieres iniciar desde cero (con un proyecto vacío), entonces instala manualmente el marco de desarrollo ABP y usa los siguientes tutoriales: - -* [Aplicación de Consola](Getting-Started-Console-Application.md) -* [Aplicación web con ASP.NET Core](Getting-Started-AspNetCore-Application.md) - -## Código fuente - -ABP está hospedado en GitHub. Mira el [código fuente](https://github.com/abpframework/abp). - -## ¿Quieres contribuir? - -ABP es un proyecto de código abierto impulsado por la comunidad. Mira la [guía de contribución](Contribution/Index.md) si quieres formar parte de este proyecto. \ No newline at end of file diff --git a/docs/es/images/create-aspnet-core-application.png b/docs/es/images/create-aspnet-core-application.png deleted file mode 100644 index afd1447ba8..0000000000 Binary files a/docs/es/images/create-aspnet-core-application.png and /dev/null differ diff --git a/docs/es/images/create-new-aspnet-core-application-v2.png b/docs/es/images/create-new-aspnet-core-application-v2.png deleted file mode 100644 index d2bce84775..0000000000 Binary files a/docs/es/images/create-new-aspnet-core-application-v2.png and /dev/null differ diff --git a/docs/es/images/select-empty-web-application-v2.png b/docs/es/images/select-empty-web-application-v2.png deleted file mode 100644 index 9bfd2ec6a8..0000000000 Binary files a/docs/es/images/select-empty-web-application-v2.png and /dev/null differ diff --git a/docs/pt-BR/Aspect-Oriented-Programming.md b/docs/pt-BR/Aspect-Oriented-Programming.md deleted file mode 100644 index bfc7b0f705..0000000000 --- a/docs/pt-BR/Aspect-Oriented-Programming.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic Proxying / Interceptors - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Audit-Logging.md b/docs/pt-BR/Audit-Logging.md deleted file mode 100644 index 609565d6f5..0000000000 --- a/docs/pt-BR/Audit-Logging.md +++ /dev/null @@ -1,3 +0,0 @@ -# Audit Logging - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Authorization.md b/docs/pt-BR/Authorization.md deleted file mode 100644 index 941666a9bf..0000000000 --- a/docs/pt-BR/Authorization.md +++ /dev/null @@ -1,3 +0,0 @@ -## Authorization - -Façam \ No newline at end of file diff --git a/docs/pt-BR/AutoMapper-Integration.md b/docs/pt-BR/AutoMapper-Integration.md deleted file mode 100644 index 24dd8ebfc4..0000000000 --- a/docs/pt-BR/AutoMapper-Integration.md +++ /dev/null @@ -1,3 +0,0 @@ -## AutoMapper Integration - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Background-Jobs-Hangfire.md b/docs/pt-BR/Background-Jobs-Hangfire.md deleted file mode 100644 index 66e6944e88..0000000000 --- a/docs/pt-BR/Background-Jobs-Hangfire.md +++ /dev/null @@ -1,3 +0,0 @@ -# Hangfire Background Job Manager - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Background-Jobs-RabbitMq.md b/docs/pt-BR/Background-Jobs-RabbitMq.md deleted file mode 100644 index a984a2a3ab..0000000000 --- a/docs/pt-BR/Background-Jobs-RabbitMq.md +++ /dev/null @@ -1,3 +0,0 @@ -# RabbitMQ Background Job Manager - -TODO \ No newline at end of file diff --git a/docs/pt-BR/CLI.md b/docs/pt-BR/CLI.md deleted file mode 100644 index 87ddfc9942..0000000000 --- a/docs/pt-BR/CLI.md +++ /dev/null @@ -1,149 +0,0 @@ -# ABP CLI - -O ABP CLI (Command Line Interface) é uma ferramenta de linha de comando para executar algumas operações comuns para soluções baseadas em ABP. - -## Instalação - -O ABP CLI é uma [ferramenta global dotnet](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) . Instale-o usando uma janela de linha de comando: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -Para atualizar uma instalação existente: - -````bash -dotnet tool update -g Volo.Abp.Cli -```` - -## Comandos - -### Novo - -Gera uma nova solução baseada nos [modelos de inicialização](Startup-Templates/Index.md) do ABP . - -Uso básico: - -````bash -abp new [options] -```` - -Examplo: - -````bash -abp new Acme.BookStore -```` - -* `Acme.BookStore` é o nome da solução aqui. -* A convenção comum é nomear uma solução como *YourCompany.YourProject* . No entanto, você pode usar nomes diferentes, como *YourProject* (namespacing de nível único) ou *YourCompany.YourProduct.YourModule* (namespacing de três níveis). - -#### Opções - -* `--template`ou `-t`: especifica o nome do modelo. O nome do modelo padrão é `app`, que gera um aplicativo da web. Modelos disponíveis: - * `app`(padrão): [modelo de aplicativo](https://docs.abp.io/en/abp/latest/Startup-Templates/Application) . Opções adicionais: - * `--ui`ou `-u`: Especifica a UI framework. Framework padrão é `mvc`. Framework disponíveis: - * `mvc`: ASP.NET Core MVC. Existem algumas opções adicionais para este modelo: - * `--tiered`: Cria uma solução em camadas em que as camadas da Web e da API HTTP são fisicamente separadas. Se não especificado, ele cria uma solução em camadas que é menos complexa e adequada para a maioria dos cenários. - * `angular`: Angular. Existem algumas opções adicionais para este modelo: - * `--separate-auth-server`: Separa o aplicativo do servidor de identidade do aplicativo host da API. Se não especificado, você terá um único ponto de extremidade no lado do servidor. - * `--database-provider` Ou `-d`: especifica o provedor de banco de dados. O provedor padrão é `ef`. Fornecedores disponíveis: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `module`: [Exemplo de Módulo](Startup-Templates/Module.md). Opções adicionais: - * `--no-ui`: Especifica para não incluir a UI. Isso possibilita a criação de módulos somente de serviço (também conhecidos como microsserviços - sem interface do usuário). -* `--output-folder` ou `-o`: especifica a pasta de saída. O valor padrão é o diretório atual. -* `--version` ou `-v`: Especifica a ABP & versão de exemplo . Pode ser uma [release tag](https://github.com/abpframework/abp/releases) ou um [branch name](https://github.com/abpframework/abp/branches). Usa a versão mais recente, se não especificado. Na maioria das vezes, você desejará usar a versão mais recente. - -### add-package - -Adiciona um pacote ABP a um projeto por, - -- Adicionando pacote de nuget relacionado como uma dependência ao projeto. -- Adicionando `[DependsOn(...)]`atributo à classe de módulo no projeto (consulte o [documento de desenvolvimento](https://docs.abp.io/en/abp/latest/Module-Development-Basics) do [módulo](https://docs.abp.io/en/abp/latest/Module-Development-Basics) ). - -> Observe que o módulo adicionado pode exigir uma configuração adicional, geralmente indicada na documentação do pacote relacionado. - -Uso básico: - -```bash -abp add-package [options] -``` - -Bater - -cópia de - -Exemplo: - -``` -abp add-package Volo.Abp.MongoDB -``` - -- Este exemplo adiciona o pacote Volo.Abp.MongoDB ao projeto. - -#### Opções - -- `--project`ou `-p`: especifica o caminho do arquivo do projeto (.csproj). Se não especificado, a CLI tenta encontrar um arquivo .csproj no diretório atual. - -### add-module - -Adiciona um [módulo de aplicativo com vários pacotes](Modules/Index.md) a uma solução, localizando todos os pacotes do módulo, localizando projetos relacionados na solução e adicionando cada pacote ao projeto correspondente na solução. - -> Um módulo de negócios geralmente consiste em vários pacotes (devido a camadas, diferentes opções de provedor de banco de dados ou outros motivos). O uso do `add-module`comando simplifica drasticamente a adição de um módulo a uma solução. No entanto, cada módulo pode exigir algumas configurações adicionais, geralmente indicadas na documentação do módulo relacionado. - -Uso básico: - -```bash -abp add-module [options] -``` - -Exemplo: - -```bash -abp add-module Volo.Blogging -``` - -- Este exemplo adiciona o módulo Volo.Blogging à solução. - -#### Opções - -- `--solution`ou `-s`: especifica o caminho do arquivo da solução (.sln). Se não especificado, a CLI tenta encontrar um arquivo .sln no diretório atual. -- `--skip-db-migrations`: Para o provedor de banco de dados EF Core, ele adiciona automaticamente um novo código à primeira migração ( `Add-Migration`) e atualiza o banco de dados ( `Update-Database`), se necessário. Especifique esta opção para pular esta operação. -- `-sp`ou `--startup-project`: caminho relativo para a pasta do projeto de inicialização. O valor padrão é a pasta atual. - -### atualizar - -A atualização de todos os pacotes relacionados ao ABP pode ser entediante, pois existem muitos pacotes da estrutura e dos módulos. Este comando atualiza automaticamente todos os pacotes NuGet e NPM relacionados ao ABP em uma solução ou projeto para as versões mais recentes. - -Uso: - -```bash -abp update [options] -``` - -- Se você executar em um diretório com um arquivo .sln, ele atualizará todos os pacotes relacionados ao ABP de todos os projetos da solução para as versões mais recentes. -- Se você executar em um diretório com um arquivo .csproj, ele atualizará todos os pacotes relacionados ao ABP do projeto para as versões mais recentes. - -#### Opções - -- `--include-previews`ou `-p`: inclui pacotes de visualização, beta e rc enquanto verifica as versões mais recentes. - -### Socorro - -Grava informações básicas de uso da CLI. - -Uso: - -```bash -abp help [command-name] -``` - -Exemplos: - -```bash -abp help # Shows a general help. -abp help new # Shows help about the "new" command. -``` - - - \ No newline at end of file diff --git a/docs/pt-BR/Caching.md b/docs/pt-BR/Caching.md deleted file mode 100644 index cc28531e90..0000000000 --- a/docs/pt-BR/Caching.md +++ /dev/null @@ -1,3 +0,0 @@ -# Caching - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Contribution/Index.md b/docs/pt-BR/Contribution/Index.md deleted file mode 100644 index e51ff4646c..0000000000 --- a/docs/pt-BR/Contribution/Index.md +++ /dev/null @@ -1,60 +0,0 @@ -## Guia de Contribuição - -O ABP é um projeto de [código aberto](https://github.com/abpframework) e orientado à comunidade. Este guia tem como objetivo ajudar alguém que queira contribuir com o projeto. - -### Contribuição de código - -Você sempre pode enviar solicitações pull ao repositório do Github. - -- Clone o [repositório ABP](https://github.com/abpframework/abp/) do Github. -- Faça as alterações necessárias. -- Envie uma solicitação de recebimento. - -Antes de fazer qualquer alteração, discuta-a sobre os [problemas](https://github.com/abpframework/abp/issues) do [Github](https://github.com/abpframework/abp/issues) . Dessa forma, nenhum outro desenvolvedor trabalhará no mesmo problema e seu PR terá uma chance melhor de ser aceito. - -#### Correções de bugs e aprimoramentos - -Você pode corrigir um bug conhecido ou trabalhar em uma melhoria planejada. Veja [a lista de problemas](https://github.com/abpframework/abp/issues) no Github. - -#### Solicitações de recursos - -Se você tem uma ideia de recurso para a estrutura ou módulos, [crie um problema](https://github.com/abpframework/abp/issues/new) no Github ou participe de uma discussão existente. Então você pode implementá-lo se for adotado pela comunidade. - -### Tradução de documentos - -Você pode traduzir a [documentação](https://abp.io/documents/) completa (incluindo esta) para o idioma materno. Nesse caso, siga estas etapas: - -- Clone o [repositório ABP](https://github.com/abpframework/abp/) do Github. -- Para adicionar um novo idioma, crie uma nova pasta dentro da pasta [docs](https://github.com/abpframework/abp/tree/master/docs) . Os nomes das pastas podem ser "en", "es", "fr", "tr" e assim por diante, com base no idioma (consulte [todos os códigos de cultura](https://msdn.microsoft.com/en-us/library/hh441729.aspx) ). -- Obtenha a [pasta "en"](https://github.com/abpframework/abp/tree/master/docs/en) como uma referência para os nomes de arquivos e a estrutura de pastas. Mantenha o mesmo nome se estiver traduzindo a mesma documentação. -- Envie uma solicitação de recebimento (PR) depois de traduzir qualquer documento. Traduza documentos e envie PRs um por um. Não espere para terminar as traduções de todos os documentos. - -Alguns documentos fundamentais precisam ser traduzidos antes da publicação de um idioma no [site de documentação](https://docs.abp.io/) da [ABP](https://docs.abp.io/) : - -- Documentos de introdução -- Tutoriais -- CLI - -Um novo idioma é publicado após a conclusão dessas traduções mínimas. - -### Localização de Recursos - -A estrutura ABP possui um [sistema de localização](../Localization.md) flexível . Você pode criar interfaces de usuário localizadas para seu próprio aplicativo. - -Além disso, os módulos de estrutura e pré-construção já localizaram textos. Como exemplo, veja [os textos de localização para o pacote Volo.Abp.UI](https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json) . Você pode criar um novo arquivo na [mesma pasta](https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi) para traduzi-lo. - -- Clone o [repositório ABP](https://github.com/abpframework/abp/) do Github. -- Crie um novo arquivo para o idioma de destino para um arquivo de texto de localização (json) (próximo ao arquivo en.json). -- Copie todos os textos do arquivo en.json. -- Traduzir os textos. -- Enviar solicitação de recebimento no Github. - -ABP é uma estrutura modular. Portanto, existem muitos recursos de texto de localização, um por módulo. Para encontrar todos os arquivos .json, você pode procurar por "en.json" após clonar o repositório. Você também pode verificar [esta lista](https://docs.abp.io/en/abp/latest/Contribution/Localization-Text-Files) para obter uma lista de arquivos de texto de localização. - -### Posts e tutoriais do blog - -Se você decidir criar alguns tutoriais ou postagens de blog no ABP, informe-nos (criando um [problema no Github](https://github.com/abpframework/abp/issues) ), para que possamos adicionar um link ao seu tutorial / publicação na documentação oficial e podemos anunciá-lo em nossa [conta do Twitter](https://twitter.com/abpframework) . - -### Relatório de erro - -Se você encontrar algum erro, [crie um problema no repositório do Github](https://github.com/abpframework/abp/issues/new) . \ No newline at end of file diff --git a/docs/pt-BR/CorrelationId.md b/docs/pt-BR/CorrelationId.md deleted file mode 100644 index 97b18c5099..0000000000 --- a/docs/pt-BR/CorrelationId.md +++ /dev/null @@ -1,3 +0,0 @@ -# Correlation ID - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Data-Filtering.md b/docs/pt-BR/Data-Filtering.md deleted file mode 100644 index 58b1431f2c..0000000000 --- a/docs/pt-BR/Data-Filtering.md +++ /dev/null @@ -1,3 +0,0 @@ -# Data Filtering - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Data-Seeding.md b/docs/pt-BR/Data-Seeding.md deleted file mode 100644 index 75eea2bcb9..0000000000 --- a/docs/pt-BR/Data-Seeding.md +++ /dev/null @@ -1,3 +0,0 @@ -# Data Seeding - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Data-Transfer-Objects.md b/docs/pt-BR/Data-Transfer-Objects.md deleted file mode 100644 index a29865a13c..0000000000 --- a/docs/pt-BR/Data-Transfer-Objects.md +++ /dev/null @@ -1,3 +0,0 @@ -## Data Transfer Objects - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Distributed-Event-Bus.md b/docs/pt-BR/Distributed-Event-Bus.md deleted file mode 100644 index da8e2ff516..0000000000 --- a/docs/pt-BR/Distributed-Event-Bus.md +++ /dev/null @@ -1,3 +0,0 @@ -# Distributed Event Bus - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Domain-Services.md b/docs/pt-BR/Domain-Services.md deleted file mode 100644 index 2a211453fa..0000000000 --- a/docs/pt-BR/Domain-Services.md +++ /dev/null @@ -1,3 +0,0 @@ -# ABP Documentation - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Dynamic-Proxying-Interceptors.md b/docs/pt-BR/Dynamic-Proxying-Interceptors.md deleted file mode 100644 index bfc7b0f705..0000000000 --- a/docs/pt-BR/Dynamic-Proxying-Interceptors.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic Proxying / Interceptors - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Emailing.md b/docs/pt-BR/Emailing.md deleted file mode 100644 index ca704cad19..0000000000 --- a/docs/pt-BR/Emailing.md +++ /dev/null @@ -1,3 +0,0 @@ -# Emailing - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Event-Bus.md b/docs/pt-BR/Event-Bus.md deleted file mode 100644 index d95792bbb7..0000000000 --- a/docs/pt-BR/Event-Bus.md +++ /dev/null @@ -1,3 +0,0 @@ -# Event Bus - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Extension-Methods-And-Helpers.md b/docs/pt-BR/Extension-Methods-And-Helpers.md deleted file mode 100644 index fb4d33c7e7..0000000000 --- a/docs/pt-BR/Extension-Methods-And-Helpers.md +++ /dev/null @@ -1,3 +0,0 @@ -# Extension Methods & Helpers - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Getting-Started-Angular-Template.md b/docs/pt-BR/Getting-Started-Angular-Template.md deleted file mode 100644 index f1b3abe790..0000000000 --- a/docs/pt-BR/Getting-Started-Angular-Template.md +++ /dev/null @@ -1,126 +0,0 @@ -## Introdução ao modelo de aplicativo Angular - -Este tutorial explica como criar um novo aplicativo Angular usando o modelo de inicialização, configurar e executá-lo. - -### Criando um novo projeto - -Este tutorial usa o **ABP CLI** para criar um novo projeto. Consulte a página [Introdução](https://abp.io/get-started) para outras opções. - -Instale a CLI ABP usando uma janela de linha de comando, se você não tiver instalado antes: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Use o `abp new`comando em uma pasta vazia para criar seu projeto: - -```bash -abp new Acme.BookStore -u angular -``` - -> Você pode usar diferentes níveis de namespaces; por exemplo, BookStore, Acme.BookStore ou Acme.Retail.BookStore. - -`-u angular`A opção especifica que a estrutura da interface do usuário seja Angular. O provedor de banco de dados padrão é o EF Core. Consulte a [documentação](CLI.md) da [CLI](CLI.md) para todas as opções disponíveis. - -#### Pré requisitos - -A solução criada requer; - -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### A Estrutura da Solução - -Abra a solução no **Visual Studio** : - -![livraria-visual-studio-solução](images/bookstore-visual-studio-solution-for-spa.png) - -A solução possui uma estrutura em camadas (baseada no [Domain Driven Design](Domain-Driven-Design.md) ) e contém projetos de teste de unidade e integração adequadamente configurados para trabalhar com o **banco de dados** **EF Core** & **SQLite in-memory**. - -> Consulte o [documento do modelo do aplicativo](Startup-Templates/Application.md) para entender a estrutura da solução em detalhes. - -### String de Conexão de Banco de Dados - -Verifique o **connection string** no `appsettings.json`arquivo no `.HttpApi.Host`projeto: - -```json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -``` - -A solução está configurada para usar o **Entity Framework Core** com o **MS SQL Server** . O EF Core suporta [vários](https://docs.microsoft.com/en-us/ef/core/providers/) provedores de banco de dados, para que você possa usar outro DBMS, se desejar. Mude a cadeia de conexão, se necessário. - -### Criar banco de dados e aplicar migrações de banco de dados - -Você tem duas opções para criar o banco de dados. - -#### Usando o aplicativo DbMigrator - -A solução contém um aplicativo de console (nomeado `Acme.BookStore.DbMigrator`nesta amostra) que pode criar banco de dados, aplicar migrações e propagar dados iniciais. É útil no desenvolvimento e no ambiente de produção. - -> `.DbMigrator`projeto tem o seu próprio `appsettings.json`. Portanto, se você alterou a cadeia de conexão acima, também deve alterar esta. - -Clique com o botão direito do mouse no `.DbMigrator`projeto e selecione **Definir como Projeto de Inicialização** : - -![definir como projeto de inicialização](images/set-as-startup-project.png) - -Pressione F5 (ou Ctrl + F5) para executar o aplicativo. Terá uma saída como mostrado abaixo: - -![definir como projeto de inicialização](images/db-migrator-app.png) - -#### Usando o comando EF Core Update-Database - -O Ef Core possui um `Update-Database`comando que cria banco de dados, se necessário, e aplica migrações pendentes. Clique com o botão direito do mouse no `.Web`projeto e selecione **Definir como Projeto de Inicialização** : - -![definir como projeto de inicialização](images/set-as-startup-project.png) - -Abra o **Console do Gerenciador de Pacotes** , selecione o `.EntityFrameworkCore.DbMigrations`projeto como **Projeto Padrão** e execute o `Update-Database`comando: - -![pcm-update-database](images/pcm-update-database-v2.png) - -Isso criará um novo banco de dados com base na cadeia de conexão configurada. - -> O uso da `.Migrator`ferramenta é a maneira sugerida, porque também semeia os dados iniciais para poder executar corretamente o aplicativo Web. - -### Executando o aplicativo - -#### Execute o host da API (lado do servidor) - -Verifique se o `.HttpApi.Host`projeto é o projeto de inicialização e o aplicativo que abrirá uma interface do usuário do Swagger: - -![livraria-homepage](images/bookstore-swagger-ui-host.png) - -Você pode ver as APIs do aplicativo e testá-las aqui. Obtenha [mais informações](https://swagger.io/tools/swagger-ui/) sobre a interface do usuário do Swagger. - -##### Autorização para a interface do usuário do Swagger - -A maioria das APIs de aplicativos requer autenticação e autorização. Se você deseja testar APIs autorizadas, vá manualmente para a `/Account/Login`página, digite `admin`como o nome de usuário e `1q2w3E*`a senha para efetuar login no aplicativo. Você também poderá executar APIs autorizadas. - -#### Execute o aplicativo angular (lado do cliente) - -Vá para a `angular`pasta, abra um terminal de linha de comando, digite o `yarn`comando (sugerimos ao gerenciador de pacotes do [yarn](https://classic.yarnpkg.com/) enquanto o npm install também funcionará na maioria dos casos): - -```bash -yarn -``` - -Depois que todos os módulos do nó estiverem carregados, execute `yarn start`ou `npm start`comando: - -```bash -yarn start -``` - -Abra seu navegador favorito e vá para `localhost:4200`URL. Nome de usuário inicial é `admin`e senha é `1q2w3E*`. - -O modelo de inicialização inclui os módulos de **gerenciamento de** **identidade** e **gerenciamento de inquilino** . Após o login, o menu Administração estará disponível, onde você poderá gerenciar **inquilinos** , **funções** , **usuários** e suas **permissões** . - -> Recomendamos o [Visual Studio Code](https://code.visualstudio.com/) como editor do projeto Angular, mas você pode usar seu editor favorito. - -### Qual é o próximo? - -- [Tutorial de desenvolvimento de aplicativos](Tutorials/Angular/Part-I.md) \ No newline at end of file diff --git a/docs/pt-BR/Getting-Started-AspNetCore-Application.md b/docs/pt-BR/Getting-Started-AspNetCore-Application.md deleted file mode 100644 index 4c5fb40ce1..0000000000 --- a/docs/pt-BR/Getting-Started-AspNetCore-Application.md +++ /dev/null @@ -1,183 +0,0 @@ -# Introdução ao ABP com o AspNet Core MVC Web Application - -Este tutorial explica como iniciar o ABP do zero com dependências mínimas. Você geralmente deseja começar com o **modelo de inicialização** . - -## Criar um novo projeto - -1. Crie um novo aplicativo da Web vazio AspNet Core Web Application no Visual Studio: - -![img](images/create-new-aspnet-core-application.png) - -1. Selecionar modelo vazio - -![img](images/select-empty-web-application.png) - -Você pode selecionar outro modelo, mas quero mostrá-lo em um projeto claro. - -## Instale o pacote Volo.Abp.AspNetCore.Mvc - -Volo.Abp.AspNetCore.Mvc é um pacote de integração do AspNet Core MVC para ABP. Então, instale-o no seu projeto: - -``` -Install-Package Volo.Abp.AspNetCore.Mvc -``` - -## Criar o primeiro módulo ABP - -O ABP é uma estrutura modular e requer uma classe de **módulo de inicialização (raiz)** derivada de `AbpModule`: - -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; -using Volo.Abp.AspNetCore.Modularity; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMvcWithDefaultRoute(); - } - } -} -``` - -`AppModule` é um bom nome para o módulo de inicialização de um aplicativo. - -Os pacotes ABP definem as classes do módulo e um módulo pode depender de outro módulo. No código acima, nosso `AppModule` depende `AbpAspNetCoreMvcModule`(definido pelo pacote Volo.Abp.AspNetCore.Mvc). É comum adicionar um `DependsOn`atributo após a instalação de um novo pacote de nuget ABP. - -Em vez da classe Startup, estamos configurando o pipeline do ASP.NET Core nesta classe de módulo. - -## A classe de inicialização - -O próximo passo é modificar a classe Startup para integrar ao sistema do módulo ABP: - -```csharp -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicAspNetCoreApplication -{ - public class Startup - { - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddApplication(); - - return services.BuildServiceProviderFromFactory(); - } - - public void Configure(IApplicationBuilder app) - { - app.InitializeApplication(); - } - } -} -``` - -`ConfigureServices` Método alterado para retornar em `IServiceProvider` vez de `void`. Essa alteração nos permite substituir a injeção de dependência do AspNet Core por outra estrutura (consulte a seção de integração com Autofac abaixo). `services.AddApplication()` adiciona todos os serviços definidos em todos os módulos a partir do `AppModule`. - -`app.InitializeApplication()`O `Configure`método call in inicializa e inicia o aplicativo. - -## Olá Mundo! - -O aplicativo acima não faz nada. Vamos criar um controlador MVC que faz algo: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace BasicAspNetCoreApplication.Controllers -{ - public class HomeController : AbpController - { - public IActionResult Index() - { - return Content("Hello World!"); - } - } -} -``` - -Se você executar o aplicativo, verá um "Olá, mundo!" mensagem na página. - -Derivado `HomeController`de em `AbpController`vez de `Controller`classe padrão . Isso não é necessário, mas a `AbpController`classe possui propriedades e métodos base úteis para facilitar seu desenvolvimento. - -## Usando Autofac como framework de injeção de dependência - -Embora o sistema de Injeção de Dependência (DI) do AspNet Core seja adequado para requisitos básicos, o Autofac fornece recursos avançados, como Injeção de Propriedade e Interceptação de Método, exigidos pela ABP para executar recursos avançados da estrutura de aplicativos. - -Substituir o sistema DI do AspNet Core pelo Autofac e integrar ao ABP é bastante fácil. - -1. Instale o pacote [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) - -``` -Install-Package Volo.Abp.Autofac -``` - -1. Adicionar `AbpAutofacModule` dependência - -```csharp -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] //Add dependency to ABP Autofac module -public class AppModule : AbpModule -{ - ... -} -``` - -1. Altere a `services.AddApplication();` linha na `Startup`classe, como mostrado abaixo: - -```csharp -services.AddApplication(options => -{ - options.UseAutofac(); //Integrate to Autofac -}); -``` - -1. Atualize `Program.cs` para não usar o `WebHost.CreateDefaultBuilder()` método, pois ele usa o contêiner DI padrão: - -```csharp -public class Program -{ - public static void Main(string[] args) - { - /* - https://github.com/aspnet/AspNetCore/issues/4206#issuecomment-445612167 - CurrentDirectoryHelpers exists in: \framework\src\Volo.Abp.AspNetCore.Mvc\Microsoft\AspNetCore\InProcess\CurrentDirectoryHelpers.cs - Will remove CurrentDirectoryHelpers.cs when upgrade to ASP.NET Core 3.0. - */ - CurrentDirectoryHelpers.SetCurrentDirectory(); - - BuildWebHostInternal(args).Run(); - } - - public static IWebHost BuildWebHostInternal(string[] args) => - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIIS() - .UseIISIntegration() - .UseStartup() - .Build(); -} -``` - -## Código fonte - -Obter código-fonte do projeto de exemplo criada neste tutorial a partir de [aqui](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication) . \ No newline at end of file diff --git a/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md b/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md deleted file mode 100644 index 808c0bd94d..0000000000 --- a/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md +++ /dev/null @@ -1,104 +0,0 @@ -## Introdução ao modelo ASP.NET Core MVC - -Este tutorial explica como criar um novo aplicativo Web ASP.NET Core MVC usando o modelo de inicialização, configurá-lo e executá-lo. - -### Criando um novo projeto - -Este tutorial usa o **ABP CLI** para criar um novo projeto. Consulte a página [Introdução](https://abp.io/get-started) para outras opções. - -Instale a CLI ABP usando uma janela de linha de comando, se você não tiver instalado antes: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Use o `abp new`comando em uma pasta vazia para criar seu projeto: - -```bash -abp new Acme.BookStore -``` - -> Você pode usar diferentes níveis de namespaces; por exemplo, BookStore, Acme.BookStore ou Acme.Retail.BookStore. - -`new` O comando cria um **aplicativo MVC em camadas** com o **Entity Framework Core** como o provedor de banco de dados. No entanto, possui opções adicionais. Consulte a [documentação](CLI.md) da [CLI](CLI.md) para todas as opções disponíveis. - -#### Pré requisitos - -A solução criada requer; - -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) -* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://classic.yarnpkg.com/) - -### A Estrutura da Solução - -Abra a solução no **Visual Studio** : - -![livraria-visual-studio-solução](images/bookstore-visual-studio-solution-v3.png) - -A solução possui uma estrutura em camadas (baseada no [Domain Driven Design](Domain-Driven-Design.md) ) e contém projetos de teste de unidade e integração adequadamente configurados para trabalhar com o **banco de** dados de **memória** **EF Core** e **SQLite** . - -> Consulte o [documento do modelo de aplicativo](Startup-Templates/Application.md) para entender a estrutura da solução em detalhes. - -### Cadeia de Conexão de Banco de Dados - -Verifique a **connection string** no `appsettings.json`arquivo no `.Web`projeto: - -```json -{ - "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" - } -} -``` - -A solução está configurada para usar o **Entity Framework Core** com o **MS SQL Server** . O EF Core suporta [vários](https://docs.microsoft.com/en-us/ef/core/providers/) provedores de banco de dados, para que você possa usar outro DBMS, se desejar. Mude a cadeia de conexão, se necessário. - -### Criar banco de dados e aplicar migrações de banco de dados - -Você tem duas opções para criar o banco de dados. - -#### Usando o aplicativo DbMigrator - -A solução contém um aplicativo de console (nomeado `Acme.BookStore.DbMigrator`nesta amostra) que pode criar banco de dados, aplicar migrações e propagar dados iniciais. É útil no desenvolvimento e no ambiente de produção. - -> `.DbMigrator`projeto tem o seu próprio `appsettings.json`. Portanto, se você alterou a cadeia de conexão acima, também deve alterar esta. - -Clique com o botão direito do mouse no `.DbMigrator`projeto e selecione **Definir como Projeto de Inicialização** : - -![definir como projeto de inicialização](images/set-as-startup-project.png) - -Pressione F5 (ou Ctrl + F5) para executar o aplicativo. Terá uma saída como mostrado abaixo: - -![definir como projeto de inicialização](images/db-migrator-app.png) - -#### Usando o comando EF Core Update-Database - -O Ef Core possui um `Update-Database`comando que cria banco de dados, se necessário, e aplica migrações pendentes. Clique com o botão direito do mouse no `.Web`projeto e selecione **Definir como Projeto de Inicialização** : - -![definir como projeto de inicialização](images/set-as-startup-project.png) - -Abra o **Console do Gerenciador de Pacotes** , selecione o `.EntityFrameworkCore.DbMigrations`projeto como **Projeto Padrão** e execute o `Update-Database`comando: - -![pcm-update-database](images/pcm-update-database-v2.png) - -Isso criará um novo banco de dados com base na cadeia de conexão configurada. - -> O uso da `.Migrator`ferramenta é a maneira sugerida, porque também semeia os dados iniciais para poder executar corretamente o aplicativo Web. - -### Executando o aplicativo - -Verifique se o `.Web`projeto é o projeto de inicialização. Execute o aplicativo que abrirá a página **inicial** no seu navegador: - -![livraria-homepage](images/bookstore-homepage.png) - -Clique no botão **Login** , insira `admin` como nome de usuário e `1q2w3E*` senha para acessar o aplicativo. - -O modelo de inicialização inclui os módulos de **gerenciamento de** **identidade** e **gerenciamento de inquilino** . Após o login, o menu Administração estará disponível, onde você poderá gerenciar **inquilinos** , **funções** , **usuários** e suas **permissões** . A página de gerenciamento de usuários é mostrada abaixo: - -![livraria-gerenciamento de usuários](images/bookstore-user-management-v2.png) - -### Qual é o próximo? - -- [Tutorial de desenvolvimento de aplicativos](Tutorials/AspNetCore-Mvc/Part-I.md) \ No newline at end of file diff --git a/docs/pt-BR/Getting-Started-Console-Application.md b/docs/pt-BR/Getting-Started-Console-Application.md deleted file mode 100644 index f5a0d5d5d0..0000000000 --- a/docs/pt-BR/Getting-Started-Console-Application.md +++ /dev/null @@ -1,181 +0,0 @@ -# Introdução ao ABP com aplicativo de console - -Este tutorial explica como iniciar o ABP do zero com dependências mínimas. Você geralmente deseja começar com um **modelo de inicialização** . - -## Criar um novo projeto - -Crie um novo aplicativo regular .Net Core Console do Visual Studio: - -![img](images/create-new-net-core-console-application.png) - -## Instale o pacote Volo.Abp - -Volo.Abp.Core é o pacote principal de nuget para criar aplicativos baseados em ABP. Então, instale-o no seu projeto: - -``` -Install-Package Volo.Abp.Core -``` - -## Criar o primeiro módulo ABP - -O ABP é um framework modular e requer uma classe de **módulo de inicialização (raiz)** derivada de `AbpModule`: - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; - -namespace AbpConsoleDemo -{ - public class AppModule : AbpModule - { - - } -} -``` - -`AppModule` é um bom nome para o módulo de inicialização de um aplicativo. - -## Inicializar o aplicativo - -A próxima etapa é inicializar o aplicativo usando o módulo de inicialização criado acima: - -```csharp -using System; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create()) - { - application.Initialize(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} -``` - -`AbpApplicationFactory`é usado para criar o aplicativo e carregar todos os módulos que tomam `AppModule`como módulo de inicialização. `Initialize()`O método inicia o aplicativo. - -## Olá Mundo! - -O aplicativo acima não faz nada. Vamos criar um serviço que faça algo: - -```csharp -using System; -using Volo.Abp.DependencyInjection; - -namespace AbpConsoleDemo -{ - public class HelloWorldService : ITransientDependency - { - public void SayHello() - { - Console.WriteLine("Hello World!"); - } - } -} -``` - -`ITransientDependency`é uma interface especial do ABP que registra automaticamente o serviço como transitório (consulte o [documento de injeção de dependência](Dependency-Injection.md) ). - -Agora, podemos resolver o problema `HelloWorldService`e dizer olá. Altere o Program.cs como mostrado abaixo: - -```csharp -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create()) - { - application.Initialize(); - - //Resolve a service and use it - var helloWorldService = - application.ServiceProvider.GetService(); - helloWorldService.SayHello(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} -``` - -Embora seja suficiente para este exemplo de código simples, é sempre recomendável criar escopos no caso de resolver diretamente dependências de `IServiceProvider`(consulte a [documentação de Injeção de Dependências](Dependency-Injection.md)). - -## Usando Autofac como framework de injeção de dependência - -Embora o sistema de Injeção de Dependência (DI) do AspNet Core seja adequado para requisitos básicos, o Autofac fornece recursos avançados, como Injeção de Propriedade e Interceptação de Método, exigidos pela ABP para executar recursos avançados do framework de aplicativos. - -Substituir o sistema DI do AspNet Core pelo Autofac e integrar ao ABP é bastante fácil. - -1. Instale o pacote [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) - -``` -Install-Package Volo.Abp.Autofac -``` - -1. Adicionar `AbpAutofacModule`dependência - -```csharp -[DependsOn(typeof(AbpAutofacModule))] //Add dependency to the AbpAutofacModule -public class AppModule : AbpModule -{ - -} -``` - -1. Mude o `Program.cs`arquivo como mostrado abaixo: - -```csharp -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); //Autofac integration - })) - { - application.Initialize(); - - //Resolve a service and use it - var helloWorldService = - application.ServiceProvider.GetService(); - helloWorldService.SayHello(); - - Console.WriteLine("Press ENTER to stop application..."); - Console.ReadLine(); - } - } - } -} -``` - -Apenas chamado `options.UseAutofac()`método nas `AbpApplicationFactory.Create`opções. - -## Código fonte - -Obter código-fonte do projeto de exemplo criada neste tutorial a partir de [aqui](https://github.com/abpframework/abp/tree/master/samples/BasicConsoleApplication) . - - diff --git a/docs/pt-BR/Guid-Generation.md b/docs/pt-BR/Guid-Generation.md deleted file mode 100644 index 68c582348c..0000000000 --- a/docs/pt-BR/Guid-Generation.md +++ /dev/null @@ -1,3 +0,0 @@ -## Guid Generation - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Index.md b/docs/pt-BR/Index.md deleted file mode 100644 index 535db6478a..0000000000 --- a/docs/pt-BR/Index.md +++ /dev/null @@ -1,31 +0,0 @@ -# Documentação ABP - -O ABP é um **framework de aplicativos de código aberto** focada no desenvolvimento de aplicativos da Web baseado no ASP.NET Core, mas também suporta o desenvolvimento de outros tipos de aplicativos. - -Explore o menu de navegação esquerdo para mergulhar fundo na documentação. - -## Status do projeto - -ABP é a **próxima geração** de framework de código aberto [ASP.NET Boilerplate](https://aspnetboilerplate.com/). Atualmente, está em fase de pré-visualização e não está pronto para uso na produção. A documentação ainda está em andamento e está longe de estar completa. - -Para aplicativos de curto prazo e em nível de produção, é sugerido o uso da estrutura do [ASP.NET Boilerplate](https://aspnetboilerplate.com/) , que possui um rico conjunto de recursos, maduro, mantido ativamente e atualizado. - -## Começando - -A maneira mais fácil de iniciar um novo projeto com o ABP é usar os modelos de inicialização: - -- [Modelo de interface do usuário do ASP.NET Core MVC (Razor Pages)](Getting-Started-AspNetCore-MVC-Template.md) -- [Modelo de interface do usuário angular](Getting-Started-Angular-Template.md) - -Se você deseja começar do zero (com um projeto vazio), instale manualmente o ABP Framework e use os seguintes tutoriais: - -- [Aplicação de console](Getting-Started-Console-Application.md) -- [Aplicativo da Web principal do ASP.NET](Getting-Started-AspNetCore-Application.md) - -## Código fonte - -ABP está hospedado no GitHub. Veja [o código fonte](https://github.com/abpframework/abp) . - -## Deseja contribuir? - -O ABP é um projeto de código aberto orientado pela comunidade. Consulte [o guia de contribuição](Contribution/Index.md) se você quiser fazer parte deste projeto. \ No newline at end of file diff --git a/docs/pt-BR/Integration-Tests.md b/docs/pt-BR/Integration-Tests.md deleted file mode 100644 index 9d2f17264c..0000000000 --- a/docs/pt-BR/Integration-Tests.md +++ /dev/null @@ -1,3 +0,0 @@ -# Integration Tests - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Local-Event-Bus.md b/docs/pt-BR/Local-Event-Bus.md deleted file mode 100644 index ff4d87d210..0000000000 --- a/docs/pt-BR/Local-Event-Bus.md +++ /dev/null @@ -1,3 +0,0 @@ -# Local Event Bus - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Localization.md b/docs/pt-BR/Localization.md deleted file mode 100644 index 35b400b233..0000000000 --- a/docs/pt-BR/Localization.md +++ /dev/null @@ -1,195 +0,0 @@ -# Localização - -O sistema de localização da ABP é perfeitamente integrado ao `Microsoft.Extensions.Localization`pacote e compatível com a [documentação de localização da Microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) . Ele adiciona alguns recursos e aprimoramentos úteis para facilitar o uso em cenários de aplicativos da vida real. - -## Pacote Volo.Abp.Localization - -> Este pacote já está instalado por padrão com o modelo de inicialização. Portanto, na maioria das vezes, você não precisa instalá-lo manualmente. - -Volo.Abp.Localization é o pacote principal do sistema de localização. Instale-o no seu projeto usando o console do gerenciador de pacotes (PMC): - -``` -Install-Package Volo.Abp.Localization -``` - -Em seguida, você pode adicionar a dependência **AbpLocalizationModule** ao seu módulo: - -```csharp -using Volo.Abp.Modularity; -using Volo.Abp.Localization; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpLocalizationModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -## Criando um recurso de localização - -Um recurso de localização é usado para agrupar cadeias de localização relacionadas e separá-las de outras cadeias de localização do aplicativo. Um [módulo](Module-Development-Basics.md) geralmente define seu próprio recurso de localização. O recurso de localização é apenas uma classe simples. Exemplo: - -```csharp -public class TestResource -{ -} -``` - -Em seguida, deve ser adicionado usando `AbpLocalizationOptions`como mostrado abaixo: - -```csharp -[DependsOn(typeof(AbpLocalizationModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.FileSets.AddEmbedded(); - }); - - Configure(options => - { - //Define a new localization resource (TestResource) - options.Resources - .Add("en") - .AddVirtualJson("/Localization/Resources/Test"); - }); - } -} -``` - -Neste exemplo; - -- Adicionado um novo recurso de localização com "en" (inglês) como a cultura padrão. -- Arquivos JSON usados para armazenar as sequências de localização. -- Os arquivos JSON são incorporados ao assembly usando `AbpVirtualFileSystemOptions`(consulte [sistema de arquivos virtual](Virtual-File-System.md) ). - -Os arquivos JSON estão localizados na pasta do projeto "/ Localização / Recursos / Teste", como mostrado abaixo: - -![localization-resource-json-files](images/localization-resource-json-files.png) - -Um conteúdo do arquivo de localização JSON é mostrado abaixo: - -```json -{ - "culture": "en", - "texts": { - "HelloWorld": "Hello World!" - } -} -``` - -- Todo arquivo de localização deve definir o `culture`código para o arquivo (como "en" ou "en-US"). -- `texts` A seção contém apenas a coleção de valores-chave das sequências de localização (as chaves também podem ter espaços). - -### Nome Curto do Recurso de Localização - -Os recursos de localização também estão disponíveis no lado do cliente (JavaScript). Portanto, definir um nome abreviado para o recurso de localização facilita o uso de textos de localização. Exemplo: - -```csharp -[LocalizationResourceName("Test")] -public class TestResource -{ -} -``` - -Consulte a seção Obtendo teste localizado / lado do cliente abaixo. - -### Herdar de outros recursos - -Um recurso pode herdar de outros recursos, o que possibilita reutilizar cadeias de localização existentes sem fazer referência ao recurso existente. Exemplo: - -```csharp -[InheritResource(typeof(AbpValidationResource))] -public class TestResource -{ -} -``` - -Herança alternativa configurando o `AbpLocalizationOptions`: - -```csharp -services.Configure(options => -{ - options.Resources - .Add("en") //Define the resource by "en" default culture - .AddVirtualJson("/Localization/Resources/Test") //Add strings from virtual json files - .AddBaseTypes(typeof(AbpValidationResource)); //Inherit from an existing resource -}); -``` - -- Um recurso pode herdar de vários recursos. -- Se o novo recurso definir a mesma sequência localizada, ele substituirá a sequência. - -### Estendendo o Recurso Existente - -Herdar de um recurso cria um novo recurso sem modificar o existente. Em alguns casos, convém não criar um novo recurso, mas estender diretamente um recurso existente. Exemplo: - -```csharp -services.Configure(options => -{ - options.Resources - .Get() - .AddVirtualJson("/Localization/Resources/Test/Extensions"); -}); -``` - -- Se um arquivo de extensão define a mesma sequência localizada, ele substitui a sequência. - -## Obtendo textos localizados - -### Lado do servidor - -Obter o texto localizado no lado do servidor é bastante padrão. - -#### Uso mais simples de uma classe - -```csharp -public class MyService -{ - private readonly IStringLocalizer _localizer; - - public MyService(IStringLocalizer localizer) - { - _localizer = localizer; - } - - public void Foo() - { - var str = _localizer["HelloWorld"]; - } -} -``` - -#### Uso mais simples em uma vista / página do Razor - -```csharp -@inject IHtmlLocalizer Localizer - -

    @Localizer["HelloWorld"]

    -``` - -Consulte a [documentação de localização da Microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) para obter detalhes sobre o uso da localização no lado do servidor. - -### Lado do Cliente - -A ABP fornece serviços JavaScript para usar os mesmos textos localizados no lado do cliente. - -Obtenha um recurso de localização: - -```js -var testResource = abp.localization.getResource('Test'); -``` - -Localize uma sequência: - -```js -var str = testResource('HelloWorld'); -``` - - - \ No newline at end of file diff --git a/docs/pt-BR/MongoDB.md b/docs/pt-BR/MongoDB.md deleted file mode 100644 index c7ad2ef1c3..0000000000 --- a/docs/pt-BR/MongoDB.md +++ /dev/null @@ -1,466 +0,0 @@ -# Integração do MongoDB - -Este documento explica como integrar e configurar o MongoDB como um provedor de banco de dados para aplicações baseadas no ABP. - -## Instalação - -`Volo.Abp.MongoDB` é o pacote nuget principal para a integração do MongoDB. Instale-o em seu projeto (para uma aplicação em camadas, será sua camada de dados ou infraestrutura): - -``` -Install-Package Volo.Abp.MongoDB -``` - -Então adicione a dependência de módulo `AbpMongoDbModule` para o seu [module](Module-Development-Basics.md): - -```c# -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - // ... - } -} -``` - -## Criando um Mongo Db Context - -ABP apresenta o conceito **Mongo Db Context** (que é semelhante ao Entity Framework Core DbContext) para tornar mais fácil usar coleções e configurá-las. Um exemplo é mostrado abaixo: - -```c# -public class MyDbContext : AbpMongoDbContext -{ - public IMongoCollection Questions => Collection(); - - public IMongoCollection Categories => Collection(); - - protected override void CreateModel(IMongoModelBuilder modelBuilder) - { - base.CreateModel(modelBuilder); - - // Personalize a configuração para as suas collections. - } -} -``` - -* É derivado da classe `AbpMongoDbContext`. -* Adiciona uma propriedade pública `IMongoCollection` para cada mongo collection. Por padrão ABP usa essas propriedades para criar repositórios padrão. -* Substituir o método `CreateModel` permite definir a configuração da collection. - -### Configurar o Mapeamento para uma Collection - -ABP registra automaticamente entidades MongoDB client library para todas propriedades `IMongoCollection` em seu DbContext. Para o exemplo acima, as entidades `Question` e `Category` são registradas automaticamente. - -Para cada entidade registrada, chama `AutoMap()` e configura propriedades conhecidas de sua entidade. Por exemplo, se sua entidade implementa uma interface `IHasExtraProperties` (que já está implementada para cada raiz agregada por padrão), ele configura automaticamente `ExtraProperties`. - -Portanto, na maioria das vezes, você não precisa configurar explicitamente o registro de suas entidades. No entanto, se você precisar, pode fazer isso sobrescrevendo o método `CreateModel` em seu DbContext. Exemplo: - -````csharp -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.Entity(b => - { - b.CollectionName = "MyQuestions"; // Define o nome da collection - b.BsonMap.UnmapProperty(x => x.MyProperty); // Ignora 'MyProperty' - }); -} -```` - -Este exemplo altera o nome da collection mapeada para 'MyQuestions' no banco de dados e ignora uma propriedade na classe `Question`. - -Se você só precisa configurar o nome da collection, você também pode usar o atributo `[MongoCollection]` para a collection em seu DbContext. Exemplo: - -````csharp -[MongoCollection("MyQuestions")] // Define o nome da collection -public IMongoCollection Questions => Collection(); -```` - -### Configurar a Seleção da String de Conexão - -Se você tiver vários bancos de dados em seu aplicativo, você pode configurar o nome da string de conexão para o seu DbContext usando o atributo `[ConnectionStringName]`. Exemplo: - -````csharp -[ConnectionStringName("MySecondConnString")] -public class MyDbContext : AbpMongoDbContext -{ - -} -```` - -Se você não configurar, a string de conexão `Default` é usada. Se você configurar um nome de string de conexão, mas não definir este nome da string de conexão na configuração da aplicação, então ele retorna para a string de conexão `Default`. - -## Registrando DbContext para Injeção de Dependência - -Use o método `AddAbpDbContext` em seu module para registrar sua classe DbContext para o sistema de [injeção de dependência](Dependency-Injection.md). - -```c# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddMongoDbContext(); - - // ... - } - } -} -``` - -### Adicionar Repositories Padrão - -O ABP pode criar automaticamente [repositories genéricas](Repositories.md) padrão para as entidades em seu DbContext. Basta usar a opção `AddDefaultRepositories()` no registro: - -````C# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); -}); -```` - -Isso criará uma repository para cada [aggregate root entity](Entities.md) (classes derivadas de `AggregateRoot`) por padrão. Se você quiser criar repositories para outras entidades também, em seguida defina `includeAllEntities` para `true`: - -```c# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(includeAllEntities: true); -}); -``` - -Então você pode injetar e usar `IRepository` nas suas services. Suponha que você tenha uma entidade `Book` com a chave primária `Guid`: - -```csharp -public class Book : AggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } -} -``` - -(`BookType` é um simples `enum` aqui) E você deseja criar uma nova entidade `Book` em uma [domain service](Domain-Services.md): - -```csharp -public class BookManager : DomainService -{ - private readonly IRepository _bookRepository; - - public BookManager(IRepository bookRepository) // injetar repositório padrão - { - _bookRepository = bookRepository; - } - - public async Task CreateBook(string name, BookType type) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var book = new Book - { - Id = GuidGenerator.Create(), - Name = name, - Type = type - }; - - await _bookRepository.InsertAsync(book); // Use um método de repositório padrão - - return book; - } -} -``` - -Este exemplo usa o método `InsertAsync` para inserir uma nova entity no banco de dados. - -### Adicionar Repositories Personalizadas - -Repositories genéricas padrão são poderosas e suficiente na maioria dos casos (uma vez que implementam `IQueryable`). No entanto, pode ser necessário criar uma repository customizada para adicionar seus próprios métodos de repository. - -Suponha que você deseja excluir todos os books por type. É sugerido definir uma interface para sua repository personalizada: - -```csharp -public interface IBookRepository : IRepository -{ - Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken) - ); -} -``` - -Você geralmente vai querer derivar de `IRepository` para herdar métodos de repository padrão. No entanto, você não precisa. As interfaces de repository são definidas na camada de domínio de uma aplicação em camadas. Elas são implementadas na camada de dados ou infraestrutura (projeto `MongoDB` em um [startup template](https://abp.io/Templates)). - -Exemplo de implementação da interface `IBookRepository`: - -```csharp -public class BookRepository : - MongoDbRepository, - IBookRepository -{ - public BookRepository(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken)) - { - var collection = await GetCollectionAsync(cancellationToken); - await collection.DeleteManyAsync( - Builders.Filter.Eq(b => b.Type, type), - cancellationToken - ); - } -} -``` - -Agora é possível [injetar](Dependency-Injection.md) a `IBookRepository` e usar o método `DeleteBooksByType` quando necessário. - -#### Substituir Repository Genérica Padrão - -Mesmo se você criar uma repository personalizada, você ainda pode injetar a repository genérica padrão (`IRepository` para este exemplo). A implementação da repository padrão não usará a classe que você criou. - -Se você querer substituir a implementação da repository padrão pela sua repository personalizada, faça dentro das opções `AddMongoDbContext`: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - options.AddRepository(); //Replaces IRepository -}); -``` - -Isso é especialmente importante quando você deseja dar **override em um método da base repository** para customizar. Por exemplo, você pode querer substituir o método `DeleteAsync` para excluir uma entidade de uma forma mais eficiente: - -```csharp -public async override Task DeleteAsync( - Guid id, - bool autoSave = false, - CancellationToken cancellationToken = default) -{ - // TODO: Implementação customizada do método delete -} -``` - -### Acesso à API MongoDB - -Na maioria dos casos, você vai querer ocultar APIs do MongoDB atrás de uma repository (este é o objetivo principal da repository). No entanto, se você quiser acessar a API MongoDB através da Repository, você pode usar os métodos de extensão `GetDatabaseAsync()`, `GetCollectionAsync()` ou `GetAggregateAsync()`. Exemplo: - -```csharp -public class BookService -{ - private readonly IRepository _bookRepository; - - public BookService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task FooAsync() - { - IMongoDatabase database = await _bookRepository.GetDatabaseAsync(); - IMongoCollection books = await _bookRepository.GetCollectionAsync(); - IAggregateFluent bookAggregate = await _bookRepository.GetAggregateAsync(); - } -} -``` - -> Importante: Você deve fazer referência ao pacote `Volo.Abp.MongoDB` do projeto que deseja acessar a API MongoDB. Isso quebra o encapsulamento, mas é o que você deseja nesse caso. - -### Transactions - -O MongoDB oferece suporte multi-document transactions a partir da versão 4.0 e o ABP Framework oferece suporte para isso. No entanto, o [startup template](Startup-Templates/Index.md) **desativa** transactions por padrão. Se o seu **servidor** MongoDB suportar transactions, você pode habilitar esse recurso na classe *YourProjectMongoDbModule*: - -```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; -}); -``` - -> Ou você pode excluir este código, pois este já é o comportamento padrão. - -### Tópicos Avançados - -### Controlando o Multi-Tenancy - -Se sua solução for [multi-tenant](Multi-Tenancy.md), os tenants podem ter **bancos de dados separados**, você tem **múltiplas** classes `DbContext` em sua solução e algumas de suas classes `DbContext` devem ser utilizáveis **apenas do lado do host**, é recomendado adicionar o atributo `[IgnoreMultiTenancy]` em sua classe `DbContext`. Nesse caso, a ABP garante que o `DbContext` relacionado sempre usa a [connection string](Connection-Strings.md) do host, mesmo se você estiver em um tenant context. - -**Exemplo:** - -````csharp -[IgnoreMultiTenancy] -public class MyDbContext : AbpMongoDbContext -{ - ... -} -```` - -Não use o atributo `[IgnoreMultiTenancy]` se qualquer uma de suas entidades em seu `DbContext` puder ser persistida em um outro banco de dados de um tenant. - -> When you use repositories, ABP already uses the host database for the entities don't implement the `IMultiTenant` interface. So, most of time you don't need to `[IgnoreMultiTenancy]` attribute if you are using the repositories to work with the database. - -#### Definir Classes Repository Padrão - -Repositories genéricas padrão são implementadas pela classe `MongoDbRepository` por padrão. Você pode criar sua própria implementação e usá-la para implementação da repository padrão. - -Primeiro, defina suas classes de repository assim: - -```csharp -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} - -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -O primeiro é para [entities com chaves compostas](Entities.md), o segundo é para entities com uma única chave primária. - -É sugerido herdar da classe `MongoDbRepository` e substituir os métodos, se necessário. Caso contrário, você terá que implementar todos os métodos de repository padrão manualmente. - -Agora, você pode usar a opção `SetDefaultRepositoryClasses`: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.SetDefaultRepositoryClasses( - typeof(MyRepositoryBase<,>), - typeof(MyRepositoryBase<>) - ); - // ... -}); -``` - -#### Definir classe base MongoDbContext ou Interface para Repositories padrão - -Se seu MongoDbContext herda de outro MongoDbContext ou implementa uma interface, você pode usar essa classe base ou interface como o MongoDbContext para repositories padrão. Exemplo: - -```csharp -public interface IBookStoreMongoDbContext : IAbpMongoDbContext -{ - Collection Books { get; } -} -``` - -`IBookStoreMongoDbContext` é implementado pela classe `BookStoreMongoDbContext`. Então você pode usar sobrecarga genérica do `AddDefaultRepositories`: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - // ... -}); -``` - -Agora, seu `BookRepository` personalizado também pode usar a interface `IBookStoreMongoDbContext`: - -```csharp -public class BookRepository - : MongoDbRepository, - IBookRepository -{ - // ... -} -``` - -Uma vantagem de usar a interface para um MongoDbContext é que ela pode ser substituída por outra implementação. - -#### Substituir Outros DbContexts - -Depois de definir e usar adequadamente uma interface para um MongoDbContext, qualquer outra implementação pode usar as seguintes maneiras de substituí-lo: - -**ReplaceDbContextAttribute** - -```csharp -[ReplaceDbContext(typeof(IBookStoreMongoDbContext))] -public class OtherMongoDbContext : AbpMongoDbContext, IBookStoreMongoDbContext -{ - // ... -} -``` - -**Opção ReplaceDbContext** - -```csharp -context.Services.AddMongoDbContext(options => -{ - // ... - options.ReplaceDbContext(); -}); -``` - -Neste exemplo, `OtherMongoDbContext` implementa `IBookStoreMongoDbContext`. Este recurso permite que você tenha vários MongoDbContext (um por módulo) no desenvolvimento, mas um único MongoDbContext (implementa todas as interfaces de todos os MongoDbContexts) no tempo de execução. - -### Personalizar Operações em Massa - -Se você tiver uma lógica melhor ou usar uma biblioteca externa para operações em massa, pode substituir a lógica por meio da implementação de `IMongoDbBulkOperationProvider`. - -- Você pode usar o modelo de exemplo abaixo: - -```csharp -public class MyCustomMongoDbBulkOperationProvider - : IMongoDbBulkOperationProvider, ITransientDependency -{ - public async Task DeleteManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Sua lógica aqui. - } - - public async Task InsertManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Sua lógica aqui. - } - - public async Task UpdateManyAsync( - IMongoDbRepository repository, - IEnumerable entities, - IClientSessionHandle sessionHandle, - bool autoSave, - CancellationToken cancellationToken) - where TEntity : class, IEntity - { - // Sua lógica aqui. - } -} -``` - -## Veja Também - -* [Entities](Entities.md) -* [Repositories](Repositories.md) \ No newline at end of file diff --git a/docs/pt-BR/Nightly-Builds.md b/docs/pt-BR/Nightly-Builds.md deleted file mode 100644 index 9d3e49ccf6..0000000000 --- a/docs/pt-BR/Nightly-Builds.md +++ /dev/null @@ -1,27 +0,0 @@ -# Compilações Noturnas - -Todos os pacotes de estrutura e módulo são implantados no MyGet todas as noites durante a semana. Portanto, você pode usar ou testar o código mais recente sem aguardar o próximo lançamento. - -## Configurar o Visual Studio - -> Requer Visual Studio 2017 ou superior - -1. Vá para `Tools > Options > NuGet Package Manager > Package Source`. -2. Clique no `+` ícone verde . -3. Defina `ABP Nightly`como *Nome* e `https://www.myget.org/F/abp-nightly/api/v3/index.json`como a *Fonte,* como mostrado abaixo: ![noite-compilação-adicionar-pepita-fonte](images/night-build-add-nuget-source.png) -4. Clique no `Update` botão -5. Clique no `OK` botão para salvar as alterações. - -## Instalar pacote - -Agora, você pode instalar pacotes noturnos / de visualização no seu projeto a partir do Nuget Browser ou do Package Manager Console. - -![night-build-add-nuget-package](images/night-build-add-nuget-package.png) - -1. No Nuget Browser, selecione "Incluir pré-lançamentos". -2. Altere a fonte do pacote para "Todos". -3. Pesquise um pacote. Você verá as pré *-liberações* do pacote formatadas como `(VERSION)-preview(DATE)`(como *v0.16.0-preview20190401* neste exemplo). -4. Você pode clicar no `Install`botão para adicionar um pacote ao seu projeto. - - - \ No newline at end of file diff --git a/docs/pt-BR/Object-To-Object-Mapping.md b/docs/pt-BR/Object-To-Object-Mapping.md deleted file mode 100644 index 7d5f74a3fe..0000000000 --- a/docs/pt-BR/Object-To-Object-Mapping.md +++ /dev/null @@ -1,3 +0,0 @@ -## Object To Object Mapping - -Façam \ No newline at end of file diff --git a/docs/pt-BR/SMS-Sending.md b/docs/pt-BR/SMS-Sending.md deleted file mode 100644 index ca704cad19..0000000000 --- a/docs/pt-BR/SMS-Sending.md +++ /dev/null @@ -1,3 +0,0 @@ -# Emailing - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Settings.md b/docs/pt-BR/Settings.md deleted file mode 100644 index ffc08071a2..0000000000 --- a/docs/pt-BR/Settings.md +++ /dev/null @@ -1,3 +0,0 @@ -# Settings - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Specifications.md b/docs/pt-BR/Specifications.md deleted file mode 100644 index 19a9db8a27..0000000000 --- a/docs/pt-BR/Specifications.md +++ /dev/null @@ -1,3 +0,0 @@ -# Specifications - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Startup-Templates/Application.md b/docs/pt-BR/Startup-Templates/Application.md deleted file mode 100644 index 24120a9d1c..0000000000 --- a/docs/pt-BR/Startup-Templates/Application.md +++ /dev/null @@ -1,275 +0,0 @@ -# Modelo de inicialização do aplicativo - -## Introdução - -Este modelo fornece uma estrutura de aplicativo em camadas com base nas práticas DDD ([Domain Driven Design](../Domain-Driven-Design.md)). Este documento explica a estrutura da solução e os projetos em detalhes. Se você deseja iniciar rapidamente, siga os guias abaixo: - -- Consulte [Introdução ao modelo do ASP.NET Core MVC](../Getting-Started-AspNetCore-MVC-Template.md) para criar uma nova solução e executá-la para este modelo (usa o MVC como a estrutura da interface do usuário e o Entity Framework Core como o provedor de banco de dados). -- Consulte o [Tutorial de desenvolvimento de aplicativos do ASP.NET Core MVC](../Tutorials/AspNetCore-Mvc/Part-I.md) para aprender como desenvolver aplicativos usando este modelo (usa o MVC como a estrutura da interface do usuário e o Entity Framework Core como o provedor de banco de dados). -- Consulte o [Tutorial de desenvolvimento de aplicativos Angular](../Tutorials/Angular/Part-I.md) para aprender como desenvolver aplicativos usando este modelo (usa Angular como a estrutura da interface do usuário e MongoDB como o provedor de banco de dados). - -## Como começar? - -Você pode usar a [ABP CLI](../CLI.md) para criar um novo projeto usando este modelo de inicialização. Como alternativa, você pode criar e fazer o download diretamente na página [Introdução](https://abp.io/get-started) . A abordagem CLI é usada aqui. - -Primeiro, instale a ABP CLI se você não tiver instalado antes: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Em seguida, use o `abp new`comando em uma pasta vazia para criar uma nova solução: - -```bash -abp new Acme.BookStore -t app -``` - -- `Acme.BookStore`é o nome da solução, como *YourCompany.YourProduct* . Você pode usar nomes de nível único, dois ou três níveis. -- Este exemplo especificou o nome do modelo ( `-t`ou `--template`opção). No entanto, `app`já é o modelo padrão se você não o especificar. - -### Especifique a estrutura da interface do usuário - -Este modelo fornece várias estruturas de interface do usuário: - -- `mvc`: Interface do usuário do ASP.NET Core MVC com Razor Pages (padrão) -- `angular`: UI angular - -Use `-u`ou `--ui`opção para especificar a estrutura da interface do usuário: - -```bash -abp new Acme.BookStore -u angular -``` - -### Especifique o provedor de banco de dados - -Este modelo suporta os seguintes provedores de banco de dados: - -- `ef`: Entity Framework Core (padrão) -- `mongodb`: MongoDB - -Use a opção `-d`(ou `--database-provider`) para especificar o provedor de banco de dados: - -```bash -abp new Acme.BookStore -d mongodb -``` - -## Estrutura da solução - -Com base nas opções especificadas, você obterá uma estrutura de solução ligeiramente diferente. - -### Estrutura padrão - -Se você não especificar nenhuma opção adicional, terá uma solução como a mostrada abaixo: - -![livraria-visual-studio-solution-v3](../images/bookstore-visual-studio-solution-v3.png) - -Os projetos são organizados em `src`e `test`pastas. `src`A pasta contém o aplicativo real que está em camadas com base nos princípios [DDD](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) , como mencionado anteriormente. - -O diagrama abaixo mostra as camadas e dependências do projeto do aplicativo: - -![dependências de projeto em camadas](../images/layered-project-dependencies.png) - -Cada seção abaixo explicará o projeto relacionado e suas dependências. - -#### Projeto .Domain.Shared - -Este projeto contém constantes, enumerações e outros objetos. Na verdade, eles fazem parte da camada de domínio, mas precisam ser usados por todas as camadas / projetos da solução. - -Um `BookType`enum e uma `BookConsts`classe (que podem ter alguns campos constantes para a `Book`entidade, como `MaxNameLength`) são bons candidatos para este projeto. - -- Este projeto não depende de outros projetos na solução. Todos os outros projetos dependem disso direta ou indiretamente. - -#### .Domain Project - -Essa é a camada de domínio da solução. Ele contém principalmente [entidades, raízes agregadas](../Entities.md) , [serviços de domínio](../Domain-Services.md) , [tipos de valor](../Value-Types.md) , [interfaces de repositório](../Repositories) e outros objetos de domínio. - -Uma `Book`entidade, um `BookManager`serviço de domínio e uma `IBookRepository`interface são bons candidatos para este projeto. - -- Depende do `.Domain.Shared`porque usa constantes, enumerações e outros objetos definidos nesse projeto. - -#### .Application.Contracts Project - -Este projeto contém principalmente **interfaces de** [serviço de aplicativo](../Application-Services.md) e DTO ( [Data Transfer Objects](../Data-Transfer-Objects.md) ) da camada de aplicativo. Existe para separar a interface e a implementação da camada de aplicação. Dessa forma, o projeto de interface pode ser compartilhado com os clientes como um pacote de contrato. - -Uma `IBookAppService`interface e uma `BookCreationDto`classe são boas candidatas para este projeto. - -- Depende do `.Domain.Shared`porque ele pode usar constantes, enumerações e outros objetos compartilhados deste projeto nas interfaces de serviço de aplicativo e DTOs. - -#### Projeto de Aplicação - -Este projeto contém as **implementações** de [serviço de aplicativo](../Application-Services.md) das interfaces definidas no projeto.`.Application.Contracts` - -Uma `BookAppService`turma é uma boa candidata para este projeto. - -- Depende do `.Application.Contracts`projeto para poder implementar as interfaces e usar os DTOs. -- Depende do `.Domain`projeto para poder usar objetos de domínio (entidades, interfaces de repositório ... etc.) para executar a lógica do aplicativo. - -#### Projeto .EntityFrameworkCore - -Este é o projeto de integração para o EF Core. Ele define `DbContext`e implementa as interfaces de repositório definidas no `.Domain`projeto. - -- Depende do `.Domain`projeto para poder fazer referência a entidades e interfaces de repositório. - -> Este projeto está disponível apenas se você estiver usando o EF Core como provedor de banco de dados. Se você selecionar outro provedor de banco de dados, seu nome será diferente. - -#### Projeto .EntityFrameworkCore.DbMigrations - -Contém migrações de banco de dados EF Core para a solução. Ele foi separado `DbContext`para dedicado a gerenciar migrações. - -ABP é uma estrutura modular e com um design ideal, cada módulo tem sua própria `DbContext`classe. É aqui que a migração `DbContext`entra em ação e unifica todas as `DbContext`configurações em um único modelo para manter um único esquema de banco de dados. Para cenários mais avançados, você pode ter vários bancos de dados (cada um contém uma única ou algumas tabelas de módulos) e várias migrações `DbContext`(cada uma mantém um esquema de banco de dados diferente). - -Observe que a migração `DbContext`é usada apenas para migrações de banco de dados e *não em tempo de execução* . - -- Depende do `.EntityFrameworkCore`projeto, pois reutiliza a configuração definida para `DbContext`o aplicativo. - -> Este projeto está disponível apenas se você estiver usando o EF Core como provedor de banco de dados. - -#### Projeto .DbMigrator - -Este é um aplicativo de console que simplifica a execução de migrações de banco de dados em ambientes de desenvolvimento e produção. Quando você executa este aplicativo, ele; - -- Cria o banco de dados, se necessário. -- Aplica as migrações de banco de dados pendentes. -- Semeia os dados iniciais, se necessário. - -> Este projeto possui seu próprio `appsettings.json`arquivo. Portanto, se você deseja alterar a cadeia de conexão do banco de dados, lembre-se de alterar também esse arquivo. - -Especialmente, semear dados iniciais é importante neste momento. A ABP possui uma infraestrutura modular de semente de dados. Consulte [a documentação](../Data-Seeding.md) para obter mais informações sobre a propagação de dados. - -Embora a criação de banco de dados e a aplicação de migrações pareça necessária apenas para bancos de dados relacionais, esse projeto ocorre mesmo que você escolha um provedor de banco de dados NoSQL (como o MongoDB). Nesse caso, ele ainda semeia os dados iniciais necessários para a aplicação. - -- Depende do `.EntityFrameworkCore.DbMigrations`projeto (para EF Core), pois ele precisa acessar as migrações. -- Depende do `.Application.Contracts`projeto para poder acessar as definições de permissão, porque o semeador de dados inicial concede todas as permissões para a função de administrador por padrão. - -#### Projeto .HttpApi - -Este projeto é usado para definir seus controladores de API. - -Na maioria das vezes, você não precisa definir manualmente os controladores de API, pois o recurso de [controladores de API automática](../AspNetCore/Auto-API-Controllers.md) da ABP os cria automaticamente, com base na sua camada de aplicação. No entanto, no caso de você precisar escrever controladores de API, este é o melhor lugar para fazê-lo. - -- Depende do `.Application.Contracts`projeto para poder injetar as interfaces de serviço do aplicativo. - -#### Projeto .HttpApi.Client - -Este é um projeto que define os proxies do cliente C # para usar as APIs HTTP da solução. Você pode compartilhar essa biblioteca com clientes de terceiros, para que eles consumam facilmente suas APIs HTTP em seus aplicativos Dotnet (para outros tipos de aplicativos, eles ainda podem usar suas APIs, manualmente ou usando uma ferramenta em sua própria plataforma) - -Na maioria das vezes, você não precisa criar proxies de clientes C # manualmente, graças ao recurso [Dynamic C # API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) da ABP . - -`.HttpApi.Client.ConsoleTestApp` project é um aplicativo de console criado para demonstrar o uso dos proxies do cliente. - -- Depende do `.Application.Contracts`projeto para poder compartilhar as mesmas interfaces de serviço de aplicativo e DTOs com o serviço remoto. - -> Você pode excluir este projeto e dependências se não precisar criar proxies de cliente C # para suas APIs. - -#### Projeto .Web - -Este projeto contém a interface do usuário (UI) do aplicativo se você estiver usando a interface do usuário do ASP.NET Core MVC. Ele contém páginas Razor, arquivos JavaScript, arquivos CSS, imagens e assim por diante ... - -Este projeto contém o `appsettings.json`arquivo principal que contém a cadeia de conexão e outras configurações do aplicativo. - -- Depende da `.HttpApi`camada de interface do usuário que precisa usar APIs e interfaces de serviço de aplicativo da solução. - -> Se você verificar o código fonte do `.Web.csproj`arquivo, verá as referências aos `.Application`e aos `.EntityFrameworkCore.DbMigrations`projetos. -> -> Na verdade, essas referências não são necessárias durante a codificação da camada da interface do usuário, porque a camada da interface do usuário normalmente não depende da implementação do EF Core ou da camada do Aplicativo. Esses modelos de inicialização estão prontos para a implantação em camadas, em que a camada da API está hospedada em um servidor separado da camada da interface do usuário. -> -> No entanto, se você não escolher a opção `--tiered`, essas referências estarão no projeto .Web para poder hospedar as camadas da Web, API e aplicativos em um único ponto de extremidade do aplicativo. -> -> Isso permite que você use entidades e repositórios de domínio em sua camada de apresentação. No entanto, isso é considerado uma má prática de acordo com o DDD. - -#### Projetos de teste - -A solução possui vários projetos de teste, um para cada camada: - -- `.Domain.Tests` é usado para testar a camada de domínio. -- `.Application.Tests` é usado para testar a camada de aplicativo. -- `.EntityFrameworkCore.Tests` é usado para testar a configuração do EF Core e os repositórios personalizados. -- `.Web.Tests` é usado para testar a interface do usuário (se você estiver usando a interface do ASP.NET Core MVC). -- `.TestBase` é um projeto básico (compartilhado) para todos os testes. - -Além disso, `.HttpApi.Client.ConsoleTestApp`é um aplicativo de console (não um projeto de teste automatizado) que demonstra o uso de APIs HTTP de um aplicativo .NET. - -Projetos de teste são preparados para testes de integração; - -- É totalmente integrado à estrutura ABP e a todos os serviços em sua aplicação. -- Ele usa o banco de dados SQLite na memória para o EF Core. Para o MongoDB, ele usa a biblioteca [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) . -- A autorização está desabilitada, portanto, qualquer serviço de aplicativo pode ser facilmente usado em testes. - -Você ainda pode criar testes de unidade para suas classes, que serão mais difíceis de escrever (porque você precisará preparar objetos simulados / falsos), mas mais rápidos de executar (porque apenas testa uma única classe e ignora todo o processo de inicialização). - -#### Como correr? - -Defina `.Web`como o projeto de inicialização e execute o aplicativo. O nome de usuário padrão é `admin`e a senha é `1q2w3E*`. - -Consulte [Introdução ao modelo ASP.NET Core MVC](../Getting-Started-AspNetCore-MVC-Template.md) para obter mais informações. - -### Estrutura em camadas - -Se você selecionou a interface do usuário do ASP.NET Core e especificou a `--tiered`opção, a solução criada será uma solução em camadas. O objetivo da estrutura em camadas é poder **implantar aplicativos da Web e API HTTP em diferentes servidores** : - -![livraria-visual-studio-solution-v3](../images/tiered-solution-servers.png) - -- O navegador executa sua interface do usuário executando HTML, CSS e JavaScript. -- Os servidores da Web hospedam arquivos de interface do usuário estáticos (CSS, JavaScript, imagem ... etc.) e componentes dinâmicos (por exemplo, páginas Razor). Ele executa solicitações HTTP para o servidor da API para executar a lógica de negócios do aplicativo. -- O API Server hospeda as APIs HTTP que, em seguida, usam as camadas de aplicativo e domínio do aplicativo para executar a lógica de negócios. -- Finalmente, o servidor de banco de dados hospeda seu banco de dados. - -Portanto, a solução resultante permite uma implantação em quatro camadas, comparando com a implantação em três camadas da estrutura padrão explicada anteriormente. - -> A menos que você realmente precise de uma implantação em quatro camadas, é recomendável seguir a estrutura padrão que é mais simples de desenvolver, implantar e manter. - -A estrutura da solução é mostrada abaixo: - -![livraria-visual-studio-solution-v3](../images/bookstore-visual-studio-solution-tiered.png) - -Diferente da estrutura padrão, dois novos projetos entram em jogo: `.IdentityServer`& `.HttpApi.Host`. - -#### Projeto .IdentityServer - -Este projeto é usado como um servidor de autenticação para outros projetos. `.Web`O projeto usa a autenticação do OpenId Connect para obter tokens de identidade e acesso para o usuário atual do IdentityServer. Em seguida, usa o token de acesso para chamar o servidor da API HTTP. O servidor HTTP API usa autenticação de token de portador para obter declarações do token de acesso para autorizar o usuário atual. - -![aplicativos de solução em camadas](../images/tiered-solution-applications.png) - -O ABP usa a estrutura [IdentityServer4 de](https://identityserver.io/) código aberto para a autenticação entre aplicativos. Consulte a [documentação do IdentityServer4](http://docs.identityserver.io/) para obter detalhes sobre o protocolo IdentityServer4 e OpenID Connect. - -Ele possui seu próprio `appsettings.json`que contém conexão com o banco de dados e outras configurações. - -#### Projeto .HttpApi.Host - -Este projeto é um aplicativo que hospeda a API da solução. Ele possui seu próprio `appsettings.json`que contém conexão com o banco de dados e outras configurações. - -#### Projeto .Web - -Assim como a estrutura padrão, este projeto contém a interface do usuário (UI) do aplicativo. Ele contém páginas de barbear, arquivos JavaScript, arquivos de estilo, imagens e assim por diante ... - -Este projeto contém um `appsettings.json`arquivo, mas desta vez não possui uma cadeia de conexão porque nunca se conecta ao banco de dados. Em vez disso, ele contém principalmente o terminal do servidor de API remoto e o servidor de autenticação. - -#### Pré requisitos - -- [Redis](https://redis.io/) : os aplicativos usam Redis como cache distribuído. Então, você precisa ter o Redis instalado e funcionando. - -#### Como correr? - -Você deve executar o aplicativo com a ordem especificada: - -- Primeiro, execute o `.IdentityServer`aplicativo, pois outros aplicativos dependem dele. -- Em seguida, execute o `.HttpApi.Host`que é usado pelo `.Web`aplicativo. -- Por fim, você pode executar o `.Web`projeto e efetuar login no aplicativo (usando `admin`como nome de usuário e `1q2w3E*`senha). - -### UI angular - -Se você escolher Angular como a estrutura da interface do usuário (usando a `-u angular`opção), a solução será separada em duas pastas: - -- `angular` A pasta contém a solução Angular UI, do lado do cliente. -- `aspnet-core` A pasta contém a solução ASP.NET Core, do lado do servidor. - -O lado do servidor é muito semelhante à solução descrita acima. `.HttpApi.Host`projeto serve a API, para que o aplicativo Angular possa consumi-lo. - -Os arquivos na `angular/src/environments`pasta têm a configuração essencial do aplicativo. - -## Qual é o próximo? - -- Consulte [Introdução ao modelo ASP.NET Core MVC](../Getting-Started-AspNetCore-MVC-Template.md) para criar uma nova solução e executá-la para este modelo. -- Consulte o [Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) do [ASP.NET Core MVC](../Tutorials/AspNetCore-Mvc/Part-I.md) para aprender como desenvolver aplicativos usando este modelo. - - diff --git a/docs/pt-BR/Startup-Templates/Index.md b/docs/pt-BR/Startup-Templates/Index.md deleted file mode 100644 index 61f2d2d544..0000000000 --- a/docs/pt-BR/Startup-Templates/Index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Modelos de inicialização - -Embora você possa começar com um projeto vazio e adicionar os pacotes necessários manualmente, os modelos de inicialização facilitam e são confortáveis para iniciar uma nova solução com a estrutura ABP. Clique no nome da lista abaixo para ver a documentação do modelo de inicialização relacionado: - -- [**app**](Application.md) : modelo de aplicativo. -- [**módulo**](Module.md) : Módulo / modelo de serviço. - diff --git a/docs/pt-BR/Startup-Templates/Module.md b/docs/pt-BR/Startup-Templates/Module.md deleted file mode 100644 index 262e85c342..0000000000 --- a/docs/pt-BR/Startup-Templates/Module.md +++ /dev/null @@ -1,161 +0,0 @@ -# Modelo de inicialização do módulo MVC - -Este modelo pode ser usado para criar um **módulo de aplicativo** **reutilizável com** base nas [melhores práticas e convenções de desenvolvimento do módulo](../Best-Practices/Index.md). Também é adequado para criar **microsserviços** (com ou sem interface do usuário). - -## Como começar? - -Você pode usar a [ABP CLI](../CLI.md) para criar um novo projeto usando este modelo de inicialização. Como alternativa, você pode criar e fazer o download diretamente na página [Introdução](https://abp.io/get-started) . A abordagem CLI é usada aqui. - -Primeiro, instale a ABP CLI se você não tiver instalado antes: - -```bash -dotnet tool install -g Volo.Abp.Cli -``` - -Em seguida, use o `abp new`comando em uma pasta vazia para criar uma nova solução: - -```bash -abp new Acme.IssueManagement -t module -``` - -- `Acme.IssueManagement`é o nome da solução, como *YourCompany.YourProduct* . Você pode usar nomes de nível único, dois ou três níveis. - -### Sem interface de usuário - -O modelo vem com uma interface do usuário do MVC por padrão. Você pode usar a `--no-ui`opção para não incluir a camada da interface do usuário. - -```bash -abp new Acme.IssueManagement -t mvc-module --no-ui -``` - -## Estrutura da solução - -Com base nas opções especificadas, você obterá uma estrutura de solução ligeiramente diferente. Se você não especificar nenhuma opção, terá uma solução como a mostrada abaixo: - -![issuemanagement-module-solution](../images/issuemanagement-module-solution.png) - -Projetos são organizados como `src`, `test`e `host`pastas: - -- `src`A pasta contém o módulo real, que é estratificado com base nos princípios [DDD](../Domain-Driven-Design.md) . -- `test` pasta contém testes de unidade e integração. -- `host`A pasta contém aplicativos com configurações diferentes para demonstrar como hospedar o módulo em um aplicativo. Isso não faz parte do módulo, mas é útil no desenvolvimento. - -O diagrama abaixo mostra as camadas e dependências do projeto do módulo: - -![módulo de dependências do projeto em camadas](../images/layered-project-dependencies-module.png) - -Cada seção abaixo explicará o projeto relacionado e suas dependências. - -### Projeto .Domain.Shared - -Este projeto contém constantes, enumerações e outros objetos. Na verdade, eles fazem parte da camada de domínio, mas precisam ser usados por todas as camadas / projetos da solução. - -Um `IssueType`enum e uma `IssueConsts`classe (que podem ter alguns campos constantes para a `Issue`entidade, como `MaxTitleLength`) são bons candidatos para este projeto. - -- Este projeto não depende de outros projetos na solução. Todos os outros projetos dependem disso direta ou indiretamente. - -### .Domain Project - -Essa é a camada de domínio da solução. Ele contém principalmente [entidades, raízes agregadas](../Entities.md) , [serviços de domínio](../Domain-Services.md) , [tipos de valor](../Value-Types.md) , [interfaces de repositório](../Repositories.md) e outros objetos de domínio. - -Uma `Issue`entidade, um `IssueManager`serviço de domínio e uma `IIssueRepository`interface são bons candidatos para este projeto. - -- Depende do `.Domain.Shared`porque usa constantes, enumerações e outros objetos definidos nesse projeto. - -### .Application.Contracts Project - -Este projeto contém principalmente **interfaces de** [serviço de aplicativo](../Application-Services.md) e DTO ( [Data Transfer Objects](../Data-Transfer-Objects.md) ) da camada de aplicativo. Existe para separar a interface e a implementação da camada de aplicação. Dessa forma, o projeto de interface pode ser compartilhado com os clientes como um pacote de contrato. - -Uma `IIssueAppService`interface e uma `IssueCreationDto`classe são boas candidatas para este projeto. - -- Depende do `.Domain.Shared`porque ele pode usar constantes, enumerações e outros objetos compartilhados deste projeto nas interfaces de serviço de aplicativo e DTOs. - -### Projeto de Aplicação - -Este projeto contém as **implementações** de [serviço de aplicativo](../Application-Services.md) das interfaces definidas no projeto.`.Application.Contracts` - -Uma `IssueAppService`turma é uma boa candidata para este projeto. - -- Depende do `.Application.Contracts`projeto para poder implementar as interfaces e usar os DTOs. -- Depende do `.Domain`projeto para poder usar objetos de domínio (entidades, interfaces de repositório ... etc.) para executar a lógica do aplicativo. - -### Projeto .EntityFrameworkCore - -Este é o projeto de integração do EF Core. Ele define `DbContext`e implementa as interfaces de repositório definidas no `.Domain`projeto. - -- Depende do `.Domain`projeto para poder fazer referência a entidades e interfaces de repositório. - -> Você pode excluir este projeto se não desejar dar suporte ao EF Core para o seu módulo. - -### Projeto .MongoDB - -Este é o projeto de integração do MongoDB. - -- Depende do `.Domain`projeto para poder fazer referência a entidades e interfaces de repositório. - -> Você pode excluir este projeto se não quiser dar suporte ao MongoDB para o seu módulo. - -### Projetos de teste - -A solução possui vários projetos de teste, um para cada camada: - -- `.Domain.Tests` é usado para testar a camada de domínio. -- `.Application.Tests` é usado para testar a camada de aplicativo. -- `.EntityFrameworkCore.Tests` é usado para testar a configuração do EF Core e os repositórios personalizados. -- `.MongoDB.Tests` é usado para testar a configuração do MongoDB e os repositórios personalizados. -- `.TestBase` é um projeto básico (compartilhado) para todos os testes. - -Além disso, `.HttpApi.Client.ConsoleTestApp`é um aplicativo de console (não um projeto de teste automatizado) que demonstra o uso de APIs HTTP de um aplicativo Dotnet. - -Projetos de teste são preparados para testes de integração; - -- É totalmente integrado à estrutura ABP e a todos os serviços em sua aplicação. -- Ele usa o banco de dados SQLite na memória para o EF Core. Para o MongoDB, ele usa a biblioteca [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) . -- A autorização está desabilitada, portanto, qualquer serviço de aplicativo pode ser facilmente usado em testes. - -Você ainda pode criar testes de unidade para suas classes, que serão mais difíceis de escrever (porque você precisará preparar objetos simulados / falsos), mas mais rápidos de executar (porque apenas testa uma única classe e ignora todo o processo de inicialização). - -> Os testes de domínio e aplicativos estão usando o EF Core. Se você remover a integração do EF Core ou desejar usar o MongoDB para testar essas camadas, altere manualmente as referências do projeto e as dependências do módulo. - -### Projetos Anfitriões - -A solução possui alguns aplicativos host para executar seu módulo. Aplicativos host são usados para executar seu módulo em um aplicativo totalmente configurado. É útil no desenvolvimento. Os aplicativos host incluem alguns outros módulos além do módulo que está sendo desenvolvido: - -Os aplicativos host oferecem suporte a dois tipos de cenários. - -#### Cenário de aplicativo único (unificado) - -Se o seu módulo tiver uma interface do usuário, o `.Web.Unified`aplicativo será usado para hospedar a interface do usuário e a API em um único ponto. Ele possui seu próprio `appsettings.json`arquivo (que inclui a cadeia de conexão do banco de dados) e as migrações do banco de dados EF Core. - -Para o `.Web.Unified`aplicativo, há um único banco de dados chamado `YourProjectName_Unified`(como *IssueManagement_Unified* para esta amostra). - -> Se você selecionou a `--no-ui`opção, este projeto não estará na sua solução. - -##### Como correr? - -Defina-o como o projeto de inicialização, execute o `Update-Database`comando para o EF Core no Package Manager Console e execute seu aplicativo. O nome de usuário padrão é `admin`e a senha é `1q2w3E*`. - -#### Implantação separada e cenário de bancos de dados - -Nesse cenário, há três aplicativos; - -- `.IdentityServer`application é um servidor de autenticação usado por outros aplicativos. Ele possui seu próprio `appsettings.json`que contém conexão com o banco de dados e outras configurações. -- `.HttpApi.Host`hospeda a API HTTP do módulo. Ele possui seu próprio `appsettings.json`que contém conexões com o banco de dados e outras configurações. -- `.Web.Host`hospedar a interface do usuário do módulo. Este projeto contém um `appsettings.json`arquivo, mas não possui uma cadeia de conexão porque nunca se conecta ao banco de dados. Em vez disso, ele contém principalmente o terminal do servidor de API remoto e o servidor de autenticação. - -O diagrama abaixo mostra a relação dos aplicativos: - -![aplicativos de solução em camadas](../images/tiered-solution-applications.png) - -`.Web.Host`O projeto usa a autenticação OpenId Connect para obter tokens de identidade e acesso para o usuário atual do `.IdentityServer`. Em seguida, usa o token de acesso para chamar o `.HttpApi.Host`. O servidor HTTP API usa autenticação de token de portador para obter declarações do token de acesso para autorizar o usuário atual. - -##### Como correr? - -Você deve executar o aplicativo com a ordem especificada: - -- Primeiro, execute o `.IdentityServer`aplicativo, pois outros aplicativos dependem dele. -- Em seguida, execute o `.HttpApi.Host`que é usado pelo `.Web.Host`aplicativo. -- Por fim, você pode executar o `.Web.Host`projeto e efetuar login no aplicativo usando `admin`como nome de usuário e `1q2w3E*`senha. - - - \ No newline at end of file diff --git a/docs/pt-BR/Testing.md b/docs/pt-BR/Testing.md deleted file mode 100644 index 231db64907..0000000000 --- a/docs/pt-BR/Testing.md +++ /dev/null @@ -1,3 +0,0 @@ -# Testing - -Façam! \ No newline at end of file diff --git a/docs/pt-BR/Tutorials/Angular/Part-I.md b/docs/pt-BR/Tutorials/Angular/Part-I.md deleted file mode 100644 index 3b0b1daf1b..0000000000 --- a/docs/pt-BR/Tutorials/Angular/Part-I.md +++ /dev/null @@ -1,663 +0,0 @@ -## Tutorial Angular - Parte I - -### Sobre este tutorial - -Nesta série de tutoriais, você criará um aplicativo usado para gerenciar uma lista de livros e seus autores. **Angular** será usado como estrutura da interface do usuário e **MongoDB** será usado como provedor de banco de dados. - -Esta é a primeira parte da série de tutoriais angulares. Veja todas as peças: - -- **Parte I: Crie o projeto e uma página de lista de livros (este tutorial)** -- [Parte II: Criar, atualizar e excluir livros](Part-II) -- [Parte III: Testes de Integração](Part-III) - -Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) . - -### Criando o projeto - -Crie um novo projeto nomeado `Acme.BookStore`selecionando Angular como a estrutura da interface do usuário e MongoDB como o provedor de banco de dados, crie o banco de dados e execute o aplicativo seguindo o [documento Introdução](../../Getting-Started-Angular-Template.md) . - -### Estrutura da solução (back-end) - -É assim que a estrutura da solução em camadas cuida da criação: - -![solução de back-end da livraria](images/bookstore-backend-solution-v2.png) - -> Você pode ver o [documento do modelo de aplicativo](../../Startup-Templates/Application.md) para entender a estrutura da solução em detalhes. No entanto, você entenderá o básico com este tutorial. - -### Criar a entidade do livro - -A camada de domínio no modelo de inicialização é separada em dois projetos: - -- `Acme.BookStore.Domain`contém suas [entidades](../../Entities.md) , [serviços de domínio](../../Domain-Services.md) e outros objetos principais de domínio. -- `Acme.BookStore.Domain.Shared` contém constantes, enumerações ou outros objetos relacionados ao domínio que podem ser compartilhados com os clientes. - -Defina [entidades](../../Entities.md) na **camada de domínio** ( `Acme.BookStore.Domain`projeto) da solução. A entidade principal do aplicativo é a `Book`. Crie uma classe, chamada `Book`, no `Acme.BookStore.Domain`projeto, como mostrado abaixo: - -```csharp -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- O ABP possui duas classes base fundamentais para entidades: `AggregateRoot`e `Entity`. **A raiz agregada** é um dos conceitos de **DDD (Domain Driven Design)** . Consulte o [documento da entidade](../../Entities.md) para obter detalhes e melhores práticas. -- `Book`entidade herda `AuditedAggregateRoot`que adiciona algumas propriedades de auditoria ( `CreationTime`, `CreatorId`, `LastModificationTime`... etc.) no topo da `AggregateRoot`classe. -- `Guid`é o **tipo** de **chave primária** da `Book`entidade. - -#### BookType Enum - -Defina a `BookType`enumeração no `Acme.BookStore.Domain.Shared`projeto: - -```csharp -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -``` - -#### Adicionar entidade de livro ao seu DbContext - -Adicione uma `IMongoCollection`propriedade ao `BookStoreMongoDbContext`interior do `Acme.BookStore.MongoDB`projeto: - -```csharp -public class BookStoreMongoDbContext : AbpMongoDbContext -{ - public IMongoCollection Books => Collection(); - ... -} -``` - -#### Adicionar dados de semente (amostra) - -Esta seção é opcional, mas seria bom ter um dado inicial no banco de dados na primeira execução. O ABP fornece um [sistema de semente de dados](../../Data-Seeding.md) . Crie uma classe derivada de `IDataSeedContributor`no `.Domain`projeto: - -```csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - } - ); - } - } -} -``` - -`BookStoreDataSeederContributor`simplesmente insere dois livros no banco de dados se não houver nenhum livro adicionado antes. O ABP descobre e executa automaticamente essa classe quando você propaga o banco de dados executando o `Acme.BookStore.DbMigrator`projeto. - -### Crie o serviço de aplicativo - -O próximo passo é criar um [serviço de aplicativo](../../Application-Services.md) para gerenciar (criar, listar, atualizar, excluir ...) os livros. A camada de aplicativo no modelo de inicialização é separada em dois projetos: - -- `Acme.BookStore.Application.Contracts` contém principalmente seus DTOs e interfaces de serviço de aplicativo. -- `Acme.BookStore.Application` contém as implementações dos seus serviços de aplicativo. - -#### BookDto - -Crie uma classe DTO denominada `BookDto`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- **As** classes **DTO** são usadas para **transferir dados** entre a *camada de apresentação* e a *camada de aplicativo* . Consulte o [documento Objetos de transferência de dados](../../Data-Transfer-Objects.md) para obter mais detalhes. -- `BookDto` é usado para transferir dados do livro para a camada de apresentação para mostrar as informações do livro na interface do usuário. -- `BookDto`é derivado do `AuditedEntityDto`que possui propriedades de auditoria exatamente como a `Book`classe definida acima. - -Será necessário converter `Book`entidades em `BookDto`objetos enquanto retorna os livros para a camada de apresentação. A biblioteca do [AutoMapper](https://automapper.org/) pode automatizar essa conversão quando você define o mapeamento adequado. O modelo de inicialização é fornecido com o AutoMapper configurado, para que você possa definir o mapeamento na `BookStoreApplicationAutoMapperProfile`classe no `Acme.BookStore.Application`projeto: - -```csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -``` - -#### CreateUpdateBookDto - -Crie uma classe DTO denominada `CreateUpdateBookDto`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -``` - -- Essa classe DTO é usada para obter informações do livro a partir da interface do usuário ao criar ou atualizar um livro. -- Ele define atributos de anotação de dados (como `[Required]`) para definir validações para as propriedades. Os DTOs são [validados automaticamente](../../Validation.md) pela estrutura ABP. - -Em seguida, adicione um mapeamento `BookStoreApplicationAutoMapperProfile`do `CreateUpdateBookDto`objeto à `Book`entidade: - -```csharp -CreateMap(); -``` - -#### IBookAppService - -Defina uma interface nomeada `IBookAppService`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -``` - -- A definição de interfaces para serviços de aplicativos não é requerida pela estrutura. No entanto, é sugerido como uma prática recomendada. -- `ICrudAppService`define comuns **CRUD** métodos: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync`e `DeleteAsync`. Não é necessário estendê-lo. Em vez disso, você pode herdar da `IApplicationService`interface vazia e definir seus próprios métodos manualmente. -- Existem algumas variações de `ICrudAppService`onde você pode usar DTOs separados para cada método. - -#### BookAppService - -Implemente `IBookAppService`como nomeado `BookAppService`no `Acme.BookStore.Application`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -``` - -- `BookAppService`é derivado do `CrudAppService<...>`qual implementa todos os métodos CRUD definidos acima. -- `BookAppService`injeta `IRepository`qual é o repositório padrão da `Book`entidade. O ABP cria automaticamente repositórios padrão para cada raiz (ou entidade) agregada. Veja o [documento do repositório](../../Repositories) . -- `BookAppService`usa `IObjectMapper`para converter `Book`objetos em `BookDto`objetos e `CreateUpdateBookDto`objetos em `Book`objetos. O modelo de inicialização usa a biblioteca [AutoMapper](http://automapper.org/) como o provedor de mapeamento de objetos. Você definiu os mapeamentos antes, para que funcionem conforme o esperado. - -### Controladores de API automática - -Você normalmente cria **controladores** para expor serviços de aplicativos como pontos de extremidade da **API HTTP** . Assim, permite que navegadores ou clientes de terceiros os chamem via AJAX. O ABP pode configurar [**automaticamente**](../../AspNetCore/Auto-API-Controllers.md) seus serviços de aplicativo como controladores de API MVC por convenção. - -#### UI do Swagger - -O modelo de inicialização está configurado para executar a [interface do usuário do swagger](https://swagger.io/tools/swagger-ui/) usando a biblioteca [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) . Execute o `Acme.BookStore.HttpApi.Host`aplicativo e insira `https://localhost:XXXX/swagger/`(substitua XXXX por sua própria porta) como URL no seu navegador. - -Você verá alguns pontos de extremidade de serviço internos, bem como o `Book`serviço e seus pontos de extremidade no estilo REST: - -![livraria-arrogância](images/bookstore-swagger-api.png) - -O Swagger tem uma ótima interface para testar APIs. Você pode tentar executar a `[GET] /api/app/book`API para obter uma lista de livros. - -### Crie a página de livros - -Neste tutorial; - -- [A CLI angular](https://angular.io/cli) será usada para criar módulos, componentes e serviços -- [NGXS](https://ngxs.gitbook.io/ngxs/) será usado como a biblioteca de gerenciamento de estado -- [O Bootstrap](https://ng-bootstrap.github.io/#/home) será usado como a biblioteca de componentes da interface do usuário. -- [O Visual Studio Code](https://code.visualstudio.com/) será usado como editor de código (você pode usar seu editor favorito). - -#### Instalar pacotes NPM - -Abra uma janela do terminal, vá para a `angular`pasta e execute o `yarn` comando para instalar os pacotes NPM: - -``` -yarn -``` - -#### BooksModule - -Execute a seguinte linha de comando para criar um novo módulo, denominado `BooksModule`: - -```bash -yarn ng generate module books --route books --module app.module -``` - -![Creating-Books-Module.terminal](images/bookstore-creating-books-module-terminal.png) - -Execute `yarn start`, aguarde Angular para executar o aplicativo e abra `http://localhost:4200/books`em um navegador: - -![página inicial dos livros](images/bookstore-initial-books-page.png) - -#### Encaminhamento - -Abra `app-routing.module.ts`e substitua `books`conforme mostrado abaixo: - -```js -import { ApplicationLayoutComponent } from '@abp/ng.theme.basic';- - -//... -{ - path: 'books', - component: ApplicationLayoutComponent, - loadChildren: () => import('./books/books.module').then(m => m.BooksModule), - data: { - routes: { - name: 'Books', - } as ABP.Route, - }, -}, -``` - -`ApplicationLayoutComponent`configuração define o layout do aplicativo para a nova página. Se você deseja ver sua rota na barra de navegação (menu principal), também deve adicionar o `data`objeto com `name`propriedade à sua rota. - -![página inicial dos livros](images/bookstore-initial-books-page-with-layout.png) - -#### Componente da lista de livros - -Primeiro, substitua pela `books.component.html`seguinte linha para colocar a saída do roteador: - -```html - -``` - -Em seguida, execute o comando abaixo no terminal na pasta raiz para gerar um novo componente, chamado book-list: - -```bash -yarn ng generate component books/book-list -``` - -![terminal-criando-lista-de-livros](images/bookstore-creating-book-list-terminal.png) - -Importe `SharedModule`para `BooksModule`para reutilizar alguns componentes e serviços definidos em: - -```js -import { SharedModule } from '../shared/shared.module'; - -@NgModule({ - //... - imports: [ - //... - SharedModule, - ], -}) -export class BooksModule {} -``` - -Em seguida, atualize o `routes`no `books-routing.module.ts`para adicionar o novo componente book-list: - -```js -import { BookListComponent } from './book-list/book-list.component'; - -const routes: Routes = [ - { - path: '', - component: BooksComponent, - children: [{ path: '', component: BookListComponent }], - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class BooksRoutingModule {} -``` - -![página inicial da lista de livros](images/bookstore-initial-book-list-page.png) - -#### Criar BooksState - -Execute o seguinte comando no terminal para criar um novo estado, denominado `BooksState`: - -```shell -yarn ng generate ngxs-schematic:state books -``` - -Este comando cria vários novos arquivos e edições `app.modules.ts`para importar o `NgxsModule`com o novo estado: - -```js -// app.module.ts - -import { BooksState } from './store/states/books.state'; - -@NgModule({ - imports: [ - //... - NgxsModule.forRoot([BooksState]), - ], - //... -}) -export class AppModule {} -``` - -#### Obter dados de livros do back-end - -Primeiro, crie tipos de dados para mapear os dados que retornam do back-end (você pode verificar a interface do swagger ou a API do back-end para conhecer o formato dos dados). - -Modifique o `books.ts`como mostrado abaixo: - -```js -export namespace Books { - export interface State { - books: Response; - } - - export interface Response { - items: Book[]; - totalCount: number; - } - - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } - - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } -} -``` - -Adicionada `Book`interface que representa um objeto de livro e `BookType`enum representa uma categoria de livro. - -#### BooksService - -Agora, crie um novo serviço, nomeado `BooksService`para executar chamadas HTTP para o servidor: - -```bash -yarn ng generate service books/shared/books -``` - -![serviço-terminal-saída](images/bookstore-service-terminal-output.png) - -Modifique `books.service.ts`como mostrado abaixo: - -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } -} -``` - -Adicionado o `get`método para obter a lista de livros executando uma solicitação HTTP no terminal relacionado. - -Substitua o `books.actions.ts`conteúdo conforme mostrado abaixo: - -```js -export class GetBooks { - static readonly type = '[Books] Get'; -} -``` - -#### Implementar o BooksState - -Abra o `books.state.ts`e altere o arquivo, como mostrado abaixo: - -```js -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from '../actions/books.actions'; -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; - -@State({ - name: 'BooksState', - defaults: { books: {} } as Books.State, -}) -@Injectable() -export class BooksState { - @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; - } - - constructor(private booksService: BooksService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { - ctx.patchState({ - books: booksResponse, - }); - }), - ); - } -} -``` - -Adicionada a `GetBooks`ação que usa o `BookService`definido acima para obter os livros e corrigir o estado. - -> O NGXS exige retornar o observável sem assiná-lo, conforme feito nesta amostra (na função get). - -#### BookListComponent - -Modifique o `book-list.component.ts`como mostrado abaixo: - -```js -import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; -import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], -}) -export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) - books$: Observable; - - booksType = Books.BookType; - - loading = false; - - constructor(private store: Store) {} - - ngOnInit() { - this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); - } -} -``` - -> Consulte as [ações de despacho](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) e [selecione](https://ngxs.gitbook.io/ngxs/concepts/select) na documentação do NGXS para obter mais informações sobre esses recursos do NGXS. - -Substitua o `book-list.component.html`conteúdo conforme mostrado abaixo: - -```html -
    -
    -
    -
    -
    - Books -
    -
    -
    -
    -
    - - - - Book name - Book type - Publish date - Price - - - - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - - -
    -
    -``` - -> Usamos a [tabela PrimeNG](https://www.primefaces.org/primeng/#/table) neste componente. - -A página de livros resultante é mostrada abaixo: - -![livraria-lista-de-livros](images/bookstore-book-list.png) - -E esta é a estrutura de pastas e arquivos no final deste tutorial: - -![img](images/bookstore-angular-file-tree.png) - -> Este tutorial segue o [Guia de estilo angular](https://angular.io/guide/styleguide#file-tree) . - -### Próxima parte - -Veja a [próxima parte](Part-II.md) deste tutorial. - - - \ No newline at end of file diff --git a/docs/pt-BR/Tutorials/Angular/Part-II.md b/docs/pt-BR/Tutorials/Angular/Part-II.md deleted file mode 100644 index f948bca55a..0000000000 --- a/docs/pt-BR/Tutorials/Angular/Part-II.md +++ /dev/null @@ -1,582 +0,0 @@ -## Tutorial Angular - Parte II - -### Sobre este tutorial - -Esta é a segunda parte da série de tutoriais angulares. Veja todas as peças: - -- [Parte I: Crie o projeto e uma página da lista de livros](Part-I.md) -- **Parte II: Criar, atualizar e excluir livros (este tutorial)** -- [Parte III: Testes de Integração](Part-III.md) - -Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) . - -### Criando um novo livro - -Nesta seção, você aprenderá como criar um novo formulário de diálogo modal para criar um novo livro. - -#### Definição do tipo - -Criar uma interface, com o nome `CreateUpdateBookInput`no `books.ts`como mostrado abaixo: - -```js -export namespace Books { - //... - export interface CreateUpdateBookInput { - name: string; - type: BookType; - publishDate: string; - price: number; - } -} -``` - -`CreateUpdateBookInput`interface corresponde ao `CreateUpdateBookDto`no back-end. - -#### Método de Serviço - -Abra o `books.service.ts`e adicione um novo método, nomeado `create`para executar uma solicitação HTTP POST no servidor: - -```js -create(createBookInput: Books.CreateUpdateBookInput): Observable { - return this.restService.request({ - method: 'POST', - url: '/api/app/book', - body: createBookInput - }); -} -``` - -- `restService.request`A função obtém parâmetros genéricos para os tipos enviados e recebidos do servidor. Este exemplo envia um `CreateUpdateBookInput`objeto e recebe um `Book`objeto (você pode definir o tipo `void`de solicitação ou retorno, se não for usado). - -#### Definições de estado - -Adicione a `CreateUpdateBook`ação ao `books.actions.ts`conforme mostrado abaixo: - -```js -import { Books } from '../models'; - -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput) {} -} -``` - -Abra `books.state.ts`e defina o `save`método que ouvirá uma `CreateUpdateBook`ação para criar um livro: - -```js -import { ... , CreateUpdateBook } from '../actions/books.actions'; -import { ... , switchMap } from 'rxjs/operators'; -//... -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - return this.booksService - .create(action.payload) - .pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -Quando a `SaveBook`ação é despachada, o método save é executado. Ele chama o `create`método do `BooksService`definido anteriormente. Após a chamada de serviço, `BooksState`despacha a `GetBooks`ação para obter livros novamente do servidor para atualizar a página. - -#### Adicionar um modal ao BookListComponent - -Abra o `book-list.component.html`e adicione o `abp-modal`para mostrar / ocultar o modal para criar um novo livro. - -```html - - -

    New Book

    -
    - - - - - - -
    -``` - -`abp-modal`é um componente pré-construído para mostrar os modais. Embora você possa usar outra abordagem para mostrar um modal, `abp-modal`fornece benefícios adicionais. - -Adicione um botão rotulado `New book`para mostrar o modal: - -```html -
    -
    -
    - Books -
    -
    -
    - -
    -
    -``` - -Abra a variável `book-list.component.ts`e adicione `isModalOpen`e `createBook`método para mostrar / ocultar o modal. - -```js -isModalOpen = false; - -//... - -createBook() { - this.isModalOpen = true; -} -``` - -![modal vazio](images/bookstore-empty-new-book-modal.png) - -#### Criar um formulário reativo - -> [Os formulários reativos](https://angular.io/guide/reactive-forms) fornecem uma abordagem orientada a modelo para lidar com entradas de formulário cujos valores mudam ao longo do tempo. - -Adicione uma `form`variável e injete um `FormBuilder`serviço `book-list.component.ts`como mostrado abaixo (lembre-se de adicionar a instrução de importação). - -```js -import { FormGroup, FormBuilder } from '@angular/forms'; - -form: FormGroup; - -constructor( - //... - private fb: FormBuilder -) {} -``` - -> O serviço [FormBuilder](https://angular.io/api/forms/FormBuilder) fornece métodos convenientes para gerar controles. Reduz a quantidade de clichê necessária para criar formulários complexos. - -Adicione o `buildForm`método para criar um formulário de livro. - -```js -buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); -} -``` - -- O `group`método de `FormBuilder`( `fb`) cria a `FormGroup`. -- Adicionado `Validators.required`método estático que valida o elemento de formulário relacionado. - -Modifique o `createBook`método como mostrado abaixo: - -```js -createBook() { - this.buildForm(); - this.isModalOpen = true; -} -``` - -#### Crie os elementos DOM do formulário - -Abra `book-list.component.html`e adicione o formulário no modelo de corpo do modal. - -```html - -
    -
    - * - -
    - -
    - * - -
    - -
    - * - -
    - -
    - * - -
    -
    -
    -``` - -- Este modelo cria um formulário com os campos Nome, Preço, Tipo e Data de publicação. - -> Usamos o [datepicker do NgBootstrap](https://ng-bootstrap.github.io/#/components/datepicker/overview) neste componente. - -Abra o `book-list.component.ts`e crie uma matriz chamada `bookTypes`: - -```js -//... -form: FormGroup; - -bookTypes = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' -); -``` - -O `bookTypes`contém os campos da `BookType`enumeração. A matriz resultante é mostrada abaixo: - -```js -['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] -``` - -Essa matriz foi usada no modelo de formulário anterior (no `ngFor`loop). - -#### Requisitos do Datepicker - -Você precisa importar `NgbDatepickerModule`para o `books.module.ts`: - -```js -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - imports: [ - // ... - NgbDatepickerModule, - ], -}) -export class BooksModule {} -``` - -Abra o `book-list.component.ts`e adicione `providers`como mostrado abaixo: - -```js -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - // ... - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookListComponent implements OnInit { -// ... -``` - -> O `NgbDateAdapter`valor do Datepicker converte em `Date`tipo. Consulte os [adaptadores datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) para obter mais detalhes. - -![forma de livro novo](images/bookstore-new-book-form.png) - -#### Salvando o livro - -Abra o `book-list.component.html`e adicione um `abp-button`para salvar o formulário. - -```html - - - - -``` - -Isso adiciona um botão Salvar à área inferior do modal: - -![livraria-novo-livro-formulário-v2](images/bookstore-new-book-form-v2.png) - -Em seguida, defina um `save`método no `BookListComponent`: - -```js -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -### Atualizando um livro existente - -#### BooksService - -Abra o `books.service.ts`e adicione os métodos `getById`e `update`. - -```js -getById(id: string): Observable { - return this.restService.request({ - method: 'GET', - url: `/api/app/book/${id}` - }); -} - -update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { - return this.restService.request({ - method: 'PUT', - url: `/api/app/book/${id}`, - body: updateBookInput - }); -} -``` - -#### Ação CreateUpdateBook - -Abra o parâmetro `books.actins.ts`e adicione `id`à `CreateUpdateBook`ação: - -```js -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} -} -``` - -Abra `books.state.ts`e modifique o `save`método conforme mostrado abaixo: - -```js -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - let request; - - if (action.id) { - request = this.booksService.update(action.payload, action.id); - } else { - request = this.booksService.create(action.payload); - } - - return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -#### BookListComponent - -Injectar `BooksService`dependência, adicionando-o ao `book-list.component.ts`construtor e adicione uma variável chamada `selectedBook`. - -```js -import { BooksService } from '../shared/books.service'; -//... -selectedBook = {} as Books.Book; - -constructor( - //... - private booksService: BooksService -) -``` - -`booksService`é usado para obter o livro de edição para preparar o formulário. Modifique o `buildForm`método para reutilizar o mesmo formulário ao editar um livro. - -```js -buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: this.selectedBook.type || null, - publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - price: this.selectedBook.price || null, - }); -} -``` - -Adicione o `editBook`método como mostrado abaixo: - -```js - editBook(id: string) { - this.booksService.getById(id).subscribe(book => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } -``` - -Adicionado `editBook`método para obter o livro de edição, criar o formulário e mostrar o modal. - -Agora, adicione a `selectedBook`definição ao `createBook`método para reutilizar o mesmo formulário ao criar um novo livro: - -```js - createBook() { - this.selectedBook = {} as Books.Book; - //... - } -``` - -Modifique o `save`método para passar o ID do livro selecionado, como mostrado abaixo: - -```js -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -#### Adicione o menu suspenso "Ações" à tabela - -Abra o `book-list.component.html` e adicione modifique o `p-table` como mostrado abaixo: - -```html - - - - Actions - Book name - Book type - Publish date - Price - - - - - -
    - -
    - -
    -
    - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - -
    -
    -``` - -- Adicionado um `th`para a coluna "Ações". -- Adicionado `button`com `ngbDropdownToggle`para abrir ações quando clicamos no botão. - -> Nós costumávamos usar o [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) no menu suspenso de ações. - -A interface do usuário final é semelhante a: - -![botões de ações](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png) - -Atualize o cabeçalho modal para alterar o título com base na operação atual: - -```html - -

    {%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}

    -
    -``` - -![botões de ações](images/bookstore-edit-modal.png) - -### Exclusão de um livro existente - -#### BooksService - -Abra `books.service.ts`e inclua um `delete`método para excluir um livro com o `id`, executando uma solicitação HTTP no nó de extremidade relacionado: - -```js -delete(id: string): Observable { - return this.restService.request({ - method: 'DELETE', - url: `/api/app/book/${id}` - }); -} -``` - -#### Ação DeleteBook - -Adicione uma ação chamada `DeleteBook`para `books.actions.ts`: - -```js -export class DeleteBook { - static readonly type = '[Books] Delete'; - constructor(public id: string) {} -} -``` - -Abra o `books.state.ts`e adicione o `delete`método que ouvirá a `DeleteBook`ação para excluir um livro: - -```js -import { ... , DeleteBook } from '../actions/books.actions'; -//... -@Action(DeleteBook) -delete(ctx: StateContext, action: DeleteBook) { - return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -- Adicionado `DeleteBook`à lista de importação. -- Usa `bookService`para excluir o livro. - -\#### Adicionar um botão Excluir - -Abra `book-list.component.html`e modifique `ngbDropdownMenu`para adicionar o botão excluir, como mostrado abaixo: - -```html -
    - ... - -
    -``` - -A interface do usuário suspensa de ações finais é semelhante a abaixo: - -![livraria-final-ações-suspensa](images/bookstore-final-actions-dropdown.png) - -\#### Caixa de diálogo Excluir confirmação - -Abra `book-list.component.ts`e injete o `ConfirmationService`. - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; -//... -constructor( - //... - private confirmationService: ConfirmationService -) -``` - -> `ConfirmationService` é um serviço simples fornecido pela estrutura ABP que usa internamente o PrimeNG. - -Adicione um método de exclusão ao `BookListComponent`: - -```js -import { ... , DeleteBook } from '../../store/actions'; -import { ... , Toaster } from '@abp/ng.theme.shared'; -//... -delete(id: string, name: string) { - this.confirmationService - .error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') - .subscribe(status => { - if (status === Toaster.Status.confirm) { - this.store.dispatch(new DeleteBook(id)); - } - }); -} -``` - -O `delete`método mostra um pop-up de confirmação e assina a resposta do usuário. `DeleteBook`ação despachada somente se o usuário clicar no `Yes`botão O pop-up de confirmação é exibido abaixo: - -![livraria-confirmação-pop-up](images/bookstore-confirmation-popup.png) - -### Próxima parte - -Veja a [próxima parte](Part-III.md) deste tutorial. - diff --git a/docs/pt-BR/Tutorials/Angular/Part-III.md b/docs/pt-BR/Tutorials/Angular/Part-III.md deleted file mode 100644 index e51082e176..0000000000 --- a/docs/pt-BR/Tutorials/Angular/Part-III.md +++ /dev/null @@ -1,181 +0,0 @@ -## Tutorial do ASP.NET Core MVC - Parte III - -### Sobre este tutorial - -Esta é a terceira parte da série de tutoriais Angular. Veja todas as peças: - -- [Parte I: Crie o projeto e uma página da lista de livros](Part-I.md) -- [Parte II: Criar, atualizar e excluir livros](Part-II.md) -- **Parte III: Testes de Integração (este tutorial)** - -Esta parte abrange os testes do **lado** do **servidor** . Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) . - -### Testar projetos na solução - -Existem vários projetos de teste na solução: - -![livraria-teste-projetos](images/bookstore-test-projects-v3.png) - -Cada projeto é usado para testar o projeto de aplicativo relacionado. Os projetos de teste usam as seguintes bibliotecas para teste: - -- [xunit](https://xunit.github.io/) como a principal estrutura de teste. -- [Shouldly](http://shouldly.readthedocs.io/en/latest/) como uma biblioteca de asserções. -- [NSubstitute](http://nsubstitute.github.io/) como uma biblioteca de zombaria. - -### Adicionando dados de teste - -O modelo de inicialização contém a `BookStoreTestDataSeedContributor`classe no `Acme.BookStore.TestBase`projeto que cria alguns dados para executar os testes. - -Mude a `BookStoreTestDataSeedContributor`classe como mostrado abaixo: - -```csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 1", - Type = BookType.Fantastic, - PublishDate = new DateTime(2015, 05, 24), - Price = 21 - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 2", - Type = BookType.Science, - PublishDate = new DateTime(2014, 02, 11), - Price = 15 - } - ); - } - } -} -``` - -- Injetado `IRepository`e usado no `SeedAsync`para criar duas entidades de livro como dados de teste. -- `IGuidGenerator`Serviço usado para criar GUIDs. Embora `Guid.NewGuid()`funcionasse perfeitamente para testes, `IGuidGenerator`possui recursos adicionais especialmente importantes ao usar bancos de dados reais (consulte o documento de geração do [Guid](../../Guid-Generation.md) para obter mais informações). - -### Testando o BookAppService - -Crie uma classe de teste denominada `BookAppService_Tests`no `Acme.BookStore.Application.Tests`projeto: - -```csharp -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -``` - -- `Should_Get_List_Of_Books`O teste simplesmente usa o `BookAppService.GetListAsync`método para obter e verificar a lista de usuários. - -Adicione um novo teste que crie um novo livro válido: - -```csharp -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -``` - -Adicione um novo teste que tente criar um livro inválido e falhe: - -```csharp -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -``` - -- Como o `Name`está vazio, o ABP lança um `AbpValidationException`. - -Abra a **janela Test Explorer** (use o menu Test -> Windows -> Test Explorer, se não estiver visível) e **execute Todos os** testes: - -![testes de serviço de livraria](images/bookstore-test-explorer.png) - -Parabéns, ícones verdes mostram que os testes foram aprovados com sucesso! - - - \ No newline at end of file diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-actions-buttons.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-actions-buttons.png deleted file mode 100644 index e09aad6400..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-actions-buttons.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-angular-file-tree.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-angular-file-tree.png deleted file mode 100644 index ec117a46b6..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-angular-file-tree.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-backend-solution-v2.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-backend-solution-v2.png deleted file mode 100644 index 7160300deb..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-backend-solution-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-book-list.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-book-list.png deleted file mode 100644 index b80410a0ef..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-confirmation-popup.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-confirmation-popup.png deleted file mode 100644 index 47c32d9246..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-confirmation-popup.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png deleted file mode 100644 index 63b3cbaed8..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index ac11bed270..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-edit-modal.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-edit-modal.png deleted file mode 100644 index 3a1b37511d..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-edit-modal.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-empty-new-book-modal.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-empty-new-book-modal.png deleted file mode 100644 index 58b34a0bc2..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-empty-new-book-modal.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-final-actions-dropdown.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-final-actions-dropdown.png deleted file mode 100644 index 7d1bdfb006..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-final-actions-dropdown.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-book-list-page.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-book-list-page.png deleted file mode 100644 index c66dad8bf8..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-book-list-page.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png deleted file mode 100644 index 317857f0f0..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page.png deleted file mode 100644 index 9044eac641..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-initial-books-page.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form-v2.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form-v2.png deleted file mode 100644 index 9c06825eea..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form.png deleted file mode 100644 index aecc1d4a1a..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-new-book-form.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-service-terminal-output.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-service-terminal-output.png deleted file mode 100644 index 5bbdb6560b..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-service-terminal-output.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-swagger-api.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-swagger-api.png deleted file mode 100644 index 83c416618a..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-swagger-api.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-test-explorer.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-test-explorer.png deleted file mode 100644 index 004b5bf089..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-test-explorer.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/Angular/images/bookstore-test-projects-v3.png b/docs/pt-BR/Tutorials/Angular/images/bookstore-test-projects-v3.png deleted file mode 100644 index 42cb175da1..0000000000 Binary files a/docs/pt-BR/Tutorials/Angular/images/bookstore-test-projects-v3.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-I.md deleted file mode 100644 index f48ac88f37..0000000000 --- a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-I.md +++ /dev/null @@ -1,459 +0,0 @@ -## Tutorial do ASP.NET Core MVC - Parte I - -### Sobre este tutorial - -Nesta série de tutoriais, você criará um aplicativo usado para gerenciar uma lista de livros e seus autores. **O Entity Framework Core** (EF Core) será usado como o provedor ORM, pois é o provedor de banco de dados padrão. - -Esta é a primeira parte da série de tutoriais do ASP.NET Core MVC. Veja todas as peças: - -- **Parte I: Crie o projeto e uma página de lista de livros (este tutorial)** -- [Parte II: Criar, atualizar e excluir livros](Part-II.md) -- [Parte III: Testes de Integração](Part-III.md) - -Você pode acessar o **código fonte** do aplicativo [no repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore) . - -> Você também pode assistir a [este curso em vídeo](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) preparado por um membro da comunidade ABP, com base neste tutorial. - -### Criando o projeto - -Crie um novo projeto chamado `Acme.BookStore`, crie o banco de dados e execute o aplicativo seguindo o [documento Introdução](Getting-Started-AspNetCore-MVC-Template.md). - -### Estrutura da solução - -É assim que a estrutura da solução em camadas cuida da criação: - -![livraria-visual-studio-solução](images/bookstore-visual-studio-solution-v3.png) - -> Você pode ver o [documento do modelo de aplicativo](https://docs.abp.io/en/abp/latest/Startup-Templates/Application) para entender a estrutura da solução em detalhes. No entanto, você entenderá o básico com este tutorial. - -### Criar a entidade do livro - -A camada de domínio no modelo de inicialização é separada em dois projetos: - -- `Acme.BookStore.Domain`contém suas [entidades](https://docs.abp.io/en/abp/latest/Entities.md) , [serviços de domínio](https://docs.abp.io/en/abp/latest/Domain-Services) e outros objetos principais de domínio. -- `Acme.BookStore.Domain.Shared` contém constantes, enumerações ou outros objetos relacionados ao domínio que podem ser compartilhados com os clientes. - -Defina [entidades](https://docs.abp.io/en/abp/latest/Entities) na **camada de domínio** ( `Acme.BookStore.Domain`projeto) da solução. A entidade principal do aplicativo é a `Book`. Crie uma classe, chamada `Book`, no `Acme.BookStore.Domain`projeto, como mostrado abaixo: - -```csharp -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- O ABP possui duas classes base fundamentais para entidades: `AggregateRoot`e `Entity`. **A raiz agregada** é um dos conceitos de **DDD (Domain Driven Design)** . Consulte o [documento da entidade](https://docs.abp.io/en/abp/latest/Entities) para obter detalhes e melhores práticas. -- `Book`entidade herda `AuditedAggregateRoot`que adiciona algumas propriedades de auditoria ( `CreationTime`, `CreatorId`, `LastModificationTime`... etc.) no topo da `AggregateRoot`classe. -- `Guid`é o **tipo** de **chave primária** da `Book`entidade. - -#### BookType Enum - -Defina a `BookType`enumeração no `Acme.BookStore.Domain.Shared`projeto: - -```csharp -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -``` - -#### Adicionar entidade de livro ao seu DbContext - -O EF Core exige que você relacione entidades com seu DbContext. A maneira mais fácil de fazer isso é adicionar uma `DbSet`propriedade à `BookStoreDbContext`classe no `Acme.BookStore.EntityFrameworkCore`projeto, conforme mostrado abaixo: - -```csharp - public class BookStoreDbContext : AbpDbContext - { - public DbSet Books { get; set; } - ... - } -``` - -#### Configure sua entidade do livro - -Abra o `BookStoreDbContextModelCreatingExtensions.cs`arquivo no `Acme.BookStore.EntityFrameworkCore`projeto e adicione o seguinte código ao final do `ConfigureBookStore`método para configurar a entidade Livro: - -```csharp -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); -}); -``` - -#### Adicionar nova migração e atualizar o banco de dados - -O modelo de inicialização usa [as primeiras migrações do código principal EF](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) para criar e manter o esquema do banco de dados. Abra o **Gerenciador de Console Package (PMC)** (sob as *Ferramentas / Gerente Nuget Package* menu), selecione o `Acme.BookStore.EntityFrameworkCore.DbMigrations`como o **projeto padrão** e execute o seguinte comando: - -![livraria-pmc-add-book-migration](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png) - -Isso criará uma nova classe de migração dentro da `Migrations`pasta. Em seguida, execute o `Update-Database`comando para atualizar o esquema do banco de dados: - -``` -PM> Update-Database -``` - -#### Adicionar dados de amostra - -`Update-Database`O comando criou a `AppBooks`tabela no banco de dados. Abra seu banco de dados e insira algumas linhas de amostra, para que você possa mostrá-las na página: - -![livraria-livros-mesa](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png) - -### Crie o serviço de aplicativo - -O próximo passo é criar um [serviço de aplicativo](https://docs.abp.io/en/abp/latest/Application-Services) para gerenciar (criar, listar, atualizar, excluir ...) os livros. A camada de aplicativo no modelo de inicialização é separada em dois projetos: - -- `Acme.BookStore.Application.Contracts` contém principalmente seus DTOs e interfaces de serviço de aplicativo. -- `Acme.BookStore.Application` contém as implementações dos seus serviços de aplicativo. - -#### BookDto - -Crie uma classe DTO denominada `BookDto`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- **As** classes **DTO** são usadas para **transferir dados** entre a *camada de apresentação* e a *camada de aplicativo* . Consulte o [documento Objetos de transferência de dados](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) para obter mais detalhes. -- `BookDto` é usado para transferir dados do livro para a camada de apresentação para mostrar as informações do livro na interface do usuário. -- `BookDto`é derivado do `AuditedEntityDto`que possui propriedades de auditoria exatamente como a `Book`classe definida acima. - -Será necessário converter `Book`entidades em `BookDto`objetos enquanto retorna os livros para a camada de apresentação. [A](https://automapper.org/) biblioteca do [AutoMapper](https://automapper.org/) pode automatizar essa conversão quando você define o mapeamento adequado. O modelo de inicialização é fornecido com o AutoMapper configurado, para que você possa definir o mapeamento na `BookStoreApplicationAutoMapperProfile`classe no `Acme.BookStore.Application`projeto: - -```csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -``` - -#### CreateUpdateBookDto - -Crie uma classe DTO denominada `CreateUpdateBookDto`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -``` - -- Essa classe DTO é usada para obter informações do livro a partir da interface do usuário ao criar ou atualizar um livro. -- Ele define atributos de anotação de dados (como `[Required]`) para definir validações para as propriedades. Os DTOs são [validados automaticamente](https://docs.abp.io/en/abp/latest/Validation) pela estrutura ABP. - -Em seguida, adicione um mapeamento `BookStoreApplicationAutoMapperProfile`do `CreateUpdateBookDto`objeto à `Book`entidade: - -```csharp -CreateMap(); -``` - -#### IBookAppService - -Defina uma interface nomeada `IBookAppService`no `Acme.BookStore.Application.Contracts`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -``` - -- A definição de interfaces para serviços de aplicativos não é requerida pela estrutura. No entanto, é sugerido como uma prática recomendada. -- `ICrudAppService`define comuns **CRUD** métodos: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync`e `DeleteAsync`. Não é necessário estendê-lo. Em vez disso, você pode herdar da `IApplicationService`interface vazia e definir seus próprios métodos manualmente. -- Existem algumas variações de `ICrudAppService`onde você pode usar DTOs separados para cada método. - -#### BookAppService - -Implemente `IBookAppService`como nomeado `BookAppService`no `Acme.BookStore.Application`projeto: - -```csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -``` - -- `BookAppService`é derivado do `CrudAppService<...>`qual implementa todos os métodos CRUD definidos acima. -- `BookAppService`injeta `IRepository`qual é o repositório padrão da `Book`entidade. O ABP cria automaticamente repositórios padrão para cada raiz (ou entidade) agregada. Veja o [documento](https://docs.abp.io/en/abp/latest/Repositories) do [repositório](https://docs.abp.io/en/abp/latest/Repositories) . -- `BookAppService`usa `IObjectMapper`para converter `Book`objetos em `BookDto`objetos e `CreateUpdateBookDto`objetos em `Book`objetos. O modelo de inicialização usa a biblioteca [AutoMapper](http://automapper.org/) como o provedor de mapeamento de objetos. Você definiu os mapeamentos antes, para que funcionem conforme o esperado. - -### Controladores de API automática - -Você normalmente cria **controladores** para expor serviços de aplicativos como pontos de extremidade da **API HTTP** . Assim, permite que navegadores ou clientes de terceiros os chamem via AJAX. O ABP pode configurar [**automaticamente**](https://docs.abp.io/en/abp/latest/AspNetCore/Auto-API-Controllers) seus serviços de aplicativo como controladores de API MVC por convenção. - -#### UI do Swagger - -O modelo de inicialização está configurado para executar a [interface do usuário](https://swagger.io/tools/swagger-ui/) do [swagger](https://swagger.io/tools/swagger-ui/) usando a biblioteca [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) . Execute o aplicativo e insira `https://localhost:XXXX/swagger/`(substitua XXXX por sua própria porta) como URL no seu navegador. - -Você verá alguns pontos de extremidade de serviço internos, bem como o `Book`serviço e seus pontos de extremidade no estilo REST: - -![livraria-arrogância](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png) - -O Swagger tem uma ótima interface para testar APIs. Você pode tentar executar a `[GET] /api/app/book`API para obter uma lista de livros. - -### Proxies dinâmicos de JavaScript - -É comum chamar pontos de extremidade da API HTTP via AJAX do lado do **JavaScript** . Você pode usar `$.ajax`ou outra ferramenta para chamar os pontos de extremidade. No entanto, o ABP oferece uma maneira melhor. - -O ABP cria **dinamicamente** **proxies** JavaScript para todos os pontos de extremidade da API. Portanto, você pode usar qualquer **terminal,** assim como chamar uma **função JavaScript** . - -#### Testando no console do desenvolvedor do navegador - -Você pode testar facilmente os proxies JavaScript usando o **Console** do **desenvolvedor** do seu navegador favorito agora. Execute o aplicativo, abra as **ferramentas de desenvolvedor** do navegador (atalho: F12), vá para a guia **Console** , digite o seguinte código e pressione enter: - -```js -acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); -``` - -- `acme.bookStore`é o espaço para nome do `BookAppService`convertido em [camelCase](https://en.wikipedia.org/wiki/Camel_case) . -- `book`é o nome convencional para o `BookAppService`(postfix do AppService removido e convertido em camelCase). -- `getList`é o nome convencional para o `GetListAsync`método definido na `AsyncCrudAppService`classe base (postfix assíncrono removido e convertido em camelCase). -- `{}`O argumento é usado para enviar um objeto vazio ao `GetListAsync`método que normalmente espera um objeto do tipo `PagedAndSortedResultRequestDto`usado para enviar opções de paginação e classificação ao servidor (todas as propriedades são opcionais, para que você possa enviar um objeto vazio). -- `getList`A função retorna a `promise`. Portanto, você pode passar um retorno de chamada para a função `done`(ou `then`) para obter o resultado do servidor. - -A execução desse código produz a seguinte saída: - -![livraria-teste-js-proxy-getlist](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png) - -Você pode ver a **lista de livros** retornada do servidor. Você também pode verificar a guia de **rede** das ferramentas do desenvolvedor para ver a comunicação do cliente com o servidor: - -![livraria-teste-js-proxy-getlist-rede](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png) - -Vamos **criar um novo livro** usando a `create`função: - -```js -acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); -``` - -Você deve ver uma mensagem no console, algo assim: - -``` -successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 -``` - -Verifique a `Books`tabela no banco de dados para ver a nova linha do livro. Você pode tentar `get`, `update`e `delete`funciona mesmo. - -### Crie a página de livros - -É hora de criar algo visível e utilizável! Em vez do MVC clássico, usaremos a nova abordagem de [interface do usuário do Razor Pages,](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) recomendada pela Microsoft. - -Crie uma nova `Books`pasta na `Pages`pasta do `Acme.BookStore.Web`projeto e adicione uma nova página Razor denominada `Index.cshtml`: - -![livraria-add-index-page](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png) - -Abra `Index.cshtml`e altere o conteúdo, como mostrado abaixo: - -```html -@page -@using Acme.BookStore.Web.Pages.Books -@model IndexModel - -

    Books

    -``` - -- Verifique se o `IndexModel`( *Index.cshtml.cs)* possui o `Acme.BookStore.Pages.Books`espaço para nome ou atualize-o no `Index.cshtml`. - -#### Adicionar página de livros ao menu principal - -Abra a `BookStoreMenuContributor`classe na `Menus`pasta e adicione o seguinte código ao final do `ConfigureMainMenuAsync`método: - -```csharp -context.Menu.AddItem( - new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) - .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) -); -``` - -#### Localizando os itens de menu - -Os textos de localização estão localizados na `Localization/BookStore`pasta do `Acme.BookStore.Domain.Shared`projeto: - -![arquivos de localização de livraria](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png) - -Abra o `en.json`arquivo e adicione textos de localização `Menu:BookStore`e `Menu:Books`chaves ao final do arquivo: - -```json -{ - "culture": "en", - "texts": { - "Menu:BookStore": "Book Store", - "Menu:Books": "Books" - } -} -``` - -- O sistema de localização da ABP é construído no sistema de [localização padrão do ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) e o estende de várias maneiras. Consulte o [documento de localização](https://docs.abp.io/en/abp/latest/Localization) para obter detalhes. -- Os nomes das chaves de localização são arbitrários. Você pode definir qualquer nome. Preferimos adicionar `Menu:`prefixo aos itens de menu para distinguir de outros textos. Se um texto não estiver definido no arquivo de localização, ele **recuará** para a chave de localização (comportamento padrão do ASP.NET Core). - -Execute o aplicativo e veja se o novo item de menu foi adicionado à barra superior: - -![itens-menu-livraria](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png) - -Quando você clica no item de menu Livros, você é redirecionado para a nova página Livros. - -#### Lista de livros - -Usaremos o plug-in [Datatables.net](https://datatables.net/) JQuery para mostrar a lista de tabelas na página. As tabelas de dados podem funcionar completamente via AJAX, são rápidas e oferecem uma boa experiência ao usuário. O plug-in Datatables está configurado no modelo de inicialização, para que você possa usá-lo diretamente em qualquer página sem incluir nenhum estilo ou arquivo de script em sua página. - -##### Index.cshtml - -Altere o `Pages/Books/Index.cshtml`seguinte: - -```html -@page -@model Acme.BookStore.Web.Pages.Books.IndexModel -@section scripts -{ - -} - - -

    @L["Books"]

    -
    - - - - - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - - -
    -``` - -- `abp-script` [O auxiliar de marca](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) é usado para adicionar **scripts** externos à página. Possui muitos recursos adicionais em comparação com a `script`tag padrão . Ele lida com **minificação** e **controle** de **versão,** por exemplo. Consulte o [documento de compactação e redução](https://docs.abp.io/en/abp/latest/AspNetCore/Bundling-Minification) para obter detalhes. -- `abp-card`e `abp-table`são **auxiliares de tags** para o [componente de cartão](http://getbootstrap.com/docs/4.1/components/card/) do Twitter Bootstrap . Existem muitos auxiliares de tag no ABP para usar facilmente a maioria dos componentes de [autoinicialização](https://getbootstrap.com/) . Você também pode usar tags HTML regulares em vez desses auxiliares de tag, mas o uso de tag reduz o código HTML e evita erros com a ajuda do intellisense e da verificação do tipo de tempo de compilação. Consulte o [documento auxiliares](https://docs.abp.io/en/abp/latest/AspNetCore/Tag-Helpers) da [tag](https://docs.abp.io/en/abp/latest/AspNetCore/Tag-Helpers) . -- Você pode **localizar** os nomes das colunas no arquivo de localização, como fez nos itens de menu acima. - -##### Adicionar um arquivo de script - -Crie um `index.js`arquivo JavaScript na `Pages/Books/`pasta: - -![arquivo-index-js-bookstore](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png) - -`index.js` o conteúdo é mostrado abaixo: - -```js -$(function () { - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); -}); -``` - -- `abp.libs.datatables.createAjax` é uma função auxiliar para adaptar os proxies dinâmicos da API JavaScript da ABP ao formato do Datatable. -- `abp.libs.datatables.normalizeConfiguration`é outra função auxiliar. Não há necessidade de usá-lo, mas simplifica a configuração das tabelas de dados, fornecendo valores convencionais para as opções ausentes. -- `acme.bookStore.book.getList` é a função para obter a lista de livros (você já viu isso antes). -- Consulte [a documentação do Datatable](https://datatables.net/manual/) para obter mais opções de configuração. - -A interface do usuário final é mostrada abaixo: - -![livraria-lista-de-livros](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png) - -### Próxima parte - -Veja a [próxima parte](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-II) deste tutorial. diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md deleted file mode 100644 index 5ee4531131..0000000000 --- a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md +++ /dev/null @@ -1,465 +0,0 @@ -## Tutorial do ASP.NET Core MVC - Parte II - -### Sobre este tutorial - -Esta é a segunda parte da série de tutoriais do ASP.NET Core MVC. Veja todas as peças: - -- [Parte I: Crie o projeto e uma página da lista de livros](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-I) -- **Parte II: Criar, atualizar e excluir livros (este tutorial)** -- [Parte III: Testes de Integração](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-III) - -Você pode acessar o **código fonte** do aplicativo [no repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore) . - -> Você também pode assistir a [este curso em vídeo](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) preparado por um membro da comunidade ABP, com base neste tutorial. - -### Criando um novo livro - -Nesta seção, você aprenderá como criar um novo formulário de diálogo modal para criar um novo livro. A caixa de diálogo do resultado será assim: - -![livraria-criar-diálogo](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png) - -#### Crie o formulário modal - -Crie uma nova página de navalha, nomeada `CreateModal.cshtml`sob a `Pages/Books`pasta do `Acme.BookStore.Web`projeto: - -![livraria-adicionar-criar-diálogo](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -Abra o `CreateModal.cshtml.cs`arquivo ( `CreateModalModel`classe) e substitua pelo seguinte código: - -```csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -``` - - - -- Esta classe é derivada do em `BookStorePageModel`vez do padrão `PageModel`. `BookStorePageModel`herda o `PageModel`e adiciona algumas propriedades / métodos comuns que podem ser usados pelas classes de modelo de página. -- `[BindProperty]`O atributo na `Book`propriedade vincula os dados de solicitação posterior a essa propriedade. -- Essa classe simplesmente injeta o `IBookAppService`em seu construtor e chama o `CreateAsync`método no `OnPostAsync`manipulador. - -##### CreateModal.cshtml - -Abra o `CreateModal.cshtml`arquivo e cole o código abaixo: - -```html -@page -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -``` - - - -- Este modal usa o - - ``` - abp-dynamic-form - ``` - - auxiliar de marca para criar automaticamente o formulário a partir da - - ``` - CreateBookViewModel - ``` - - classe. - - - `abp-model`O atributo indica o objeto do modelo, a `Book`propriedade neste caso. - - `data-ajaxForm` O atributo faz com que o formulário seja enviado via AJAX, em vez de uma postagem de página clássica. - - `abp-form-content`O auxiliar de marca é um espaço reservado para renderizar os controles do formulário (isso é opcional e necessário apenas se você tiver adicionado outro conteúdo à `abp-dynamic-form`marca, como nesta página). - -#### Adicione o botão "Novo livro" - -Abra `Pages/Books/Index.cshtml`e altere a `abp-card-header`tag, como mostrado abaixo: - -```html - - - -

    @L["Books"]

    -
    - - - -
    -
    -``` - - - -Acabei de adicionar um botão **Novo livro** no canto **superior direito** da tabela: - -![livraria-novo-livro-botão](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png) - -Abra o `pages/books/index.js`e adicione o seguinte código logo após a configuração da tabela de dados: - -```js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -``` - - - -- `abp.ModalManager`é uma classe auxiliar para abrir e gerenciar modais no lado do cliente. Ele usa internamente o modal padrão do Twitter Bootstrap, mas abstrai muitos detalhes, fornecendo uma API simples. - -Agora, você pode **executar o aplicativo** e adicionar novos livros usando o novo formulário modal. - -### Atualizando um livro existente - -Crie uma nova página de navalha, nomeada `EditModal.cshtml`sob a `Pages/Books`pasta do `Acme.BookStore.Web`projeto: - -![livraria-adicionar-editar-diálogo](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png) - -#### EditModal.cshtml.cs - -Abra o `EditModal.cshtml.cs`arquivo ( `EditModalModel`classe) e substitua pelo seguinte código: - -```csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -``` - - - -- `[HiddenInput]`e `[BindProperty]`são atributos padrão do ASP.NET Core MVC. Utilizado `SupportsGet`para obter o valor do ID a partir do parâmetro da string de consulta da solicitação. -- Mapeado `BookDto`(recebido de `BookAppService.GetAsync`) para `CreateUpdateBookDto`no `GetAsync`método -- O `OnPostAsync`simplesmente usa `BookAppService.UpdateAsync`para atualizar a entidade. - -#### Mapeamento de BookDto para CreateUpdateBookDto - -A fim de executar `BookDto`a `CreateUpdateBookDto`opor mapeamento, abrir o `BookStoreWebAutoMapperProfile.cs`no `Acme.BookStore.Web`projecto e alterá-lo como se mostra abaixo: - -```csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -``` - - - -- Apenas adicionado `CreateMap();`como a definição de mapeamento. - -#### EditModal.cshtml - -Substitua o `EditModal.cshtml`conteúdo pelo seguinte: - -```html -@page -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - -``` - - - -Esta página é muito semelhante à `CreateModal.cshtml`exceção; - -- Ele inclui um `abp-input`para a `Id`propriedade armazenar o ID do livro de edição (que é uma entrada oculta). -- Ele usa `Books/EditModal`como URL de postagem e texto de *atualização* como cabeçalho modal. - -#### Adicione o menu suspenso "Ações" à tabela - -Adicionaremos um botão suspenso ("Ações") para cada linha da tabela. A interface do usuário final é assim: - -![livraria-livros-mesa-ações](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png) - -Abra a `Pages/Books/Index.cshtml`página e altere a seção da tabela como mostrado abaixo: - -```html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -``` - - - -- Acabei de adicionar uma nova `th`tag para as "Ações". - -Abra `pages/books/index.js`e substitua o conteúdo como abaixo: - -```js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -``` - - - -- Utilizado `abp.localization.getResource('BookStore')`para poder usar os mesmos textos de localização definidos no lado do servidor. -- Adicionado um novo `ModalManager`nome `createModal`para abrir a caixa de diálogo criar modal. -- Adicionado um novo `ModalManager`nome `editModal`para abrir a caixa de diálogo modal de edição. -- Adicionada uma nova coluna no início da `columnDefs`seção. Esta coluna é usada para o botão suspenso "Ações". -- A ação "Novo livro" simplesmente chama `createModal.open`para abrir a caixa de diálogo Criar. -- A ação "Editar" simplesmente chama `editModal.open`para abrir a caixa de diálogo de edição. `Você pode executar o aplicativo e editar qualquer livro selecionando a ação de edição. - -### Exclusão de um livro existente - -Abra o `pages/books/index.js`e adicione um novo item ao `rowAction` `items`: - -```js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -``` - - - -- `confirmMessage`A opção é usada para fazer uma pergunta de confirmação antes de executar o `action`. -- Utilizou a `acme.bookStore.book.delete`função de proxy javascript para executar uma solicitação AJAX para excluir um livro. -- `abp.notify.info` é usado para mostrar uma notificação toastr logo após a exclusão. - -O `index.js`conteúdo final é mostrado abaixo: - -```js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -``` - - - -Abra o `en.json`no `Acme.BookStore.Domain.Shared`projeto e adicione a seguinte linha: - -```json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -``` - - - -Execute o aplicativo e tente excluir um livro. - -### Próxima parte - -Veja a [próxima parte](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-III) deste tutorial. diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-III.md deleted file mode 100644 index 2acc09a87b..0000000000 --- a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-III.md +++ /dev/null @@ -1,182 +0,0 @@ -## Tutorial do ASP.NET Core MVC - Parte III - -### Sobre este tutorial - -Esta é a terceira parte da série de tutoriais do ASP.NET Core MVC. Veja todas as peças: - -- [Parte I: Crie o projeto e uma página da lista de livros](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-I) -- [Parte II: Criar, atualizar e excluir livros](https://docs.abp.io/en/abp/latest/Tutorials/AspNetCore-Mvc/Part-II) -- **Parte III: Testes de Integração (este tutorial)** - -Você pode acessar o **código fonte** do aplicativo [no repositório GitHub](https://github.com/abpframework/abp-samples/tree/master/BookStore). - -> Você também pode assistir a [este curso em vídeo](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) preparado por um membro da comunidade ABP, com base neste tutorial. - -### Testar projetos na solução - -Existem vários projetos de teste na solução: - -![livraria-teste-projetos-v2](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png) - -Cada projeto é usado para testar o projeto de aplicativo relacionado. Os projetos de teste usam as seguintes bibliotecas para teste: - -- [xunit](https://xunit.github.io/) como a principal estrutura de teste. -- [Altamente](http://shouldly.readthedocs.io/en/latest/) como uma biblioteca de asserções. -- [NSubstitute](http://nsubstitute.github.io/) como uma biblioteca de zombaria. - -### Adicionando dados de teste - -O modelo de inicialização contém a `BookStoreTestDataSeedContributor`classe no `Acme.BookStore.TestBase`projeto que cria alguns dados para executar os testes. - -Mude a `BookStoreTestDataSeedContributor`classe como mostrado abaixo: - -```csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 1", - Type = BookType.Fantastic, - PublishDate = new DateTime(2015, 05, 24), - Price = 21 - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 2", - Type = BookType.Science, - PublishDate = new DateTime(2014, 02, 11), - Price = 15 - } - ); - } - } -} -``` - -- Injetado `IRepository`e usado no `SeedAsync`para criar duas entidades de livro como dados de teste. -- `IGuidGenerator`Serviço usado para criar GUIDs. Embora `Guid.NewGuid()`funcionasse perfeitamente para testes, `IGuidGenerator`possui recursos adicionais especialmente importantes ao usar bancos de dados reais (consulte o [documento de geração](https://docs.abp.io/en/abp/latest/Guid-Generation) do [Guid](https://docs.abp.io/en/abp/latest/Guid-Generation) para obter mais informações). - -### Testando o BookAppService - -Crie uma classe de teste denominada `BookAppService_Tests`no `Acme.BookStore.Application.Tests`projeto: - -```csharp -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -``` - -- `Should_Get_List_Of_Books`O teste simplesmente usa o `BookAppService.GetListAsync`método para obter e verificar a lista de usuários. - -Adicione um novo teste que crie um novo livro válido: - -```csharp -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -``` - -Adicione um novo teste que tente criar um livro inválido e falhe: - -```csharp -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -``` - - - -- Como o `Name`está vazio, o ABP lança um `AbpValidationException`. - -Abra a **janela Test Explorer** (use o menu Test -> Windows -> Test Explorer, se não estiver visível) e **execute Todos os** testes: - -![testes de serviço de livraria](https://raw.githubusercontent.com/abpframework/abp/master/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png) - -Parabéns, ícones verdes mostram que os testes foram aprovados com sucesso! \ No newline at end of file diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png deleted file mode 100644 index fd06f3e4e5..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png deleted file mode 100644 index adfc036d0b..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png deleted file mode 100644 index a4760261c6..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png deleted file mode 100644 index 142ef57e22..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png deleted file mode 100644 index 6fb475deab..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png deleted file mode 100644 index fe2fd38349..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png deleted file mode 100644 index 431fb2defc..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png deleted file mode 100644 index 7254a97566..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png deleted file mode 100644 index eb84d88065..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png deleted file mode 100644 index f09f2f394f..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png deleted file mode 100644 index bae34a3b64..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png deleted file mode 100644 index 2db5ab1a5e..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png deleted file mode 100644 index a3616088d2..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png deleted file mode 100644 index ef3c404855..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png deleted file mode 100644 index b173926a29..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png deleted file mode 100644 index ffecdf70c7..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png deleted file mode 100644 index cb3b6440c7..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png deleted file mode 100644 index 83c416618a..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png deleted file mode 100644 index ffa63dc581..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png deleted file mode 100644 index 7fe3cead35..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png deleted file mode 100644 index 45d08ecea3..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png deleted file mode 100644 index d7d3429826..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png and /dev/null differ diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/pt-BR/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png and /dev/null differ diff --git a/docs/pt-BR/Unit-Of-Work.md b/docs/pt-BR/Unit-Of-Work.md deleted file mode 100644 index af662f86de..0000000000 --- a/docs/pt-BR/Unit-Of-Work.md +++ /dev/null @@ -1,3 +0,0 @@ -## Unit of Work - -Façam \ No newline at end of file diff --git a/docs/pt-BR/Validation.md b/docs/pt-BR/Validation.md deleted file mode 100644 index 662235431a..0000000000 --- a/docs/pt-BR/Validation.md +++ /dev/null @@ -1,182 +0,0 @@ -# Validação - -O sistema de validação é utilizado para validar a entrada do usuário ou a requisição do cliente para uma ação de um controller ou por um serviço. - -O ABP é compatível com o sistema de Validação de Modelos do ASP.NET Core e tudo escrito na [sua documentação](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) é válido para aplicações baseadas no ABP. Logo, esse documento foca nas funcionalidades do ABP ao invés de repetir a documentação da Microsoft. - -Além disso, o ABP adiciona os seguintes benefícios: - -* Define `IValidationEnabled` para adicionar validação automática para uma classe qualquer. Como todos os [serviços de aplicação](Application-Services.md) já o implementam, eles também são validados automaticamente. -* Automaticamente traduz os erros de validação para os atributos de anotação de dados. -* Provê serviços extensíveis para validar a chamada de um método ou o estado de um objeto. -* Provê integração com o [FluentValidation](https://fluentvalidation.net/) - -## Validando DTOs - -Essa seção introduz brevemente o sistema de validação. Para mais detalhes, veja a [Documentação da Validação de Modelo em ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation). - -### Atributos de anotação de dados - -Utilizar anotações de dados é uma maneira simples de implementar uma validação formal para um [DTO](Data-Transfer-Objects.md) de uma forma declarativa. Exemplo: - -````csharp -public class CreateBookDto -{ - [Required] - [StringLength(100)] - public string Name { get; set; } - - [Required] - [StringLength(1000)] - public string Description { get; set; } - - [Range(0, 999.99)] - public decimal Price { get; set; } -} -```` -Quando você utilizar essa classe como parâmetro para um [serviço da aplicação](Application-Services.md) ou um controller, ele será automaticamente validado e a validação traduzida será lançada ([e tratada](Exception-Handling.md) pelo ABP framework). - -### IValidatableObject - -`IValidatableObject` pode ser implementado por um DTO para executar uma lógica customizada de validação. O `CreateBookDto` no exemplo a seguir implementa essa interface e verifica se o `Name` é igual a `Description` e retorna um erro de validação nesse caso. - -````csharp -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateBookDto : IValidatableObject - { - [Required] - [StringLength(100)] - public string Name { get; set; } - - [Required] - [StringLength(1000)] - public string Description { get; set; } - - [Range(0, 999.99)] - public decimal Price { get; set; } - - public IEnumerable Validate( - ValidationContext validationContext) - { - if (Name == Description) - { - yield return new ValidationResult( - "Name and Description can not be the same!", - new[] { "Name", "Description" } - ); - } - } - } -} -```` - -#### Resolvendo um serviço. - -Se você precisar resolver um serviço do [sistema de injeção de dependências](Dependency-Injection.md), você pode utilizar o objeto `ValidationContext`. - -````csharp -var myService = validationContext.GetRequiredService(); -```` - -> Enquanto resolver os serviços no método `Validate` permite várias possibilidades, não é um boa prática implementar sua lógica de validação do domínio nos DTOs. Mantenha os DTOs simples. Seu propósito é transferir dados (DTO: Data Transfer Object, ou Objeto de Transferência de Dados). - -## Infraestrutura de Validação. - -Essa seção explica alguns serviços adicionais fornecidos pelo ABP Framework. - -### Interface IValidationEnabled - -`IValidationEnabled` é um marcador vazio de interface que pode ser implementado por qualquer classe (registrada e resolvida a partir do [DI](Dependency-Injection.md)) para permitir que o ABP framework realize o sistema de validação para os métodos da classe. Por exemplo: - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Validation; - -namespace Acme.BookStore -{ - public class MyService : ITransientDependency, IValidationEnabled - { - public virtual async Task DoItAsync(MyInput input) - { - //... - } - } -} -```` - -> O ABP framework utiliza o sistema de [Proxying Dinâmico / Interceptadores](Dynamic-Proxying-Interceptors.md) para realizar a validação. Para fazê-lo funcionar, seu método deve ser **virtual** ou seu serviço deve ser injetado e utilizado através de uma **interface** (como `IMyService`). - -#### Habilitando e Desabilitando Validações - -Você pode utilizar o `[DisableValidation]` e desabilitar a validação para métodos, classes e propriedades. - -````csharp -[DisableValidation] -public Void MyMethod() -{ -} - -[DisableValidation] -public class InputClass -{ - public string MyProperty { get; set; } -} - -public class InputClass -{ - [DisableValidation] - public string MyProperty { get; set; } -} -```` - -### AbpValidationException - -Uma vez que o ABP determina um erro de validação, é lançada uma validação do tipo `AbpValidationException`. O código da sua aplicação poderá lançar o `AbpValidationException`, mas na maioria das vezes não será necessário. - -* A propriedade `ValidationErrors` do `AbpValidationException` contem a lista com os erros de validação. -* O nível de log do `AbpValidationException` é definido como `Warning`. Todos os erros de validação são logados no [Sistema de Logging](Logging.md). -* `AbpValidationException` é tratado automaticamente pelo ABP framework e é convertido em um erro utilizável com o código de status HTTP 400. Veja a documentação de [Manipulação de Exceção](Exception-Handling.md) para mais informações. - -## Tópicos Avançados - -### IObjectValidator - -Além da validação automática, você pode querer validar um objeto manualmente. Nesse caso, [injete](Dependency-Injection.md) e use o serviço `IObjectValidator`: - -* O método `ValidateAsync` valida o objeto informado baseado nas regras de validação e lança uma `AbpValidationException` se não estiver em um estado válido. - -* `GetErrorsAsync` não lança uma exceção, somente retorna os erros de validação. - -`IObjectValidator` é implementado pelo `ObjectValidator` por padrão. `ObjectValidator` é extensível; você pode implementar a interface `IObjectValidationContributor` para contribuir com uma lógica customizada. Exemplo: - -````csharp -public class MyObjectValidationContributor - : IObjectValidationContributor, ITransientDependency -{ - public Task AddErrorsAsync(ObjectValidationContext context) - { - //Get the validating object - var obj = context.ValidatingObject; - - //Add the validation errors if available - context.Errors.Add(...); - return Task.CompletedTask; - } -} -```` - -* Lembre-se de registrar sua classe no [DI](Dependency-Injection.md) (implementar `ITransientDependency` faz isso no exemplo anterior) -* ABP vai automaticamente descobrir sua classe e utilizá-la em qualquer tipo de validação de objetos (incluindo chamadas de métodos de validação automáticas). - -### IMethodInvocationValidator - -`IMethodInvocationValidator` é utilizado para validar a chamada de um método. Ele utiliza internamente o `IObjectValidator` para validar os objetos passados na chamada do método. Você normalmente não precisa deste serviço, já que ele é utilizado automaticamente pelo framework, mas você pode querer reutilizar ou substituir na sua aplicação em alguns casos raros. - -## Integração com FluentValidation - -O pacote Volo.Abp.FluentValidation integra a biblioteca FluentValidation com o sistema de validação (implementando o `IObjectValidationContributor`). Veja o [documento de Integração com o FluentValidation](FluentValidation.md) para mais informações. diff --git a/docs/pt-BR/Value-Objects.md b/docs/pt-BR/Value-Objects.md deleted file mode 100644 index 647e6d2f0a..0000000000 --- a/docs/pt-BR/Value-Objects.md +++ /dev/null @@ -1,3 +0,0 @@ -## Value Objects - -Façam \ No newline at end of file diff --git a/docs/pt-BR/docs-nav.json b/docs/pt-BR/docs-nav.json deleted file mode 100644 index a189b83c09..0000000000 --- a/docs/pt-BR/docs-nav.json +++ /dev/null @@ -1,328 +0,0 @@ -{ - "items": [ - { - "text": "Começando", - "items": [ - { - "text": "De Modelo de Inicialização", - "items": [ - { - "text": "Aplicativo com MVC (Razor Pages) UI", - "path": "Getting-Started-AspNetCore-MVC-Template.md" - }, - { - "text": "Aplicativo com Angular UI", - "path": "Getting-Started-Angular-Template.md" - } - ] - }, - { - "text": "De Projetos Vazios", - "items": [ - { - "text": "Com Aplicativo ASP.NET Core Web", - "path": "Getting-Started-AspNetCore-Application.md" - }, - { - "text": "Com Aplicativo Console", - "path": "Getting-Started-Console-Application.md" - } - ] - } - ] - }, - { - "text": "Tutoriais", - "items": [ - { - "text": "Desenvolvimento de Aplicações", - "items": [ - { - "text": "Com ASP.NET Core MVC UI", - "path": "Tutorials/AspNetCore-Mvc/Part-I.md" - }, - { - "text": "Com Angular UI", - "path": "Tutorials/Angular/Part-I.md" - } - ] - } - ] - }, - { - "text": "CLI", - "path": "CLI.md" - }, - { - "text": "Fundamentos", - "items": [ - { - "text": "Injeção de Dependência", - "path": "Dependency-Injection.md", - "items": [ - { - "text": "Integração com Autofac", - "path": "Autofac-Integration.md" - } - ] - }, - { - "text": "Sistema de Arquivos Virtual", - "path": "Virtual-File-System.md" - }, - { - "text": "Localização", - "path": "Localization.md" - }, - { - "text": "Manipulação de Exceção", - "path": "Exception-Handling.md" - }, - { - "text": "Validação" - }, - { - "text": "Autorização" - }, - { - "text": "Armazenamento em Cache" - }, - { - "text": "Auditoria" - }, - { - "text": "Gerenciamento de Configurações" - } - ] - }, - { - "text": "Eventos", - "items": [ - { - "text": "Barramento de Eventos (local)" - }, - { - "text": "Barramento de Eventos Distribuídos", - "items": [ - { - "text": "Integração RabbitMQ" - } - ] - } - ] - }, - { - "text": "Serviços", - "items": [ - { - "text": "Serialização de Objetos" - }, - { - "text": "Serialização JSON" - }, - { - "text": "Emailing" - }, - { - "text": "GUIDs" - }, - { - "text": "Threading" - }, - { - "text": "Timing" - } - ] - }, - { - "text": "Múltiplos Inquilinos", - "path": "Multi-Tenancy.md" - }, - { - "text": "Desenvolvimento de Módulos", - "items": [ - { - "text": "Fundamentos", - "path": "Module-Development-Basics.md" - }, - { - "text": "Módulos de plug-in" - }, - { - "text": "Melhores práticas", - "path": "Best-Practices/Index.md" - } - ] - }, - { - "text": "Design Orientado a Domínio", - "path": "Domain-Driven-Design.md", - "items": [ - { - "text": "Camada de Domínio", - "items": [ - { - "text": "Entidades & Raízes Agregadas", - "path": "Entities.md" - }, - { - "text": "Value Objects" - }, - { - "text": "Repositórios", - "path": "Repositories.md" - }, - { - "text": "Serviços de Domínio" - }, - { - "text": "Especificações" - } - ] - }, - { - "text": "Camada de Aplicação", - "items": [ - { - "text": "Serviços de Aplicação", - "path": "Application-Services.md" - }, - { - "text": "Objetos de Transferência de Dados" - }, - { - "text": "Unidade de Trabalho" - } - ] - } - ] - }, - { - "text": "ASP.NET Core", - "items": [ - { - "text": "API", - "items": [ - { - "text": "Controladores de API Automática", - "path": "AspNetCore/Auto-API-Controllers.md" - }, - { - "text": "Clientes da API C # Dinâmica", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" - } - ] - }, - { - "text": "Interface de Usuário", - "items": [ - { - "text": "Gerenciamento de Pacotes do Lado do Cliente", - "path": "AspNetCore/Client-Side-Package-Management.md" - }, - { - "text": "Compactação & Minificação", - "path": "AspNetCore/Bundling-Minification.md" - }, - { - "text": "Tag Helpers", - "path": "AspNetCore/Tag-Helpers/Index.md" - }, - { - "text": "Widgets", - "path": "AspNetCore/Widgets.md" - }, - { - "text": "Theming", - "path": "AspNetCore/Theming.md" - } - ] - } - ] - }, - { - "text": "Acesso de Dados", - "items": [ - { - "text": "Integração do Entity Framework Core", - "path": "Entity-Framework-Core.md", - "items": [ - { - "text": "Integração do PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - } - ] - }, - { - "text": "Integração do MongoDB", - "path": "MongoDB.md" - }, - { - "text": "Integração do Dapper", - "path": "Dapper.md" - } - ] - }, - { - "text": "Background", - "items": [ - { - "text": "Trabalhos em Segundo Plano", - "path": "Background-Jobs.md", - "items": [ - { - "text": "Integração do Hangfire", - "path": "Background-Jobs-Hangfire.md" - }, - { - "text": "Integração do RabbitMQ", - "path": "Background-Jobs-RabbitMq.md" - } - ] - } - ] - }, - { - "text": "Modelos de Inicialização", - "path": "Startup-Templates/Index.md", - "items": [ - { - "text": "Aplicativo", - "path": "Startup-Templates/Application.md" - }, - { - "text": "Módulo", - "path": "Startup-Templates/Module.md" - } - ] - }, - { - "text": "Exemplos", - "items": [ - { - "text": "Demonstração de Microsserviços", - "path": "Samples/Microservice-Demo.md" - } - ] - }, - { - "text": "Módulos de Aplicação", - "path": "Modules/Index.md" - }, - { - "text": "Arquitetura de Microsserviços", - "path": "Microservice-Architecture.md" - }, - { - "text": "Testando" - }, - { - "text": "Compilações Noturnas", - "path": "Nightly-Builds.md" - }, - { - "text": "Guia de Contribuição", - "path": "Contribution/Index.md" - } - ] -} \ No newline at end of file diff --git a/docs/pt-BR/images/MonthlyProfitWidgetFiles.png b/docs/pt-BR/images/MonthlyProfitWidgetFiles.png deleted file mode 100644 index c3e4d6f1ab..0000000000 Binary files a/docs/pt-BR/images/MonthlyProfitWidgetFiles.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-apis.png b/docs/pt-BR/images/bookstore-apis.png deleted file mode 100644 index b7928c9637..0000000000 Binary files a/docs/pt-BR/images/bookstore-apis.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-create-template.png b/docs/pt-BR/images/bookstore-create-template.png deleted file mode 100644 index bae34a3b64..0000000000 Binary files a/docs/pt-BR/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-homepage.png b/docs/pt-BR/images/bookstore-homepage.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/pt-BR/images/bookstore-homepage.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-swagger-ui-host.png b/docs/pt-BR/images/bookstore-swagger-ui-host.png deleted file mode 100644 index 7ebd8d8e37..0000000000 Binary files a/docs/pt-BR/images/bookstore-swagger-ui-host.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-user-management-v2.png b/docs/pt-BR/images/bookstore-user-management-v2.png deleted file mode 100644 index cd66010e05..0000000000 Binary files a/docs/pt-BR/images/bookstore-user-management-v2.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-visual-studio-solution-for-spa.png b/docs/pt-BR/images/bookstore-visual-studio-solution-for-spa.png deleted file mode 100644 index d114ed188c..0000000000 Binary files a/docs/pt-BR/images/bookstore-visual-studio-solution-for-spa.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-visual-studio-solution-tiered.png b/docs/pt-BR/images/bookstore-visual-studio-solution-tiered.png deleted file mode 100644 index 9affe841aa..0000000000 Binary files a/docs/pt-BR/images/bookstore-visual-studio-solution-tiered.png and /dev/null differ diff --git a/docs/pt-BR/images/bookstore-visual-studio-solution-v3.png b/docs/pt-BR/images/bookstore-visual-studio-solution-v3.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/pt-BR/images/bookstore-visual-studio-solution-v3.png and /dev/null differ diff --git a/docs/pt-BR/images/build-action-embedded-resource-sample.png b/docs/pt-BR/images/build-action-embedded-resource-sample.png deleted file mode 100644 index 700e9921f4..0000000000 Binary files a/docs/pt-BR/images/build-action-embedded-resource-sample.png and /dev/null differ diff --git a/docs/pt-BR/images/create-new-aspnet-core-application.png b/docs/pt-BR/images/create-new-aspnet-core-application.png deleted file mode 100644 index 2c38289810..0000000000 Binary files a/docs/pt-BR/images/create-new-aspnet-core-application.png and /dev/null differ diff --git a/docs/pt-BR/images/create-new-net-core-console-application.png b/docs/pt-BR/images/create-new-net-core-console-application.png deleted file mode 100644 index 0c2b3dbcb8..0000000000 Binary files a/docs/pt-BR/images/create-new-net-core-console-application.png and /dev/null differ diff --git a/docs/pt-BR/images/dashboard1.png b/docs/pt-BR/images/dashboard1.png deleted file mode 100644 index 8c542b8786..0000000000 Binary files a/docs/pt-BR/images/dashboard1.png and /dev/null differ diff --git a/docs/pt-BR/images/db-migrator-app.png b/docs/pt-BR/images/db-migrator-app.png deleted file mode 100644 index d2248d4588..0000000000 Binary files a/docs/pt-BR/images/db-migrator-app.png and /dev/null differ diff --git a/docs/pt-BR/images/docs-create-project.jpg b/docs/pt-BR/images/docs-create-project.jpg deleted file mode 100644 index d2baa3242a..0000000000 Binary files a/docs/pt-BR/images/docs-create-project.jpg and /dev/null differ diff --git a/docs/pt-BR/images/docs-module_download-new-abp-project.png b/docs/pt-BR/images/docs-module_download-new-abp-project.png deleted file mode 100644 index bc7aaacbd6..0000000000 Binary files a/docs/pt-BR/images/docs-module_download-new-abp-project.png and /dev/null differ diff --git a/docs/pt-BR/images/docs-module_download-sample-navigation-menu.png b/docs/pt-BR/images/docs-module_download-sample-navigation-menu.png deleted file mode 100644 index 8d8eb42d52..0000000000 Binary files a/docs/pt-BR/images/docs-module_download-sample-navigation-menu.png and /dev/null differ diff --git a/docs/pt-BR/images/docs-module_solution-explorer.png b/docs/pt-BR/images/docs-module_solution-explorer.png deleted file mode 100644 index 97bd3fc18e..0000000000 Binary files a/docs/pt-BR/images/docs-module_solution-explorer.png and /dev/null differ diff --git a/docs/pt-BR/images/github-access-token-private-repo.jpg b/docs/pt-BR/images/github-access-token-private-repo.jpg deleted file mode 100644 index cb74f1eea3..0000000000 Binary files a/docs/pt-BR/images/github-access-token-private-repo.jpg and /dev/null differ diff --git a/docs/pt-BR/images/github-access-token-public-repo.jpg b/docs/pt-BR/images/github-access-token-public-repo.jpg deleted file mode 100644 index d091a6d511..0000000000 Binary files a/docs/pt-BR/images/github-access-token-public-repo.jpg and /dev/null differ diff --git a/docs/pt-BR/images/github-myusername.jpg b/docs/pt-BR/images/github-myusername.jpg deleted file mode 100644 index a723c17713..0000000000 Binary files a/docs/pt-BR/images/github-myusername.jpg and /dev/null differ diff --git a/docs/pt-BR/images/issuemanagement-module-solution.png b/docs/pt-BR/images/issuemanagement-module-solution.png deleted file mode 100644 index d5f64b01d2..0000000000 Binary files a/docs/pt-BR/images/issuemanagement-module-solution.png and /dev/null differ diff --git a/docs/pt-BR/images/layered-project-dependencies-module.png b/docs/pt-BR/images/layered-project-dependencies-module.png deleted file mode 100644 index de3b7a412f..0000000000 Binary files a/docs/pt-BR/images/layered-project-dependencies-module.png and /dev/null differ diff --git a/docs/pt-BR/images/layered-project-dependencies.png b/docs/pt-BR/images/layered-project-dependencies.png deleted file mode 100644 index ed3e03fe4d..0000000000 Binary files a/docs/pt-BR/images/layered-project-dependencies.png and /dev/null differ diff --git a/docs/pt-BR/images/localization-resource-json-files.png b/docs/pt-BR/images/localization-resource-json-files.png deleted file mode 100644 index 1a1d43403c..0000000000 Binary files a/docs/pt-BR/images/localization-resource-json-files.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-authserver-home.png b/docs/pt-BR/images/microservice-sample-authserver-home.png deleted file mode 100644 index 684fdb1a5a..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-authserver-home.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-authserver-login.png b/docs/pt-BR/images/microservice-sample-authserver-login.png deleted file mode 100644 index 99d898ccd2..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-authserver-login.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-backend-ui-permissions.png b/docs/pt-BR/images/microservice-sample-backend-ui-permissions.png deleted file mode 100644 index 3e1610294f..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-backend-ui-permissions.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-backend-ui.png b/docs/pt-BR/images/microservice-sample-backend-ui.png deleted file mode 100644 index e649c8da59..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-backend-ui.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-blogservice-permission-in-database.png b/docs/pt-BR/images/microservice-sample-blogservice-permission-in-database.png deleted file mode 100644 index 45d7f2b115..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-blogservice-permission-in-database.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-diagram-2.png b/docs/pt-BR/images/microservice-sample-diagram-2.png deleted file mode 100644 index 414a942aca..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-diagram-2.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-diagram.png b/docs/pt-BR/images/microservice-sample-diagram.png deleted file mode 100644 index 47e6443852..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-diagram.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-kibana-1.png b/docs/pt-BR/images/microservice-sample-kibana-1.png deleted file mode 100644 index 51777f6bd7..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-kibana-1.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-kibana-2.png b/docs/pt-BR/images/microservice-sample-kibana-2.png deleted file mode 100644 index cb1d0b748c..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-kibana-2.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-product-module-in-solution.png b/docs/pt-BR/images/microservice-sample-product-module-in-solution.png deleted file mode 100644 index 2c07ad4b99..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-product-module-in-solution.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-public-product-list.png b/docs/pt-BR/images/microservice-sample-public-product-list.png deleted file mode 100644 index 932b0b531c..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-public-product-list.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-solution.png b/docs/pt-BR/images/microservice-sample-solution.png deleted file mode 100644 index d1497d9be2..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-solution.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-update-database-authserver.png b/docs/pt-BR/images/microservice-sample-update-database-authserver.png deleted file mode 100644 index 094fd20fa6..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-update-database-authserver.png and /dev/null differ diff --git a/docs/pt-BR/images/microservice-sample-update-database-products.png b/docs/pt-BR/images/microservice-sample-update-database-products.png deleted file mode 100644 index 32a0927c1f..0000000000 Binary files a/docs/pt-BR/images/microservice-sample-update-database-products.png and /dev/null differ diff --git a/docs/pt-BR/images/module-layers-and-packages.jpg b/docs/pt-BR/images/module-layers-and-packages.jpg deleted file mode 100644 index f71a91eb8d..0000000000 Binary files a/docs/pt-BR/images/module-layers-and-packages.jpg and /dev/null differ diff --git a/docs/pt-BR/images/night-build-add-nuget-package.png b/docs/pt-BR/images/night-build-add-nuget-package.png deleted file mode 100644 index f475d3aaa0..0000000000 Binary files a/docs/pt-BR/images/night-build-add-nuget-package.png and /dev/null differ diff --git a/docs/pt-BR/images/night-build-add-nuget-source.png b/docs/pt-BR/images/night-build-add-nuget-source.png deleted file mode 100644 index df3176aa12..0000000000 Binary files a/docs/pt-BR/images/night-build-add-nuget-source.png and /dev/null differ diff --git a/docs/pt-BR/images/pcm-update-database-v2.png b/docs/pt-BR/images/pcm-update-database-v2.png deleted file mode 100644 index 72d02e9186..0000000000 Binary files a/docs/pt-BR/images/pcm-update-database-v2.png and /dev/null differ diff --git a/docs/pt-BR/images/pcm-update-database.png b/docs/pt-BR/images/pcm-update-database.png deleted file mode 100644 index a9379d2571..0000000000 Binary files a/docs/pt-BR/images/pcm-update-database.png and /dev/null differ diff --git a/docs/pt-BR/images/select-empty-web-application.png b/docs/pt-BR/images/select-empty-web-application.png deleted file mode 100644 index f4b884140d..0000000000 Binary files a/docs/pt-BR/images/select-empty-web-application.png and /dev/null differ diff --git a/docs/pt-BR/images/set-as-startup-project.png b/docs/pt-BR/images/set-as-startup-project.png deleted file mode 100644 index 8a5445bf38..0000000000 Binary files a/docs/pt-BR/images/set-as-startup-project.png and /dev/null differ diff --git a/docs/pt-BR/images/tiered-solution-applications.png b/docs/pt-BR/images/tiered-solution-applications.png deleted file mode 100644 index df8d2b5f4a..0000000000 Binary files a/docs/pt-BR/images/tiered-solution-applications.png and /dev/null differ diff --git a/docs/pt-BR/images/tiered-solution-servers.png b/docs/pt-BR/images/tiered-solution-servers.png deleted file mode 100644 index 68e72990d7..0000000000 Binary files a/docs/pt-BR/images/tiered-solution-servers.png and /dev/null differ diff --git a/docs/pt-BR/images/volodocs-iis-add-website.png b/docs/pt-BR/images/volodocs-iis-add-website.png deleted file mode 100644 index aa7da8095b..0000000000 Binary files a/docs/pt-BR/images/volodocs-iis-add-website.png and /dev/null differ diff --git a/docs/pt-BR/images/volodocs-iis-application-pool.png b/docs/pt-BR/images/volodocs-iis-application-pool.png deleted file mode 100644 index 28ccfa5c42..0000000000 Binary files a/docs/pt-BR/images/volodocs-iis-application-pool.png and /dev/null differ diff --git a/docs/pt-BR/images/widget-basic-files.png b/docs/pt-BR/images/widget-basic-files.png deleted file mode 100644 index c692abd9e0..0000000000 Binary files a/docs/pt-BR/images/widget-basic-files.png and /dev/null differ diff --git a/docs/zh-Hans/API/API-Versioning.md b/docs/zh-Hans/API/API-Versioning.md deleted file mode 100644 index 3e63ec045e..0000000000 --- a/docs/zh-Hans/API/API-Versioning.md +++ /dev/null @@ -1,340 +0,0 @@ -# ABP版本控制系统 - -ABP框架集成了[ASPNET-API-版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki)功能并适配C#和JavaScript静态代理和[自动API控制器](API/Auto-API-Controllers.md). - -## 启用API版本控制 - -```cs -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} -``` - -## C# 和 JavaScript 静态客户端代理 - -这个功能不兼容[URL路径版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), 建议你始终使用[Query-String版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String) - -### 示例 - -**Application Services:** - -```cs -public interface IBookAppService : IApplicationService -{ - Task GetAsync(); -} - -public interface IBookV2AppService : IApplicationService -{ - Task GetAsync(); - - Task GetAsync(string isbn); -} -``` - -**HttpApi Controillers:** - -```cs -[Area(BookStoreRemoteServiceConsts.ModuleName)] -[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] -[ApiVersion("1.0", Deprecated = true)] -[ApiController] -[ControllerName("Book")] -[Route("api/BookStore/Book")] -public class BookController : BookStoreController, IBookAppService -{ - private readonly IBookAppService _bookAppService; - - public BookController(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - [HttpGet] - public async Task GetAsync() - { - return await _bookAppService.GetAsync(); - } -} - -[Area(BookStoreRemoteServiceConsts.ModuleName)] -[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] -[ApiVersion("2.0")] -[ApiController] -[ControllerName("Book")] -[Route("api/BookStore/Book")] -public class BookV2Controller : BookStoreController, IBookV2AppService -{ - private readonly IBookV2AppService _bookAppService; - - public BookV2Controller(IBookV2AppService bookAppService) - { - _bookAppService = bookAppService; - } - - [HttpGet] - public async Task GetAsync() - { - return await _bookAppService.GetAsync(); - } - - [HttpGet] - [Route("{isbn}")] - public async Task GetAsync(string isbn) - { - return await _bookAppService.GetAsync(isbn); - } -} -``` - -**生成 CS 和 JS 代理:** - -```cs -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IBookAppService), typeof(BookClientProxy))] -public partial class BookClientProxy : ClientProxyBase, IBookAppService -{ - public virtual async Task GetAsync() - { - return await RequestAsync(nameof(GetAsync)); - } -} - -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IBookV2AppService), typeof(BookV2ClientProxy))] -public partial class BookV2ClientProxy : ClientProxyBase, IBookV2AppService -{ - public virtual async Task GetAsync() - { - return await RequestAsync(nameof(GetAsync)); - } - - public virtual async Task GetAsync(string isbn) - { - return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue - { - { typeof(string), isbn } - }); - } -} -``` - -```js -// controller bookStore.books.book - -(function(){ - -abp.utils.createNamespace(window, 'bookStore.books.book'); - -bookStore.books.book.get = function(api_version, ajaxParams) { - var api_version = api_version ? api_version : '1.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -})(); - -// controller bookStore.books.bookV2 - -(function(){ - -abp.utils.createNamespace(window, 'bookStore.books.bookV2'); - -bookStore.books.bookV2.get = function(api_version, ajaxParams) { - var api_version = api_version ? api_version : '2.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams) { - var api_version = api_version ? api_version : '2.0'; - return abp.ajax($.extend(true, { - url: abp.appPath + 'api/BookStore/Book/' + isbn + '' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', - type: 'GET' - }, ajaxParams)); -}; - -})(); -``` - -## 手动更改版本 - -如果应用服务支持多版本, 你可以注入 `ICurrentApiVersionInfo` 来切换版本. - -```cs -var currentApiVersionInfo = _abpApplication.ServiceProvider.GetRequiredService(); -var bookV4AppService = _abpApplication.ServiceProvider.GetRequiredService(); -using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.0"))) -{ - book = await bookV4AppService.GetAsync(); - logger.LogWarning(book.Title); - logger.LogWarning(book.ISBN); -} - -using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.1"))) -{ - book = await bookV4AppService.GetAsync(); - logger.LogWarning(book.Title); - logger.LogWarning(book.ISBN); -} -``` - -在JS代理中有一个默认版本, 当然你也可以手动更改. - -```js - -bookStore.books.bookV4.get("4.0") // Manually change the version. -//Title: Mastering ABP Framework V4.0 - -bookStore.books.bookV4.get() // The latest supported version is used by default. -//Title: Mastering ABP Framework V4.1 -``` - -## 自动API控制器 - -```cs -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - //2.0 Version - options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => - { - opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v2.TodoAppService).Namespace; - opts.ApiVersions.Add(new ApiVersion(2, 0)); - }); - - //1.0 Compatibility version - options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => - { - opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v1.TodoAppService).Namespace; - opts.ApiVersions.Add(new ApiVersion(1, 0)); - }); - }); -} - -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var preActions = context.Services.GetPreConfigureActions(); - Configure(options => - { - preActions.Configure(options); - }); - - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - - options.ConfigureAbp(preActions.Configure()); - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} -``` - -## Swagger/VersionedApiExplorer - -```cs - -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Show neutral/versionless APIs. - context.Services.AddTransient(); - context.Services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - }).AddApiExplorer(options => { - // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service - // note: the specified format code will format the version as "'v'major[.minor][-status]" - options.GroupNameFormat = "'v'VVV"; - - // note: this option is only necessary when versioning by url segment. the SubstitutionFormat - // can also be used to control the format of the API version in route templates - options.SubstituteApiVersionInUrl = true; - }); - - context.Services.AddTransient, ConfigureSwaggerOptions>(); - - context.Services.AddAbpSwaggerGen( - options => - { - // add a custom operation filter which sets default values - options.OperationFilter(); - - options.CustomSchemaIds(type => type.FullName); - }); - - Configure(options => - { - options.ChangeControllerModelApiExplorerGroupName = false; - }); -} - -public override void OnApplicationInitialization(ApplicationInitializationContext context) -{ - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseErrorPage(); - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseAbpRequestLocalization(); - - app.UseSwagger(); - app.UseAbpSwaggerUI( - options => - { - var provider = app.ApplicationServices.GetRequiredService(); - // build a swagger endpoint for each discovered API version - foreach (var description in provider.ApiVersionDescriptions) - { - options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); - } - }); - - app.UseConfiguredEndpoints(); -} -``` - -## 自定义多版本API控制器 - -ABP框架不会影响你的API, 你可以根据微软文档自由的实现你的API. - -参阅: https://github.com/dotnet/aspnet-api-versioning/wiki - -## 示例源码 - -你可以在这里得到完整的示例源码: https://github.com/abpframework/abp-samples/tree/master/Api-Versioning \ No newline at end of file diff --git a/docs/zh-Hans/API/Application-Configuration.md b/docs/zh-Hans/API/Application-Configuration.md deleted file mode 100644 index 50e74d5bd7..0000000000 --- a/docs/zh-Hans/API/Application-Configuration.md +++ /dev/null @@ -1,22 +0,0 @@ -# 应用程序配置端点 - -ABP框架提供了一个预构建的标准端点,其中包含一些有关应用程序/服务的有用信息. 这里是此端点的一些基本信息的列表: - -* [本地化](../Localization.md)值, 支持应用程序的当前语言. -* 当前用户可用和已授予的[策略](../Authorization.md)(权限). -* 当前用户的[设置](../Settings.md)值. -* 关于[当前用户](../CurrentUser.md)的信息 (如 id 和用户名). -* 关于当前[租户](../Multi-Tenancy.md)的信息 (如 id 和名称). -* 当前用户的[时区](../Timing.md)信息和应用程序的[时钟](../Timing.md)类型. - -## HTTP API - -如果您导航到基于ABP框架的web应用程序或HTTP服务的 `/api/abp/application-configuration` URL, 你可以得到JSON对象形式配置. 该端点对于创建应用程序的客户端很有用. - -## Script - -对于ASP.NET Core MVC(剃刀页)应用程序,同样的配置值在JavaScript端也可用. `/Abp/ApplicationConfigurationScript` 是基于上述HTTP API自动生成的脚本的URL. - -参阅 [JavaScript API文档](../UI/AspNetCore/JavaScript-API/Index.md) 了解关于ASP.NET Core UI. - -其他UI类型提供相关平台的本地服务. 例如查看[Angular UI本地化文档](../UI/Angular/Localization.md)来学习如何使用这个端点公开的本地化值. \ No newline at end of file diff --git a/docs/zh-Hans/API/Auto-API-Controllers.md b/docs/zh-Hans/API/Auto-API-Controllers.md deleted file mode 100644 index e284d05c3d..0000000000 --- a/docs/zh-Hans/API/Auto-API-Controllers.md +++ /dev/null @@ -1,140 +0,0 @@ -# 自动API控制器 - -创建[应用程序服务](Application-Services.md)后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做. - -ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义. - -## 配置 - -基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -此示例代码配置包含类`BookStoreApplicationModule`的程序集中的所有应用程序服务.下图显示了[Swagger UI](https://swagger.io/tools/swagger-ui/)上的API内容. - -![bookstore-apis](../images/bookstore-apis.png) - -### 例子 - -一些示例方法名称和按约定生成的相应路由: - -| 服务方法名称 | HTTP Method | 路由 | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP在确定服务方法的HTTP Method时使用命名约定: - -- **Get**: 如果方法名称以`GetList`,`GetAll`或`Get`开头. -- **Put**: 如果方法名称以`Put`或`Update`开头. -- **Delete**: 如果方法名称以`Delete`或`Remove`开头. -- **Post**: 如果方法名称以`Create`,`Add`,`Insert`或`Post`开头. -- **Patch**: 如果方法名称以`Patch`开头. -- 其他情况, **Post** 为 **默认方式**. - -如果需要为特定方法自定义HTTP Method, 则可以使用标准ASP.NET Core的属性([HttpPost], [HttpGet], [HttpPut]... 等等.). 这需要添加[Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core)的Nuget包. - -### 路由 - -路由根据一些惯例生成: - -* 它始终以 **/api**开头. -* 接着是**路由路径**. 默认值为"**/app**", 可以进行如下配置: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -然后获得一本书的路由将是'**/api/volosoft/book-store/book/{id}**'. 此示例使用两级根路径,但通常使用单个级别的深度. - -* 接着 **标准化控制器/服务名称**. 会删除`AppService`,`ApplicationService`和`Service`的后缀并将其转换为 **camelCase**. 如果你的应用程序服务类名称为`BookAppService`.那么它将变为`/book`. - * 如果要自定义命名, 则设置`UrlControllerNameNormalizer`选项. 它是一个委托允许你自定义每个控制器/服务的名称. -* 如果该方法具有 '**id**'参数, 则会在路由中添加'**/{id}**'. -* 如有必要,它会添加操作名称. 操作名称从服务上的方法名称获取并标准化; - * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. - * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. - * 将结果转换为**camelCase**. - * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. - * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. -* 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). - -## 服务选择 - -创建的HTTP API控制器并不是应用服务所独有的功能. - -### IRemoteService 接口 - -如果一个类实现了`IRemoteService`接口, 那么它会被自动选择为API控制器. 由于应用程序服务本身实现了`IRemoteService`接口, 因此它自然就成为API控制器. - -### RemoteService Attribute - -`RemoteService`可用于将实现`IRemoteService`接口的类标记为远程服务或禁用它. 例如: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate 选项 - -你可以通过提供`TypePedicate`选项进一步过滤类以成为API控制器: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -如果你不想将此类型公开为API控制器, 则可以在类型检查时返回`false`. - -## API Explorer - -API Explorer是可以由客户端获取API结构的服务. Swagger使用它为endpoint创建文档和test UI. - -默认情况下, HTTP API控制器会自动启用API Explorer, 可以使用`RemoteService`按类或方法的级别控制它. 例如: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -禁用`IsMetadataEnabled`从而从API Explorer中隐藏此服务, 并且无法被发现. 但是它仍然可以被知道确切API路径/路由的客户端使用. \ No newline at end of file diff --git a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md deleted file mode 100644 index 82473f9bbd..0000000000 --- a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md +++ /dev/null @@ -1,165 +0,0 @@ -# 动态 C# API 客户端 - -ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 `HttpClient` 或者其他低级的HTTP功能调用远程服务并获取数据. - -## 服务接口 - -你的service或controller需要实现一个在服务端和客户端共享的接口.因此,首先需要在一个共享的类库项目中定义一个服务接口.例如: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -为了能自动被发现,你的接口需要实现`IRemoteService`接口.由于`IApplicationService`继承自`IRemoteService`接口.所以`IBookAppService`完全满足这个条件. - -在你的服务中实现这个类,你可以使用[auto API controller system](Auto-API-Controllers.md)将你的服务暴漏为一个REST API 端点. - -## 客户端代理生成 - -首先,将[Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget包添加到你的客户端项目中: - -```` -Install-Package Volo.Abp.Http.Client -```` - -然后给你的模块添加`AbpHttpClientModule`依赖: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //添加依赖 -public class MyClientAppModule : AbpModule -{ -} -```` - -现在,已经可以创建客户端代理了.例如: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //用来创建客户端代理 - typeof(BookStoreApplicationContractsModule) //包含应用服务接口 - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //创建动态客户端代理 - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly - ); - } -} -```` - -`AddHttpClientproxies`方法获得一个程序集,找到这个程序集中所有的服务接口,创建并注册代理类. - -### Endpoint配置 - -`appsettings.json`文件中的`RemoteServices`节点被用来设置默认的服务地址.下面是最简单的配置: - -```` -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -```` - -查看下面的"AbpRemoteServiceOptions"章节获取更多详细配置. - -## 使用 - -可以很直接地使用.只需要在你的客户端程序中注入服务接口: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -本例注入了上面定义的`IBookAppService`服务接口.当客户端调用服务方法的时候动态客户端代理就会创建一个HTTP调用. - -### IHttpClientProxy接口 - -你可以像上面那样注入`IBookAppService`来使用客户端代理,也可以注入`IHttpClientProxy`获取更多明确的用法.这种情况下你可以使用`IHttpClientProxy`接口的`Service`属性. - -## 配置 - -### AbpRemoteServiceOptions - -默认情况下`AbpRemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### 多个远程服务端点 - -上面的例子已经配置了"Default"远程服务端点.你可能需要为不同的服务创建不同的端点.(就像在微服务方法中一样,每个微服务具有不同的端点).在这种情况下,你可以在你的配置文件中添加其他的端点: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies`方法有一个可选的参数来定义远程服务的名字: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly, - remoteServiceConfigurationName: "BookStore" -); -```` - -`remoteServiceConfigurationName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. - -### 作为默认服务 - -当你为`IBookAppService`创建了一个服务代理,你可以直接注入`IBookAppService`来使用代理客户端(像上面章节中将的那样).你可以传递`asDefaultService:false`到`AddHttpClientProxies`方法来禁用此功能. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationContractsModule).Assembly, - asDefaultServices: false -); -```` - -如果你的程序中已经有一个服务的实现并且你不想用你的客户端代理重写或替换其他的实现,就需要使用`asDefaultServices:false` - -> 如果你禁用了`asDefaultService`,你只能使用`IHttpClientProxy`接口去使用客户端代理.(参见上面的相关章节). \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Auth.md b/docs/zh-Hans/API/JavaScript-API/Auth.md deleted file mode 100644 index 60c9eb8866..0000000000 --- a/docs/zh-Hans/API/JavaScript-API/Auth.md +++ /dev/null @@ -1,3 +0,0 @@ -# abp.auth JavaScript API - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Index.md b/docs/zh-Hans/API/JavaScript-API/Index.md deleted file mode 100644 index 854ec8214b..0000000000 --- a/docs/zh-Hans/API/JavaScript-API/Index.md +++ /dev/null @@ -1,23 +0,0 @@ -# JavaScript API - -ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. - -## APIs - -* abp.ajax -* [abp.auth](Auth.md) -* abp.currentUser -* abp.dom -* abp.event -* abp.features -* abp.localization -* abp.log -* abp.ModalManager -* abp.notify -* abp.security -* abp.setting -* abp.ui -* abp.utils -* abp.ResourceLoader -* abp.WidgetManager -* Other APIs \ No newline at end of file diff --git a/docs/zh-Hans/Ambient-Context-Pattern.md b/docs/zh-Hans/Ambient-Context-Pattern.md deleted file mode 100644 index d00a721ce2..0000000000 --- a/docs/zh-Hans/Ambient-Context-Pattern.md +++ /dev/null @@ -1,3 +0,0 @@ -## Ambient Context Pattern - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md deleted file mode 100644 index 6d358c3b52..0000000000 --- a/docs/zh-Hans/Application-Services.md +++ /dev/null @@ -1,383 +0,0 @@ -## 应用服务 - -应用服务实现应用程序的**用例**, 将**领域层逻辑公开给表示层**. - -从表示层(可选)调用应用服务,**DTO ([数据传对象](Data-Transfer-Objects.md))** 作为参数. 返回(可选)DTO给表示层. - -## 示例 - -### 图书实体 - -假设你有一个`Book`实体(聚合根), 如下所示: - -````csharp -public class Book : AggregateRoot -{ - public const int MaxNameLength = 128; - - public virtual string Name { get; protected set; } - - public virtual BookType Type { get; set; } - - public virtual float? Price { get; set; } - - protected Book() - { - - } - - public Book(Guid id, [NotNull] string name, BookType type, float? price = 0) - { - Id = id; - Name = CheckName(name); - Type = type; - Price = price; - } - - public virtual void ChangeName([NotNull] string name) - { - Name = CheckName(name); - } - - private static string CheckName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException($"name can not be empty or white space!"); - } - - if (name.Length > MaxNameLength) - { - throw new ArgumentException($"name can not be longer than {MaxNameLength} chars!"); - } - - return name; - } -} -```` - -* `Book`实体中定义`MaxNameLength`限制`Name`属性的最大长度. -* `Book`构造函数与`ChangeName`确保`Name`属性值的有效性. 请注意, `Name`的setter不是`public`. - -ABP不会强制开发者这样设计实体, 可以将所有的属性设置Public set/get. 由你来决定是否全面实施DDD. - -### IBookAppService接口 - -在ABP中应用程序服务应该实现`IApplicationService接口`. 推荐每个应用程序服务创建一个接口: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task CreateAsync(CreateBookDto input); -} -```` - -我们将实现Create方法作为示例. CreateBookDto定义如下: - -```csharp -public class CreateBookDto -{ - [Required] - [StringLength(Book.MaxNameLength)] - public string Name { get; set; } - - public BookType Type { get; set; } - - public float? Price { get; set; } -} -``` - -有关DTO更多的教程,请参见[数据传输对象文档](Entities.md) - -### BookAppService(实现) - -````csharp -public class BookAppService : ApplicationService, IBookAppService -{ - private readonly IRepository _bookRepository; - - public BookAppService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task CreateAsync(CreateBookDto input) - { - var book = new Book( - GuidGenerator.Create(), - input.Name, - input.Type, - input.Price - ); - - await _bookRepository.InsertAsync(book); - } -} -```` - -* `BookAppService`继承了基类`ApplicationService`· 这不是必需的, 但是`ApplicationService`提供了应用服务常见的需求(比如本示例服务中使用的`GuidGenerator`). 如果不继承它, 我们需要在服务中手动注入`IGuidGenerator`(参见[Guid生成](Guid-Generation.md)文档) -* `BookAppService`按照预期实现了`IBookAppService` -* `BookAppService` 注入了 `IRepository`(请参见[仓储](Repositories.md))在CreateAsync方法内部使用仓储将新实体插入数据库. -* `CreateAsync`使用`Book`实体的构造函数从给定的Input值创建新的`Book`对象 - -### 数据传输对象 - -应用服务使用并返回DTO而不是实体. ABP不会强制执行此规则. 但是将实体暴露给表示层(或远程客户端)存在重大问题, 所以不建议返回实体. - -有关更多信息, 请参见[DTO文档](Entities.md). - -### 对象到对象映射 - -`CreateBook`方法使用参数`CreateBookDto`对象手动创建`Book`实体. 因为`Book`实体的构造函数强制执行(我们是这样设计的). - -但是在很多情况下使用**自动对象映射**从相似对象设置对象的属性更加方便实用. ABP提供了一个[对象到对象映射](Object-To-Object-Mapping.md)基础设施,使其变得更加容易. - -让我们创建另一种获取`Book`的方法. 首先,在`IBookAppService`接口中定义方法: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task CreateAsync(CreateBookDto input); - - Task GetAsync(Guid id); //New method -} -```` - -`BookDto`是一个简单的[DTO](Data-Transfer-Objects.md)类, 定义如下: - -````csharp -public class BookDto -{ - public Guid Id { get; set; } - - public string Name { get; set; } - - public BookType Type { get; set; } - - public float? Price { get; set; } -} -```` - -我们创建一个Automapper的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. 例如: - -```csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - } -} -``` - -然后使用`AbpAutoMapperOptions`注册配置文件: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Add all mappings defined in the assembly of the MyModule class - options.AddMaps(); - }); - } -} -```` - -`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). 更多信息请参考[对象到对象映射](Object-To-Object-Mapping.md)文档 - -然后你可以实现`GetAsync`方法. 如下所示: - -````csharp -public async Task GetAsync(Guid id) -{ - var book = await _bookRepository.GetAsync(id); - return book.MapTo(); -} -```` - -`MapTo`扩展方法通过复制具有相同命名的所有属性将`Book`对象转换为`BookDto`对象. - -`MapTo`的另一种替代方法是使用`IObjectMapper`服务: - -````csharp -public async Task GetAsync(Guid id) -{ - var book = await _bookRepository.GetAsync(id); - return ObjectMapper.Map(book); -} -```` - -虽然第二种语法编写起来有点困难,但是如果你编写单元测试,它会更好地工作. -有关更多信息,请参阅[对象到对象映射](Object-To-Object-Mapping)文档. - -### 验证 - -自动验证应用服务方法的输入(如ASP.NET Core 控制器的actions). 你可以使用标准数据注释属性或自定义验证方法来执行验证. ABP还确保输入不为空. - -请参阅[验证](Validation.md)文档了解更多信息. - -### 授权 - -可以对应用程序服务方法使用声明性和命令式授权. - -请参阅[授权](Authorization.md)文档了解更多信息. - -### CRUD应用服务 - -如果需要创建具有Create,Update,Delete和Get方法的简单**CRUD应用服务**,则可以使用ABP的基类轻松构建服务. 你可以继承CrudAppService. - -示例: - -创建继承`ICrudAppService`接口的`IBookAppService`接口. - -````csharp -public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book -{ -} -```` - -`ICrudAppService` 有泛型参数来获取实体的主键类型和CRUD操作的DTO类型(它不获取实体类型,因为实体类型未向客户端公开使用此接口). - -> 为应用程序服务创建一个接口是最佳做法,但是ABP框架并不强制你这么做,你可以跳过接口部分. - -`ICrudAppService`声明以下方法: - -````csharp -public interface ICrudAppService< - TEntityDto, - in TKey, - in TGetListInput, - in TCreateInput, - in TUpdateInput> - : IApplicationService - where TEntityDto : IEntityDto -{ - Task GetAsync(TKey id); - - Task> GetListAsync(TGetListInput input); - - Task CreateAsync(TCreateInput input); - - Task UpdateAsync(TKey id, TUpdateInput input); - - Task DeleteAsync(TKey id); -} -```` - -示例中使用的DTO类是`BookDto`和`CreateUpdateBookDto`: - -````csharp -public class BookDto : AuditedEntityDto -{ - public string Name { get; set; } - - public BookType Type { get; set; } - - public float Price { get; set; } -} - -public class CreateUpdateBookDto -{ - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public float Price { get; set; } -} -```` - -DTO类的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. - -```csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - CreateMap(); - } -} -``` - -* `CreateUpdateBookDto`由创建和更新操作共享,但你也可以使用单独的DTO类. - -最后`BookAppService`实现非常简单: - -````csharp -public class BookAppService : - CrudAppService, - IBookAppService -{ - public BookAppService(IRepository repository) - : base(repository) - { - } -} -```` - -`CrudAppService`实现了`ICrudAppService`接口中声明的所有方法. 然后,你可以添加自己的自定义方法或重写和自定义实现. - -> `CrudAppService` 有不同数量泛型参数的版本,你可以选择适合的使用. - -### AbstractKeyCrudAppService - -`CrudAppService` 要求你的实体拥有一个Id属性做为主键. 如果你使用的是复合主键,那么你无法使用它. - -`AbstractKeyCrudAppService` 实现了相同的 `ICrudAppService` 接口,但它没有假设你的主键. - -#### 示例 - -假设你有实体 `District`,它的`CityId` 和 `Name` 做为复合主键,使用 `AbstractKeyCrudAppService` 时需要你自己实现 `DeleteByIdAsync` 和 `GetEntityByIdAsync` 方法: - -````csharp -public class DistrictAppService - : AbstractKeyCrudAppService -{ - public DistrictAppService(IRepository repository) - : base(repository) - { - } - - protected async override Task DeleteByIdAsync(DistrictKey id) - { - await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); - } - - protected async override Task GetEntityByIdAsync(DistrictKey id) - { - return await AsyncQueryableExecuter.FirstOrDefaultAsync( - Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) - ); - } -} -```` - -这个实现需要你创建一个类做为复合键: - -````csharp -public class DistrictKey -{ - public Guid CityId { get; set; } - - public string Name { get; set; } -} -```` - -### 生命周期 - -应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. diff --git a/docs/zh-Hans/Apps/VoloDocs.md b/docs/zh-Hans/Apps/VoloDocs.md deleted file mode 100644 index f8cee69ad5..0000000000 --- a/docs/zh-Hans/Apps/VoloDocs.md +++ /dev/null @@ -1,174 +0,0 @@ -# VoloDocs - -## 什么是VoloDocs? - -VoloDocs是一个跨平台的应用程序,帮助你简单的创建良好的文档与构建开发人员社区. 它借助Github集成简化了软件文档. 使用Github的强大功能进行版本控制,托管文档,并允许用户编译文档. - -## 主要功能 - -- 集成Github从仓库提供文档. -- 支持Markdown/HTML文档格式. -- 支持版本控制 (集成Github release). -- 支持多个项目. -- 允许用户在Github上编辑文档. -- 跨平台,可部署到Windows/Linux/macOS. - -## Github源码仓库 - -它是免费并且开源的. 你可以查看VoloDocs源码,我们也欢迎PR: - -https://github.com/abpframework/abp/tree/master/modules/docs - -## 下载 - -你可以在下面的链接下载VoloDocs release版: - -http://apps.abp.io/VoloDocs/VoloDocs.win-x64.zip - **Windows 64 bit** - -http://apps.abp.io/VoloDocs/VoloDocs.win-x86.zip - **Windows 32 bit** - -http://apps.abp.io/VoloDocs/VoloDocs.osx-x64.zip - **MacOS** - -http://apps.abp.io/VoloDocs/VoloDocs.linux-x64.zip - **Linux** - -上面所有的release都是自包含运行时的部署文件. 包含所有的第三方依赖与.Net Core Runtime,所以你不需要安装任何.Net Core SDK/Runtime. - -## 目录结构 - -解压 `VoloDocs.*.zip` 文件, 你会得到一个 `Web` 文件夹和 `Migrator` 文件夹. `Web` 文件夹中包含网站文件, `Migrator` 文件夹中包含构建数据库的应用程序. 在发布网站之前需要创建数据库或将现有的数据库更新到最新迁移. 如果这是你第一次安装VoloDocs, `Migrator` 会创建一个新数据库. 如果是升级已有的VoloDocs则会将数据库升级到最新的迁移. 使用 `Migrator` 你需要在 `appsettings.json` 文件中更改 `ConnectionString`. 参阅下一节了解如何配置VoloDocs应用程序. - -## 逐步部署 - -- ### 数据库迁移 - - 进入 `Migrator` 目录中创建或更新数据库. - - 打开 `appsettings.json` 文件设置连接字符串,如果你不清楚如何编写连接字符串,可以查看 https://www.connectionstrings.com/. - - 设置连接字符串后运行 `Migrate.bat` 如果是Linux或MacOS运行 `DotNet VoloDocs.Migrator.dll` 命令. - -- ### 配置网站 - - 进入 `Web` 目录下的VoloDocs文件夹. 打开 `appsettings.json` 文件设置连接字符串(与 `Migrator` 的 `appsettings.json` 相同). 你也可以为网站设置你想要的 `title`,它会显示在网站的左上角. 做完这些现在可以发布网站了. - -- ### 部署网站 - - 在上一步我们创建/更新了数据库(确保数据库链接字符串正确). - - - #### 部署到IIS - - - 移动 `Web` 文件夹到你的 `wwwroot ` 文件夹中. - - 重命名 `Web` 文件夹为 `VoloDocs` (现在我们有了 `C:\inetpub\wwwroot\VoloDocs`).![Add IIS Website](../images/volodocs-iis-add-website.png) - - 创建网站 `VoloDocs` 会自动创建相同名称的应用程序池. 打开 **应用程序池** 并双击 `VoloDocs` 应用程序池做以下设置 - - **.NET CLR 版本**: `无托管代码` - - **托管管道模式**: `集成` - - ![Add IIS Website](../images/volodocs-iis-application-pool.png) - - - - - 如果发生发生发现错误,表示未在服务器安装托管捆绑包. 请参阅文档 [this document](https://docs.microsoft.com/aspnet/core/host-and-deploy/iis/#install-the-net-core-hosting-bundle) 了解如何安装或[下载托管捆绑包](https://www.microsoft.com/net/permalink/dotnetcore-current-windows-runtime-bundle-installer). - - ``` - Handler "aspNetCore" has a bad module "AspNetCoreModuleV2" in its module list using IIS - ``` - - - 关于托管VoloDocs的更多信息,请参阅 [在IIS上托管ASP.NET Core应用程序的微软官方文档](https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/iis). - - - #### 部署到Azure - - 微软有一篇关于将ASP.NET Core应用程序部署到Azure App Service的文档. 我们建议你此[文档](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-get-started-dotnet). - - - #### 使用命令行运行应用程序 - - 你可以从命令行运行应用程序,进入 `VoloDocs\Web` 文件夹并运行 `VoloDocs.Web.exe`,如果是Linux或MacOS运行 `DotNet VoloDocs.Web.dll` 命令. - -- ### 首次运行 - - 打开浏览器导航到你的网址. - - 首次运行网站,我们需要创建一个项目. - - #### 创建项目 - - 跳转到以下网址创建项目 - - - `http:///Account/Login?returnUrl=/Docs/Admin/Projects` - - ##### 默认凭据 - - 使用管理员身份登陆到管理后台,用户名密码如下: - - * **用户名**: `admin` - - * **密码**: `1q2w3E*` - - ##### 示例项目的定义 - - 以下是一个使用了Github源的示例项目信息. - - 配置VoloDocs展示存储在Github仓库中ABP Framework的文档. - - 这是ABP Framework文档在Github的链接: - - https://github.com/abpframework/abp/tree/master/docs/zh-Hans - - - - * **名称**: `ABP Framework` - - * **简称**: `abp` - - * **格式**: `markdown` - - * **默认文档名称**: `Index` - - * **文档导航名称**: `docs-nav.json` ([参见示例](https://github.com/abpframework/abp/blob/master/docs/zh-Hans/docs-nav.json)) - - * **最小版本**: *留空* *(隐藏之前的版本)* - - * **主网站网址**: `/` - - * **最新版本分支名称**: 留空 - - * **GitHub链接**: `https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/` - - * **GitHub访问令牌**: [参见如何创建Github访问令牌](#retrieving-github-access-token) - - * **GitHub用户名称**: [参见如何找到自己的Github用户名称](#learn-your-github-username) - - ![Creating a new project](../images/docs-create-project.jpg) - - ##### 创建访问令牌 - - 在Github页右上角找到 `Settings`,然后在左侧导航中点击 `Developer Settings`,在这里选择 `Personal access tokens`. 点击创建新token,输入名称与描述并勾选repo复选框. 如果已有访问令牌但已经忘记,可以选择[重新生成](https://github.com/settings/tokens/new). - - ###### 创建仅授权访问公共仓库的token - - 想要仅授权访问公共仓库,仅勾选 `public_repo` 就可以了,然后点击页面底部的 `Generate Token` 按钮完成操作. - - ![Retrieve GitHub Access Token for Public Repo](../images/github-access-token-public-repo.jpg) - - ###### 创建可访问私有仓库的token - - 想要授权访问私有仓库,勾选 `repo` 下的所有选项. 然后点击页面底部的 `Generate Token` 按钮完成操作. - - ![Retrieve GitHub Access Token for Private Repo](../images/github-access-token-private-repo.jpg) - - ###### 查看你的Github用户名称 - - 在Github页面右上角点击头像图片,"Signed in as ..." 后面就是你的用户名称 - - ![Your GitHub Username](../images/github-myusername.jpg) - - - -保存项目后转到网站根地址,你会看到文档已经正常显示. - -`http:///documents` - -### 发现问题? - -如果你在安装,使用时遇到问题或者发现bug,请发起issues与我们联系: - -https://github.com/abpframework/abp/issues/new \ No newline at end of file diff --git a/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md b/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md deleted file mode 100644 index 21cba49bfb..0000000000 --- a/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md +++ /dev/null @@ -1,3 +0,0 @@ -# ASP.NET Boilerplate v5+ 迁移到 ABP Framework - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md b/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md deleted file mode 100644 index 19a26440eb..0000000000 --- a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到自动API控制器文档](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/AspNetCore/Bundling-Minification.md deleted file mode 100644 index c102c89fd2..0000000000 --- a/docs/zh-Hans/AspNetCore/Bundling-Minification.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到ASP.NET Core MVC 捆绑 & 压缩文档](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md deleted file mode 100644 index f61d4bd623..0000000000 --- a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到ASP.NET Core MVC 客户端包管理文档](../UI/AspNetCore/Client-Side-Package-Management.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md deleted file mode 100644 index 6f45ba4549..0000000000 --- a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到动态 C# API 客户端文档](../API/Dynamic-CSharp-API-Clients.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md deleted file mode 100644 index 857606a0f2..0000000000 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到JavaScript Auth文档](../../API/JavaScript-API/Auth.md) diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md deleted file mode 100644 index 9f3e4d1645..0000000000 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到JavaScript API文档](../../API/JavaScript-API/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md deleted file mode 100644 index ebfeb6f2aa..0000000000 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md +++ /dev/null @@ -1,94 +0,0 @@ -# 按钮 - -ABP框架定义了Tag Helper用于简单的创建bootstrap按钮. - -`` - -## 属性 - -`` 有7个不同的属性. - -* [`button-type`](#button-type) -* [`size`](#size) -* [`busy-text`](#busy-text) -* [`text`](#text) -* [`icon`](#icon) -* [`disabled`](#disabled) -* [`icon-type`](#icon-type) - -### `button-type` - -`button-type` 是一个可选参数. 它的默认值是 `Default`. - -`Button` - -你可以为按钮选择以下按钮类型: - -* `Default` -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` -* `Outline_Primary` -* `Outline_Secondary` -* `Outline_Success` -* `Outline_Danger` -* `Outline_Warning` -* `Outline_Info` -* `Outline_Light` -* `Outline_Dark` -* `Link` - -### `size` - -`size` 是一个可选参数. 它的默认值是 `Default`. - -`Button` - -你可以为按钮选择以下size类型: - -* `Default` -* `Small` -* `Medium` -* `Large` -* `Block` -* `Block_Small` -* `Block_Medium` -* `Block_Large` - -### `busy-text` - -`busy-text` 是一个字符串类型参数. 当按钮繁忙时设置该文本. - -### `text` - -`text` 是一个字符串类型参数,显示在按钮上. - -### `icon` - -`icon` 是一个字符串类型参数. 它的值取决于[`icon-type`](#`icon-type`). 默认情况下,我们对图标使用[Font Awesome](https://fontawesome.com/). 要使用它,你需要将 `icon` 参数设置为图标名称. - -##### 示例 - -[fa-address-card](https://fontawesome.com/icons/address-card): ![fa-address-card](fa-address-card.png "Address Card") - -`` - -> 不要忘记: 你不需要写前缀,如果你没有更改 `icon-type` ,它会为[Font Awesome](https://fontawesome.com/)图标自动添加 `fa` 前缀. - -### `disabled` - -`disabled` 是一个布尔类型参数. 如果你设值为 `true`, 按钮会被禁用. - -### `icon-type` - -`icon-type` 是一个可选参数.它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. - -你可以为按钮选择以下图标类型: - -* `FontAwesome` -* `Other` diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md deleted file mode 100644 index 71e25d25ea..0000000000 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到Dynamic Forms文档](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md deleted file mode 100644 index 96ca033863..0000000000 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md +++ /dev/null @@ -1,3 +0,0 @@ -文档已经移动到其他位置. - -[点击链接跳转到ABP Tag Helpers文档](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png b/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png deleted file mode 100644 index 3e25f1ff4d..0000000000 Binary files a/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png and /dev/null differ diff --git a/docs/zh-Hans/AspNetCore/Theming.md b/docs/zh-Hans/AspNetCore/Theming.md deleted file mode 100644 index 4a863567f1..0000000000 --- a/docs/zh-Hans/AspNetCore/Theming.md +++ /dev/null @@ -1,4 +0,0 @@ - -文档已经移动到其他位置. - -[点击链接跳转到Theming文档](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Widgets.md b/docs/zh-Hans/AspNetCore/Widgets.md deleted file mode 100644 index 423169b6fc..0000000000 --- a/docs/zh-Hans/AspNetCore/Widgets.md +++ /dev/null @@ -1,4 +0,0 @@ - -文档已经移动到其他位置. - -[点击链接跳转到小部件文档](../UI/AspNetCore/Widgets.md) \ No newline at end of file diff --git a/docs/zh-Hans/Audit-Logging.md b/docs/zh-Hans/Audit-Logging.md deleted file mode 100644 index 5463fdb03c..0000000000 --- a/docs/zh-Hans/Audit-Logging.md +++ /dev/null @@ -1,376 +0,0 @@ -# 审计日志 - -[维基百科](https://en.wikipedia.org/wiki/Audit_trail): "*审计跟踪(也称为**审计日志**)是一种安全相关的按时间顺序记录,记录集或记录目的和来源. 这种记录提供了在任何特定时间的操作,过程或事件产生影响活动顺序的文件证据* ". - -ABP框架提供一个可扩展的**审计日志系统**,自动化的根据**约定**记录审计日志,并提供**配置**控制审计日志的级别. - -一个**审计日志对象**(参见下面的审计日志对象部分)通常是针对每个web请求创建和保存的.包括; - -* **请求和响应的细节** (如URL,HTTP方法,浏览器信息,HTTP状态代码...等). -* **执行的动作** (控制器操作和应用服务方法调用及其参数). -* **实体的变化** (在Web请求中). -* **异常信息** (如果在执行请求发生操作). -* **请求时长** (测量应用程序的性能). - -> [启动模板](Startup-Templates/Index.md)已经将审计日志系统配置为适用于大多数应用程序. 本文档介绍了对审计日志系统更精细的控制. - -## 数据库提供程序支持 - -* [Entity Framework Core](Entity-Framework-Core.md)提供程序完全支持. -* [MongoDB](MongoDB.md)提供程序不支持实体更改审计记录. 其他功能按预期工作. - -## UseAuditing() - -`UseAuditing()` 中间件应该被添加到ASP.NET Core请求管道,用于创建和保存审计日志. 如果你使用[启动模板](Startup-Templates/Index.md)创建的应用程序,它已经默认添加. - -## AbpAuditingOptions - -`AbpAuditingOptions` 是配置审计日志系统的主要[options对象](Options.md). 你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中进行配置: - -````csharp -Configure(options => -{ - options.IsEnabled = false; //Disables the auditing system -}); -```` - -这里是你可以配置的选项列表: - -* `IsEnabled` (默认值: `true`): 启用或禁用审计系统的总开关. 如果值为 `false`,则不使用其他选项. -* `HideErrors` (默认值: `true`): 在保存审计日志对象时如果发生任何错误,审计日志系统会将错误隐藏并写入常规[日志](Logging.md). 如果保存审计日志对系统非常重要那么将其设置为 `false` 以便在隐藏错误时抛出异常. -* `IsEnabledForAnonymousUsers` (默认值: `true`): 如果只想为经过身份验证的用户记录审计日志,请设置为 `false`.如果为匿名用户保存审计日志,你将看到这些用户的 `UserId` 值为 `null`. -* `AlwaysLogOnException`(默认值: `true`): 如果设置为 `true`,将始终在异常/错误情况下保存审计日志,不检查其他选项(`IsEnabled` 除外,它完全禁用了审计日志). -* `IsEnabledForGetRequests` (默认值: `false`): HTTP GET请求通常不应该在数据库进行任何更改,审计日志系统不会为GET请求保存审计日志对象. 将此值设置为 `true` 可为GET请求启用审计日志系统. -* `ApplicationName`: 如果有多个应用程序保存审计日志到单一的数据库,使用此属性设置为你的应用程序名称区分不同的应用程序日志. -* `DisableLogActionInfo` (默认值: `false`): 如果设置为 `true`, 将不再记录 `AuditLogActionInfo`. -* `IgnoredTypes`: 审计日志系统忽略的 `Type` 列表. 如果它是实体类型,则不会保存此类型实体的更改. 在序列化操作参数时也使用此列表. -* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. -* `Contributors`: `AuditLogContributor` 实现的列表. 贡献者是扩展审计日志系统的一种方式. 有关详细信息请参阅下面的"审计日志贡献者"部分. - -### 实体历史选择器 - -保存你的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. - -要保存的所有实体的所有更改,只需使用 `AddAllEntities()` 扩展方法. - -````csharp -Configure(options => -{ - options.EntityHistorySelectors.AddAllEntities(); -}); -```` - -`options.EntityHistorySelectors` 实际上是一个类型谓词的列表,你可以写一个lambda表达式定义过滤器. - -下面的示例中与使用 `AddAllEntities()` 扩展方法效果相同: - -````csharp -Configure(options => -{ - options.EntityHistorySelectors.Add( - new NamedTypeSelector( - "MySelectorName", - type => - { - if (typeof(IEntity).IsAssignableFrom(type)) - { - return true; - } - else - { - return false; - } - } - ) - ); -}); -```` - -条件 `typeof(IEntity).IsAssignableFrom(type)` 对于任何实现 `IEntity` 接口的类(从技术上来这些都是你应用程序中的实体) 结果都为 `true` . 你可以根据自己的逻辑编写条件并返回 `true` 或 `false`. - -`options.EntityHistorySelectors` 是一种灵活动态的选择实体进行审计日志记录的方法. 另一种方法是为每个实体使用 `Audited` 和 `DisableAuditing` attribute. - -## 启用/禁用审计日志服务 - -### 启用/禁用 Controllers & Actions - -默认所有的控制器动作都会被记录下来(有关GET请求,请参阅上面的 `IsEnabledForGetRequests` ). - -你可以使用 `[DisableAuditing]` 来禁用特定的控制器: - -````csharp -[DisableAuditing] -public class HomeController : AbpController -{ - //... -} -```` - -使用 `[DisableAuditing]` 在action级别控制: - -````csharp -public class HomeController : AbpController -{ - [DisableAuditing] - public async Task Home() - { - //... - } - - public async Task OtherActionLogged() - { - //... - } -} -```` - -### 启用/禁用 应用服务&方法 - -[应用服务](Application-Services.md)也默认包含在审计日志中. 你可在服务或方法级别使用 `[DisableAuditing]`. - -#### 启用/禁用 其他服务 - -可以为任何类型的类(注册到[依赖注入](Dependency-Injection.md)并从依赖注入解析)启用审计日志,默认情况下仅对控制器和应用程序服务启用. - -对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,你的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. - -### 启用/禁用 实体 & 属性 - -以下情况下实体在实体更改审计日志记录中忽略实体; - -* 如果将实体类型添加到 `AbpAuditingOptions.IgnoredTypes`(如前所述),它在审计日志系统中被完全忽略. -* 如果对象不是[实体](Entities.md)(没有直接或固有的实现 `IEntity` - 所有实体默认实现这个接口). -* 如果实体访问级别不是public的. - -你可以使用 `Audited` 来启用实体更改审计日志: - -````csharp -[Audited] -public class MyEntity : Entity -{ - //... -} -```` - -或者禁用实体: - -````csharp -[DisableAuditing] -public class MyEntity : Entity -{ - //... -} -```` - -只有前面提到的 `AbpAuditingOptions.EntityHistorySelector` 选择实体时才有必要禁用审计日志记录. - -你可以仅禁用实体的某些属性的审计,以审计日志记录进行精细控制: - -````csharp -[Audited] -public class MyUser : Entity -{ - public string Name { get; set; } - - public string Email { get; set; } - - [DisableAuditing] //Ignore the Passoword on audit logging - public string Password { get; set; } -} -```` - -审计日志系统保存 `MyUser` 实体的更改,出于安全的目的忽略 `Password` 属性. - -在某些情况下你可能要保存一些属性,但忽略所有其他属性. 为忽略的属性编写 `[DisableAuditing]` 将很乏味. 这种情况下将 `[Audited]` 用于所需的属性,使用 `[DisableAuditing]` 属性标记该实体: - -````csharp -[DisableAuditing] -public class MyUser : Entity -{ - [Audited] //Only log the Name change - public string Name { get; set; } - - public string Email { get; set; } - - public string Password { get; set; } -} -```` - -## IAuditingStore - -`IAuditingStore` 是一个接口,用于保存ABP框架的审计日志对象(下面说明). 如果需要将审计日志对象保存到自定义数据存储中,可以在自己的应用程序中实现 `IAuditingStore` 并在[依赖注入系统](Dependency-Injection.md)替换. - -如果没有注册审计存储,则使用 `SimpleLogAuditingStore`. 它只是将审计对象写入标准[日志系统](Logging.md). - -[审计日志模块](Modules/Audit-Logging.md)已在[启动模板](Startup-Templates/Index.md)中配置,它将审计日志对象保存到数据库中(支持多个数据库提供程序). 所以大多数时候你并不需要关心 `IAuditingStore` 是如何实现和使用的. - -## 审计日志对象 - -默认为每个**web请求**创建一个**审计日志对象**,审计日志对象可以由以下关系图表示: - -![**auditlog-object-diagram**](images/auditlog-object-diagram.png) - -* **AuditLogInfo**: 具有以下属性: - * `ApplicationName`: 当你保存不同的应用审计日志到同一个数据库,这个属性用来区分应用程序. - * `UserId`:当前用户的Id,用户未登录为 `null`. - * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). - * `TenantId`: 当前租户的Id,对于多租户应用. - * `TenantName`: 当前租户的名称,对于多租户应用. - * `ExecutionTime`: 审计日志对象创建的时间. - * `ExecutionDuration`: 请求的总执行时间,以毫秒为单位. 可以用来观察应用程序的性能. - * `ClientId`: 当前客户端的Id,如果客户端已经通过认证.客户端通常是使用HTTP API的第三方应用程序. - * `ClientName`: 当前客户端的名称,如果有的话. - * `ClientIpAddress`: 客户端/用户设备的IP地址. - * `CorrelationId`: 当前[相关Id](CorrelationId.md). 相关Id用于在单个逻辑操作中关联由不同应用程序(或微服务)写入的审计日志. - * `BrowserInfo`: 当前用户的浏览器名称/版本信息,如果有的话. - * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). - * `HttpStatusCode`: HTTP响应状态码. - * `Url`: 请求的URL. -* **AuditLogActionInfo**: 一个 审计日志动作通常是web请求期间控制器动作或[应用服务](Application-Services.md)方法调用. 一个审计日志可以包含多个动作. 动作对象具有以下属性: - * `ServiceName`:执行的控制器/服务的名称. - * `MethodName`:控制器/服务执行的方法的名称. - * `Parameters`:传递给方法的参数的JSON格文本. - * `ExecutionTime`: 执行的时间. - * `ExecutionDuration`: 方法执行时长,以毫秒为单位. 可以用来观察方法的性能. -* **EntityChangeInfo**: 表示一个实体在Web请求中的变更. 审计日志可以包含0个或多个实体的变更. 实体变更具有以下属性: - * `ChangeTime`: 当实体被改变的时间. - * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). - * `EntityId`: 更改实体的Id. - * `EntityTenantId`:实体所属的租户Id. - * `EntityTypeFullName`: 实体的类型(类)的完整命名空间名称(例如Book实体的*Acme.BookStore.Book*. -* **EntityPropertyChangeInfo**: 表示一个实体的属性的更改.一个实体的更改信息(上面已说明)可含有具有以下属性的一个或多个属性的更改: - * `NewValue`: 属性的新值. 如果实体已被删除为 `null`. - * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. - * `PropertyName`: 实体类的属性名称. - * `PropertyTypeFullName`:属性类型的完整命名空间名称. -* **Exception**: 审计日志对象可能包含零个或多个异常. 可以得到失败请求的异常信息. -* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. - -除了上面说明的标准属性之外,`AuditLogInfo`, `AuditLogActionInfo` 和 `EntityChangeInfo` 对象还实现了`IHasExtraProperties` 接口,你可以向这些对象添加自定义属性. - -## 审计日志贡献者 - -你可以创建类继承 `AuditLogContributor`类 来扩展审计系统,该类定义了 `PreContribute` 和 `PostContribute` 方法. - -唯一预构建的贡献者是 `AspNetCoreAuditLogContributor` 类,它设置HTTP请求的相关属性. - -贡献者可以设置 `AuditLogInfo` 类的属性和集合来添加更多信息. - -例: - -````csharp -public class MyAuditLogContributor : AuditLogContributor -{ - public override void PreContribute(AuditLogContributionContext context) - { - var currentUser = context.ServiceProvider.GetRequiredService(); - context.AuditInfo.SetProperty( - "MyCustomClaimValue", - currentUser.FindClaimValue("MyCustomClaim") - ); - } - - public override void PostContribute(AuditLogContributionContext context) - { - context.AuditInfo.Comments.Add("Some comment..."); - } -} -```` - -* `context.ServiceProvider` 可以从[依赖注入系统](Dependency-Injection.md)中解析服务. -* `context.AuditInfo` 可以用来访问当前审计日志的对象并进行操作. - -创建贡献者后,需要将其添加到 `AbpAuditingOptions.Contributors` 列表中: - -````csharp -Configure(options => -{ - options.Contributors.Add(new MyAuditLogContributor()); -}); -```` - -## IAuditLogScope & IAuditingManager - -本节介绍用于高级用例的 `IAuditLogScope` 和 `IAuditingManager` 服务. - -**审计日志范围**是**构建**和**保存**审计日志对象的[环境范围](Ambient-Context-Pattern.md)(前面解释过). 默认审计日志中间件会为Web请求创建审计日志范围(请参阅上面的 `UseAuditing()` 部分). - -### 获取当前审计日志范围 - -上面提到,审计日志贡献者是操作审计日志对象的全局方法. 你可从服务中获得值. - -如果需要在应用程序的任意位置上操作审计日志对象,可以访问当前审计日志范围并获取当前审计日志对象(与范围的管理方式无关). -例: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAuditingManager _auditingManager; - - public MyService(IAuditingManager auditingManager) - { - _auditingManager = auditingManager; - } - - public async Task DoItAsync() - { - var currentAuditLogScope = _auditingManager.Current; - if (currentAuditLogScope != null) - { - currentAuditLogScope.Log.Comments.Add( - "Executed the MyService.DoItAsync method :)" - ); - - currentAuditLogScope.Log.SetProperty("MyCustomProperty", 42); - } - } -} -```` - -总是检查 `_auditingManager.Current` 是否为空,因为它是在外部范围中控制的,在调用方法之前你不知道是否创建了审计日志范围. - -### 手动创建审计日志范围 - -你很少需要手动创建审计日志的范围,但如果你需要,可以使用 `IAuditingManager` 创建审计日志的范围. -例: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAuditingManager _auditingManager; - - public MyService(IAuditingManager auditingManager) - { - _auditingManager = auditingManager; - } - - public async Task DoItAsync() - { - using (var auditingScope = _auditingManager.BeginScope()) - { - try - { - //Call other services... - } - catch (Exception ex) - { - //Add exceptions - _auditingManager.Current.Log.Exceptions.Add(ex); - } - finally - { - //Always save the log - await auditingScope.SaveAsync(); - } - } - } -} -```` - -你可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. - -## 审计日志模块 - -审计日志模块基本上实现了 `IAuditingStore`, 将审计日志对象保存到数据库中并支持多个数据库提供程序. 默认此模块已添加到启动模板中. - -参见[审计日志模块文档](Modules/Audit-Logging.md)了解更多. \ No newline at end of file diff --git a/docs/zh-Hans/Authentication/Social-External-Logins.md b/docs/zh-Hans/Authentication/Social-External-Logins.md deleted file mode 100644 index a1cdbb86ce..0000000000 --- a/docs/zh-Hans/Authentication/Social-External-Logins.md +++ /dev/null @@ -1,3 +0,0 @@ -# 社交/外部登录 - -> 文档已经移动其他位置. 参阅[账户模块](../Modules/Account.md)文档. \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md deleted file mode 100644 index 39c64c0905..0000000000 --- a/docs/zh-Hans/Authorization.md +++ /dev/null @@ -1,438 +0,0 @@ -# 授权 - -授权用于在应用程序中判断是否允许用户执行某些特定的操作. - -ABP扩展了[ASP.NET Core 授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction), 将 **权限** 添加为自动[策略](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies)并且使授权系统在 **[应用服务](Application-Services.md)** 同样可用. - -所以ASP.NET Core授权的功能特性和它的文档在基于ABP的应用程序是可用的. -本文中着重介绍在ASP.NET Core授权功能基础上添加的功能. - -## Authorize Attribute - -ASP.NET Core 定义了 [**Authorize**](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/simple) attribute 用于在控制器,控制器方法以及页面上授权. 现在ABP将它带到了[应用服务](Application-Services.md). - -示例: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - [Authorize] - public class AuthorAppService : ApplicationService, IAuthorAppService - { - public Task> GetListAsync() - { - ... - } - - [AllowAnonymous] - public Task GetAsync(Guid id) - { - ... - } - - [Authorize("BookStore_Author_Create")] - public Task CreateAsync(CreateAuthorDto input) - { - ... - } - } -} - -```` - -* `Authorize` attribute 使用户必须登陆到应用程序才可以访问 `AuthorAppService` 中的方法. 所以`GetListAsync` 方法仅可用于通过身份验证的用户. -* `AllowAnonymous` 禁用身份验证. 所以 `GetAsync` 方法任何人都可以访问,包括未授权的用户. -* `[Authorize("BookStore_Author_Create")]` 定义了一个策略 (参阅 [基于策略的授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies)),它用于检查当前用户的权限. - -"BookStore_Author_Create" 是一个策略名称. 如果你想要使用策略的授权方式,需要在ASP.NET Core授权系统中预先定义它. - -你可以按照ASP.NET Core文档进行实施策略授权,但对于简单的 `true/false` 条件(比如是否授予了用户策略) ABP定义了权限系统,在下一部分中会进行讲解. - -## 权限系统 - -权限系统是为特定用户,角色或客户端授权或禁止的简单策略. - -### 定义权限 - -创建一个继承自 `PermissionDefinitionProvider` 的类,如下所示: - -````csharp -using Volo.Abp.Authorization.Permissions; - -namespace Acme.BookStore.Permissions -{ - public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider - { - public override void Define(IPermissionDefinitionContext context) - { - var myGroup = context.AddGroup("BookStore"); - - myGroup.AddPermission("BookStore_Author_Create"); - } - } -} -```` - -> ABP会自动发现这个类,不需要进行配置! - -你需要在 `Define` 方法中添加**权限组**或者获取已存在的权限组,并向权限组中添加**权限**. - -在定义权限后就可以在ASP.NET Core权限系统中当做**策略**名称使用. 在角色的权限管理模态框中同样可以看到: - -![authorization-new-permission-ui](images/authorization-new-permission-ui.png) - -* 左边的选项卡显示了 "BookStore" 权限组. -* 右侧的 "BookStore_Author_Create" 是权限名称,在这里可以为角色授权或禁止. - -保存后更改会持久化到数据库并在授权系统使用. - -> 只有在你安装了identity模块时,才会出现上图中的管理对话框. 该模块用于用户和角色的. 启动模板中已经预装identity模块. - -#### 本地化权限名称 - -"BookStore_Author_Create" 名称对于权限系统来说很适合,但对于UI展示会让操作人员费解. 幸运的是 `AddPermission` 和 `AddGroup` 方法提供了 `LocalizableString` 参数: - -````csharp -var myGroup = context.AddGroup( - "BookStore", - LocalizableString.Create("BookStore") -); - -myGroup.AddPermission( - "BookStore_Author_Create", - LocalizableString.Create("Permission:BookStore_Author_Create") -); -```` - -然后在本地化文档中对 "BookStore" 和 "Permission:BookStore_Author_Create" 键添加本地化语言: - -````json -"BookStore": "Book Store", -"Permission:BookStore_Author_Create": "Creating a new author" -```` - -> 有关更多信息请参阅[本地化系统文档](Localization.md). - -下图展示了本地化后的效果: - -![authorization-new-permission-ui-localized](images/authorization-new-permission-ui-localized.png) - -#### 多租户 - -权限系统同样支持在ABP中做为一等公民的 [多租户](Multi-Tenancy.md). 在定义新权限时可以设置多租户选项. 有下面三个值: - -* **Host**: 权限仅适用于宿主. -* **Tenant**: 权限仅适用于租户. -* **Both** (默认): 权限适用与宿主和租户. - -> 如果你的应用程序不是多租户的,可以忽略这个选项. - -`AddPermission` 方法的第三个参数用于设置多租户选项: - -````csharp -myGroup.AddPermission( - "BookStore_Author_Create", - LocalizableString.Create("Permission:BookStore_Author_Create"), - multiTenancySide: MultiTenancySides.Tenant //set multi-tenancy side! -); -```` - -#### 启用/禁用权限 - -权限默认为启用. 它也可以被禁用,禁用权限所有的用户将无法使用它. 你仍然可以检查这个权限,但它总是会返回被禁止. - -定义示例: - -````csharp -myGroup.AddPermission("Author_Management", isEnabled: false); -```` - -通常你不需要定义禁用权限(除非你暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. - -> 注意:检查一个未定义的权限会抛出异常,而被禁用的权限的返回禁止(false). - -#### 子权限 - -权限可以具有子权限,当你想要创建一个层次结构的权限树时它特别有用. 在这个树中一个权限可能含有子权限,并且子权限只有在授权父权限时才可用. - -定义示例: - -````csharp -var authorManagement = myGroup.AddPermission("Author_Management"); -authorManagement.AddChild("Author_Management_Create_Books"); -authorManagement.AddChild("Author_Management_Edit_Books"); -authorManagement.AddChild("Author_Management_Delete_Books"); -```` - -在页面上如下所示 (你可能想要本地化权限名称): - -![authorization-new-permission-ui-hierarcy](images/authorization-new-permission-ui-hierarcy.png) - -下面的示例代码是一个典型的应用服务: - -````csharp -[Authorize("Author_Management")] -public class AuthorAppService : ApplicationService, IAuthorAppService -{ - public Task> GetListAsync() - { - ... - } - - public Task GetAsync(Guid id) - { - ... - } - - [Authorize("Author_Management_Create_Books")] - public Task CreateAsync(CreateAuthorDto input) - { - ... - } - - [Authorize("Author_Management_Edit_Books")] - public Task UpdateAsync(CreateAuthorDto input) - { - ... - } - - [Authorize("Author_Management_Delete_Books")] - public Task DeleteAsync(CreateAuthorDto input) - { - ... - } -} -```` - -* 拥有`Author_Management`权限的用户可以访问`GetListAsync`和 `GetAsync`方法. -* 示例中的其他方法需要额外的权限. - -### 自定义策略覆盖已有权限 - -有时我们需要扩展扩展应用程序中预构建模块的权限,可以定义并注册一个与权限名称相同的策略到ASP.Net Core授权系统,策略会覆盖已有权限. - -参阅 [基于策略的授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies) 文档了解如何自定义策略. - -### 更改依赖模块的权限定义 - -从 `PermissionDefinitionProvider` 派生的类(就像上面的示例一样) 可以获取现有的权限定义(由依赖[模块](Module-Development-Basics.md)定义)并更改其定义. - -示例: - -````csharp -context - .GetPermissionOrNull(IdentityPermissions.Roles.Delete) - .IsEnabled = false; -```` - -当你在权限提供程序编写了这行代码,它会找到[身份模块](Modules/Identity.md)的 "role deletion" 权限并且禁用它,因此没有人可以在应用程序中删除角色. - -> 提供: 更好的方式应该检查 `GetPermissionOrNull` 返回值,如果权限未定义,它会返回null值. - -## IAuthorizationService - -ASP.NET Core 提供了 `IAuthorizationService` 用于检查权限. 注入后使用它进行条件控制权限. - -示例: - -````csharp -public async Task CreateAsync(CreateAuthorDto input) -{ - var result = await AuthorizationService - .AuthorizeAsync("Author_Management_Create_Books"); - if (result.Succeeded == false) - { - //throw exception - throw new AbpAuthorizationException("..."); - } - - //continue to the normal flow... -} -```` - -> 因为应用服务会经常检查权限,`ApplicationService` 已经属性注入了`AuthorizationService`, 所有继承自 `ApplicationService` 的类都可以直接使用. 你也可以直接 [注入](Dependency-Injection.md)到类中. - -上面的示例代码是检查权限的标准代码,ABP提供了一种简化的方式来编写它. - -示例: - -````csharp -public async Task CreateAsync(CreateAuthorDto input) -{ - await AuthorizationService.CheckAsync("Author_Management_Create_Books"); - - //continue to the normal flow... -} -```` - -如果未授权 `CheckAsync` 扩展方法会抛出 `AbpAuthorizationException` 异常. 还有一个 `IsGrantedAsync` 扩展方法会返回 `true` 或 `false`. - -`IAuthorizationService` 中有多个 `AuthorizeAsync` 方法重载. [ASP.NET Core 授权文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction)中有详细的解释. - -> 提示: 尽可能使用声明式的 `Authorize` attribute,因为它比较简单不会侵入方法内部. 如果你需要在业务代码中有条件的检查权限,那么请使用 `IAuthorizationService`. - -### 在JavaScript中检查权限 - -有时你会需要在客户端检查策略/权限. 在ASP.NET Core MVC/Razor页面应用程序可以使用 `abp.auth` API. 示例: - -````js -abp.auth.isGranted('MyPermissionName'); -```` - -## 权限管理 - -通常权限管理是管理员用户使用权限管理模态框进行授权: - -![authorization-new-permission-ui-localized](images/authorization-new-permission-ui-localized.png) - -如果你想要通过代码管理权限, 可以注入使用 `IPermissionManager`. 如下所示: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IPermissionManager _permissionManager; - - public MyService(IPermissionManager permissionManager) - { - _permissionManager = permissionManager; - } - - public async Task GrantPermissionForUserAsync(Guid userId, string permissionName) - { - await _permissionManager.SetForUserAsync(userId, permissionName, true); - } - - public async Task ProhibitPermissionForUserAsync(Guid userId, string permissionName) - { - await _permissionManager.SetForUserAsync(userId, permissionName, false); - } -} -```` - -`SetForUserAsync` 方法用于设置用户的权限 (true/false). 类似的还有 `SetForRoleAsync` 和 `SetForClientAsync` 扩展方法. - -`IPermissionManager` 由权限管理模块定义, 更多信息请参阅 [权限管理模块文档](Modules/Permission-Management.md). - -## 高级主题 - -### Permission Value Providers - -权限检查是可扩展的. 继承自 `PermissionValueProvider` (或实现 `IPermissionValueProvider`) 的任何类都可以参与权限检查. 有三个预定义的Provider: - -* `UserPermissionValueProvider` 从当前的声明中拿到当前用户ID并检查用户授权. 用户声明由 `AbpClaimTypes.UserId` 静态属性定义. -* `RolePermissionValueProvider` 从当前的声明中拿到授予当前用户的角色集合并且判断角色是否具有指定的权限. 角色声明由 `AbpClaimTypes.Role` 静态属性定义. -* `ClientPermissionValueProvider` 从当前声明中拿到当前客户端并检查客户端是否具有指定的权限. 这在没有当前登录用户的客户端交互特别有用. 客户端声明由 `AbpClaimTypes.ClientId` 静态属性定义. - -你可以定义自己的`PermissionValueProvider`扩展权限检查系统. - -示例: - -````csharp -public class SystemAdminPermissionValueProvider : PermissionValueProvider -{ - public SystemAdminPermissionValueProvider(IPermissionStore permissionStore) - : base(permissionStore) - { - } - - public override string Name => "SystemAdmin"; - - public async override Task - CheckAsync(PermissionValueCheckContext context) - { - if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") - { - return PermissionGrantResult.Granted; - } - - return PermissionGrantResult.Undefined; - } -} -```` - -示例`SystemAdminPermissionValueProvider`允许声明`User_Type`值为`SystemAdmin`的用户授予所有权限. 通常在`Provider`中使用当前声明和 `IPermissionStore`. - -`PermissionValueProvider` 的 `CheckAsync` 应该返回下面三个值之一: - -* `PermissionGrantResult.Granted` 授予用户权限,如果没有其他的授权值提供程序返回 `Prohibited`, 那么最后会返回 `Granted`. -* `PermissionGrantResult.Prohibited` 禁止授权用户,任何一个授权值提供程序返回了 `Prohibited`, 那么其他的提供程序返回的值都不再重要. -* `PermissionGrantResult.Undefined` 代表当前无法确定是否授予或禁止权限, 返回`UnDefined`由其他权限值提供程序检查权限. - -定义`Provider`后将其添加到 `AbpPermissionOptions`,如下所示: - -````csharp -Configure(options => -{ - options.ValueProviders.Add(); -}); -```` - -### Permission Store - -`IPermissionStore` 是唯一需要从持久化源(通常是数据库)中读取权限值的接口. 它的实现在权限管理模块. 参见 [权限管理模块](Modules/Permission-Management.md) 了解更多信息 - -### AlwaysAllowAuthorizationService - -`AlwaysAllowAuthorizationService` 类可以绕过授权服务. 通常用于在需要禁用授权系统的集成测试中. - -使用 `IServiceCollection.AddAlwaysAllowAuthorization()` 扩展方法将 `AlwaysAllowAuthorizationService` 注册到 [依赖注入](Dependency-Injection.md) 系统中: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.AddAlwaysAllowAuthorization(); -} -```` - -启动模板的集成测试已经禁用了授权服务. - -### Claims Principal Factory - -声明是认证和授权的重要组成部分. ABP 使用 `IAbpClaimsPrincipalFactory` 来服务创建身份认证声明. 该服务被设计为可扩展的. 如果你需要将自定义声明添加到身份认证票证中, 可以在你的应用程序中实现 `IAbpClaimsPrincipalContributor`. - -**示例:添加一个 `SocialSecurityNumber ` 声明并获取它:** - -```csharp -public class SocialSecurityNumberClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency -{ - public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context) - { - var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); - var userId = identity?.FindUserId(); - if (userId.HasValue) - { - var userService = context.ServiceProvider.GetRequiredService(); //Your custom service - var socialSecurityNumber = await userService.GetSocialSecurityNumberAsync(userId.Value); - if (socialSecurityNumber != null) - { - identity.AddClaim(new Claim("SocialSecurityNumber", socialSecurityNumber)); - } - } - } -} - -Configure(options=> -{ - options.RequestedClaims.Add("SocialSecurityNumber") -}) - -public static class CurrentUserExtensions -{ - public static string GetSocialSecurityNumber(this ICurrentUser currentUser) - { - return currentUser.FindClaimValue("SocialSecurityNumber"); - } -} -``` - -## 接下来 - -* [权限管理模块](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor 页面 JavaScript Auth API](API/JavaScript-API/Auth.md) -* [Angular界面中的权限管理](UI/Angular/Permission-Management.md) diff --git a/docs/zh-Hans/Autofac-Integration.md b/docs/zh-Hans/Autofac-Integration.md deleted file mode 100644 index 6fd2a91e6f..0000000000 --- a/docs/zh-Hans/Autofac-Integration.md +++ /dev/null @@ -1,81 +0,0 @@ -# 集成 Autofac - -Autofac 是.Net世界中最常用的依赖注入框架之一. 相比.Net Core标准的依赖注入库, 它提供了更多高级特性, 比如动态代理和属性注入. - -## 安装 Autofac - -> 所有的启动模板和示例都已经集成了 Autofac. 所以, 多数时候你无需手动安装这个包. - -安装 [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget 包到你的项目 (对于一个多项目应用程序, 建议安装到可执行项目或者Web项目中.) - -```` -Install-Package Volo.Abp.Autofac -```` - -然后为你的模块添加 `AbpAutofacModule` 依赖: - -```csharp -using Volo.Abp.Modularity; -using Volo.Abp.Autofac; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAutofacModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -最后, 配置 `AbpApplicationCreationOptions` 用 Autofac 替换默认的依赖注入服务. 根据应用程序类型, 情况有所不同. - -### ASP.NET Core 应用程序 - -如下所示, 在 **Program.cs** 文件中调用 `UseAutofac()`: - -````csharp -public class Program -{ - public static int Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .UseAutofac(); //Integrate Autofac! -} -```` - -### 控制台应用程序 - -如下所示, 在 `AbpApplicationFactory.Create` 中用options调用 `UseAutofac()` 方法: - -````csharp -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; - -namespace AbpConsoleDemo -{ - class Program - { - static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); //Autofac integration - })) - { - //... - } - } - } -} -```` - diff --git a/docs/zh-Hans/Background-Jobs-Hangfire.md b/docs/zh-Hans/Background-Jobs-Hangfire.md deleted file mode 100644 index d3fb341bab..0000000000 --- a/docs/zh-Hans/Background-Jobs-Hangfire.md +++ /dev/null @@ -1,118 +0,0 @@ -# Hangfire后台作业管理 - -[Hangfire](https://www.hangfire.io/)是一个高级的后台作业管理. 你可以用ABP框架集成Hangfire代替[默认后台作业管理](Background-Jobs.md). 通过这种方式你可以使用相同的后台作业API,将你的代码独立于Hangfire. 如果你喜欢也可以直接使用Hangfire的API. - -> 参阅[后台作业文档](Background-Jobs.md),学习如何使用后台作业系统. 本文只介绍了如何安装和配置Hangfire集成. - -## 安装 - -建议使用[ABP CLI](CLI.md)安装包. - -### 使用ABP CLI - -在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: - -````bash -abp add-package Volo.Abp.BackgroundJobs.HangFire -```` - -### 手动安装 - -如果你想手动安装; - -1. 添加 [Volo.Abp.BackgroundJobs.HangFire](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.HangFire) NuGet包添加到你的项目: - - ```` - Install-Package Volo.Abp.BackgroundJobs.HangFire - ```` - -2. 添加 `AbpBackgroundJobsHangfireModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsHangfireModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## 配置 - -你可以安装任何Hangfire存储. 最常用的是SQL Server(参阅[Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer)NuGet包). - -当你安装NuGet包后,你需要为你的项目配置Hangfire. - -1.首先, 我们需要更改 `Module` 类 (例如: `HttpApiHostModule`) 的 `ConfigureServices` 方法去配置Hangfire存储和连接字符串: - -````csharp - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - var hostingEnvironment = context.Services.GetHostingEnvironment(); - - //... other configarations. - - ConfigureHangfire(context, configuration); - } - - private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) - { - context.Services.AddHangfire(config => - { - config.UseSqlServerStorage(configuration.GetConnectionString("Default")); - }); - } -```` - -> 你必须为Hangfire配置一个存储 - -### 指定队列 - -你可以使用 [`QueueAttribute`](https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html) 来指定队列. - -````csharp -using System.Threading.Tasks; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; - -namespace MyProject -{ - [Queue("alpha")] - public class EmailSendingJob - : AsyncBackgroundJob, ITransientDependency - { - private readonly IEmailSender _emailSender; - - public EmailSendingJob(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public override async Task ExecuteAsync(EmailSendingArgs args) - { - await _emailSender.SendAsync( - args.EmailAddress, - args.Subject, - args.Body - ); - } - } -} -```` - -1. 如果你想要使用Hangfire的面板,你可以在 `Module` 类的 `OnApplicationInitialization` 方法添加: `UseHangfireDashboard` - -````csharp - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - - // ... others - - app.UseHangfireDashboard(); - - } -```` \ No newline at end of file diff --git a/docs/zh-Hans/Background-Jobs-Quartz.md b/docs/zh-Hans/Background-Jobs-Quartz.md deleted file mode 100644 index 427276baaf..0000000000 --- a/docs/zh-Hans/Background-Jobs-Quartz.md +++ /dev/null @@ -1,157 +0,0 @@ -# Quartz 后台作业管理 - -[Quartz](https://www.quartz-scheduler.net/)是一个高级的作业管理. 你可以用ABP框架集成Quartz代替[默认后台作业管理](Background-Jobs.md). 通过这种方式你可以使用相同的后台作业API,将你的代码独立于Quartz. 如果你喜欢也可以直接使用Quartz的API. - -> 参阅[后台作业文档](Background-Jobs.md),学习如何使用后台作业系统. 本文只介绍了如何安装和配置Quartz集成. - -## 安装 - -建议使用[ABP CLI](CLI.md)安装包. - -### 使用ABP CLI - -在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: - -````bash -abp add-package Volo.Abp.BackgroundJobs.Quartz -```` - -### 手动安装 - -如果你想手动安装; - -1. 添加 [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet包添加到你的项目: - - ```` - Install-Package Volo.Abp.BackgroundJobs.Quartz - ```` - -2. 添加 `AbpBackgroundJobsQuartzModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## 配置 - -Quartz是一个可配置的类库,对此ABP框架提供了 `AbpQuartzOptions`. 你可以在模块预配置此选项,ABP在初始化Quartz模块时将使用它. 例: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - PreConfigure(options => - { - options.Properties = new NameValueCollection - { - ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp", - ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", - ["quartz.jobStore.tablePrefix"] = "QRTZ_", - ["quartz.serializer.type"] = "json", - ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"), - ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer", - ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", - }; - }); - } -} -```` - -从ABP3.1版本开始,我们在 `AbpQuartzOptions` 添加了 `Configurator` 用于配置Quartz. 例: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - PreConfigure(options => - { - options.Configurator = configure => - { - configure.UsePersistentStore(storeOptions => - { - storeOptions.UseProperties = true; - storeOptions.UseJsonSerializer(); - storeOptions.UseSqlServer(configuration.GetConnectionString("Quartz")); - storeOptions.UseClustering(c => - { - c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); - c.CheckinInterval = TimeSpan.FromSeconds(10); - }); - }); - }; - }); - } -} -```` - -> 你可以选择你喜爱的方式来配置Quaratz. - -Quartz**默认**将作业与调度信息存储在**内存**中,示例中我们使用[选项模式](Options.md)的预配置将其更改为存储到数据库中. 有关Quartz的更多配置请参阅[Quartz文档](https://www.quartz-scheduler.net/). - -## 异常处理 - -### 默认异常处理策略 - -当后台作业发生异常时ABP提供了**默认**异常处理策略,它会为你的作业重试3次,每次间隔3秒. 你可以通过 `AbpBackgroundJobQuartzOptions` 更改默认重试次数与间隔时间: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.RetryCount = 1; - options.RetryIntervalMillisecond = 1000; - }); - } -} -``` - -### 自定义异常处理策略 - -你可以通过 `AbpBackgroundJobQuartzOptions` 选项自定义异常处理策略: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.RetryStrategy = async (retryIndex, executionContext, exception) => - { - // customize exception handling - }; - }); - } -} -``` \ No newline at end of file diff --git a/docs/zh-Hans/Background-Jobs-RabbitMq.md b/docs/zh-Hans/Background-Jobs-RabbitMq.md deleted file mode 100644 index 8bf61846e7..0000000000 --- a/docs/zh-Hans/Background-Jobs-RabbitMq.md +++ /dev/null @@ -1,159 +0,0 @@ -# RabbitMQ 后台作业管理 - -RabbitMQ 是一个标准的消息队列中间件,虽然它常用于消息传递/分布式事件,但也非常适合存储 FIFO(先进先出) 顺序的后台作业. - -ABP Framework 提供了 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包,将使用 RabbitMQ 来执行后台作业. - -> 参阅 [后台作业文档](Background-Jobs.md) 学习如何使用后台作业系统,本文只介绍了如何安装和配置 RabbitMQ 集成. - -## 安装 - -使用 ABP CLI 将 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包添加到你的项目: - -- 如果之前没有安装过 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI),请先安装它. -- 跳转到待安装后台作业管理的项目目录中(包含 `.csproj` 文件的目录),打开终端管理器. -- 执行 `abp add-package Volo.Abp.BackgroundJobs.RabbitMQ` 命令. - -如果你想要手动安装,请先用 NuGet 包管理器安装 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包到指定项目,之后使在你的 [模块](Module-Development-Basics.md) 上面添加 `[DependsOn(typeof(AbpBackgroundJobsRabbitMqModule))]` 配置依赖. - -## 配置 - -### 默认配置 - -默认配置将会使用标准端口和主机名(localhost)连接到 RabbitMQ 服务,**你不需要进行额外配置**. - -### RabbitMQ 连接 - -你可以使用 ASP.NET Core 的 [标准配置系统](Configuration.md) 对 RabbitMQ 进行详细配置,比如 `appsettings.json` 或者是 [选项类](Options.md). - -#### 通过 `appsettings.json` 文件配置 - -这种方式是配置 RabbitMQ 连接最简单的方式,你可以使用其他的配置源(例如环境变量).这些强大的功能都是由 [ASP.NET Core](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/) 提供的支持. - -**示例: 配置默认的 RabbitMQ 连接** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123", - "Port": "5672" - } - } - } -} -``` - -你可以在配置文件使用所有 [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) 的属性,关于这些属性的具体含义,可以查看 RabbitMQ 的 [官方文档](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues). - -目前我们允许定义多个连接,多连接的情况适用于不同的后台作业,具体配置信息可以参考下面的 RabbitMQ 后台作业配置说明. - -**示例: 定义两个 RabbitMQ 连接** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - }, - "SecondConnection": { - "HostName": "321.321.321.321" - } - } - } -} -``` - -如果需要连接到 RabbitMQ 集群,你可以指定多个 HostName. - -**示例: 连接到 RabbitMQ 集群** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123;234.234.234.234" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -``` - -#### 使用选项类 - -`AbpRabbitMqOptions` 类型用于配置 RabbitMQ 的连接字符串,你可以在 [模块](Module-Development-Basics.md) 的 `ConfigureService` 方法中进行配置. - -**示例: 配置 RabbitMQ 连接** - -```csharp -Configure(options => -{ - options.Connections.Default.UserName = "user"; - options.Connections.Default.Password = "pass"; - options.Connections.Default.HostName = "123.123.123.123"; - options.Connections.Default.Port = 5672; -}); -``` - -关于选项类,可以结合 `appsettings.json` 文件一起使用.针对同一个属性,在选项类里面对该值进行了设定,会覆盖掉 `appsettings.json` 的值. - -### RabbitMQ 后台作业配置说明 - -#### 后台作业队列的名称 - -默认情况下,每个后台作业都会使用一个单独的队列,结合标准前缀和作业名称来构造一个完整的队列名称.默认的前缀为 `AbpBackgroundJobs`,所以有一个作业的名称是 `EmailSending` 的话,在 RabbitMQ 的队列名称就是 `AbpBackgroundJobs.EmailSending`. - -> 在后台作业的参数类上,可以使用 `BackgroundJobName` 特性指定后台作业的名称.否则的话,后台作业的名称将会是后台作业类的全名(也包含命名空间). - -#### 后台作业使用的连接 - -默认情况下,后台作业都会使用 `Default` 作为默认连接. - -#### 自定义 - -`AbpRabbitMqBackgroundJobOptions` 可以自定义队列名和作业使用的 RabbitMQ 连接. - -**示例: ** - -```csharp -Configure(options => -{ - options.DefaultQueueNamePrefix = "my_app_jobs."; - options.DefaultDelayedQueueNamePrefix = "my_app_jobs.delayed" - options.PrefetchCount = 1; - options.JobQueues[typeof(EmailSendingArgs)] = - new JobQueueConfiguration( - typeof(EmailSendingArgs), - queueName: "my_app_jobs.emails", - connectionName: "SecondConnection", - delayedQueueName:"my_app_jobs.emails.delayed" - ); -}); -``` - -- 这个示例将默认的队列名前缀设置为 `my_app_jobs.`并且设置默认的延迟队列名为 `my_app_jobs.delayed`,如果多个项目都使用的同一个 RabbitMQ 服务,设置不同的前缀可以避免执行其他项目的后台作业. -- 设置了预取数量, 用于所有队列. -- 这里还设置了 `EmailSendingArgs` 绑定的 RabbitMQ 连接. - -`JobQueueConfiguration` 类的构造函数中,还有一些其他的可选参数. - -- `queueName`: 指定后台作业对应的队列名称(全名). -* `DelayedQueueName`: 指定后台延迟执行的作业对于的队列名称(全名). -- `connectionName`: 后台作业对应的 RabbitMQ 连接名称,默认是 `Default`. -- `durable`: 可选参数,默认为 `true`. -- `exclusive`: 可选参数,默认为 `false`. -- `autoDelete`: 可选参数,默认为 `false`. -* `PrefetchCount` (可选参数, 默认为: null) - -如果你想要更多地了解 `durable`,`exclusive`,`autoDelete` 的用法,请阅读 RabbitMQ 提供的文档. - -## 另请参阅 - -- [后台作业](Background-Jobs.md) diff --git a/docs/zh-Hans/Background-Jobs.md b/docs/zh-Hans/Background-Jobs.md deleted file mode 100644 index affe2d9afa..0000000000 --- a/docs/zh-Hans/Background-Jobs.md +++ /dev/null @@ -1,181 +0,0 @@ -# 后台作业 - -## 介绍 - -后台作业用来在后台里执行应用里的一些任务, 出于几个原因, 你可能需要后台工作, 以下是一些例子: - -- 为执行**长时间运行的任务**而用户无需等待, 例如:用户按了一下"报告"按钮开始一个长时间运行的报告任务, 你把这个任务添加到**队列**里,并在完成后通过电子邮件将报告的结果发送给你的用户. -- 创建**可重试**和**持久的任务**以**确保**代码将**成功执行**. 例如, 你可以在后台作业中发送电子邮件以克服**临时故障**并**保证**最终发送. 这样用户不需要在发送电子邮件时等待. - -后台作业是**持久性的**这意味着即使你的应用程序崩溃了, 后台作业也会在稍后**重试**并**执行**. - -ABP为后台作业提供了一个**抽象**模块和几个后台作业**实现**. 它具有内置/默认的实现以及与Hangfire和RabbitMQ的集成. - -## 抽象模块 - -ABP为后台作业提供了一个 **抽象** 模块和 **多个实现**. 它有一个内置/默认实现以及Hangfire,RabbitMQ与Quartz集成. - -`Volo.Abp.BackgroundJobs.Abstractions` nuget package 提供了创建后台作业和队列作业所需要的服务. 如果你的模块只依赖这个包,那么它可以独立于其实现/集成. - -> `Volo.Abp.BackgroundJobs.Abstractions` package 默认在启动模板中已经安装. - -### 创建后台作业 - -后台作业是一个实现`IBackgroundJob`接口或继承自`BackgroundJob`类的类.`TArgs`是一个简单的C#类, 用于存储作业数据. - -在示例中使用后台作业发送电子邮件,首先定义一个类来存储后台作业的参数 - -````csharp -public class EmailSendingArgs -{ - public string EmailAddress { get; set; } - public string Subject { get; set; } - public string Body { get; set; } -} -```` - -然后创建后台作业类,它使用 `EmailSendingArgs` 对象发送电子邮件: - -````csharp -using Volo.Abp.BackgroundJobs; -using Volo.Abp.Emailing; - -namespace MyProject -{ - public class EmailSendingJob : BackgroundJob, ITransientDependency - { - private readonly IEmailSender _emailSender; - - public EmailSendingJob(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public override void Execute(EmailSendingArgs args) - { - _emailSender.Send( - args.EmailAddress, - args.Subject, - args.Body - ); - } - } -} -```` - -这个作业简单的使用了 `IEmailSender` 发送电子邮件 (请参阅 [邮件发送文档](Emailing.md)). - -#### 异常处理 - -后台作业不应该隐藏异常. 如果它抛出一个异常, 在稍后后台作业将会自动重试. 只有在你不想为当前参数重新运行后台作业时才隐藏异常. - -### 队列作业 - -现在, 你可以使用 `IBackgroundJobManager` 服务向队列中添加一个发送电子邮件作业: - -````csharp -public class RegistrationService : ApplicationService -{ - private readonly IBackgroundJobManager _backgroundJobManager; - - public RegistrationService(IBackgroundJobManager backgroundJobManager) - { - _backgroundJobManager = backgroundJobManager; - } - - public async Task RegisterAsync(string userName, string emailAddress, string password) - { - //TODO: 创建一个新用户到数据库中... - - await _backgroundJobManager.EnqueueAsync( - new EmailSendingArgs - { - EmailAddress = emailAddress, - Subject = "You've successfully registered!", - Body = "..." - } - ); - } -} -```` - -刚才我们注入 `IBackgroundJobManager` 服务了并且使用它的 `EnqueueAsync` 方法添加一个新的作业到队列中. - -Enqueue方法接收一些可选参数用于控制后台作业: - -* **priority** 用于控制作业项的优先级. 它接收一个 `BackgroundJobPriority` 类型的枚举,它有 `Low`, `BelowNormal`, `Normal` (默认), `AboveNormal` 和 `Hight` 字段. -* **delay** 用于作业第一次重试之前的等待时间 (`TimeSpan`)类型. - -### 禁用作业执行 - -你可能希望在你的应用程序中禁用后台作业执行. 如果你希望在另一个进程中执行后台作业并在当前进程中禁用它,通常可以使用以下命令. - -使用 `AbpBackgroundJobOptions` 配置作业执行: - -````csharp -[DependsOn(typeof(AbpBackgroundJobsModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsJobExecutionEnabled = false; //禁用作业执行 - }); - } -} -```` - -> 默认后台管理器(见下文)不支持多进程执行相同的作业队列. 所以, 如果你的应用程序中有多个正在运行的实现,并且使用的是默认的后台管理器, 你应该只在一个应用程序实例进程中启用作业队列. - -## 默认后台作业管理器 - -ABP framework 包含一个简单的 `IBackgroundJobManager` 实现; - -- 在**单线程**中**FIFO(先入先出)**. -- **重试**作业执行直到作业**执行成功**或**超时**. 默认作业超时时间是2天. 记录所有异常 . -- 作业执行成功时从存储中(数据库)**删除**作业. 如果超时, 作业会在数据库中被设置为**abandoned**. -- 作业的**重试等待时间会越来越长**. 作业第一次重试等待1分钟, 第二次重试等待2分钟, 第三次重试等待4分钟,以此类推. -- 以固定的时间间隔轮询存储中的作业. 查询作业, 按优先级排序(asc)然后按尝试次数排序(asc). - -> `Volo.Abp.BackgroundJobs` nuget package 包含默认的后台作业管理器并且在默认在启动模板中已经安装. - -### 配置 - -在你的[模块类](Module-Development-Basics.md)中使用 `AbpBackgroundJobWorkerOptions` 配置默认作业管理器. -示例中更改后台作业的的超时时间: - -````csharp -[DependsOn(typeof(AbpBackgroundJobsModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.DefaultTimeout = 864000; //10 days (as seconds) - }); - } -} -```` - -### 数据存储 - -默认的后台作业管理器需要数据存储用来保存和读取作业. 它将 `IBackgroundJobStore` 定义为抽象的. 所以, 如果你想要的话你可以替换它的实现. - -后台作业模块使用各种数据访问提供程序实现 `IBackgroundJobStore`. 参阅 [后台工作模块文档](Modules/Background-Jobs.md). - -> 默认情况下,后台作业模块已经安装到启动模板中,它基于你的ORM/数据访问选项. - -## 集成 - -后台作业系统是可扩展的,你可以使用自己的实现或预先构建的集成更改默认后台作业管理器. - -请参阅预构建的作业管理器备选方案: - -* [Hangfire 后台作业管理器](Background-Jobs-Hangfire.md) -* [RabbitMQ 后台作业管理器](Background-Jobs-RabbitMq.md) -* [Quartz 后台作业管理器](Background-Jobs-Quartz.md) - -## 另请参阅 -* [后台工作者](Background-Workers.md) \ No newline at end of file diff --git a/docs/zh-Hans/Background-Workers-Hangfire.md b/docs/zh-Hans/Background-Workers-Hangfire.md deleted file mode 100644 index bdd546eba8..0000000000 --- a/docs/zh-Hans/Background-Workers-Hangfire.md +++ /dev/null @@ -1,152 +0,0 @@ -# Hangfire Background Worker Manager - -[Hangfire](https://https://www.hangfire.io/)是一个高级的后台工作者管理. 你可以用ABP框架Hangfire集成代替[默认后台工作者管理](Background-Workers.md). - -主要优点是你可以使用相同的服务器群来管理你的后台作业和工作线程以及利用 Hangfire 提供的[Recurring Jobs](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html?highlight=recurring)高级调度功能. - -## 安装 - -建议使用[ABP CLI](CLI.md)安装包. - - -### 使用ABP CLI - -在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: - -````bash -abp add-package Volo.Abp.BackgroundWorkers.Hangfire -```` - -### 手动安装 - -如果你想手动安装; - -1. 添加 [Volo.Abp.BackgroundWorkers.Hangfire](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Hangfire) NuGet包添加到你的项目: - - ```` - Install-Package Volo.Abp.BackgroundWorkers.Hangfire - ```` - -2. 添加 `AbpBackgroundWorkersHangfireModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersHangfireModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -> Hangfire后台工作者集成提供了 `HangfirePeriodicBackgroundWorkerAdapter` 来适配 `PeriodicBackgroundWorkerBase` 和 `AsyncPeriodicBackgroundWorkerBase` 派生类. 所以你依然可以按照[后台工作者文档](Background-Workers.md)来定义后台作业. - -## 配置 - -你可以安装任何Hangfire存储. 最常用的是SQL Server(参阅[Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer)NuGet包). - -当你安装NuGet包后,你需要为你的项目配置Hangfire. - -1.首先, 我们需要更改 `Module` 类 (例如: `HttpApiHostModule`) 的 `ConfigureServices` 方法去配置Hangfire存储和连接字符串: - -````csharp - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - var hostingEnvironment = context.Services.GetHostingEnvironment(); - - //... other configarations. - - ConfigureHangfire(context, configuration); - } - - private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) - { - context.Services.AddHangfire(config => - { - config.UseSqlServerStorage(configuration.GetConnectionString("Default")); - }); - } -```` - -> 你必须为Hangfire配置一个存储 - -## 创建后台工作者 - -`HangfireBackgroundWorkerBase` 是创建一个后台工作者简单的方法. - -```` csharp -public class MyLogWorker : HangfireBackgroundWorkerBase -{ - public MyLogWorker() - { - RecurringJobId = nameof(MyLogWorker); - CronExpression = Cron.Daily(); - } - - public override Task DoWorkAsync(CancellationToken cancellationToken = default) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -* **RecurringJobId** 是一个可选参数, 参阅 [Hangfire文档](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html) -* **CronExpression** 是CRON表达式, 参阅 [CRON 表达式](https://en.wikipedia.org/wiki/Cron#CRON_expression) - -> 你可以直接实现 `IHangfireBackgroundWorker`, 但是 `HangfireBackgroundWorkerBase` 提供了一些有用的属性,例如 `Logger`. - -### UnitOfWork - -```csharp -public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker -{ - public MyLogWorker() - { - RecurringJobId = nameof(MyLogWorker); - CronExpression = Cron.Daily(); - } - - public override Task DoWorkAsync(CancellationToken cancellationToken = default) - { - using (var uow = LazyServiceProvider.LazyGetRequiredService().Begin()) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } - } -} -``` - -## 注册到后台工作者管理器 - -创建一个后台工作者后, 你应该添加到 `IBackgroundWorkerManager`, 最常用的地方是在你模块类的 `OnApplicationInitializationAsync` 方法中: - -```` csharp -[DependsOn(typeof(AbpBackgroundWorkersModule))] -public class MyModule : AbpModule -{ - public override async Task OnApplicationInitializationAsync( - ApplicationInitializationContext context) - { - await context.AddBackgroundWorkerAsync(); - } -} -```` - -`context.AddBackgroundWorkerAsync(...)` 是一个是以下代码快捷的扩展方法: - -```` csharp -context.ServiceProvider - .GetRequiredService() - .AddAsync( - context - .ServiceProvider - .GetRequiredService() - ); -```` - -它解析给定的后台工作者并添加到 `IBackgroundWorkerManager`. - -虽然我们通常在 `OnApplicationInitializationAsync` 中添加后台工作者, 但对此没有限制. 你可以在任何地方注入 `IBackgroundWorkerManager` 并在运行时添加后台工作者. diff --git a/docs/zh-Hans/Background-Workers-Quartz.md b/docs/zh-Hans/Background-Workers-Quartz.md deleted file mode 100644 index d5bbf7e676..0000000000 --- a/docs/zh-Hans/Background-Workers-Quartz.md +++ /dev/null @@ -1,146 +0,0 @@ -# Quartz 后台工作者管理 - -[Quartz](https://www.quartz-scheduler.net/)是一个高级的后台工作者管理. 你可以用ABP框架集成Quartz代替[默认后台工作者管理](Background-Workers.md). ABP简单的集成了Quartz. - -## 安装 - -建议使用[ABP CLI](CLI.md)安装包. - -### 使用ABP CLI - -在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: - -````bash -abp add-package Volo.Abp.BackgroundWorkers.Quartz -```` - -### 手动安装 - -如果你想手动安装; - -1. 添加 [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet包添加到你的项目: - - ```` - Install-Package Volo.Abp.BackgroundWorkers.Quartz - ```` - -2. 添加 `AbpBackgroundWorkersQuartzModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -> Quartz后台工作者集成提供了 `QuartzPeriodicBackgroundWorkerAdapter` 来适配 `PeriodicBackgroundWorkerBase` 和 `AsyncPeriodicBackgroundWorkerBase` 派生类. 所以你依然可以按照[后台工作者文档](Background-Workers.md)来定义后台作业. - -## 配置 - -参阅[配置](Background-Jobs-Quartz.md#配置). - -## 创建后台工作者 - -后台工作者是一个继承自 `QuartzBackgroundWorkerBase` 基类的类. 一个简单的工作者如下所示: - -```` csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).StartNow().Build(); - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -示例中我们重写了 `Execute` 方法写入日志. 后台工作者默认是**单例**. 如果你需要,也可以实现[依赖接口](Dependency-Injection.md#依赖接口)将其注册为其他的生命周期. - -> 提示: 为后台工作者添加标识是最佳实践,Quartz根据标识区分作业. 如果未指定标识会重复添加工作者到Quartz. - -## 添加到BackgroundWorkerManager - -默认后台工作者会在应用程序启动时**自动**添加到 `BackgroundWorkerManager`,如果你想要手动添加,可以将 `AutoRegister` 属性值设置为 `false`: - -```` csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - AutoRegister = false; - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).StartNow().Build(); - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -```` - -尽管你可以使用 `AutoRegister` 跳过自动添加,但如果你想要全局禁用这样会比较繁琐. 你可以通过 `AbpBackgroundWorkerQuartzOptions` 选项全局禁用: - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.IsAutoRegisterEnabled = false; - }); - } -} -``` - -## 高级主题 - -### 自定义ScheduleJob - -例如你有一个每10分钟执行一次的工作者,但由于服务器不可用30分钟导致工作者错过了3次执行,你想要在服务器恢复正常后执行所有错过的执行. 你应该这样定义你的工作者: - -```csharp -public class MyLogWorker : QuartzBackgroundWorkerBase -{ - public MyLogWorker() - { - JobDetail = JobBuilder.Create().WithIdentity(nameof(MyLogWorker)).Build(); - Trigger = TriggerBuilder.Create().WithIdentity(nameof(MyLogWorker)).WithSimpleSchedule(s=>s.WithIntervalInMinutes(1).RepeatForever().WithMisfireHandlingInstructionIgnoreMisfires()).Build(); - - ScheduleJob = async scheduler => - { - if (!await scheduler.CheckExists(JobDetail.Key)) - { - await scheduler.ScheduleJob(JobDetail, Trigger); - } - }; - } - - public override Task Execute(IJobExecutionContext context) - { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; - } -} -``` - -在示例中我们定义了工作者执行间隔为10分钟,并且设置 `WithMisfireHandlingInstructionIgnoreMisfires` ,另外自定义 `ScheduleJob` 仅当工作者不存在时向quartz添加调度作业. - -## 更多 - -参阅Quartz[文档](https://www.quartz-scheduler.net/documentation/index.html)了解更多信息. \ No newline at end of file diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md deleted file mode 100644 index b2cc2a1580..0000000000 --- a/docs/zh-Hans/Background-Workers.md +++ /dev/null @@ -1,139 +0,0 @@ -# 后台工作者 - -## 介绍 - -后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如; - -* 后台工作者可以定期**删除过时的日志**. -* 后台工作者可以定期检查**不活跃的用户**并且向其**发送邮件**使用户继续使用你的应用程序. - -## 创建一个后台工作者 - -后台工作者应该直接或间接的继承 `IBackgroundWorker` 接口. - -> 后台工作者是[单例](Dependency-Injection.md)的. 所以实例化运行你的工作者类的单个实例. - -### BackgroundWorkerBase - -`BackgroundWorkerBase` 是创建后台工作者的简单方法. - -````csharp -public class MyWorker : BackgroundWorkerBase -{ - public override Task StartAsync(CancellationToken cancellationToken = default) - { - //... - } - - public override Task StopAsync(CancellationToken cancellationToken = default) - { - //... - } -} -```` - -`StartAsync` 开始你的工作者(在应用程序启动时),`StopAsync` 停止它(在应用程序关闭时). - -> 你可以直接实现 `IBackgroundWorker`, 但 `BackgroundWorkerBase` 提供了一些像 `Logger` 的常用属性. - -### AsyncPeriodicBackgroundWorkerBase - -假设我们要设置用户为不活跃用户(如果用户最近30天未登录应用程序).`AsyncPeriodicBackgroundWorkerBase` 类简化了创建定期工作者的过程,我们在下面的示例中使用它: - -````csharp -public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase -{ - public PassiveUserCheckerWorker( - AbpAsyncTimer timer, - IServiceScopeFactory serviceScopeFactory - ) : base( - timer, - serviceScopeFactory) - { - Timer.Period = 600000; //10 minutes - } - - protected async override Task DoWorkAsync( - PeriodicBackgroundWorkerContext workerContext) - { - Logger.LogInformation("Starting: Setting status of inactive users..."); - - //Resolve dependencies - var userRepository = workerContext - .ServiceProvider - .GetRequiredService(); - - //Do the work - await userRepository.UpdateInactiveUserStatusesAsync(); - - Logger.LogInformation("Completed: Setting status of inactive users..."); - } -} -```` - -* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpAsyncTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性. -* 它需要实现 `DoWorkAsync` 方法**执行**定期任务. -* 最好使用 `PeriodicBackgroundWorkerContext` **解析依赖** 而不是构造函数. 因为 `AsyncPeriodicBackgroundWorkerBase` 使用 `IServiceScope` 在你的任务执行结束时会对其 **disposed**. -* `AsyncPeriodicBackgroundWorkerBase` **捕获并记录** 由 `DoWorkAsync` 方法抛出的 **异常**. - -## 注册后台工作者 - -创建一个后台工作者后,你应该将其添加到 `IBackgroundWorkerManager`. 最常见的地方是模块类的 `OnApplicationInitializationAsync` 方法: - -````csharp -[DependsOn(typeof(AbpBackgroundWorkersModule))] -public class MyModule : AbpModule -{ - public override async Task OnApplicationInitialization( - ApplicationInitializationContext context) - { - await context.AddBackgroundWorkerAsync(); - } -} -```` - -`context.AddBackgroundWorkerAsync(...)` 是以下代码的简化扩展方法: - -````csharp -context.ServiceProvider - .GetRequiredService() - .AddAsync( - context - .ServiceProvider - .GetRequiredService() - ); -```` - -所以,它解析了给定的后台工作者并添加到 `IBackgroundWorkerManager`. - -如果我们通常在 `OnApplicationInitializationAsync` 添加工作者,但并不是强制的. 你可以在应用程序的任何地方注入 `IBackgroundWorkerManager` 并在运行时添加工作者. 在你的应用程序关闭时Background worker manager会释放所有已注册的后台工作者. - -## Options - -`AbpBackgroundWorkerOptions` 是用于设置后台工作者的选择. 目前只有一个选项: - -* `IsEnabled` (默认值: true): 用于为你的应用程序启动或禁用后台工作者系统. - -## 让应用程序始终运行 - -后台工作者只有在你的应用程序运行时才会工作. 如果你将后台作业托管在web应用程序中(这是默认行为),那么你应该确保你的web应用程序被配置为始终运行. 否则只有在你的应用程序正在运行时后台作业才会工作. - -## 在集群运行 - -如果你在集群环境中运行同时运行应用程序的多个实现,这种情况下要小心,每个应用程序都运行相同的后台工作者,如果你的工作者在相同的资源上运行(例如处理相同的数据),那么可能会产生冲突. - -如果这对你的工作者是一个问题,你有两个选项: - -* 使用上面提到的 `AbpBackgroundWorkerOptions` 禁用其他的后台工作者系统,只保留一个实例. -* 所有的应用程序都禁用后台工作者系统,创建一个特殊的应用程序在一个服务上运行执行工作者. - -## 集成 - -后台工作者系统是可扩展的,你可以更改默认的后台工作者管理器为你自己的实现,或者使用以下预构建的集成: - -* [Quartz 后台工作者管理器](Background-Workers-Quartz.md) -* [Hangfire 后台工作者管理器](Background-Workers-Hangfire.md) - -## 另请参阅 - -* [后台作业](Background-Jobs.md) \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/Application-Services.md b/docs/zh-Hans/Best-Practices/Application-Services.md deleted file mode 100644 index 0032fe1c11..0000000000 --- a/docs/zh-Hans/Best-Practices/Application-Services.md +++ /dev/null @@ -1,225 +0,0 @@ -## 应用服务最佳实践 & 约定 - -* **推荐** 为每个 **聚合根** 创建一个应用服务. - -### 应用服务接口 - -* **推荐** 在 **application.contracts**层中为每一个应用服务定义一个`接口`. -* **推荐** 继承 `IApplicationService` 接口 . -* **推荐** 接口名称使用`AppService` 后缀 (如: `IProductAppService`). -* **推荐** 为服务创建输入输出DTO(数据传输对象). -* **不推荐** 服务中含有返回实体的方法. -* **推荐** 根据[DTO 最佳实践](Data-Transfer-Objects.md)定义DTO. - -#### 输出 - -* **避免** 为相同或相关实体定义过多的输出DTO. 为实体定义 **基础** 和 **详细** DTO. - -##### 基础DTO - -**推荐** 为聚合根定义一个**基础**DTO. - -- 直接包含实体中所有的**原始属性**. - - 例外: 出于**安全**原因,可以**排除**某些属性(像 `User.Password`). -- 包含实体中所有**子集合**, 每个集合项都是一个简单的**关系DTO**. - -示例: - -```c# -[Serializable] -public class IssueDto : ExtensibleFullAuditedEntityDto -{ - public string Title { get; set; } - public string Text { get; set; } - public Guid? MilestoneId { get; set; } - public Collection Labels { get; set; } -} - -[Serializable] -public class IssueLabelDto -{ - public Guid IssueId { get; set; } - public Guid LabelId { get; set; } -} -``` - -##### 详细DTO - -**推荐** 如果实体持有对其他聚合根的引用,那么应该为其定义**详细**DTO. - -* 直接包含实体中所有的 **原始属性**. - - 例外-1: 出于**安全**原因,可以**排除**某些属性(像 `User.Password`). - - 例外-2: **推荐** 排除引用属性(如上例中的 `MilestoneId`). 为其添加引用属性的详细信息. -* 为每个引用属性添加其**基本DTO** . -* 包含实体的**所有子集合**, 集合中的每项都是相关实体的基本DTO. - -示例: - -````C# -[Serializable] -public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto -{ - public string Title { get; set; } - public string Text { get; set; } - public MilestoneDto Milestone { get; set; } - public Collection Labels { get; set; } -} - -[Serializable] -public class MilestoneDto : ExtensibleEntityDto -{ - public string Name { get; set; } - public bool IsClosed { get; set; } -} - -[Serializable] -public class LabelDto : ExtensibleEntityDto -{ - public string Name { get; set; } - public string Color { get; set; } -} -```` - -#### 输入 - -* **不推荐** 在输入DTO中定义未在服务类中使用的属性. -* **不推荐** 在应用服务方法之间共享输入DTO. -* **不推荐** 继承另一个输入DTO类. - * **可以** 继承自抽象基础DTO类, 并以这种方式在不同的DTO之间共享一些属性. 但是在这种情况下需要非常小心, 因为更新基础DTO会影响所有相关的DTO和服务方法. 所以避免这样做是一种好习惯. - -#### 方法 - -* **推荐** 为异步方法使用 **Async** 后缀. -* **不推荐** 在方法名中重复实体的名称. - * 例如: 在 `IProductAppService` 中定义`GetAsync(...)` 而不是 `GetProductAsync(...)` . - -##### 获取单一实体 - -* **推荐** 使用 `GetAsync` 做为**方法名**. -* **推荐** 使用id做为方法参数. -* 返回 **详细DTO**. 示例: - -````C# -Task GetAsync(Guid id); -```` - -##### 获取实体集合 - -* **推荐** 使用 `GetListAsync` 做为**方法名**. -* **推荐** 如果需要获取单个DTO可以使用参数进行 **过滤**, **排序** 和 **分页**. - * **推荐** 尽可能让过滤参数可选. - * **推荐** 将排序与分页属性设置为可选, 并且提供默认值. - * **推荐** 限制最大页数大小 (基于性能考虑). -* **推荐** 返回 **详细DTO**集合. 示例: - -````C# -Task> GetListAsync(QuestionListQueryDto queryDto); -```` - -##### 创建一个新实体 - -* **推荐** 使用 `CreateAsync` 做为**方法名**. -* **推荐** 使用**专门的输入DTO**来创建实体. -* **推荐** DTO类从 `ExtensibleObject` 类继承(或任何实现 `ExtensibleObject`的类) 以允许在需要时传递额外的属性. -* **推荐** 使用 **data annotations** 进行输入验证. - * 尽可能在**领域**之间共享常量(通过**domain shared** package定义的常量). -* **推荐** 只需要创建实体的**最少**信息, 但是提供了其他可选属性. - -示例**方法**: - -````C# -Task CreateAsync(CreateQuestionDto questionDto); -```` - -输入**DTO**: - -````C# -[Serializable] -public class CreateQuestionDto : ExtensibleObject -{ - [Required] - [StringLength(QuestionConsts.MaxTitleLength, MinimumLength = QuestionConsts.MinTitleLength)] - public string Title { get; set; } - - [StringLength(QuestionConsts.MaxTextLength)] - public string Text { get; set; } //Optional - - public Guid? CategoryId { get; set; } //Optional -} -```` - -##### 更新已存在的实体 - -- **推荐** 使用 `UpdateAsync` 做为**方法名**. -- **推荐** 使用**专门的输入DTO**来更新实体. -- **推荐** DTO类从 `ExtensibleObject` 类继承(或任何实现 `ExtensibleObject`的类) 以允许在需要时传递额外的属性. -- **推荐** 获取实体的id做为分离的原始参数. 不要包含更新DTO. -- **推荐** 使用 **data annotations** 进行输入验证. - - 尽可能在**领域**之间共享常量(通过**domain shared** package定义的常量). -- **推荐** 返回更新实体的**详细**DTO. - -示例: - -````C# -Task UpdateAsync(Guid id, UpdateQuestionDto updateQuestionDto); -```` - -##### 删除已存在的实体 - -- **推荐** 使用 `DeleteAsync` 做为**方法名**. -- **推荐** 使用原始参数 id. 示例: - -````C# -Task DeleteAsync(Guid id); -```` - -##### 其他方法 - -* **可以** 定义其他方法以对实体执行操作. 示例: - -````C# -Task VoteAsync(Guid id, VoteType type); -```` - -此方法为试题投票并返回试题的当前分数. - -### 应用服务实现 - -* **推荐** 开发**完全独立于web层**的应用层. -* **推荐** 在**应用层**实现应用服务接口. - * **推荐** 使用命名约定. 如: 为 `IProductAppService` 接口创建 `ProductAppService` 类. - * **推荐** 继承自 `ApplicationService` 基类. -* **推荐** 将所有的公开方法定义为 **virtual**, 以便开发人员继承和覆盖它们. -* **不推荐** 定义 **private** 方法. 应该定义为 **protected virtual**, 这样开发人员可以继承和覆盖它们. - -#### 使用仓储 - -* **推荐** 使用专门设计的仓储 (如 `IProductRepository`). -* **不推荐** 使用泛型仓储 (如 `IRepository`).z` - -#### 查询数据 - -* **不推荐** 在应用服务方法中使用linq/sql查询来自数据库的数据. 让仓储负责从数据源执行linq/sql查询. - -#### 额外的属性 - -* **推荐** 使用 `MapExtraPropertiesTo` 扩展方法 ([参阅](../Object-Extensions.md)) 或配置对象映射 (`MapExtraProperties`) 以允许应用开发人员能够扩展对象和服务. - -#### 操作/删除 实体 - -* **推荐** 总是从数据库中获取所有的相关实体以对他们执行操作. -* **推荐** 更新实体后调用存储的Update/UpdateAsync方法.因为并非所有数据库API都支持更改跟踪和自动更新. - -#### 处理文件 - -* **不推荐** 在应用服务中使用任何web组件, 例如`IFormFile`和`Stream`. 如果你想接收一个文件, 可以使用`byte[]`. -* **推荐** 使用`Controller`来处理文件上传, 然后将文件的`byte[]`传递给应用服务的方法。 - -#### 使用其他应用服务 - -* **不推荐** 在同一个模块/应用中使用其他应用服务. 相反; - * 使用领域层执行所需的任务. - * 提取新类并在应用服务之间共享, 在必要时代码重用. 但要小心不要结合两个用例. 它们在开始时可能看起来相似, 但可能会随时间演变为不同的方向. 请谨慎使用代码共享. -* **可以** 在以下情况下使用其他应用服务; - * 它们是另一个模块/微服务的一部分. - * 当前模块仅引用已使用模块的application contracts. diff --git a/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md b/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md deleted file mode 100644 index 7705dd179a..0000000000 --- a/docs/zh-Hans/Best-Practices/Data-Transfer-Objects.md +++ /dev/null @@ -1,9 +0,0 @@ -## 数据传输对象最佳实践 & 约定 - -* **推荐** 在 **application.contracts** 层中定义DTO. -* **推荐** 在可能和必要的情况下从预构建的 **基础DTO类** 继承 (如 `EntityDto`, `CreationAuditedEntityDto`, `AuditedEntityDto`, `FullAuditedEntityDto` 等). -* **推荐** 从**聚合根**的**扩展DTO**继承(如 `ExtensibleAuditedEntityDto`), 因为聚合根是可扩展的额外的属性使用这种方式映射到DTO. -* **推荐** 定义 **public getter 和 setter** 的DTO成员 . -* **推荐** 使用 **data annotations** **验证** service输入DTO的属性. -* **不推荐** 在DTO中添加任何 **逻辑**, 在必要的时候可以实现 `IValidatableObject` 接口. -* **推荐** 为所有的DTO标记 **[Serializable]** Attribute. 因为它们已经是可序列化的, 开发人员可能会希望进行二进制序列化. diff --git a/docs/zh-Hans/Best-Practices/Domain-Services.md b/docs/zh-Hans/Best-Practices/Domain-Services.md deleted file mode 100644 index c844b18060..0000000000 --- a/docs/zh-Hans/Best-Practices/Domain-Services.md +++ /dev/null @@ -1,3 +0,0 @@ -## 领域服务最佳实践 & 约定 - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/Entities.md b/docs/zh-Hans/Best-Practices/Entities.md deleted file mode 100644 index fd709ef35f..0000000000 --- a/docs/zh-Hans/Best-Practices/Entities.md +++ /dev/null @@ -1,153 +0,0 @@ -## 实体最佳实践 & 约定 - -### 实体 - -每个聚合根也是一个实体, 所以这些规则对聚合根也是有效的, 除非聚合根的某些规则覆盖了它们. - -- **推荐** 在 **领域层** 中定义实体. - -#### 主构造函数 - -* **推荐** 定义一个 **主构造函数** 确保实体在创建时的有效性, 在代码中通过主构造函数创建实体的新实例. - -- **推荐** 根据需求把主构造函数定义为 `public`,`internal` 或 `protected internal` . 如果它不是public的, 那么应该由领域服务来创建实体. -- **推荐** 总是在主构造函数中初始化子集合. -- **不推荐** 在主构造函数中生成 `Guid` 键, 应该将其做为参数获取, 在调用时推荐使用 `IGuidGenerator` 生成新的 `Guid` 值做为参数. - -#### 无参构造函数 - -- **推荐** 总是定义 `protected` 无参构造函数与ORM兼容. - -#### 引用 - -- **推荐** 总是通过 **id** **引用** 其他聚合根, 不要将导航属性添加到其他聚合根中. - -#### 类的其他成员 - -- **推荐** 总是将属性与方法定义为 `virtual` (除了`私有`方法 ). 因为有些ORM和动态代理工具需要. -- **推荐** 保持实体在自身边界内始终 **有效** 和 **一致**. - - **推荐** 使用 `private`,`protected`,`internal`或`protected internal` setter定义属性, 保护实体的一致性和有效性. - - **推荐** 定义 `public`, `internal` 或 `protected internal` (virtual)**方法**在必要时更改属性值(使用非public setters时). - -### 聚合根 - -#### 主键 - -* **推荐** 总是使用 **Id** 属性做为聚合根主键. -* **不推荐** 在聚合根中使用 **复合主键**. -* **推荐** 所有的聚合根都使用 **Guid** 类型 **主键**. - -#### 基类 - -* **推荐** 根据需求继承 `AggregateRoot` 或以下一个审计类 (`CreationAuditedAggregateRoot`, `AuditedAggregateRoot` 或 `FullAuditedAggregateRoot`). - -#### 聚合边界 - -* **推荐** 聚合**尽可能小**. 大多数聚合只有原始属性, 不会有子集合. 把这些视为设计决策: - * 加载和保存聚合的 **性能** 与 **内存** 成本 (请记住,聚合通常是做为一个单独的单元被加载和保存的). 较大的聚合会消耗更多的CPU和内存. - * **一致性** & **有效性** 边界. - -### 示例 - -#### 聚合根 - -````C# -public class Issue : FullAuditedAggregateRoot //使用Guid作为键/标识符 -{ - public virtual string Title { get; private set; } //使用 SetTitle() 方法set - public virtual string Text { get; set; } //可以直接set,null值也是允许的 - public virtual Guid? MilestoneId { get; set; } //引用其他聚合根 - public virtual bool IsClosed { get; private set; } - public virtual IssueCloseReason? CloseReason { get; private set; } //一个枚举类型 - public virtual Collection Labels { get; protected set; } //子集合 - - protected Issue() - { - /* 此构造函数是提供给ORM用来从数据库中获取实体. - * - 无需初始化Labels集合 - 因为它会被来自数据库的值覆盖. - - It's protected since proxying and deserialization tools - 可能不适用于私有构造函数. - */ - } - - //主构造函数 - public Issue( - Guid id, //从调用代码中获取Guid值 - [NotNull] string title, //表示标题不能为空. - string text = null, - Guid? milestoneId = null) //可选参数 - { - Id = id; - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //验证 - Text = text; - MilestoneId = milestoneId; - - Labels = new Collection(); //总是初始化子集合 - } - - public virtual void SetTitle([NotNull] string title) - { - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //验证 - } - - /* AddLabel和RemoveLabel方法管理Labels集合 - * 安全的方式(防止两次添加相同的标签) */ - - public virtual void AddLabel(Guid labelId) - { - if (Labels.Any(l => l.LabelId == labelId)) - { - return; - } - - Labels.Add(new IssueLabel(Id, labelId)); - } - - public virtual void RemoveLabel(Guid labelId) - { - Labels.RemoveAll(l => l.LabelId == labelId); - } - - /* Close和ReOpen方法可保护一致性 - * IsClosed 与 CloseReason 属性. */ - - public virtual void Close(IssueCloseReason reason) - { - IsClosed = true; - CloseReason = reason; - } - - public virtual void ReOpen() - { - IsClosed = false; - CloseReason = null; - } -} -```` - -#### 实体 - -````C# -public class IssueLabel : Entity -{ - public virtual Guid IssueId { get; private set; } - public virtual Guid LabelId { get; private set; } - - protected IssueLabel() - { - - } - - public IssueLabel(Guid issueId, Guid labelId) - { - IssueId = issueId; - LabelId = labelId; - } -} -```` - -### 参考文献 - -* Effective Aggregate Design by Vaughn Vernon - http://dddcommunity.org/library/vernon_2011 diff --git a/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md b/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md deleted file mode 100644 index 8a54ee85bb..0000000000 --- a/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md +++ /dev/null @@ -1,210 +0,0 @@ -## Entity Framework Core 集成最佳实践 - -> 有关EF Core 集成的基础知识,请参阅[Entity Framework Core 集成文档](../Entity-Framework-Core.md). - -- **推荐** 为每个模块定义单独的 `DbContext` 接口和类. -- **不推荐** 在应用程序开发中使用延迟加载. -- **不推荐** 为 `DbContext` 启用延迟加载. - -### DbContext Interface - -- **推荐** 继承自`IEfCoreDbContext` 的 `DbContext` 定义一个相应的 **interface**. -- **推荐** 添加 `ConnectionStringName` **attribute** 到 `DbContext` 接口. -- **推荐** 将 `DbSet` **properties** 添加到 `DbContext` 接口中,注意: 仅适用于聚合根. 例如: - -````C# -[ConnectionStringName("AbpIdentity")] -public interface IIdentityDbContext : IEfCoreDbContext -{ - DbSet Users { get; set; } - DbSet Roles { get; set; } -} -```` - -### DbContext class - -* **推荐** `DbContext` 继承自 `AbpDbContext` 类. -* **推荐** 添加 `ConnectionStringName` **attribute** 到 `DbContext` 类. -* **推荐** 实现 `DbContext` 类实现其相应的接口. 例如: - -````C# -[ConnectionStringName("AbpIdentity")] -public class IdentityDbContext : AbpDbContext, IIdentityDbContext -{ - public DbSet Users { get; set; } - public DbSet Roles { get; set; } - - public IdentityDbContext(DbContextOptions options) - : base(options) - { - - } - - //code omitted for brevity -} -```` - -### 表前缀与架构 - -- **推荐** 添加静态 **properties** `TablePrefix` 与 `Schema` 到 `DbContext` 类. 使用常量为其设置一个默认值. 例如: - -````C# -public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; -public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema; -```` - - - **推荐** 总是使用简短的 `TablePrefix` 值为模块在共享数据库中创建 **unique table names**. `Abp` 前缀是为ABP Core模块保留的. - - **推荐** `Schema` 默认赋值为 `null`. - -### Model Mapping - -- **推荐** 重写 `DbContext` 的 `OnModelCreating` 方法显式 **配置所有实体**. 例如: - -````C# -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - builder.ConfigureIdentity(options => - { - options.TablePrefix = TablePrefix; - options.Schema = Schema; - }); -} -```` - -- **不推荐** 直接在 `OnModelCreating` 方法中配置model, 而是为 `ModelBuilder` 定义一个 **扩展方法**. 使用Configure*ModuleName*作为方法名称. 例如: - -````C# -public static class IdentityDbContextModelBuilderExtensions -{ - public static void ConfigureIdentity( - [NotNull] this ModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new IdentityModelBuilderConfigurationOptions(); - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "Users", options.Schema); - b.ConfigureByConvention(); - //code omitted for brevity - }); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "UserClaims", options.Schema); - b.ConfigureByConvention(); - //code omitted for brevity - }); - //code omitted for brevity - } -} -```` - -* **推荐** 为每个Enttiy映射调用 `b.ConfigureByConvention();`(如上所示). -* **推荐** 通过继承 `AbpModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如: - -````C# -public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions -{ - public IdentityModelBuilderConfigurationOptions() - : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) - { - } -} -```` - -### 仓储实现 - -- **推荐** 从 `EfCoreRepository` 类 **继承** 仓储并实现相应的仓储接口. 例如: - -````C# -public class EfCoreIdentityUserRepository - : EfCoreRepository, IIdentityUserRepository -{ - public EfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -```` - -* **推荐** 使用 `DbContext` 接口而不是类来作为泛型参数. -* **推荐** 使用 `GetCancellationToken` 帮助方法将 `cancellationToken` 传递给EF Core. 例如: - -````C# -public virtual async Task FindByNormalizedUserNameAsync( - string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default) -{ - return await DbSet - .IncludeDetails(includeDetails) - .FirstOrDefaultAsync( - u => u.NormalizedUserName == normalizedUserName, - GetCancellationToken(cancellationToken) - ); -} -```` - -如果调用者代码中未提供取消令牌,则 `GetCancellationToken` 会从`ICancellationTokenProvider.Token` 获取取消令牌. - -- **推荐** 为具有 **子集合** 的聚合根创建 `IQueryable` 返回类型的 `IncludeDetails` **扩展方法**. 例如: - -````C# -public static IQueryable IncludeDetails( - this IQueryable queryable, - bool include = true) -{ - if (!include) - { - return queryable; - } - - return queryable - .Include(x => x.Roles) - .Include(x => x.Logins) - .Include(x => x.Claims) - .Include(x => x.Tokens); -} -```` - -* **推荐** 推荐在仓储其他方法中使用 `IncludeDetails` 扩展方法, 就像上面的示例代码一样(参阅 FindByNormalizedUserNameAsync). - -- **推荐** 覆盖具有 **子集合** 的聚合根仓储中的 `WithDetails` 方法. 例如: - -````C# -public override IQueryable WithDetails() -{ - return GetQueryable().IncludeDetails(); // Uses the extension method defined above -} -```` - -### Module Class - -- **推荐** 为Entity Framework Core集成包定义一个Module类. -- **推荐** 使用 `AddAbpDbContext` 方法将 `DbContext` 添加到 `IServiceCollection`. -- **推荐** 将已实现的仓储添加到 `AddAbpDbContext` 方法的options中. 例如: - -````C# -[DependsOn( - typeof(AbpIdentityDomainModule), - typeof(AbpEntityFrameworkCoreModule) - )] -public class AbpIdentityEntityFrameworkCoreModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(options => - { - options.AddRepository(); - options.AddRepository(); - }); - } -} -```` diff --git a/docs/zh-Hans/Best-Practices/Index.md b/docs/zh-Hans/Best-Practices/Index.md deleted file mode 100644 index 863e397ac0..0000000000 --- a/docs/zh-Hans/Best-Practices/Index.md +++ /dev/null @@ -1,26 +0,0 @@ -## 模块开发最佳实践 & 约定 - -### 介绍 - -这篇文档描述了想要满足以下规范的**模块**的**最佳实践**与**约定**: - -* 开发应用**领域驱动设计**模式的最佳实践的模块. -* 开发 **DBMS 与 ORM 独立** 的模块. -* 开发可用作 **远程服务 / 微服务** 的模块, 并可以集成到 **单体** 应用程序中. - -本指南主要用于 **应用程序** 开发. - -### 指南 - -* 总体 - * [模块架构](Module-Architecture.md) -* 领域层 - * [实体](Entities.md) - * [仓储](Repositories.md) - * [领域服务](Domain-Services.md) -* 应用程序层 - * [应用程序服务](Application-Services.md) - * [数据传输对象](Data-Transfer-Objects.md) -* 数据访问 - * [Entity Framework Core 集成](Entity-Framework-Core-Integration.md) - * [MongoDB 集成](MongoDB-Integration.md) \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/Module-Architecture.md b/docs/zh-Hans/Best-Practices/Module-Architecture.md deleted file mode 100644 index 8a7cef183c..0000000000 --- a/docs/zh-Hans/Best-Practices/Module-Architecture.md +++ /dev/null @@ -1,89 +0,0 @@ -## 模块化架构最佳实践 & 约定 - -### 解决方案结构 - -* **推荐** 在Visual Sudio中为每个模块创建一个单独的解决方案. -* **推荐** 将解决方案命名为*CompanyName.ModuleName*(对于ABP核心模块来说,它的命名方式是*Volo.Abp.ModuleName*). -* **推荐** 一个模块做为分层项目开发,因为它有几个包(项目)是相互关联的. - * 每个包都有自己的模块定义文件,并显式声明所依赖的包/模块的依赖关系. - -### 层(layers) & 包(packages) - -下面展示了一个分层良好的模块中的包以及它们之间的依赖关系: - -![module-layers-and-packages](../images/module-layers-and-packages.jpg) - -最终的目地是让应用程序以灵活的方式使用该模块. 示例应用程序: - -* **A)** **单体**应用程序; - * 添加对**Web**和**Application**包的引用. - * 根据需要添加对**EF Core** 或 **MongoDB** 包的引用. - * 效果; - * 应用程序可以显示**模块的UI**. - * 它在**同一进程**中托管**应用层**和**领域层** (这就是为什么它引用对数据库集成包). - * 此应用程序还提供了模块的**HTTP API**(因为它通过Web包引用了HttpApi包). -* **B)** 仅为**微服务**提供模块的应用程序; - * 添加对**HttpApi**和**Application**包的引用. - * 根据需要添加对**EF Core** 或 **MongoDB** 包的引用. - * 效果; - * 应用程序**无法显示模块的UI**, 因为它没有对Web包的引用. - * 它在**同一进程**中托管**应用层**和**领域层** (这就是为什么它引用对数据库集成包). - * 此应用程序提供了模块的**HTTP API**(它的主要目标). -* **C)** 显示模块**UI**但是不托管应用层的应用程序(仅将其用作由应用程序A或B托管的远程服务) - * 添加对**Web**和**HttpApi.Client**包的引用. - * 配置HttpApi.Client包的远程端点. - * 效果; - * 应用程序可以显示**模块的UI**. - * 它不会在同一进程中托管模块的应用层和领域层. 而是将其用作**远程服务**. - * 此应用程序还提供了模块的**HTTP API**(因为它通过Web包引用了HttpApi包). -* **D)** **客户端**应用程序 (或微服务) 只使用模块作为远程服务(由应用程序A,B或C托管); - * 添加对**HttpApi.Client**包的引用. - * 配置HttpApi.Client包的远程端点. - * 效果; - * 应用程序可以使用模块的所有功能作为**远程客户端**. - * 应用程序只是一个客户端,**无法提供**模块的**HTTP API**. - * 应用程序只是一个客户端,**无法显示**模块的**UI**. -* **E**) 托管模块的HTTP API但只是将所有请求转发给另一个应用程序的代理应用程序 (由应用程序A, B或C托管); - * 添加对**HttpApi**和**HttpApi.Client**包的引用. - * 配置HttpApi.Client包的远程端点. - * 效果; - * 应用程序可以将模块的所有功能用作**远程客户端**. - * 应用程序也服务于模块的**HTTP API**, 但实际上它的工作方式与代理一样,将所有请求(模块)重定向到另一个远程服务器. - -下一节将详细地介绍这些包. - -#### 领域层 - -* **推荐** 将领域层划分为两个项目: - * **Domain.Shared** 包(项目) 命名为*CompanyName.ModuleName.Domain.Shared*,包含常量,枚举和其他类型, 它不能包含实体,存储库,域服务或任何其他业务对象. 可以安全地与模块中的所有层使用. 此包也可以与第三方客户端使用. - * **Domain** 包(项目) 命名为*CompanyName.ModuleName.Domain*, 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象. - * Domain 包依赖于 **Domain.Shared** 包. - -#### 应用服务层 - -* **推荐** 将应用服务层划分为两个项目: - * **Application.Contracts** 包(项目) 命名为*CompanyName.ModuleName.Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO). - * Application contract 包依赖于 **Domain.Shared** 包. - * **Application** 包(项目)命名为*CompanyName.ModuleName.Application*,包含应用服务实现. - * Application 包依赖于 **Domain** 包和 **Application.Contracts** 包. - -#### 基础设施层 - -* **推荐** 为每个orm/数据库集成创建一个独立的集成包, 比如Entity Framework Core 和 MongoDB. - * **推荐** 例如, 创建一个抽象Entity Framework Core集成的*CompanyName.ModuleName.EntityFrameworkCore* 包. ORM 集成包依赖于 **Domain** 包. - * **不推荐** 依赖于orm/数据库集成包中的其他层. -* **推荐** 为每个主要的库创建一个独立的集成包, 在不影响其他包的情况下可以被另一个库替换. - -#### HTTP 层 - -* **推荐** 创建命名为*CompanyName.ModuleName.HttpApi*的**HTTP API**包, 为模块开发REST风格的HTTP API. - * HTTP API 包只依赖于 **Application.Contracts** 包. 不要依赖 Application 包. - * **推荐** 为每个应用服务创建一个Controller (通常通过实现其接口). 这些控制器使用应用服务接口来委托操作. 它根据需要配置路由, HTTP方法和其他与Web相关的东西. -* **推荐** 创建一个为HTTP API包提供客户端服务的**HTTP API Client**包, 它的命名为*Companyname.ModuleName.HttpApi.Client*. 这些客户端服务将应用服务接口实现远程端点的客户端. - * HTTP API Client 包仅依赖于 **Application.Contracts** 包. - * **推荐** 使用ABP框架提供的动态代理HTTP C#客户端的功能. - -#### Web 层 - -* **推荐** 创建命名为*CompanyName.ModuleName.Web*的 **Web**包. 包含页面,视图,脚本,样式,图像和其他UI组件. - * Web 包仅依赖于 **HttpApi** 包. \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/MongoDB-Integration.md b/docs/zh-Hans/Best-Practices/MongoDB-Integration.md deleted file mode 100644 index b7b033b1fa..0000000000 --- a/docs/zh-Hans/Best-Practices/MongoDB-Integration.md +++ /dev/null @@ -1,202 +0,0 @@ -## MongoDB 集成 - -* **推荐** 为每个模块定义一个独立的 `MongoDbContext` 接口与实现类. - -### MongoDbContext 接口 - -- **推荐** 定义 `MongoDbContext` **接口** 时继承自 `IAbpMongoDbContext`. -- **推荐** 添加 `ConnectionStringName` **attribute** 到 `MongoDbContext` 接口. -- **推荐** 只把聚合根做为 `IMongoCollection` **properties** 添加到 `MongoDbContext` 接口. 示例: - -````C# -[ConnectionStringName("AbpIdentity")] -public interface IAbpIdentityMongoDbContext : IAbpMongoDbContext -{ - IMongoCollection Users { get; } - IMongoCollection Roles { get; } -} -```` - -### MongoDbContext 类 - -- **推荐** `MongoDbContext` 继承自 `AbpMongoDbContext` 类. -- **推荐** 添加 `ConnectionStringName` **attribute** 到 `MongoDbContext` 类. -- **推荐** `MongoDbContext` 类实现相对应的**接口**. 示例: - -```c# -[ConnectionStringName("AbpIdentity")] -public class AbpIdentityMongoDbContext : AbpMongoDbContext, IAbpIdentityMongoDbContext -{ - public IMongoCollection Users => Collection(); - public IMongoCollection Roles => Collection(); - - //code omitted for brevity -} -``` - -### Collection 前缀 - -- **推荐** 添加静态 `CollectionPrefix` **property** 到 `DbContext` 类中并使用常量为其设置默认值. 示例: - -```c# -public static string CollectionPrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; -``` - -在此示例中使用与EF Core集成表前缀相同的常量. - -- **推荐** 总是使用简短的 `CollectionPrefix` 值为模块在共享数据库中创建 **unique collection names**. `Abp` collection前缀是为ABP Core模块保留的. - -### Collection 映射 - -- **推荐** 通过重写 `MongoDbContext` 的 `CreateModel` 方法 **配置所有的聚合根** . 示例: - -```c# -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.ConfigureIdentity(options => - { - options.CollectionPrefix = CollectionPrefix; - }); -} -``` - -- **不推荐** 直接在 `CreateModel` 方法中配置model,而是为 `IMongoModelBuilder` 定义一个 **扩展方法**. 使用Configure*ModuleName*作为方法名称. 示例: - -```c# -public static class AbpIdentityMongoDbContextExtensions -{ - public static void ConfigureIdentity( - this IMongoModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new IdentityMongoModelBuilderConfigurationOptions(); - - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.CollectionName = options.CollectionPrefix + "Users"; - }); - - builder.Entity(b => - { - b.CollectionName = options.CollectionPrefix + "Roles"; - }); - } -} -``` - -- **推荐** 通过继承 `AbpMongoModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 示例: - -```c# -public class IdentityMongoModelBuilderConfigurationOptions - : AbpMongoModelBuilderConfigurationOptions -{ - public IdentityMongoModelBuilderConfigurationOptions() - : base(AbpIdentityConsts.DefaultDbTablePrefix) - { - } -} -``` - -* **推荐** 创建一个静态方法, 显示地为所有的实体配置 `BsonClassMap`. 示例: - -````C# -public static class AbpIdentityBsonClassMap -{ - private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); - - public static void Configure() - { - OneTimeRunner.Run(() => - { - BsonClassMap.RegisterClassMap(map => - { - map.AutoMap(); - map.ConfigureExtraProperties(); - }); - - BsonClassMap.RegisterClassMap(map => - { - map.AutoMap(); - }); - }); - } -} -```` - -`BsonClassMap` 适用于静态方法. 所以只需要在应用程序配置一次实体. `OneTimeRunner` 以线程安全的方式运行, 并且在应用程序生命周期中只运行一次. 上面代码中的映射确保单元测试可以正确运行. 此代码将由下面的**模块类**调用. - -### 仓储实现 - -- **推荐** 仓储 **继承自** `MongoDbRepository` 类并且实现其相应的接口. 示例: - -```c# -public class MongoIdentityUserRepository - : MongoDbRepository, - IIdentityUserRepository -{ - public MongoIdentityUserRepository( - IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -- **推荐** 使用 `GetCancellationToken` 帮助方法将 `cancellationToken` 传递给MongoDB驱动程序. 示例: - -```c# -public async Task FindByNormalizedUserNameAsync( - string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default) -{ - return await GetMongoQueryable() - .FirstOrDefaultAsync( - u => u.NormalizedUserName == normalizedUserName, - GetCancellationToken(cancellationToken) - ); -} -``` - -如果调用者代码中未提供取消令牌, 则 `GetCancellationToken` 会从`ICancellationTokenProvider.Token` 获取取消令牌 -`GetCancellationToken`. - -* **推荐** 忽略仓储实现中的 `includeDetails` 参数, 因为MongoDB在默认情况下将聚合根作为一个整体(包括子集合)加载. -* **推荐** 使用 `GetMongoQueryable()` 方法获取 `IQueryable` 以尽可能执行查询use the `GetMongoQueryable()` method to obtain an `IQueryable` to perform queries wherever possible. 因为; - * `GetMongoQueryable()` 方法在内部使用 `ApplyDataFilters` 方法根据当前的过滤器 (如 软删除与多租户)过滤数据. - * 使用`IQueryable`让代码与EF Core仓储实现类似, 易于使用. -* **推荐** 如果无法使用 `GetMongoQueryable()` 方法, 则应自行实现数据过滤. - -### 模块类 - -- **推荐** 为MongoDB集成包定义一个模块类. -- **推荐** 使用 `AddMongoDbContext` 方法将 `MongoDbContext` 添加到 `IServiceCollection`. -- **推荐** 将已实现的仓储添加到 `AddMongoDbContext` 方法options中. 示例: - -```c# -[DependsOn( - typeof(AbpIdentityDomainModule), - typeof(AbpUsersMongoDbModule) - )] -public class AbpIdentityMongoDbModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - AbpIdentityBsonClassMap.Configure(); - - context.Services.AddMongoDbContext(options => - { - options.AddRepository(); - options.AddRepository(); - }); - } -} -``` - -需要注意的是, 模块类还调用上面定义的静态 `BsonClassMap` 配置方法. \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/Repositories.md b/docs/zh-Hans/Best-Practices/Repositories.md deleted file mode 100644 index 26431a633f..0000000000 --- a/docs/zh-Hans/Best-Practices/Repositories.md +++ /dev/null @@ -1,92 +0,0 @@ -## 仓储最佳实践 & 约定 - -### 仓储接口 - -* **推荐** 在**领域层**中定义仓储接口. -* **推荐** 为**每个聚合根**定义仓储接口(如 `IIdentityUserRepository`)并创建相应的实现. - * **推荐** 在应用代码中使用仓储时应该注入仓储接口. - * **不推荐** 在应用代码中使用泛型仓储接口(如 `IRepository`). - * **不推荐** 在应用代码(领域, 应用... 层)中使用 `IQueryable` 特性. - -聚合根的示例: - -````C# -public class IdentityUser : AggregateRoot -{ - //... -} -```` - -定义仓储接口, 如下所示: - -````C# -public interface IIdentityUserRepository : IBasicRepository -{ - //... -} -```` - -* **不推荐** 仓储接口继承 `IRepository` 接口. 因为它继承了 `IQueryable` 而仓储不应该将`IQueryable`暴漏给应用. -* **推荐** 通常仓储接口继承自 `IBasicRepository` 或更低级别的接口, 如 `IReadOnlyRepository` (在需要的时候). -* **不推荐** 为实体定义仓储接口,因为它们**不是聚合根**. - -### 仓储方法 - -* **推荐** 所有的仓储方法定义为 **异步**. -* **推荐** 为仓储的每个方法添加 **可选参数** `cancellationToken` . 例: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - CancellationToken cancellationToken = default -); -```` - -* **推荐** 为仓储的每个异步方法创建一个 **同步扩展** 方法. 示例: - -````C# -public static class IdentityUserRepositoryExtensions -{ - public static IdentityUser FindByNormalizedUserName( - this IIdentityUserRepository repository, - [NotNull] string normalizedUserName) - { - return AsyncHelper.RunSync( - () => repository.FindByNormalizedUserNameAsync(normalizedUserName) - ); - } -} -```` - -对于同步方法而言, 这会让它们更方便的调用仓储方法. - -* **推荐** 为仓储中返回**单个实体**的方法添加一个可选参数 `bool includeDetails = true` (默认值为`true`). 示例: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default -); -```` - -该参数由ORM实现, 用来加载实体子集合. - -* **推荐** 为仓储中返回**实体列表**的方法添加一个可选参数 `bool includeDetails = false` (默认值为`false`). 示例: - -````C# -Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, - bool includeDetails = false, - CancellationToken cancellationToken = default -); -```` - -* **不推荐** 创建复合类通过调用仓储单个方法返回组合实体. 比如: *UserWithRoles*, *UserWithTokens*, *UserWithRolesAndTokens*. 相反, 正确的使用 `includeDetails` 选项, 在需要时加载实体所有的详细信息. -* **避免** 为了从仓储中获取实体的部分属性而为实体创建投影类. 比如: 避免通过创建BasicUserView来选择所需的一些属性. 相反可以直接使用聚合根类. 不过这条规则有例外情况: - * 性能对于用例来说非常重要,而且使用整个聚合根对性能的影响非常大. - -### 另外请参阅 - -* [Entity Framework Core 集成](Entity-Framework-Core-Integration.md) -* [MongoDB 集成](MongoDB-Integration.md) diff --git a/docs/zh-Hans/Best-Practices/images/postgresql-delete-initial-migrations.png b/docs/zh-Hans/Best-Practices/images/postgresql-delete-initial-migrations.png deleted file mode 100644 index 14788c5fb8..0000000000 Binary files a/docs/zh-Hans/Best-Practices/images/postgresql-delete-initial-migrations.png and /dev/null differ diff --git a/docs/zh-Hans/Best-Practices/images/postgresql-update-database.png b/docs/zh-Hans/Best-Practices/images/postgresql-update-database.png deleted file mode 100644 index 30a5f3abe1..0000000000 Binary files a/docs/zh-Hans/Best-Practices/images/postgresql-update-database.png and /dev/null differ diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md deleted file mode 100644 index eb67fdbab5..0000000000 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ /dev/null @@ -1,77 +0,0 @@ -# BLOB Storing Aliyun提供程序 - -BLOB存储Aliyun提供程序可以将BLOB存储在[Aliyun Blob storage](https://help.aliyun.com/product/31815.html)中. - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置Aliyun提供程序. - -## 安装 - -使用ABP CLI添加[Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun)NuGet包到你的项目: - -* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. -* 在要添加 `Volo.Abp.BlobStoring.Aliyun` 包的 `.csproj` 文件目录打开命令行. -* 运行 `Volo.Abp.BlobStoring.Aliyun` 命令. - -如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.Aliyun` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringAliyunModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. - -## 配置 - -如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. - -**示例: 配置为默认使用Aliyun存储提供程序** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAliyun(aliyun => - { - aliyun.AccessKeyId = "your aliyun access key id"; - aliyun.AccessKeySecret = "your aliyun access key secret"; - aliyun.Endpoint = "your oss endpoint"; - aliyun.RegionId = "your sts region id"; - aliyun.RoleArn = "the arn of ram role"; - aliyun.RoleSessionName = "the name of the certificate"; - aliyun.Policy = "policy"; - aliyun.DurationSeconds = "expiration date"; - aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = true; - }); - }); -}); -```` - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. - -### 选项 - -* **AccessKeyId** ([NotNull]string): 云账号AccessKey是访问阿里云API的密钥,具有该账户完全的权限,请你务必妥善保管!强烈建议遵循[阿里云安全最佳实践](https://help.aliyun.com/document_detail/102600.html),使用RAM子用户AccessKey来进行API调用. -* **AccessKeySecret** ([NotNull]string): 同上. -* **Endpoint** ([NotNull]string): Endpoint表示OSS对外服务的访问域名. [访问域名和数据中心](https://help.aliyun.com/document_detail/31837.html) -* **UseSecurityTokenService** (bool): 是否使用STS临时授权访问OSS,默认false. [STS临时授权访问OSS](https://help.aliyun.com/document_detail/100624.html) -* **RegionId** (string): STS服务的接入地址,每个地址的功能都相同,请尽量在同地域进行调用. [接入地址](https://help.aliyun.com/document_detail/66053.html) -* **RoleArn** ([NotNull]string): STS所需角色ARN. -* **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分. -* **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html). -* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. -* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): - * 只能包含小写字母,数字和短横线(-) - * 必须以小写字母和数字开头和结尾 - * Bucket名称的长度限制在**3**到**63**个字符之间 -* **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果aliyun中不存在容器, `AliyunBlobProvider` 将尝试创建它. -* **TemporaryCredentialsCacheKey** (bool): STS凭证缓存Key,默认Guid.NewGuid().ToString("N"). - -## Aliyun BLOB 名称计算器 - -Aliyun BLOB提供程序组织BLOB名称并实现一些约定. 默认情况下BLOB的全名由以下规则确定: - -* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串. -* 如果当前租户不为 `null`,则追加 `tenants/` 字符串. -* 追加 BLOB 名称. - -## 其他服务 - -* `AliyunBlobProvider` 是实现Aliyun BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `AliyunBlobProvider` 类). -* `IAliyunBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultAliyunBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. -* `IOssClientFactory` 服务用于生成OSS客户端. 默认实现是 `DefaultOssClientFactory` . 如果你想自定义OSS客户端生成,可以替换/覆盖它. diff --git a/docs/zh-Hans/Blob-Storing-Aws.md b/docs/zh-Hans/Blob-Storing-Aws.md deleted file mode 100644 index fc98c5a948..0000000000 --- a/docs/zh-Hans/Blob-Storing-Aws.md +++ /dev/null @@ -1,84 +0,0 @@ -# BLOB Storing Aws提供程序 - -BLOB存储Aws提供程序可以将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/cn/s3/)中. - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置Aws提供程序. - -## 安装 - -使用ABP CLI添加[Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws)NuGet包到你的项目: - -* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. -* 在要添加 `Volo.Abp.BlobStoring.Aws` 包的 `.csproj` 文件目录打开命令行. -* 运行 `Volo.Abp.BlobStoring.Aws` 命令. - -如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.Aws` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringAwsModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. - -## 配置 - -如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. - -**示例: 配置为默认使用Aws存储提供程序** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAws(Aws => - { - Aws.AccessKeyId = "your Aws access key id"; - Aws.SecretAccessKey = "your Aws access key secret"; - Aws.UseCredentials = "set true to use credentials"; - Aws.UseTemporaryCredentials = "set true to use temporary credentials"; - Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; - Aws.ProfileName = "the name of the profile to get credentials from"; - Aws.ProfilesLocation = "the path to the aws credentials file to look at"; - Aws.Region = "the system name of the service"; - Aws.Name = "the name of the federated user"; - Aws.Policy = "policy"; - Aws.DurationSeconds = "expiration date"; - Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = true; - }); - }); -}); -```` - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. - -### 选项 - -* **AccessKeyId** (string): AWS Access Key ID. -* **SecretAccessKey** (string): AWS Secret Access Key. -* **UseCredentials** (bool): 使用[本地凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html)访问AWS服务,默认: `false`. -* **UseTemporaryCredentials** (bool): 使用[临时凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingTempSessionToken.html)访问AWS服务,默认: `false`. -* **UseTemporaryFederatedCredentials** (bool): 使用[联合身份用户临时凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingTempFederationToken.html)访问AWS服务, 默认: `false`. -* **ProfileName** (string): [本地凭证配置文件](https://docs.aws.amazon.com/zh_cn/sdk-for-net/v3/developer-guide/net-dg-config-creds.html)名称. -* **ProfilesLocation** (string): 本地配置文件位置. -* **Region** (string): 服务的地区名称. -* **Policy** (string): JSON格式的IAM策略. -* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为129600. -* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): - * Bucket名称必须介于 3 到 63 个字符之间. - * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. - * Bucket名称必须以字母或数字开头和结尾. - * Bucket名称不能是ip (例如, 192.168.5.4). - * Bucket名称不能以 **xn--** 开头, (对于2020年2月以后创建的 Bucket). - * Bucket名称在分区中必须唯一 . - * Bucket 与 Amazon S3 Transfer Acceleration 一起使用时名称中不能有句点 (.). -* **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果Aws中不存在容器, `AwsBlobProvider` 将尝试创建它. - -## Aws BLOB 名称计算器 - -Aws BLOB提供程序组织BLOB名称并实现一些约定. 默认情况下BLOB的全名由以下规则确定: - -* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串. -* 如果当前租户不为 `null`,则追加 `tenants/` 字符串. -* 追加 BLOB 名称. - -## 其他服务 - -* `AwsBlobProvider` 是实现Aws BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `AwsBlobProvider` 类). -* `IAwsBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultAwsBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. -* `IAmazonS3ClientFactory` 服务用于生成AWS S3客户端. 默认实现是 `DefaultAmazonS3ClientFactory` . 如果你想自定义AWS S3客户端生成,可以替换/覆盖它. diff --git a/docs/zh-Hans/Blob-Storing-Azure.md b/docs/zh-Hans/Blob-Storing-Azure.md deleted file mode 100644 index e1ed3a6d4e..0000000000 --- a/docs/zh-Hans/Blob-Storing-Azure.md +++ /dev/null @@ -1,61 +0,0 @@ -# BLOB Storing Azure提供程序 - -BLOB存储Azure提供程序可以将BLOB存储在[Azure Blob storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置Azure提供程序. - -## 安装 - -使用ABP CLI添加[Volo.Abp.BlobStoring.Azure](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Azure)NuGet包到你的项目: - -* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. -* 在要添加 `Volo.Abp.BlobStoring.Azure` 包的 `.csproj` 文件目录打开命令行. -* 运行 `Volo.Abp.BlobStoring.Azure` 命令. - -如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.Azure` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringAzureModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. - -## 配置 - -如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. - -**示例: 配置为默认使用Azure存储提供程序** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseAzure(azure => - { - azure.ConnectionString = "your azure connection string"; - azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = true; - }); - }); -}); -```` - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. - -### 选项 - -* **ConnectionString** (string): 连接字符串包括应用程序在运行时使用共享密钥授权访问Azure存储帐户中的数据所需的授权信息. 请参考[Azure文档](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string). -* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): - * 容器名称必须以字母或数字开头或结尾,并且只能包含字母,数字和破折号(-)字符. - * 每个破折号(-)必须紧跟在字母或数字之后;容器名称中不允许使用连续的破折号. - * 容器名称中的所有字母都必须**小写**. - * 容器名称的长度必须在**3**到**63**个字符之间. -* **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果azure中不存在容器, `AzureBlobProvider` 将尝试创建它. - -## Azure BLOB 名称计算器 - -Azure BLOB提供程序组织BLOB名称并实现一些约定. 默认情况下BLOB的全名由以下规则确定: - -* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串. -* 如果当前租户不为 `null`,则追加 `tenants/` 字符串. -* 追加 BLOB 名称. - -## 其他服务 - -* `AzureBlobProvider` 是实现Azure BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `AzureBlobProvider` 类). -* `IAzureBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultAzureBlobNameCalculator` . 如果你想自定义文件路径计算,可以替换/覆盖它. diff --git a/docs/zh-Hans/Blob-Storing-Custom-Provider.md b/docs/zh-Hans/Blob-Storing-Custom-Provider.md deleted file mode 100644 index a7f43cd7a4..0000000000 --- a/docs/zh-Hans/Blob-Storing-Custom-Provider.md +++ /dev/null @@ -1,177 +0,0 @@ -# BLOB 存储: 创建自定义提供程序 - -本文档通过一个示例说明如何为BLOB存储系统创建新的存储提供程序. - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何创建新存储提供程序. - -## 示例实现 - -第一步是创建一个实现 `IBlobProvider` 接口或 `BlobProviderBase` 抽象类继承的类. - -````csharp -using System.IO; -using System.Threading.Tasks; -using Volo.Abp.BlobStoring; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency - { - public override Task SaveAsync(BlobProviderSaveArgs args) - { - //TODO... - } - - public override Task DeleteAsync(BlobProviderDeleteArgs args) - { - //TODO... - } - - public override Task ExistsAsync(BlobProviderExistsArgs args) - { - //TODO... - } - - public override Task GetOrNullAsync(BlobProviderGetArgs args) - { - //TODO... - } - } -} -```` - -* `MyCustomBlobProvider` 继承 `BlobProviderBase` 并覆盖 `abstract` 方法. 实际的实现取决于你. -* 实现 `ITransientDependency` 接口将这个类注做为瞬态服务注册到[依赖注入](Dependency-Injection.md)系统. - -> **注意: 命名约定很重要**. 如果类名没有以 `BlobProvider` 结尾,则必须手动注册/公开你的服务为 `IBlobProvider`. - -这是所有. 现在你可以配置容器(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中)使用 `MyCustomBlobProvider` 类: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.ProviderType = typeof(MyCustomBlobProvider); - }); -}); -```` - -> 如果你想配置特定的容器,请参阅[BLOB存储文档](Blob-Storing.md). - -### BlobContainerConfiguration 扩展方法 - -如果你想提供一个更简单的配置方式,可以为 `BlobContainerConfiguration` 类创建一个扩展方法: - -````csharp -public static class MyBlobContainerConfigurationExtensions -{ - public static BlobContainerConfiguration UseMyCustomBlobProvider( - this BlobContainerConfiguration containerConfiguration) - { - containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); - return containerConfiguration; - } -} -```` - -然后你可以使用扩展方法更容易地配置容器: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMyCustomBlobProvider(); - }); -}); -```` - -### 额外的配置选项 - -`BlobContainerConfiguration` 允许添加/删除提供程序特定的配置对象. 如果你的提供者需要额外的配置,你可以为 `BlobContainerConfiguration` 创建一个包装类提供的类型安全配置选项: - -````csharp - public class MyCustomBlobProviderConfiguration - { - public string MyOption1 - { - get => _containerConfiguration - .GetConfiguration("MyCustomBlobProvider.MyOption1"); - set => _containerConfiguration - .SetConfiguration("MyCustomBlobProvider.MyOption1", value); - } - - private readonly BlobContainerConfiguration _containerConfiguration; - - public MyCustomBlobProviderConfiguration( - BlobContainerConfiguration containerConfiguration) - { - _containerConfiguration = containerConfiguration; - } - } -```` - -然后你可以这样更改 `MyBlobContainerConfigurationExtensions` 类: - -````csharp -public static class MyBlobContainerConfigurationExtensions -{ - public static BlobContainerConfiguration UseMyCustomBlobProvider( - this BlobContainerConfiguration containerConfiguration, - Action configureAction) - { - containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); - - configureAction.Invoke( - new MyCustomBlobProviderConfiguration(containerConfiguration) - ); - - return containerConfiguration; - } - - public static MyCustomBlobProviderConfiguration GetMyCustomBlobProviderConfiguration( - this BlobContainerConfiguration containerConfiguration) - { - return new MyCustomBlobProviderConfiguration(containerConfiguration); - } -} -```` - -* 向 `UseMyCustomBlobProvider` 方法添加了一个参数,允许开发人员设置其他选项. -* 添加了一个新的 `GetMyCustomBlobProviderConfiguration` 方法,该方法将在 `MyCustomBlobProvider` 类内使用获取配置的值. - -然后任何人都可以如下设置 `MyOption1`: - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMyCustomBlobProvider(provider => - { - provider.MyOption1 = "my value"; - }); - }); -}); -```` - -最后你可以使用 `GetMyCustomBlobProviderConfiguration` 方法访问额外的选项: - -````csharp -public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency -{ - public override Task SaveAsync(BlobProviderSaveArgs args) - { - var config = args.Configuration.GetMyCustomBlobProviderConfiguration(); - var value = config.MyOption1; - - //... - } -} -```` - -## 贡献? - -如果你创建了一个新的提供程序,并且认为它对其他开发者有用,请考虑为GitHub上的ABP框架做出[贡献](Contribution/Index.md). \ No newline at end of file diff --git a/docs/zh-Hans/Blob-Storing-Database.md b/docs/zh-Hans/Blob-Storing-Database.md deleted file mode 100644 index cc688e4396..0000000000 --- a/docs/zh-Hans/Blob-Storing-Database.md +++ /dev/null @@ -1,96 +0,0 @@ -# BLOB存储数据库提供程序 - -BLOB存储数据库提供程序可以将BLOB存储在关系或非关系数据库中. - -有两个数据库提供程序实现; - -* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) 包实现[EF Core](Entity-Framework-Core.md), 它可以通过EF Core存储BLOB在[任何支持的DBMS](https://docs.microsoft.com/en-us/ef/core/providers/)中. -* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) 包实现了[MongoDB](MongoDB.md). - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置数据库提供程序. - -## 安装 - -### 自动安装 - -如果你已基于[应用程序启动模板](Startup-Templates/Application.md)创建了解决方案,则可以使用 `abp add-module` [CLI](CLI.md)命令将相关软件包自动添加到解决方案中. - -在包含解决方案(`.sln`)文件的文件夹中打开命令行运行以下命令: - -````bash -abp add-module Volo.Abp.BlobStoring.Database -```` - -此命令将所有NuGet软件包添加到解决方案的相应层. 如果使用的是EF Core,它会添加必要的配置,添加新的数据库迁移并更新数据库. - -### 手动安装 - -这里是此提供程序定义的所有包: - -* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain.Shared) -* [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) -* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) -* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) - -你可以只安装 `Volo.Abp.BlobStoring.Database.EntityFrameworkCore` 或 `Volo.Abp.BlobStoring.Database.MongoDB` (根据你的偏好),因为它们依赖其他包. - -安装完成后,添加 `DepenedsOn` 属性到相关[模块](Module-Development-Basics.md).下面是由上面列出的相关NuGet包定义的模块类列表: - -* `BlobStoringDatabaseDomainModule` -* `BlobStoringDatabaseDomainSharedModule` -* `BlobStoringDatabaseEntityFrameworkCoreModule` -* `BlobStoringDatabaseMongoDbModule` - -如果你正在使用EF Core,还需要配置你的**Migration DbContext**将BLOB存储表添加到你的数据库. 在 `OnModelCreating` 方法中调用 `builder.ConfigureBlobStoring()` 扩展方法来包含到DbContext的映射. 你可以使用标准的 `Add-Migration` 和 `Update-Database` [命令](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)在数据库中创建必要的表. - -## 配置 - -### 连接字符串 - -如果你要使用你的 `Default` 连接字符串,则不需要做任何其他配置. - -如果要将BLOB存储到单独的数据库,请在配置文件(`appsettings.json`)中将 `AbpBlobStoring` 用作连接字符串名称. 请阅读[EF Core Migrations](Entity-Framework-Core-Migrations.md)文档了解如何为所需模块创建和使用其他数据库. - -### 配置容器 - -如果只使用数据库存储提供程序,则不需要手动配置,因为它是自动完成的. 如果使用多个存储提供程序,可能需要对其进行配置. - -如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. - -**示例: 配置为默认使用数据库系统存储提供程序** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseDatabase(); - }); -}); -```` - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. - -## 附加信息 - -它需要使用[BLOB存储服务](Blob-Storing.md)来使用BLOB存储系统. 但是如果要处理数据库表/实体,可以使用以下信息. - -### 实体 - -此模块定义的实体: - -* `DatabaseBlobContainer` (aggregate root) 表示存储在数据库中的容器. -* `DatabaseBlob` (aggregate root) 表示数据库中的BLOB. - -参阅[实体文档](Entities.md)了解什么是实体和聚合根. - -### 仓储 - -* `IDatabaseBlobContainerRepository` -* `IDatabaseBlobRepository` - -你还可以使用 `IRepository` 和 `IRepository` 来获得 `IQueryable` 能力. 更多信息请参阅[仓储文档](Repositories.md). - -### 其他服务 - -* `DatabaseBlobProvider` 是实现数据库BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `DatabaseBlobProvider` 类). \ No newline at end of file diff --git a/docs/zh-Hans/Blob-Storing-File-System.md b/docs/zh-Hans/Blob-Storing-File-System.md deleted file mode 100644 index 58c4bdb1d7..0000000000 --- a/docs/zh-Hans/Blob-Storing-File-System.md +++ /dev/null @@ -1,58 +0,0 @@ -# BLOB存储文件系统提供程序 - -文件系统存储提供程序用于将BLOB作为文件夹中的标准文件存储在本地文件系统中. - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置文件系统. - -## 介绍 - -使用ABP CLI添加[Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem)NuGet包到你的项目: - -* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. -* 在要添加 `Volo.Abp.BlobStoring.FileSystem` 包的 `.csproj` 文件目录打开命令行. -* 运行 `abp add-package Volo.Abp.BlobStoring.FileSystem` 命令. - -如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.FileSystem` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringFileSystemModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. - -## 配置 - -如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. - -**示例: 配置为默认使用文件系统存储提供程序** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseFileSystem(fileSystem => - { - fileSystem.BasePath = "C:\\my-files"; - }); - }); -}); -```` - -`UseFileSystem` 扩展方法用于为容器设置文件系统提供程序并配置文件系统选项. - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. - -### 选项 - -* **BasePath** (string): 存储BLOB的基本文件夹路径,它是必选的. -* **AppendContainerNameToBasePath** (bool; 默认: `true`): 指定是否在基本文件夹中创建具有容器名称的文件夹. 如果你在同一个 `BaseFolder` 中存储多个容器,请将其保留为`true`. 如果你不喜欢不必要的更深层次的文件夹,你可以将它设置为 `false`. - -## 文件路径计算 - -文件系统提供程序在文件夹中组织BLOB文件并实现一些约定. 默认情况下,BLOB文件的完整路径由以下规则确定: - -* 它以如上所述配置的 `BasePath` 开始. -* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 文件夹. -* 如果当前租户不为 `null`,则追加 `tenants/` 文件夹. -* 如果 `AppendContainerNameToBasePath` 为`true`,则追加容器的名称. 如果容器名称包含 `/`,将导致文件夹嵌套. -* 追加BLOB名称,如果BLOB名称包含 `/` 它创建文件夹. 如果BLOB名称包含 `.` 它将有一个文件扩展名. - -## 扩展文件系统提供程序 - -* `FileSystemBlobProvider` 是实现文件系统存储的主要服务. 你可以从这个类继承并[覆盖](Customizing-Application-Modules-Overriding-Services.md)方法进行自定义. -* `IBlobFilePathCalculator` 服务用于计算文件路径. 默认实现是 `DefaultBlobFilePathCalculator` . 如果你想自定义文件路径计算,可以替换/覆盖它. \ No newline at end of file diff --git a/docs/zh-Hans/Blob-Storing-Minio.md b/docs/zh-Hans/Blob-Storing-Minio.md deleted file mode 100644 index bc0268b5cf..0000000000 --- a/docs/zh-Hans/Blob-Storing-Minio.md +++ /dev/null @@ -1,69 +0,0 @@ -# BLOB Storing Minio 提供程序 - -BLOB Storing Minio提供程序帮助你存储对象到 [MinIO Object storage](https://min.io/), - -> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统, 本文档仅介绍如何为容器配置Minio提供程序, - -## 安装 - -使用 ABP CLI 来安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet 包到你的项目: - -* 如果你没有安装ABP CLI,请先安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), -* 在要添加 `Volo.Abp.BlobStoring.Minio` 包的 `.csproj` 文件目录打开命令行, -* 执行 `abp add-package Volo.Abp.BlobStoring.Minio` 命令, - -如果你要手动安装, 通过NuGet安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) 到你的项目,然后添加 `[DependsOn(typeof(AbpBlobStoringMinioModule))]` 特性到你的 [ABP module](Module-Development-Basics.md) 类上, - -## 配置 - -配置在你的[module](Module-Development-Basics.md)类中的`ConfigureServices`方法中完成, - -**例: 配置使用Minio存储** - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - container.UseMinio(minio => - { - minio.EndPoint = "你的 minio endPoint"; - minio.AccessKey = "你的 minio accessKey"; - minio.SecretKey = "你的 minio secretKey"; - minio.BucketName = "你的 minio bucketName"; - }); - }); -}); -```` - -> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序, - -### 选项 - -* **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档:https://docs.min.io/docs/dotnet-client-quickstart-guide.html -* **AccessKey** (string): Access key是唯一标识你的账户的用户ID, -* **SecretKey** (string): Secret Key是你的账户的密码 -* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlobContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): - * Bucket名称必须介于 3 到 63 个字符之间. - * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. - * Bucket名称必须以字母或数字开头和结尾. - * Bucket名称不能是ip (例如, 192.168.5.4). - * Bucket名称不能以 **xn--** 开头, (对于2020年2月以后创建的 Bucket). - * Bucket名称在分区中必须唯一 . - * Bucket 与 Amazon S3 Transfer Acceleration 一起使用时名称中不能有句点 (.). -* **WithSSL** (bool): 默认 `false`,代表使用HTTPS, -* **CreateContainerIfNotExists** (bool): 默认 `false`,如果不存在bucket, `MinioBlobProvider` 将会创建一个, - - -## Minio BLOB 名称计算器 - -默认情况下BLOB的全名由以下规则确定: - -* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串, -* 如果当前租户不为 `null`,则追加 `tenants/` 字符串, -* 追加 BLOB 名称, - -## 其他服务 - -* `MinioBlobProvider` 是实现Minio BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `MinioBlobProvider` 类). -* `IMinioBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultMinioBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. diff --git a/docs/zh-Hans/Blob-Storing.md b/docs/zh-Hans/Blob-Storing.md deleted file mode 100644 index 9321c51cb9..0000000000 --- a/docs/zh-Hans/Blob-Storing.md +++ /dev/null @@ -1,309 +0,0 @@ -# BLOB 存储 - -通常将文件内容存储在应用程序中并根据需要读取这些文件内容. 不仅是文件你可能还需要将各种类型的[BLOB](https://en.wikipedia.org/wiki/Binary_large_object)(大型二进制对象)保存到存储中. 例如你可能要保存用户个人资料图片. - -BLOB通常是一个**字节数组**. 有很多地方可以存储BLOB项. 可以选择将其存储在本地文件系统中,共享数据库中或[Azure BLOB存储](https://azure.microsoft.com/zh-cn/services/storage/blobs/)中. - -ABP框架为BLOB提供了抽象,并提供了一些可以轻松集成到的预构建存储提供程序. 抽象有一些好处; - -* 你可以通过几行配置**轻松的集成**你喜欢的BLOB存储提供程序. -* 你可以**轻松的更改**BLOB存储,而不用改变你的应用程序代码. -* 如果你想创建**可重用的应用程序模块**,无需假设BLOB的存储方式. - -ABP BLOB存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md). - -## BLOB 存储提供程序 - -ABP框架已经有以下存储提供程序的实现; - -* [File System](Blob-Storing-File-System.md):将BLOB作为标准文件存储在本地文件系统的文件夹中. -* [Database](Blob-Storing-Database.md): 将BLOB存储在数据库中. -* [Azure](Blob-Storing-Azure.md): 将BLOB存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. -* [Aliyun](Blob-Storing-Aliyun.md): 将BLOB存储在[Aliyun Storage Service](https://help.aliyun.com/product/31815.html)中. -* [Minio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中. -* [Aws](Blob-Storing-Aws.md): 将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/s3/)中. - -以后会实现更多的提供程序,你可以为自己喜欢的提供程序创建[请求](https://github.com/abpframework/abp/issues/new),或者你也可以[自己实现](Blob-Storing-Custom-Provider.md)它并[贡献](Contribution/Index.md)到ABP框架. - -可以在**容器系统**的帮助下一起**使用多个提供程序**,其中每个容器可以使用不同的提供程序. - -> 除非你**配置存储提供程序**否则BLOB存储系统无法工作. 有关存储提供程序配置请参考链接的文档. - -## 安装 - -[Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring)是定义BLOB存储服务的主要包. 你可以用此包使用BLOB存储系统而不依赖特定存储提供程序. - -使用ABP CLI这个包添加到你的项目: - -* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. -* 在要添加 `Volo.Abp.BlobStoring` 包的 `.csproj` 文件目录打开命令行. -* 运行 `abp add-package Volo.Abp.BlobStoring` 命令. - -如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. - -## IBlobContainer - -`IBlobContainer` 是存储和读取BLOB的主要接口. 应用程序可能有多个容器,每个容器都可以单独配置. 有一个**默认容器**可以通过注入 `IBlobContainer` 来简单使用. - -**示例: 简单地保存和读取命名BLOB的字节** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.BlobStoring; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IBlobContainer _blobContainer; - - public MyService(IBlobContainer blobContainer) - { - _blobContainer = blobContainer; - } - - public async Task SaveBytesAsync(byte[] bytes) - { - await _blobContainer.SaveAsync("my-blob-1", bytes); - } - - public async Task GetBytesAsync() - { - return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); - } - } -} -```` - -该服务用 `my-blob-1` 名称保存给定的字节,然后以相同的名称获取先前保存的字节. - -> 一个BLOB是一个命名对象,**每个BLOB都应该有一个唯一的名称**,它是一个任意的字符串. - -`IBlobContainer` 可以处理 `Stream` 和 `byte[]` 对象,在下一节中将详细介绍. - -### 保存 BLOB - -`SaveAsync` 方法用于保存新的或替换现有的BLOB. 默认情况下,它可以保存 `Stream`,但是有一个快捷的扩展方法来保存字节数组. - -`SaveAsync` 有以下参数: - -* **name** (string): 唯一的BLOB名称. -* **stream** (Stream) or **byte** (byte[]): 读取BLOB内容或字节数组的流. -* **overrideExisting** (bool): 设置为 `true`,如果BLOB内容已经存在,则替换它. 默认值为 `false`,则抛出 `BlobAlreadyExistsException` 异常. - -### 读取/获取 BLOB - -* `GetAsync`: 返回给定BLOB名称可用于读取BLOB内容的 `Stream` 对象. 使用后始终要**dispose流**. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. -* `GetAllBytesAsync`: 返回 `byte[]` 而不是 `Stream`. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. - -### 删除 BLOB - -`DeleteAsync` 使用给定BLOB名称删除BLOB数据. 如果找不到给定的BLOB不会引发任何异常. 相反如果你关心BLOB,它会返回一个 `bool`,表示BLOB实际上是否已删除. - -### 其他方法 - -* `ExistsAsync` 方法简单的检查容器中是否存在具有给定名称的BLOB. - -### 关于命名BLOB - -没有命名BLOB的规则. BLOB名称只是每个容器(和每个租户-参见"*多租户*"部分)唯一的字符串. 但是不同的存储提供程序可能会按惯例实施某些做法. 例如[文件系统提供程序](Blob-Storing-File-System.md)在BLOB名称中使用目录分隔符 (`/`) 和文件扩展名(如果BLOB名称为 `images/common/x.png` ,则在根容器文件夹下的 `images/common` 文件夹中存储 `x.png`). - -## 类型化 IBlobContainer - -类型化BLOB容器系统是一种在应用程序中创建和管理**多个容器**的方法; - -* **每个容器分别存储**. 这意味着BLOB名称在一个容器中应该是唯一的,两个具有相同名称的BLOB可以存在不同的容器中不会互相影响. -* **每个容器可以单独配置**,因此每个容器可以根据你的配置使用不同的存储提供程序. - -要创建类型化容器,需要创建一个简单的用 `BlobContainerName` 属性装饰的类: - -````csharp -using Volo.Abp.BlobStoring; - -namespace AbpDemo -{ - [BlobContainerName("profile-pictures")] - public class ProfilePictureContainer - { - - } -} -```` - -> 如果不使用 `BlobContainerName` attribute,ABP Framework将使用类的全名(带有名称空间),但是始终建议使用稳定的容器名称,即使重命名该类也不会被更改. - -创建容器类后,可以为容器类型注入 `IBlobContainer`. - -**示例: 用于保存和读取[当前用户](CurrentUser.md)的个人资料图片的[应用服务](Application-Services.md)** - -````csharp -[Authorize] -public class ProfileAppService : ApplicationService -{ - private readonly IBlobContainer _blobContainer; - - public ProfileAppService(IBlobContainer blobContainer) - { - _blobContainer = blobContainer; - } - - public async Task SaveProfilePictureAsync(byte[] bytes) - { - var blobName = CurrentUser.GetId().ToString(); - await _blobContainer.SaveAsync(blobName, bytes); - } - - public async Task GetProfilePictureAsync() - { - var blobName = CurrentUser.GetId().ToString(); - return await _blobContainer.GetAllBytesOrNullAsync(blobName); - } -} -```` - -`IBlobContainer` 有与 `IBlobContainer` 相同的方法. - -> 在开发可重复使用的模块时,**始终使用类型化的容器是一个好习惯**,这样最终的应用程序就可以为你的容器配置提供程序,而不会影响其他容器. - -### 默认容器 - -如果不使用泛型参数,直接注入 `IBlobContainer` (如上所述),会得到默认容器. 注入默认容器的另一种方法是使用 `IBlobContainer`,它返回完全相同的容器. - -默认容器的名称是 `default`. - -### 命令容器 - -类型容器只是命名容器的快捷方式. 你可以注入并使用 `IBlobContainerFactory` 来获得一个BLOB容器的名称: - -````csharp -public class ProfileAppService : ApplicationService -{ - private readonly IBlobContainer _blobContainer; - - public ProfileAppService(IBlobContainerFactory blobContainerFactory) - { - _blobContainer = blobContainerFactory.Create("profile-pictures"); - } - - //... -} -```` - -## IBlobContainerFactory - -`IBlobContainerFactory` 是用于创建BLOB容器的服务. 上面提供了一个示例. - -**示例: 通过名称创建容器** - -````csharp -var blobContainer = blobContainerFactory.Create("profile-pictures"); -```` - -**示例: 通过类型创建容器** - -````csharp -var blobContainer = blobContainerFactory.Create(); -```` - -> 通常你不需要使用 `IBlobContainerFactory`, 因为在注入 `IBlobContainer` 或`IBlobContainer` 时会在内部使用它. - -## 配置容器 - -在使用容器之前应先对其进行配置. 最基本的配置是选择一个 **BLOB存储提供程序**(请参阅上面的"*BLOB存储提供程序*"部分). - -`AbpBlobStoringOptions` 是用于配置容器的[选项类](Options.md). 你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中配置选项. - -### 配置单个容器 - -````csharp -Configure(options => -{ - options.Containers.Configure(container => - { - //TODO... - }); -}); -```` - -这个例子配置 `ProfilePictureContainer`. 你还可以通过容器名称进行配置: - -````csharp -Configure(options => -{ - options.Containers.Configure("profile-pictures", container => - { - //TODO... - }); -}); -```` - -### 配置默认容器 - -````csharp -Configure(options => -{ - options.Containers.ConfigureDefault(container => - { - //TODO... - }); -}); -```` - -> 默认容器有一个特殊情况;如果不为容器指定配置,则**返回到默认容器配置**. 这是一种为所有容器配置默认值并在需要时专门针对特定容器进行配置的好方法. - -### 配置所有容器 - -````csharp -Configure(options => -{ - options.Containers.ConfigureAll((containerName, containerConfiguration) => - { - //TODO... - }); -}); -```` - -这是配置所有容器的方式. - -> 与配置默认容器的主要区别在于, `ConfigureAll` 会覆盖配置,即使它是专门为特定容器配置的. - -## 多租户 - - -如果你的应用程序是多租户的,BLOB存储系统可以**与[多租户](Multi-Tenancy.md)无缝协作**. 所有提供程序都将多租户实现为标准功能. 它们将不同租户的**BLOB彼此隔离**,因此它们只能访问自己的BLOB. 这意味着你可以**为不同的租户使用相同的BLOB名称**. - -如果应用程序是多租户的,则可能需要单独控制容器的**多租户行为**. 例如你可能希望**禁用特定容器的多租户**,这样容器中的BLOB将对**所有租户可用**. 这是在所有租户之间共享BLOB的一种方法. - -**示例: 禁用特定容器的多租户** - -````csharp -Configure(options => -{ - options.Containers.Configure(container => - { - container.IsMultiTenant = false; - }); -}); -```` - -> 如果你的应用程序不是多租户的,不用担心,它会正常工作. 你不需要配置 `IsMultiTenant` 选项. - -## 扩展BLOB存储系统 - -大多数时候除了创建定制的BLOB存储提供程序外,你不需要[自定义BLOB存储系统](Blob-Storing-Custom-Provider.md).但是如果需要,你可以替换任何服务(通过[依赖注入](Dependency-Injection.md)). 这里有一些上面没有提到的其他服务,但你可能想知道: - -* `IBlobProviderSelector` 用于通过容器名称获取 `IBlobProvider` 实例. 默认实现(`DefaultBlobProviderSelector`)使用配置选择提供程序. -* `IBlobContainerConfigurationProvider` 用于获取给定容器名称的`BlobContainerConfiguration`. 默认实现(`DefaultBlobContainerConfigurationProvider`)从上述 `AbpBlobStoringOptions` 获取配置. - -## BLOB 存储 vs 文件管理系统 - -注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOB的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的. - -如果你想创建文件夹并在文件夹之间移动文件,为文件分配权限并在用户之间共享文件,那么你需要在BLOB存储系统上实现你自己的应用程序. - -## 另请参阅 - -* [创建自定义BLOB存储提供程序](Blob-Storing-Custom-Provider.md) diff --git a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md deleted file mode 100644 index 7d633ee9e0..0000000000 --- a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md +++ /dev/null @@ -1,209 +0,0 @@ -# ABP vNext介绍 - -## 介绍 - -过去的一段时间,我们正在设计一个新的主要版本的ASP.NET Boilerplate框架.现在,是时候与社区分享了.我们非常兴奋,相信你也是. - - -## 命名 - -框架的名称保持不变,除了我们将其仅称为“ABP”而不是“ASP.NET Boilerplate”.因为,“Boilerplate(样板)”会导致误解,并没反映出它是一个框架(而不是样板代码).我们继续使用“ABP”名称,因为它是当前ASP.NET Boilerplate框架的继承者,除了它是完全重写的. - - -## 如何开始 - -我们已经创建了一个启动模板.你可以从[abp.io/Templates](https://abp.io/Templates)创建一个新项目并开始开发.欲了解更多信息,请访问[abp.io](https://abp.io/). - - -## 为什么要完全重写? - -为什么我们花了宝贵的时间从头开始重写它而不是增量更改和改进.为什么? - -### ASP.NET Core - -当我们第一次介绍ABP框架时,那是2013年(5年前)!那时候没有.Net Core和ASP.NET Core,也没有Angular2 +.ABP发布后,它们都是从头开发的. - -ASP.NET Core引入了许多内置解决方案(扩展库),用于依赖注入,日志记录,缓存,本地化,配置等.它们实际上独立于ASP.NET Core,可用于任何类型的应用程序. - -我们之前使用第三方库和我们自己的解决方案来满足这些要求.在它们发布后我们立即集成到ASP.NET Core功能中.但这是集成,而不是在这些扩展库之上构建ABP框架.例如,当前的ASP.NET Boilerplate依赖于Castle Windsor进行依赖注入,即使它已集成到ASP.NET Core的DI系统中. - -我们希望依赖这些新的扩展库而不是第三方和自定义解决方案,这会改变框架的基本结构. - -### 自身模块化 - -虽然目前的ABP本身已经是模块化的,并且包含许多包,但我们仍然希望将功能分解为更细粒度的nuget包. - -例如,核心Abp包包含许多功能,如DDD类,审计,授权,后台作业,事件总线,json序列化,本地化,多租户,线程,时间等......我们希望将所有这些功能拆分到各自的包中并使它们可选. - -### 放弃对传统技术的支持 - -是的,新的ABP框架将不支持ASP.NET MVC 5.x,Entity Framework 6.x和其他传统技术. - -这些传统技术会由Microsoft维护,但不会再添加任何新功能.因此,如果你仍在使用这些技术,则可以继续使用当前的ASP.NET Boilerplate框架.我们将继续维护它,修复错误并添加新功能. - -删除对这些传统库的支持将提高我们的开发速度(因为我们目前正在复制我们的某些功能的工作)并专注于.Net Core和ASP.NET Core. - -新的ABP框架将基于.net standard.因此,仍然可以在新的ABP框架中使用完整的.net framework或.net core. - - -## 目标 - -我们从社区学到了很多东西,并且有开发当前ASP.NET Boilerplate框架的经验.新的ABP框架有着重要而令人兴奋的目标. - -### 应用程序模块化 - -第一个目标是提供良好的基础设施来开发应用程序模块.我们将模块视为一组应用程序功能,具有自己的数据库,自己的实体,服务,API,UI页面,组件等. - -我们将创建一个包含免费和付费应用程序模块的模块市场.你还可以在市场上发布自己的模块.更多信息即将推出. - -### 微服务 - -我们正在设计新的ABP框架可方便的开发微服务并使它们相互通信. - -我们正在设计应用程序模块,以便它们可以作为微服务单独部署,或者可以嵌入到单个应用程序中. - -我们正在为此创建[规范/最佳实践文档](https://github.com/abpframework/abp/blob/master/docs/Best-Practices/Index.md) - -### 主题和UI组合 - -新的ABP框架将基于最新的Twitter Bootstrap 4.x提供主题基础设施.我们开发了一个仅使用普通Bootstrap 4.x样式的基本主题.它是免费和开源的.我们还在开发高级和付费主题. - -UI组合是主要目标之一.为此,主题系统将提供菜单,工具栏和其他可扩展区域,以允许其他模块交互(contribute). - -### ORM / 数据库无关性和MongoDB集成 - -虽然当前的ASP.NET Boilerplate框架已经实现了ORM /数据库无关的存储库(Repository)模式,但身份集成模块(以Abp.Zero*命名的包)与EF以外的ORM一直运行的不太好. - -使用新的ABP框架,最终目标是完全抽象的底层数据存储系统和开发与EF Core无关的模块. - -我们将MongoDB作为第一级别的数据库,并在没有任何关系数据库或ORM假设的情况下设计实体和存储库. - -### 更多可扩展性 - -新的ABP框架为内置服务提供了更多的扩展点和覆盖的能力. - - -## 一些功能 - -在本节中,我将介绍新ABP框架的一些令人兴奋的新功能. - -### Bootstrap Tag Helpers - -我们正在创建一个库将twitter bootstrap 4.x元素/组件包装到tag helper中.例: - -~~~ html - - - - Card title - -

    - This is a sample card component built by ABP bootstrap - card tag helper. ABP has tag helper wrappers for most of - the bootstrap components. -

    -
    - Go somewhere → -
    -
    -~~~ - -“abp-*”标签是ABP tag helper,用于简化为Bootstrap 4.x编写HTML. - -### 动态表单 - -动态表单tag helper允许你为给定的模型类动态地创建表单.例: - -~~~ html - -~~~ - -输出: - -![dynamic-forms](dynamic-forms.png) - -目前支持最常用的输入类型. 更多类型正在开发中. - -### 虚拟文件系统 - -虚拟文件系统允许你将视图,页面,组件,javascript,css,json和其他类型的文件嵌入到模块程序集/包(dll)中,并在任何应用程序中使用.在应用程序中你的虚拟文件就像物理文件一样, 完全的集成在ASP.NET Core中. - -更多信息请参阅[关于虚拟文件系统](https://medium.com/volosoft/designing-modularity-on-asp-net-core-virtual-file-system-2dd2cc2078bd)和[相关文档](https://github.com/abpframework/abp/blob/master/docs/Virtual-File-System.md). - -### 动态捆绑和压缩系统 - -动态捆绑和压缩系统运行在虚拟文件系统上,并且允许模块以模块化,动态和强大的方式创建,修改和交互捆绑包.一个例子: - -~~~ html - - - - - -~~~ - -这段代码通过包含bootstrap(及其依赖项,如果有)和另外两个css文件来动态创建一个新的样式包.这些文件在生产环境中捆绑和压缩,但将在开发环境中单独添加. - -有关更多信息,请参阅[文档](https://github.com/abpframework/abp/blob/master/docs/UI/AspNetCore/Bundling-Minification.md) - -### 分布式事件总线(Distributed Event Bus) - -在当前的ABP中,有一个IEventBus服务来触发和处理应用程序内的事件.除了这个本地事件总线,我们还创建了一个分布式事件总线抽象(和RabbitMQ集成)来实现分布式消息传递模式. - -### 动态C# HTTP客户端代理 - -ABP已经为所有HTTP API创建动态JavaScript代理.该功能也存在于新的ABP框架中.此外,它现在可以为所有HTTP API创建动态C#代理. - - - -## 未来的工作 - -上面提到的所有东西都已经开发出来了.但是,我们还有一些想法尚未着手. - -### 单页应用程序(Single Page Applications) - -我们在设计新框架时考虑到了SPA这一点.但是,我们还没有尝试过任何SPA框架,我们还没有为它准备一个启动模板. - -## ASP.NET Boilerplate(当前版本)和ASP.NET Zero会如何? - -我们有专门的开发和支持团队积极致力于ASP.NET Boilerplate和ASP.NET Zero项目.这些项目有一个很大的社区,我们也从社区中获得了贡献. - -我们将在很长一段时间内继续为这些项目进行改进,添加新功能和修复错误.因此,你可以安全地继续使用它们. - -## 新的ABP可用在生产环境吗? - -还没有.我们的第一个目标是使基本功能稳定,然后逐步完成其他功能. - -我们会经常发布新版本,每个新版本都可能会有重大变化.我们将在发行说明中写下重大更改. - -我们目前将其定义为实验性质.但我们希望这不会持续很长时间.我们无法确定一个准确的日期,请关注我们的发布. - -## 包和版本控制 - -新的ABP框架将从v1.0开始,而不是遵循当前的ASP.NET Boilerplate版本, 以反映它是一个重写的事实. - -我们会经常发布它.你可以期待许多重大变化,直到v1.0.从v1.0开始,我们将注意不要在1.x版本中引入重大更改. - -当前ABP的包名称以Abp前缀开头(如Abp.EntityFrameworkCore).新的包名称以Volo.Abp前缀开头(如Volo.Abp.EntityFrameworkCore). - -## 我应该用哪一个? - -如果你正在创建一个新项目,我们建议你继续使用当前的ASP.NET Boilerplate框架,因为它非常成熟,功能丰富且可用于生产环境. - -如果你愿意接受重大变化并希望体验新框架,那么你可以从新的ABP开始.我们不建议在临近截止日期和在短期内上线的项目上使用它. - -## 贡献 - -就像当前的ABP框架一样,你可为新框架做出贡献. - -* 你可以发送代码或文档的拉取请求. -* 你可以撰写关于它的博客文章或教程. -* 你可以尝试并分享你的经验. -* 你可以提出改进和功能请求. -* 你可以报告错误和其他问题. - -## 联系/链接 - -* 官方网站:[abp.io](https://abp.io/) -* Github:[github.com/abpframework](https://github.com/abpframework) -* Twitter:[@abpframework](https://twitter.com/abpframework) diff --git a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/dynamic-forms.png b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/dynamic-forms.png deleted file mode 100644 index 22911d4b72..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/dynamic-forms.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md deleted file mode 100644 index 998a454abc..0000000000 --- a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md +++ /dev/null @@ -1,54 +0,0 @@ -# 微服务演示,项目状态和路线图 - -在ABP vNext上的[第一个公告](https://abp.io/blog/abp/Abp-vNext-Announcement)之后,我们对代码库进行了很多改进([GitHub存储库](https://github.com/abpframework/abp)上的1100多次提交).我们已经创建了功能,示例,文档等等.在这篇文章中,我想告诉你一些新闻和项目的状态. - -## 微服务演示解决方案 - -ABP框架的主要目标之一是提供[创建微服务解决方案的便利基础设施](https://abp.io/documents/abp/latest/Microservice-Architecture). - -我们一直在努力开发微服务解决方案演示.初始版本已完成并[文档化](https://abp.io/documents/abp/latest/Samples/Microservice-Demo).该示例解决方案旨在演示一个简单而完整的微服务解决方案; - -- 具有多个独立的,可自我部署的**微服务**. -- 多个**Web应用程序**,每个都使用不同的API网关. -- 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库开发了多个**网关** / BFF(后端为前端(Backend for Frontends)). -- 使用[IdentityServer](https://identityserver.io/)框架开发**身份验证服务**.它也是一个带有必要UI的SSO(单点登录)应用程序. -- 有**多个数据库**.一些微服务有自己的数据库,而一些服务/应用程序共享一个数据库(以演示不同的用例). -- 具有不同类型的数据库:**SQL Server**(使用**Entity Framework Core** ORM)和**MongoDB**. -- 有一个**控制台应用程序**来显示通过身份验证使用服务的最简单方法. -- 使用[Redis](https://redis.io/)进行**分布式缓存**. -- 使用[RabbitMQ](https://www.rabbitmq.com/)进行服务到服务(service-to-service)的**消息传递**. -- 使用[Docker](https://www.docker.com/)和[Kubernates](https://kubernetes.io/)**部署**并运行所有服务和应用程序. -- 使用[Elasticsearch](https://www.elastic.co/products/elasticsearch)和[Kibana](https://www.elastic.co/products/kibana)存储和可视化日志(使用[Serilog](https://serilog.net/)编写). - -有关解决方案的详细说明,请参阅[其文档](https://abp.io/documents/abp/latest/Samples/Microservice-Demo). - -## 改进/功能 - -我们已经开发了许多功能,包括**分布式事件总线**(与RabbitMQ集成),**IdentityServer4集成**以及几乎所有功能的增强.我们不断重构和添加测试,以使框架更稳定和生产就绪.它正在[快速增长](https://github.com/abpframework/abp/graphs/contributors). - -## 路线图 - -在第一个稳定版本(v1.0)之前还有很多工作要做.你可以在GitHub仓库上看到[优先的积压项目](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog). - -根据我们的估计,我们计划在2019年第二季度(可能在五月或六月)发布v1.0.所以,不用等待太长时间了.我们也对第一个稳定版本感到非常兴奋. - -我们还将完善[文档](https://abp.io/documents/abp/latest),因为它现在还远未完成. - -第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将你的想法写为对此帖的评论. - -## 中文网 - -中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). - -## NDC {London} 2019 - -很高兴作为合作伙伴参加[NDC {London}](https://ndc-london.com/)2019 .我们已经与许多开发人员讨论过当前的ASP.NET Boilerplate和ABP vNext,我们得到了很好的反馈. - -我们还有机会与[Scott Hanselman](https://twitter.com/shanselman)和[Jon Galloway](https://twitter.com/jongalloway)交谈.他们参观了我们的展位,我们谈到了ABP vNext的想法.他们喜欢新的ABP框架的功能,方法和目标.在twitter上查看一些照片和评论: - -![scott-and-jon](scott-and-jon.png) - -## 跟上步伐 - -* 你可以标星并关注**GitHub**存储库:https://github.com/abpframework/abp -* 你可以关注官方**Twitter**帐户获取新闻:https://twitter.com/abpframework diff --git a/docs/zh-Hans/Blog-Posts/2019-02-22/scott-and-jon.png b/docs/zh-Hans/Blog-Posts/2019-02-22/scott-and-jon.png deleted file mode 100644 index 79ad21aee7..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2019-02-22/scott-and-jon.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md deleted file mode 100644 index 6b3c76d2e2..0000000000 --- a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/Post.md +++ /dev/null @@ -1,89 +0,0 @@ -# ABP CLI,v0.18版本的新模板和其他功能 - -ABP v0.18已发布, 包含解决的[80+个issue](https://github.com/abpframework/abp/milestone/16?closed=1),[550+次提交](https://github.com/abpframework/abp/compare/0.17.0.0...0.18.0) - -## 网站更改 - -[abp.io](https://abp.io)网站**完全更新**以突出ABP框架的目标和重要功能.文档和博客网址也会更改: - -- `abp.io/documents`移至[docs.abp.io](https://docs.abp.io). -- `abp.io/blog`转移到[blog.abp.io](https://blog.abp.io). - -## ABP CLI - -ABP CLI(命令行界面)是一种新的全局命令行工具,用于执行基于ABP的解决方案的一些常见操作.主要功能是; - -* **创建新的应用程序**或模块项目. -* **向应用程序添加新模块**. -* **更新**解决方案中所有与ABP相关的包. - -ABP CLI现在是创建新项目的首选方式,你仍然可以从[开始](https://abp.io/get-started)页面下载新项目. - -### 用法 - -使用命令行窗口安装ABP CLI: - -```` bash -dotnet tool install -g Volo.Abp.Cli -```` - -创建一个新应用程序: - -```` bash -abp new Acme.BookStore -```` - -将模块添加到应用程序: - -```` bash -abp add-module Volo.Blogging -```` - -更新解决方案中所有与ABP相关的包: - -```` bash -abp update -```` - -有关详细信息,请参阅[ABP CLI文档](https://docs.abp.io/en/abp/latest/CLI). - -## 新模板 - -在此版本中,我们更新了所有启动模板.主要目标是提供基于领域驱动设计层的更好的启动模板,这些模板还允许创建分层解决方案(Web和API层可以在物理上分开).它还包括针对不同层分开的单元和集成测试项目. - -下图显示了MVC应用程序的新启动项目. - -![mvc-template-solution](mvc-template-solution.png) - -有关详细信息,请参阅[启动模板文档](https://docs.abp.io/en/abp/latest/Startup-Templates/Index). - -## 更改日志 - -以下是此版本附带的一些其他功能和增强功能: - -* 新[Volo.Abp.Dapper](https://www.nuget.org/packages/Volo.Abp.Dapper)包. -* 新[Volo.Abp.Specifications](https://www.nuget.org/packages/Volo.Abp.Specifications)包. -* 具有`IDataSeeder`服务和`IDataSeedContributor`接口的新数据种子系统,允许模块化初始数据种子系统. -* 改进了MemoryDB实现,以序列化/反序列化存储在内存中的对象,因此它为单元/集成测试中的数据库模拟提供了更真实的基础结构. -* 为docs模块添加了多语言支持.用于[ABP文档](https://docs.abp.io). - -有关此版本中的所有功能,增强功能和错误修正,请参阅[GitHub发行说明](https://github.com/abpframework/abp/releases/tag/0.18.0). - -## 路线图 - -与ABP v1.0版本相关的一件事是.NET Core / ASP.NET Core 3.0版本.根据[.NET核心路线图](https://github.com/dotnet/core/blob/master/roadmap.md),计划于2019年9月发布3.0版本. - -ASP.NET Core具有很大的变化和功能.作为一个重大的突破性变化,它将[仅在.NET Core上运行](https://github.com/aspnet/Announcements/issues/324)(删除.net标准支持),因此它不能用于完整.net框架了. - -我们已宣布在2019年第二季度发布v1.0.我们应该为v1.0做的主要工作是: - -* 填补当前功能的空白. -* 重构和改进当前的API. -* 修复已知的错误. -* 完成文档和教程. - -除了我们应该做的工作之外,我们还在考虑等待ASP.NET Core 3.0发布.因为,如果我们在ASP.NET Core 3.0之前发布ABP v1.0,我们将不得不在短时间内再次发布ABP v2.0并放弃v1.0支持.因此,我们正在考虑使用ASP.NET Core 3.0 RC发布ABP v1.0 RC,并将最终发布日期与Microsoft保持一致. - -## 想要贡献? - -感谢社区对ABP开发的支持.非常感谢.如果你还想参与,请参阅[本指南](https://github.com/abpframework/abp/blob/master/docs/en/Contribution/Index.md)作为开始. diff --git a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/mvc-template-solution.png b/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/mvc-template-solution.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2019-06-19 v0_18_Release/mvc-template-solution.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md deleted file mode 100644 index 9f82f6dce3..0000000000 --- a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ /dev/null @@ -1,46 +0,0 @@ -# 发布ABP v0.19包含Angular UI选项 - -ABP v0.19已发布,包含解决的[~90个问题](https://github.com/abpframework/abp/milestone/17?closed=1)和[600+次提交](https://github.com/abpframework/abp/compare/0.18.1...0.19.0). - -## 新功能 - -### Angular UI - -终于,ABP有了一个**SPA UI**选项,使用最新的[Angular](https://angular.io/)框架.Angular的集成不是简单地创建了一个启动模板. - -* 创建了一个基础架构来处理ABP的模块化,主题和其他一些功能.此基础结构已部署为[NPM包](https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages). -* 为帐户,身份和租户管理等模块创建了Angular UI包. -* 创建了一个最小的启动模板,使用IdentityServer进行身份验证并使用ASP.NET Core做为后端.此模板使用上面提到的包. -* 更新了[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)和[下载页面](https://abp.io/get-started),以便能够使用新的UI选项生成项目. -* 创建了[教程](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I)以使用新的UI选项快速入门. - -我们基于最新的Angular工具和趋势创建了模板,文档和基础架构: - -* 使用[NgBootstrap](https://ng-bootstrap.github.io/)和[PrimeNG](https://www.primefaces.org/primeng/)作为UI组件库.你可以使用自己喜欢的库,没问题,但预构建的模块可以使用这些库. -* 使用[NGXS](https://ngxs.gitbook.io/ngxs/)作为状态管理库. - -Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我们将开始第二个UI选项的工作.虽然尚未决定,但候选的有Blazor,React和Vue.js. 等待你的反馈.你可以使用以下issue进行投票(thumb): - -* [Blazor](https://github.com/abpframework/abp/issues/394) -* [Vue.js](https://github.com/abpframework/abp/issues/1168) -* [React](https://github.com/abpframework/abp/issues/1638) - -### Widget系统 - -[Widget系统](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets)允许为ASP.NET Core MVC应用程序**定义和重用**Widget.Widget可能有自己的脚本和样式资源以及由ABP框架管理的第三方库的依赖关系. - -### 其他 - -我们已经解决了许多错误,并根据社区反馈开发了现有功能.有关所有已结束的问题,请参阅[v0.19里程碑](https://github.com/abpframework/abp/milestone/17?closed=1). - -## 路线图 - -我们决定等待**ASP.NET Core 3.0**最终发布.微软已宣布将于9月23日至25日在[.NET Conf](https://www.dotnetconf.net/)上发布它. - -我们已经计划完成我们的工作,并迁移到ASP.NET Core 3.0(预览版或RC版)在它发布之前.一旦Microsoft发布它,我们将立即开始升级并测试最终版本. - -因此,你可以期待ABP **v1.0**将在**10月上半月**发布.我们非常兴奋并努力地工作着. - -你可以关注[GitHub里程碑](https://github.com/abpframework/abp/milestones)的进度. - -我们不会在v1.0之前添加主要功能. diff --git a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md deleted file mode 100644 index c4d98244fb..0000000000 --- a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/Post.md +++ /dev/null @@ -1,21 +0,0 @@ -# 基于ASP.NET Core 3.0的ABP v0.21已发布 - -在微软发布仅仅一个小时后, 基于ASP.NET Core 3.0的ABP v0.21也紧跟着[发布了.](https://twitter.com/abpframework/status/1176185493119258624) - -v0.21没有新功能.它只是升级到稳定的ASP.NET Core 3.0. 查看[v0.20发行说明](https://github.com/abpframework/abp/releases/tag/0.20.0)以获取新功能,增强功能和错误修复. - -## 关于v1.0 - -ABP框架越来越接近v1.0.我们打算在今年10月中旬发布1.0. 现在,我们将完善测试和文档. - -## .NET Conf 2019 - -微软已经在为期3天的虚拟会议.NET Conf 2019发布了ASP.NET Core 3.0. ABP的首席开发人员[Halil ibrahim Kalkan](https://twitter.com/hibrahimkalkan)在会议上也发表了讲话,介绍了ABP框架.能够参加这一重要活动真是太棒了. - -## Techorama荷兰2019 - -[Techorama NL](https://techorama.nl/)是欧洲最大的会议之一.今年,Volosoft是会议的赞助商,并将有一个展位与软件开发人员讨论ABP框架和软件开发.我们的展位墙如下图所示: - -![volosoft-booth](volosoft-booth.png) - -如果你也参加会议,请到展位讨论ABP框架.我们还为你准备了一些私货:) diff --git a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/volosoft-booth.png b/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/volosoft-booth.png deleted file mode 100644 index 3dca4409e4..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2019-09-25 v0_21_Release/volosoft-booth.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/Post.md deleted file mode 100644 index ef34eac7b2..0000000000 --- a/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/Post.md +++ /dev/null @@ -1,29 +0,0 @@ -# ABP v1.0终于发布了! - -今天是个大日子!经过约3年的不断开发,第一个稳定的ABP版本,1.0,已经发布了.感谢为该项目做出贡献或试用过的每个人. - -![contribution-graph](contribution-graph.png) - -立即开始使用新的ABP框架:[abp.io/get-started](https://abp.io/get-started) - -## 统计 - -在这里,有关该项目的一些GitHub和NuGet统计信息: - -* 2,360星. -* 5,917次提交. -* 72位贡献者. -* 关闭了1,136个问题,276个打开问题. -* 关闭了566个PR,5个打开的PR. -* 39次发布. -* 在NuGet上有122,795次下载. - -甚至在第一个版本发布之前就有了非常好的需求. - -## 路线图 - -首要任务是完成文档,因为框架功能和模块仍然缺少很多文档.然后,我们将根据标记的优先级继续在GitHub上解决问题. - -请参阅[GitHub里程碑项目](https://github.com/abpframework/abp/milestones). - -ABP是一个社区驱动的项目.因此,我们主要根据社区的反馈和需求确定问题的优先级. \ No newline at end of file diff --git a/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/contribution-graph.png b/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/contribution-graph.png deleted file mode 100644 index abb137846f..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2019-10-22 v1_0_Release/contribution-graph.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/Post.md deleted file mode 100644 index b996bf23d3..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/Post.md +++ /dev/null @@ -1,164 +0,0 @@ -# ABP框架v2.0 和 ABP商业版 - -ABP框架2.0版已经在本周公布.这篇文章解释了为什么我们发布了一个**抢先主版本**,和2.0版本中的变化. - -除了v2.0版本,我们很高兴地宣布**ABP商业版**,这是建立在开源ABP框架的之上的一套专业的模块,工具,主题和服务. - -## ABP框架V2.0 - -### 为什么2.0,而不是1.2? - -本来在[V1.1.2](https://github.com/abpframework/abp/releases/tag/1.1.2)发布后计划发布1.2版.然而,[有报告](https://github.com/abpframework/abp/issues/2026)称1.x版在Linux上有一些**性能**和**稳定性**问题,尤其是当应用程序部署在**低配CPU和内存**的**Linux**容器上. - -我们深入研究了这一问题,并已查明问题的根本原因与**拦截`async`方法**的实现有关.此外,也有一些 **`async`套`sync`** 的用法影响了线程池的优化. - -最后,在**社区**在大力协助下我们**解决了所有的问题**.但是,我们也有一些重要的**设计决策**导致了一些**破坏性变更**,因为[语义版本](https://semver.org/),我们不得不改变框架的主版号. - -大多数的应用程序不会受到[破坏性变更](https://github.com/abpframework/abp/releases)的影响,或者只需要做一些微小的修改. - -### 破坏性变更 - -#### 删除了一些同步的API - -一些拦截器需要使用`async`的API.当他们拦截`sync`方法时,他们需要调用`async`套`sync`.这最终导致了`async`套`sync`的问题.这就是为什么我们[删除了一些同步的API](https://github.com/abpframework/abp/pull/2464). - -当你需要**在`async`方法中调用`sync`方法**时, **`async`套`sync`** 这种模式是`C#`一个经典问题.虽然有一些解决方法,但是都有相应的**缺点**,并建议**不要写**这样的代码.你可以在网上找到关于这一话题的许多文档. - -为了避免这个问题,我们已经移除: - -- `sync`[仓储](https://docs.abp.io/en/abp/latest/Repositories)方法 (如`insert`, `update`, 等...), -- `sync`[工作单元](https://docs.abp.io/en/abp/latest/Unit-Of-Work)API, -- `sync`[后台作业](https://docs.abp.io/en/abp/latest/Background-Jobs)API, -- `sync`[审计日志](https://docs.abp.io/en/abp/latest/Audit-Logging)API, - - 其他一些很少使用的`sync`API. - -如果你遇到了编译错误,只需使用这些API的`async`版本. - -#### 始终async! - -从v2.0开始,ABP框架假设你以`async`方式编写你的应用程序代码.否则,一些框架的功能可能无法正常工作. - -建议你的所有[应用服务](https://docs.abp.io/en/abp/latest/Application-Services), [仓储方法](https://docs.abp.io/en/abp/latest/Repositories), 控制器动作(ontroller actions), 页面处理器(page handlers)都是`async`. - -即使你的应用服务方法并不需要是`async`,也将其设置为`async`,因为拦截器需要执行`async`操作(授权,工作单元等).你可以在不调用`async`的方法中返回`Task.Completed`. - -示例: - -````csharp -public Task GetValueAsync() -{ - //这个方法没有任何async调用 - return Task.CompletedTask(42); -} -```` - -上述例子通常并不需要是`async`因为它不执行`async`调用.然而,将它设置为`async`,这样可以帮助ABP框架运行拦截器时避免出现`async`套sync的调用. - -此规则不强制你写的每一个方法都是`async`.这样并不好而且很乏味.只在拦截的服务上需要(特别是[应用服务](https://docs.abp.io/en/abp/latest/Application-Services)和[仓库方法](https://docs.abp.io/en/abp/latest/Repositories)) - -#### 其他破坏性变更 - -查看[发行说明](https://github.com/abpframework/abp/releases/tag/2.0.0)中的破坏性变更.他们中的大多数都不会影响你的应用程序代码. - -### 新功能 - -本次发布还包含一些新的功能和一堆改进: - -- [#2597](https://github.com/abpframework/abp/pull/2597) 新的`Volo.Abp.AspNetCore.Serilog`包. -- [#2526](https://github.com/abpframework/abp/issues/2526) `C#`客户端代理的客户端验证. -- [#2374](https://github.com/abpframework/abp/issues/2374) `async`后台作业. -- [#265](https://github.com/abpframework/abp/issues/265) 管理应用程序关闭. -- [#2472](https://github.com/abpframework/abp/issues/2472) `IdentityServer`模块实现`DeviceFlowCodes`和`TokenCleanupService`. - -功能,改进和BUG修复的完整列表, 请查看[发布说明](https://github.com/abpframework/abp/releases/tag/2.0.0). - -### 文档 - -随着v2.0的发布,我们也完成了一些缺少的文档.在接下来的几周内,我们将主要关注文档和教程. - -## ABP商业版 - -[ABP商业版](https://commercial.abp.io/)是建立在开源ABP框架之上的一套专业的**模块,工具,主题和服务**. - - - 除了ABP框架免费和[开源模块](https://docs.abp.io/en/abp/latest/Modules/Index)之外, 提供[专业模块](https://commercial.abp.io/modules). -- 包含一个漂亮的[UI主题](https://commercial.abp.io/themes), 具有5种不同的样式. -- 提供[ABP套件](https://commercial.abp.io/tools/suite); 一个让开发更具有生产力的工具. 通过配置实体属性, 它可以在几秒内创建全栈的CRUD页面. 更多的功能陆续开发中. -- 为企业提供[高级支持](ttps://commercial.abp.io/support). - -除了这些标准的功能,我们会将提供定制服务.更多细节请参见[commercial.abp.io](https://commercial.abp.io/)网站. - -### ABP框架 vs ABP商业版 - -ABP商业版**不是付费版本**的ABP框架.可以把它当作为专业公司提供的**附加套餐**.你可以用它来节省时间和更快地开发产品. - -ABP框架将永远是**开源免费**的! - -一个原则是,我们创建的主要基础设施作为开源产品, 然后销售额外的预制应用程序功能,主题和工具.类似于[ASP.NET Boilerplate](https://aspnetboilerplate.com/)和[ASP.NET Zero](https://aspnetzero.com/)产品. - -购买商业版许可极大地节省你的时间和精力,你可以专注于自己的业务,此外也可获得专门的和优先的支持.同时,你也在支持ABP核心团队,因为我们花了大部分时间来开发,维护和支持开源的ABP框架. - -有了ABP商业版,ABP现在变为一个平台.我们称之为**ABP.IO平台**, 其中包括开源ABP框架和ABP商业版. - -### 演示 - -如果你想知道ABP商业版应用程序的启动模板是什么样,你可以很容易地[创建一个演示](https://commercial.abp.io/demo),并看到它的实际效果.该演示包括所有的预制模块和主题. - -下面是一张IdentityServer管理模块UI的截图: - -![abp-commercial-demo](abp-commercial-demo.png) - -这是一张来自使用material设计风格主题的演示应用程序的截图: - -![lepton-theme-material](lepton-theme-material.png) - -### 价格 - -你可以创建**无限个工程/产品**, 销售给**无限个客户**, 部署在**无限台服务器上**, 不受任何限制. 定价主要是基于**开发人员个数**,**支持等级**和**源代码**需求上.有三个标准包; - -- **团队许可**: 包括所有的模块,主题和工具.允许最多3个开发者开发产品.可购买额外的开发者许可. -- **商业许可**: 允许下载所有的模块和主题的源代码.此外,默认包含了5个开发者许可.可购买额外的开发者许可. -- **企业许可**: 在商业许可上, 提供无限的专属支持. - -请查看[价格页面](https://commercial.abp.io/pricing)了解详细信息.除了标准包以外,我们也提供定制服务和定制许可.如有任何问题,请[联系我们](https://commercial.abp.io/contact). - -#### 许可比较 - -许可价格是根据开发者数量,支持等级和源代码访问而变化的. - -##### 源代码 - -团队许可证不包括预制模块和主题的源代码.以**NuGet和NPM包**的方式使用所有这些模块.通过这种方式,你可以很容易地通过更新包的依赖得到**新功能和bug修复**仅.但是不能访问其源代码.所以不能嵌入模块的源代码到你的应用程序里,和随意修改源代码. - -预制模块提供一定等级的**定制**和**扩展**,并允许你覆盖服务,UI部分等.我们正在努力使他们更加可定制和可扩展.如果你无需在预制模块中做很大修改的话,团队许可是你理想的选择,因为它更便宜,并且可轻松获得新的功能和bug修复. - -商业和企业许可允许你在需要时**下载任何模块和主题的源代码**.它们使用与团队许可相同的启动模板,所以所有的模块都默认使用`NuGet`和`NPM`包.但是,在需要的情况下,你可以从一个模块中删除包的依赖,并嵌入它的源代码到你自己的解决方案中,然后完全定制它.在这种情况下,当一个新版本可用时, 升级模块将不会那么容易.当然, 你不必升级!但是,如果你愿意,你也可以使用一些合并工具或Git的分支系统来做到这一点. - -#### 许可周期 - -ABP商业版许可是**永久的**,这意味着你可以**永远使用**它继续开发应用程序. - -但是,下面的服务周期为一年: - -- 高级**支持**一年后结束.你可以继续得到社区支持. -- 一年后将不会得到模块和主题的**更新**.你可以继续使用最后获得的版本.甚至可以在主版本内得到BUG修复和改进. -- 你可使用**ABP套件**一年. - -如果想继续获得这些好处,可延长许可期限.续订价格比正常价格低20%. - -## NDC London 2020 - -与[去年](https://medium.com/volosoft/impressions-of-ndc-london-2019-f8f391bb7a9c)一样, 我们是著名的软件开发会议[NDC London](https://ndc-london.com/)的合作伙伴! 去年, 我们开展了[ASP.NET Boilerplate](https://aspnetboilerplate.com/)和[ASP.NET Zero](https://aspnetzero.com/)主题: - -![ndc-london-volosoft](ndc-london-volosoft.png) - -今年,我们将着重于**ABP.IO平台**(开源ABP框架和ABP商业版).我们的展位会是这样的: - -![ndc-london-volosoft](ndc-2020-volosoft-booth-wall.png) - -如果你参加会议,记得要参观我们的展位.我们将很高兴来谈一谈ABP平台的功能,目标和软件开发. - -### 你想见ABP团队吗? - -如果你在伦敦, 而且想和我们喝杯咖啡的话, 在2月1日的下午[@hibrahimkalkan](https://twitter.com/hibrahimkalkan)和[@ismcagdas](https://twitter.com/ismcagdas)会在那. - -想见面就给info@abp.io写个邮件 :) \ No newline at end of file diff --git a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/abp-commercial-demo.png b/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/abp-commercial-demo.png deleted file mode 100644 index 9153adeb86..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/abp-commercial-demo.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/lepton-theme-material.png b/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/lepton-theme-material.png deleted file mode 100644 index 279f4b2cf1..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/lepton-theme-material.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-2020-volosoft-booth-wall.png b/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-2020-volosoft-booth-wall.png deleted file mode 100644 index 428ecc284d..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-2020-volosoft-booth-wall.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-london-volosoft.png b/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-london-volosoft.png deleted file mode 100644 index afaed37a7a..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-01-15 v2_0_Release/ndc-london-volosoft.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md deleted file mode 100644 index 73585582f5..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md +++ /dev/null @@ -1,142 +0,0 @@ -# ABP框架v2.3.0已经发布! - -在**新冠病毒**的日子里,我们发布了**ABP框架v2.3**, 这篇文章将说明本次发布**新增内容**和过去的两周**我们做了什么**. - -## 关于新冠病毒和我们的团队 - -关于冠状病毒的状况**我们很难过**.在[Volosoft](https://volosoft.com/)的团队,我们有不同国家的**远程工作者**在自己家里工作.从上周开始,我们已经**完全开始在家远程工作**,包括我们的主要办公室的员工. - -我们相信并祈祷人类会在很短的时间内克服这个问题. - -## 关于发布周期 - -从ABP v2.1.0开始,我们开始**每两周**的周四发布功能版本.本次是该决定后的第3次发布,我们看到这种方式目前运转良好,并提高了我们的灵活性. - -我们将继续每两周发布**功能版本**(如v2.4,v2.5).另外,如果需要我们会随时发布**热修复版本**(如v2.3.1,v2.3.2). - -## ABP框架v2.3.0新增内容 - -我们已在这两周的开发周期内通过**393次提交**完成和合并了 **[104](https://github.com/abpframework/abp/milestone/30?closed=1)个issue和pull request**. - -我将介绍这个版本加入的一些新功能和改善. - -### React Native移动应用程序 - -我们终于完成了**react native移动应用程序**.目前,它可以让你**登录**,管理**用户**和**租户**.它利用ABP框架相同的设置,授权和本地化系统. - -应用程序的一些截图: - -![mobile-ui](react-native-ui.png) - -它没有太多的功能,但它是你的移动应用程序一个**完美的起点**,因为它是完全集成到后端并支持多租户. - -### Angular TypeScript代理生成器 - -从我们的Angular应用程序中调用服务器中的REST端点是很常见的.这种情况下,我们一般创建**服务**(在服务器上包含各个服务的方法)和**模型对象**(对应服务器上的[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)). - -除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: - -* 它产生一个**大,单一**的.ts文件; - * 当你的应用程序增长时,它变得**太大**了. - * 它不适合ABP框架的 **[模块化](https://docs.abp.io/en/abp/latest/Module-Development-Basics)方式**. -* 它创建了有点**丑陋的代码**.我们希望有一个干净的代码(就像我们手写的). -* 它不能生成服务器端声明的相同的**方法签名**(因为swagger.json不能准确地反映后端服务的方法签名).我们已创建了公开服务器端方法约定的端点,来允许客户端生成更好的客户端代理. - -因此,我们决定创建一个ABP CLI命令来自动生成typescript客户端代理([#2222](https://github.com/abpframework/abp/issues/2222)),用于在ABP框架中开发REST API. - -它用起来很简单.只需要在你Angular应用程序的**根文件夹**运行以下命令 - -````bash -abp generate-proxy -```` - -它只会为你自己的应用程序的服务创建代理.它(默认)不会为你使用的应用程序模块创建代理.有几个选项.参见[CLI文档](https://docs.abp.io/en/abp/latest/CLI). - -### 复合主键的CRUD应用服务 - -` CrudAppService `是一个很有用的基类,用来为你的实体创建CRUD应用服务.不过,它不支持**复合主键**的实体. `AbstractKeyCrudAppService`是新开发的基类以支持复合主键的实体.更多信息请浏览[文档](https://docs.abp.io/en/abp/latest/Application-Services#abstractkeycrudappservice). - -### 添加模块的源代码 - -应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: - -* 当新版本可用时, 你可以 **轻松地[升级](https://docs.abp.io/en/abp/latest/CLI#update)** 这些模块. -* 你的解决方案**更干净**,这样你就可以专注于自己的代码. - -但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: - -````bash -abp add-module --with-source-code -```` - -该命令以源代码方式添加模块, 或者如果模块已经以包引用方式添加了, 则替换为源代码,. - -> 建议在使用此命令前**保存你的更改**到源代码控制系统, 因为它会修改很多你的代码. - -此外,我们也创建了文档来说明如何定制依赖的模块而不改变它们的源代码(见下面的部分).仍然建议以包的方式使用模块,以便在以后可以轻松升级. - -> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到你的解决方案中. - -### 切换到预览版 - -ABP框架正在迅速发展,我们经常发布新版本.不过,如果你想更紧密地追随它,你可以使用**每日预览包**. - -我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: - -````bash -abp switch-to-preview -```` - -它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: - -````bash -abp switch-to-stable -```` - -更多信息请浏览[ABP CLI文档](https://docs.abp.io/en/abp/latest/CLI#switch-to-preview). - -### 文档改进 - -#### 扩展/定制依赖应用模块 - -我们创建了一个巨大的文档来说明如何定制模块依赖而不改变其源代码.参见[文档](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide). - -除了文档以外,我们已经修订了所有模块([#3166](https://github.com/abpframework/abp/issues/3166)),来使他们的服务更容易扩展和定制. - -#### EF Core迁移指南 - -最近,我们创建了一个指南,说明ABP启动模板所使用的迁移系统. [该指南](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations)还介绍了如何定制迁移结构,拆分你的模块跨多个数据库,复用一个模块的表,等等. - -#### 从 ASP.NET Boilerplate迁移 - -如果你有建立在 ASP.NET Boilerplate的解决方案,我们[创建了一个指南](https://docs.abp.io/en/abp/latest/AspNet-Boilerplate-Migration-Guide),试着帮助迁移你的解决方案到新的ABP框架上. - -### 其他一些功能 - -#### 框架 - -* 添加`IRepository.GetAsync`和`IRepository.FindAsync`方法([#3184](https://github.com/abpframework/abp/issues/3148)). - -#### 模块 - -* 当创建新租户时获取管理员的密码和电子邮件地址,租户管理模块([#3088](https://github.com/abpframework/abp/issues/3088)). -* 集成Elastic全文检索, 文档模块([#2901](https://github.com/abpframework/abp/pull/2901)). -* 新的Quartz后台工作者模块([#2762](https://github.com/abpframework/abp/issues/2762)) - -#### 示例 - -* 微服务演示添加多租户支持([#3032](https://github.com/abpframework/abp/pull/3032)). - -所有的功能, 改善和BUG修复, 请浏览[发布说明](https://github.com/abpframework/abp/releases/tag/2.3.0). - -## 下一步? - -我们未来几个月的目标如下: - -* 完成**文档和示例**,写更多的教程. -* 使框架和现有模块的更加**可定制和可扩展**. -* 集成**gRPC**和为所有预置模块实现gRPC端点([#2882](https://github.com/abpframework/abp/issues/2882)). -* 为ABP框架创建**Blazor UI**, 并在所有模块和启动模板中实现它([#394](https://github.com/abpframework/abp/issues/394)). -* 为预置模块**添加新功能**,并为[ABP商业版](https://commercial.abp.io/)创建新模块. - -更多细节请浏览[GitHub里程碑](https://github.com/abpframework/abp/milestones). \ No newline at end of file diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png deleted file mode 100644 index 8c0a2bf241..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png deleted file mode 100644 index ab054e7cb0..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md deleted file mode 100644 index bc448179cc..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md +++ /dev/null @@ -1,247 +0,0 @@ -# ABP框架 v2.7.0已经发布! - -**ABP框架**和**ABP商业版** v2.7已经发布.我们没有为2.4,2.5和2.6发布博客文章,所以这篇文章也将涵盖这几个版本中**新增内容**和过去的2个月里**我们完成了什么**. - -## 关于发布周期与开发 - -之前说过我们已经开始**每两个星期**发布一个新的次要功能版本,一般在星期四.我们的目标是尽快提供新功能. - -在过去的7-8周里, 我们在**1,300+次提交**中完成和合并了数百个issue和pull request, 这只是ABP框架的库.每日提交的次数不断增加: - -![github-contribution-graph](github-contribution-graph.png) - -ABP.IO平台正在快速增长,我们从社区获取的贡献越来越多. - -## ABP框架有哪些新增内容? - -### 对象扩展系统(Object Extending System) - -在过去的几个版本中,我们主要集中在以NuGet/NPM包使用现有模块时, 提供扩展的方法, - -对象扩展系统允许模块开发者创建可扩展的模块并允许应用开发者更容易地定制和扩展. - -例如,你可以这样为Identity模块的User实体添加两个扩展属性: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdate(options => - { - options.AddOrUpdateProperty("SocialSecurityNumber"); - options.AddOrUpdateProperty("IsSuperUser"); - } - ); -```` - -也很容易为这些属性定义验证规则: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.Attributes.Add(new RequiredAttribute()); - options.Attributes.Add( - new StringLengthAttribute(32) { - MinimumLength = 6 - } - ); - }); -```` - -你甚至可以编写自定义代码来验证属性.它会自动适用于应用服务, 控制器或页面的参数对象. - -实体的扩展属性通常存储在数据库表的一个单独的JSON格式的字段中,但是你也可以轻松地使用EF Core映射配置该属性为一个表字段: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.MapEfCore(b => b.HasMaxLength(32)); - } - ); -```` - -请参见有关该系统的详细信息[对象扩展文档](https://docs.abp.io/en/abp/latest/Object-Extensions). - -也可参见[自定义现有模块](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide)指南,以了解所有可能的自定义选项. - -### 文本模板包 - -[Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating)是v2.7.0中新加入的包.此前,[Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing)包也有类似的功能,但它的功能是有限的,实验性质的并且和发送邮件紧密耦合在一起. - -新文本模板包允许你定义基于文本的模板, 可以很容易地本地化和重用.你可以定义布局模板并且与其它模板共享. - -目前,我们正在使用它发送邮件.一个需要发送邮件的模块通常定义了一个模板.如: - -````xml -

    {{L "PasswordReset"}}

    - -

    {{L "PasswordResetInfoInEmail"}}

    - - -```` - -这是一个典型的密码重置邮件模板. - -* 模板系统是基于开源的[Scriban库](https://github.com/lunet-io/scriban).因此,它支持if条件,循环等等. -* `model`用于将数据传递到模板(就像ASP.NET Core MVC). -* `L`是一个特殊函数用于本地化给定的字符串. - -为所有邮件使用相同的布局是一个典型应用.所以,你可以定义一个布局模板.这是框架自带的标准的布局: - -````xml - - - - - - - {{content}} - - -```` - -布局中应该有一个`{{content}}`区域用来呈现子内容(就像MVC中的`RenderBody()`). - -最终应用程序可以很容易地通过覆盖模板内容来自定义它. - -每当你需要渲染模板时,提供模板名称和model来使用`ITemplateRenderer`服务.详情请参见[文本模板文档](https://docs.abp.io/en/abp/latest/Text-Templating).我们甚至还为ABP商业版创建了UI(请参见下面的相关章节). - -### 订阅异常 - -ABP框架的[异常处理系统](https://docs.abp.io/en/abp/latest/Exception-Handling)会自动处理异常并且为客户端返回相应的结果.在某些情况下,你可能希望每当异常发生时有一个回调.通过这种方式,例如,你可以发送邮件或采取基于异常的任何动作. - -只需要在你的应用程序中创建一个从`ExceptionSubscriber`派生的类: - -````csharp -public class MyExceptionSubscriber : ExceptionSubscriber -{ - public async override Task HandleAsync(ExceptionNotificationContext context) - { - //TODO... - } -} -```` - -更多信息请参见[异常处理](https://docs.abp.io/en/abp/latest/Exception-Handling). - -### 其他 - -在之前的发布中框架也有很多小的功能和改进.这里列举几个: - -* 新增了`AbpLocalizationOptions.DefaultResourceType`用来设置应用程序的默认资源类型.通过这种方式,当资源未指定时, 本地化系统使用默认的资源.最新的应用程序启动模板已经配置了,你也可以为你现有的应用程序设置它. -* 权限定义新增了`IsEnabled`.通过这种方式,你可以从应用程序中完全禁用权限和隐藏相关功能.这可在一些应用程序中做为功能开关的方式.用法请参见[#3486](https://github.com/abpframework/abp/issues/3486). -* 框架中定义的所有本地化资源新增了荷兰语和德语本地化.感谢贡献者们. - -## ABP商业版有哪些新增内容? - -[ABP商业版](https://commercial.abp.io/)的目标是基于ABP框架项目提供预构建的应用程序功能,代码生成工具,专业的主题,先进的示例和高级支持. - -我们正在并行工作于ABP商业版与ABP框架功能对齐,并提供更多的模块,主题选项和工具. - -本节将介绍ABP商业版这边有哪些进展. - -### 模块实体扩展系统 - -模块实体扩展系统是相对于对象扩展系统(上面介绍的)的一个更高级别的API,并提供了一种简单的方法来向现有实体中增加扩展属性.一个新的扩展属性可以很容易地自动成为HTTP API和用户界面的一部分. - -例如:向Identity模块的User实体中添加`SocialSecurityNumber` - -````csharp -ObjectExtensionManager.Instance.Modules() - .ConfigureIdentity(identity => - { - identity.ConfigureUser(user => - { - user.AddOrUpdateProperty( //属性类型: string - "SocialSecurityNumber", //属性名 - property => - { - //验证规则 - property.Attributes.Add(new RequiredAttribute()); - property.Attributes.Add( - new StringLengthAttribute(64) { - MinimumLength = 4 - } - ); - - //...该属性的其它配置 - } - ); - }); - }); -```` - -仅通过这样的配置,用户界面就将具有新的属性(在表中和在创建/编辑表单中): - -![module-entity-extended-ui](module-entity-extended-ui.png) - -新属性可以轻松地本地化和验证.目前,它支持原始类型,如字符串,数字和布尔型,但我们计划添加更多高级场景(如导航/查找属性). - -请参阅[模块实体扩展](https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions)指南来了解如何使用和配置的详细信息. - -#### 其他扩展点 - -还有其他一些预先定义的点来定制和扩展依赖模块的用户界面: - -* 你可以为数据表中的实体添加新动作(下方左侧的图). -* 你可以向页面的工具栏中添加新的按钮(或其他控件)(下方右侧的图). -* 你可以向数据表中添加自定义列. - -![abp-commercial-ui-extensions](abp-commercial-ui-extensions.png) - -请参阅[自定义模块](https://docs.abp.io/en/commercial/latest/guides/customizing-modules)指南,以了解所有可能的方式来定制依赖模块. - -### 文本模板管理模块 - -我们在v2.7中推出一个新的模块:[文本模板管理](https://docs.abp.io/en/commercial/latest/modules/text-template-management).它基本上是用来在用户界面上编辑文本/邮件模板(在ABP框架2.7中加入的),并保存更改到数据库中. - -一个密码重置邮件模板的内容编辑截图: - -![text-template-content-ui](text-template-content-ui.png) - -当创建新工程时, 这个模块已经预装了. - -### 实体历史视图 - -审计日志UI模块现在显示所有应用程序中的实体变更, 带有属性修改的细节. - -![audit-log-entity-changes](audit-log-entity-changes.png) - -当点击实体的动作菜单时, 你还可以查看实体的历史信息: - -![tenant-entity-changes](tenant-entity-changes.png) - -### 更多示例 - -我们正在创建ABP商业版更多高级的示例应用程序.其中一个是简易CRM, 将在几天内提供给商业客户. - -这是一个简易CRM仪表盘的截图: - -![easy-crm](easy-crm.png) - -具有帐户,联系人,产品组,产品,订单等. - -### 新模块 - -我们将继续改善现有模块和创建新的模块.除了上面介绍的新的[文本模板管理](https://docs.abp.io/en/commercial/latest/modules/text-template-management), 还有: - -* 我们最近发布了[支付模块](https://commercial.abp.io/modules/Volo.Payment),目前对接了PayU和的2Checkout支付网关.更多网关陆续添加. -* 我们已经创建了一个简单的[Twilio短信集成](https://docs.abp.io/en/commercial/latest/modules/twilio-sms)模块,以通过Twilio发送短信. -* 我们正在开发一个**聊天模块**, 将在未来几周内可用. -* 我们正在致力于为Identity模块增加**组织单元管理**系统, 用于创建分层组织单元(Domain层将是开源和免费的). - -更多ABP商业版和ABP框架的模块,主题和工具选项正在开发中. - -## ABP框架 VS ABP商业版 - -我们([Volosoft](https://volosoft.com/) - ABP.IO平台背后的核心团队),在ABP框架和ABP商业版上花费几乎相同的精力,我们认为ABP.IO平台是一个整体. - -[ABP框架](https://abp.io/)提供了所有的基础设施和应用程序的独立框架功能,使你更具生产力,专注于自己的业务代码,并实现软件开发最佳实践.它为你提供不重复作业的一个明确和舒适的开发经验. - -[ABP商业版](https://commercial.abp.io/)提供内置功能,主题和工具, 如果你的需求涉及到这些, 就可以节省你的时间.除此之外还有框架的高级支持和预构建模块. diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png deleted file mode 100644 index 252af6b9e4..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png deleted file mode 100644 index 6a1ec44810..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png deleted file mode 100644 index 2bfa53cbcc..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png deleted file mode 100644 index e624e75e38..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png deleted file mode 100644 index 25621aabb7..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png deleted file mode 100644 index 31524187b0..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png deleted file mode 100644 index 99c7972da3..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/Post.md deleted file mode 100644 index eb193b8e41..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/Post.md +++ /dev/null @@ -1,214 +0,0 @@ -# ABP v2.8.0发布 & 路线图 - -**ABP框架**和**ABP商业版**v2.8已经发布.这篇文章将涵盖这些发布中的**新增内容**和项目的**中期路线图**. - -## ABP框架2.8有哪些新增内容? - -你可在[GitHub的发行说明](https://github.com/abpframework/abp/releases/tag/2.8.0)中看到所有的变更.这篇博客只包括重要的一些功能/变更. - -### SignalR集成包 - -我们已经发布了[一个新的包](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)用来集成SignalR到基于ABP框架应用程序中. - -> 其实跟随[标准Microsoft教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序中已经可以实现.但是,ABP提供了SignalR集成包用来简化集成和使用. - -参见[SignalR集成文档](https://docs.abp.io/en/abp/latest/SignalR-Integration)开始使用SignalR. - -#### SignalR演示应用程序 - -我们也创建了一个简单的聊天应用程序来演示如何使用它. - -![signalr-chat-demo](signalr-chat-demo.png) - -参见[应用程序的源代码.](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) - -### 控制台应用程序启动模板 - -新的控制台应用程序模板,可用来创建整合了ABP框架的控制台应用程序. - -使用ABP CLI来创建新的控制台应用程序,并为`-t`(模板)选项指定`console`: - -````bash -abp new MyApp -t console -```` - -感谢[@liangshiw](https://github.com/liangshiw)为此模板的贡献. - -### 为MVC UI增加RTL支持和阿拉伯语本地化 - -[@kgamalseif](https://github.com/kgamalseif)为MVC UI贡献了RTL实现, 看起来很棒: - -![rtl-ui](rtl-ui.png) - -他还本地化了所有的框架和模块的资源.感谢他伟大的贡献. - -### 其它 - -本次发布的其它一些亮点: - -* 转换HttpApi.Client模块的包为.netstandard 2.0, 用来与其他类型的应用程序兼容. -* 改进的对象扩展系统,以更好地处理UI,本地化和验证. -* 为Hangfire和Quartz集成实现了禁用后台作业执行. -* 为MVC UI新增JsTree集成包. -* 移动所有示例到新的[ABP-示例](https://github.com/abpframework/abp-samples)库中并创建了[索引页](https://docs.abp.io/en/abp/latest/Samples/Index)来查看所有示例. - -### 弃用 - -* 弃用了`app.UseMvcWithDefaultRouteAndArea()`,并引入了`app.UseConfiguredEndpoints()`(见[#3880](https://github.com/abpframework/abp/issues/3880)). -* 弃用了`UsePostgreSql()`,并为[Volo.Abp.EntityFrameworkCore.PostgreSql](http://nuget.org/packages/Volo.Abp.EntityFrameworkCore.PostgreSql)包引入了`UseNpgsql()`.如果你正在使用PostgreSQL, 切换到`UseNpgsql()`. - -旧方法被标记为`Obsolete`,将在下一主要版本中删除. - -## ABP商业版2.8中有哪些新增内容? - -### 新的Lepton主题 - -我们已经彻底修改[lepton主题](https://commercial.abp.io/themes).来看下不同的风格: - -![lepton-themes](lepton-themes.gif) - -ABP商业版中语言管理页面的截图: - -![lepton-abp-default-theme](lepton-abp-default-theme.png) - -(默认风格UI) - -![lepton-abp-material-theme](lepton-abp-material-theme.png) - -(Material风格UI) - -[创建一个演示](https://commercial.abp.io/demo)来现场测试所有的风格.你可以设置页面更改风格. - -### 新增聊天模块 - -第一版的[聊天模块](https://commercial.abp.io/modules/Volo.Chat)已经在本版本中发布.当前只有MVC / Razor PagesUI.Angular UI正在开发中. - -![abp-chat-module](abp-chat-module.png) - -目前,它有一个简单的**实时文本消息**功能.更多的功能,比如群组消息,发送图像/文件在路线图中. - -### 其它 - -* 为Angular UI实现了[模块实体扩展](https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions)系统.还提高该系统以更好地处理float/double/decimal,date,datetime,enum和bool属性. -* [EasyCRM示例应用程序](https://docs.abp.io/en/commercial/latest/samples/easy-crm)树形视图中管理产品组. - -## 关于下一个版本 - -我们**每2周**发布一次.因此,下一个功能版本的计划的日期为**6月4日**, 版本号为**2.9**.这(可能)会是**最后的2.x版本**, 下个版本将是**3.0**. - -### ABP框架 2.9 & 3.0 - -#### 组织单元系统 - -Identity模块的组织单元系统本来要在2.8中发布,但不幸的是我们不能肯定该功能的稳定性,所以推迟到了2.9. - -#### gRPC - -我们打算创建一个gPRC集成示例应用程序.然后,我们打算为所有[预构建模块](https://docs.abp.io/en/abp/latest/Modules/Index)和[启动模板](https://docs.abp.io/en/abp/latest/Startup-Templates/Index)创建gRPC端点.我们想为这些端点使用新计划的[Blazor](https://docs.microsoft.com/en-us/aspnet/core/blazor/) UI选项(我们知道Blazor UI有着[巨大的需求](https://github.com/abpframework/abp/issues/394).这并不意味着我们在3.0中会完成全部工作,但我们已经开始了, 并将在3.0+版本中继续. - -#### Oracle与EF Core - -我们看到,在ABP框架之外,人们使用Oracle与EF Core有一些痛点.这是因为EF Core 3.1目前还没有稳定并且免费的Oracle提供器.我们只看到了[Devart](https://www.devart.com/)创建了一个[付费的包](https://www.nuget.org/packages/Devart.Data.Oracle.EFCore). - -[@ebicoglu](https://github.com/ebicoglu)[创建了一个gist](https://gist.github.com/ebicoglu/9f364c7eff9d87315af0178866186401)用来演示如何使用它. 我们[打算](https://github.com/abpframework/abp/issues/3983)创建一个集成包来让它更简单. - -#### API文档 - -我们正[致力于](https://github.com/abpframework/abp/issues/1184)为框架创建一个API文档,并建立一个CD管线自动地在每次发布时公开它.这会更容易地探索框架中的类. - -#### 示例应用程序:在分层/分布式系统上使用SignalR - -在分布式/微服务系统上使用SignalR可能有一些麻烦,因为服务没有连接到客户端所以不能直接从服务器调用客户端函数.一个解决这个问题的方法是使用分布式消息总线(如RabbitMQ),用来向客户端转发从服务到Web应用程序的消息. - -我们将创建一个示例应用程序和文档来阐述这样一个架构,和使用ABP框架它会多么的容易. - -虽然这个话题与ABP框架没有直接关系, 而且这个问题不是ABP应用程序独有的,我们仍然觉得创建这样的指南对开发者是很有用的. - -#### 还有... - -我们会花更多的时间来写更多的文档,实现性能的提升,做更多的测试,创建更多的扩展点等等. - -### ABP商业版 2.9 & 3.0 - -#### 组织单元系统 - -并行于ABP框架的组织单元系统(如上所述),我们正在创建一个UI用来管理组织单元,将在2.9中发布. - -#### 聊天模块的Angular UI - -聊天模块(如上所述)目前只有ASP.NET Core MVC / Razor Pages UI.我们正努力为该模块创建Angular UI - -#### 新模块想法:文件管理 - -我们正在创建一个文件管理模块,用来管理(上传/下载)和在用户之间共享文件.你可将之视为一个轻量级的Google Drive :). - -#### Easy CRM Angular UI - -[Easy CRM](https://docs.abp.io/en/commercial/latest/samples/easy-crm)是我们之前发布的ABP商业版中的一个示例应用程序.在这个版本中,我们向这个应用程序中加入了更多的功能.在下一个版本中,我们会为它创建Angular UI. - -我们发现这个应用程序非常有用,因为与简单的[图书商城](https://docs.abp.io/en/commercial/latest/samples/index#book-store)相比, 它非常接近现实世界的应用程序. - -#### 还有.. - -我们正在致力于改进现有的[模块](https://commercial.abp.io/modules),[主题](https://commercial.abp.io/themes)和[工具](https://commercial.abp.io/tools), 旨在3.0版本中提供更舒适的开发者体验. - -## 路线图 - -我们经常被问到的[ABP框架](https://abp.io/)和[ABP商业版](https://commercial.abp.io/)的路线图.虽然我们已经在各种平台上回答过,但在这次发布中,我们在这些产品的文档里增加了路线图页面: - -* [ABP框架路线图](https://docs.abp.io/en/abp/latest/Road-Map) -* [ABP商业版路线图](https://docs.abp.io/en/commercial/latest/road-map) - -我也把路线图写在下面的章节中: - -### ABP框架路线图 - -你可随时在[GitHub的仓库](https://github.com/abpframework/abp/milestones)中检查里程碑规划和优先积压问题. - -虽然我们会**继续添加其它令人激动的功能**,但我们在中期将主要致力于下列内容的工作: - -* 为所有的预构建模块实现**gPRC集成**. -* 为框架和所有预构建的模块实现**Blazor UI**. -* **NET 5.0**!由于微软已经宣布了.NET 5.0将在2020年11月发布,在这之前我们会做好准备,并在微软发布后就迁移到.NET 5.0上, 我们希望能平稳过渡. - -### ABP商业版路线图 - -我们将与ABP框架同步进行, 为ABP商业版实现一些相同的内容: - -* gRPC集成 -* Blazor UI -* .NET 5.0 - -除此之外,我们将在中期致力于下列内容: - -* 一个用来创建微服务解决方案的启动模板(含有Ocelot,Redis,RabbitMQ,ElasticSearch,IdentityServer ..等等. 并且预先集成和配置好的). -* 更多的模块扩展点. -* 动态仪表板系统. -* 实时通知系统. -* SaaS的模块的认购及支付系统. -* 更多的身份认证选项. -* 新的应用模块(我们有几十个模块的想法, 将陆续与大家分享 - 上面宣布的"文件管理"就是其中之一). -* 新的主题与主题风格(包括公共/企业网站的主题). - -## 彩蛋:ABP.IO平台路线图 - -ABP框架和ABP商业版是ABP.IO平台的基本组成部分,我们还想创造一个更大的平台将.NET社区聚集在一起,利用ABP框架的统一性和标准开发模型来创建可重用模块,分享知识,彼此互相帮助. - -所以,我们有了新的*.abp.io网站的想法,我想分享给社区 - -#### market.abp.io - -一个平台用于让开发者/公司发布他们基于ABP框架的可重用的应用模块,主题, 库和工具.这个网站上会有免费/开源和商业产品. - -#### jobs.abp.io - -我们收到了太多来自其它公司的电子邮件, 想聘请其它公司的开发者来打造基于ABP.IO平台的自己的产品.我们作为[Volosoft](https://volosoft.com/),想留在产品端,而不是客户的基础工程.我们一般会把有经验的开发者和公司介绍给他们. - -我们计划创建一个网站来满足各方需要,这样你可以为你的项目找到开发者,或者找一个短期或长期的工作. - -## 跟随ABP! - -跟随社交媒体帐号,即可了解ABP.IO平台上发生的事情: - -* [@abpframework](https://twitter.com/abpframework): ABP框架官方推特帐号 -* [@abpcommercial](https://twitter.com/abpcommercial): ABP商业版官方推特帐号 diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/abp-chat-module.png b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/abp-chat-module.png deleted file mode 100644 index 5ae351ebba..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/abp-chat-module.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-default-theme.png b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-default-theme.png deleted file mode 100644 index f3c3d16581..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-default-theme.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-material-theme.png b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-material-theme.png deleted file mode 100644 index e22c3003d9..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-abp-material-theme.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-themes.gif b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-themes.gif deleted file mode 100644 index 7ce3935ef5..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/lepton-themes.gif and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/rtl-ui.png b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/rtl-ui.png deleted file mode 100644 index 34cd6581dd..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/rtl-ui.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/signalr-chat-demo.png b/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/signalr-chat-demo.png deleted file mode 100644 index c93595511a..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-05-22 v2_8_Release/signalr-chat-demo.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/Post.md deleted file mode 100644 index da13682869..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/Post.md +++ /dev/null @@ -1,294 +0,0 @@ -# ABP框架v2.9已经发布 - -**ABP框架**和**ABP商业版**2.9已经发布,这是3.0之前的最后一个版本!这篇文章将涵盖本次发布中的**新增内容**. - -## ABP框架2.9有哪些新增内容? - -你可以中[GitHub的发行说明](https://github.com/abpframework/abp/releases/tag/2.9.0)中看到所有的变更.这篇文章将只包括重要特征/变更. - -### 预编译Razor Pages - -在之前的版本, 预构建的页面(为[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index))和视图组件是在运行时编译. 现在,它们使用了预编译. 我们测量的应用程序启动时间(尤其是MVC UI)已经减少了50%以上.换句话说,它比之前的版本快**两倍**.速度变化也影响你第一次访问某一个页面时. - -这是一个v2.8和v2.9启动应用程序模板的对比结果: - -```` -### v2.8 - -2020-06-04 22:59:04.891 +08:00 [INF] Starting web host. -2020-06-04 22:59:07.662 +08:00 [INF] Now listening on: https://localhost:44391 -2020-06-04 22:59:17.315 +08:00 [INF] Request finished in 7756.6218ms 200 text/html; - -Total: 12.42s - -### v2.9 - -2020-06-04 22:59:13.720 +08:00 [INF] Starting web host. -2020-06-04 22:59:16.639 +08:00 [INF] Now listening on: https://localhost:44369 -2020-06-04 22:59:18.957 +08:00 [INF] Request finished in 1780.5461ms 200 text/html; - -Total: 5.24s -```` - -你不用做任何改动就能获得新方法带来的益处.[重写UI页/组件](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface)和之前一样也能正常工作.我们将在v3.0中继续致力于性能上的提升. - -### 组织单元系统 - -[Identity模块](https://docs.abp.io/en/abp/latest/Modules/Identity)现在有了呼声最高的功能: 组织单元! - -组织单元系统用来在应用程序中创建分层组织树.这样你可以使用该组织树来授权应用程序中的数据和功能. - -文档将很快到来...... - -### 新的Blob存储包 - -我们创建了一个新的[Blob存储包](https://www.nuget.org/packages/Volo.Abp.BlobStoring)用来存储任意二进制对象.它一般用于在应用程序中存储文件.这个包提供了一个抽象,因此任何应用程序或[模块](https://docs.abp.io/en/abp/latest/Module-Development-Basics)都能以存储提供器无关的方式来保存和获取文件. - -目前实现了两个存储提供器: - -* [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem)包, 在本地文件系统中存储对象/文件. -* [Volo.Abp.BlobStoring.Database](https://github.com/abpframework/abp/tree/dev/modules/blob-storing-database)模块, 在数据库中存储对象/文件.目前支持[Entity Framework Core](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)(因此,你可以使用[任何关系数据库](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS)和[MongoDB](https://docs.abp.io/en/abp/latest/MongoDB)). - -[Azure BLOB提供器](https://github.com/abpframework/abp/issues/4098)将会在3.0中可用. 你可请求其他的云提供器或在[GitHub库](https://github.com/abpframework/abp/issues/new)上提交你自己的贡献. - -Blob存储系统的一个好处是,它允许你创建多个容器(每个容器是一个Blob存储),并为每个容器使用不同的存储提供器. - -**示例:使用默认的容器保存和取得一个字节数组** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBlobContainer _container; - - public MyService(IBlobContainer container) - { - _container = container; - } - - public async Task FooAsync() - { - //保存一个BLOB - byte[] bytes = GetBytesFromSomeWhere(); - await _container.SaveAsync("my-unique-blob-name", bytes); - - //获取一个BLOB - bytes = await _container.GetAllBytesAsync("my-unique-blob-name"); - } -} -```` - -它可以使用`byte[]`和`Stream`对象. - -**示例:使用类型化(命名)容器来保存和获取stream** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBlobContainer _container; - - public MyService(IBlobContainer container) - { - _container = container; - } - - public async Task FooAsync() - { - //保存一个BLOB - Stream stream = GetStreamFromSomeWhere(); - await _container.SaveAsync("my-unique-blob-name", stream); - - //获取一个BLOB - stream = await _container.GetAsync("my-unique-blob-name"); - } -} -```` - -`TestContainer`只是一个用来标识容器的空类: - -````csharp -[BlobContainerName("test")] //指定容器的名字 -public class TestContainer -{ - -} -```` - -类型化(命名)容器可被配置为使用不同的存储提供器而不是默认的.在开发可复用的模块时, 始终使用类型化的容器是一个很好的做法,这样最终应用程序可以为这个容器配置提供器,而不影响其他容器. - -**示例:为`TestContainer`配置文件系统提供器** - -````csharp -Configure(options => -{ - options.Containers.Configure(configuration => - { - configuration.UseFileSystem(fileSystem => - { - fileSystem.BasePath = "C:\\MyStorageFolder"; - }); - }); -}); -```` - -查看[blob存储文档](https://docs.abp.io/en/abp/latest/Blob-Storing)以获取更多的信息. - -### Entity Framework Core的Oracle集成包 - -我们创建了一个[Oralce集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart),这样你就可以为EF Core轻松地切换到Oracle.它已经为框架和预构建的模块进行了测试. - -[查看文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle)开始使用Oracle集成包. - -### 自动判断数据库提供器 - -当你用EF Core开发一个**可复用的应用程序模块**时,你通常要将你的模块开发为**DBMS无关**的.但是,不同的DBMS有一些微小的(有时是很大的)区别.现在如何你执行基于DBMS的自定义映射,可以使用`ModelBuilder.IsUsingXXX()`扩展方法: - -````csharp -protected override void OnModelCreating(ModelBuilder modelBuilder) -{ - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - //... - if (modelBuilder.IsUsingPostgreSql()) //检查是否在使用PostgreSQL! - { - b.Property(x => x.Number).HasMaxLength(20); - } - else - { - b.Property(x => x.Number).HasMaxLength(32); - } - }); -} -```` - -除了上面这种的傻傻的例子,你可以任意配置你的映射! - -### ABP CLI:翻译命令 - -`abp translate`是一个新的命令,当你的源代码库中包含多个JSON本地化文件时, 它可用来简化翻译[本地化](https://docs.abp.io/en/abp/latest/Localization)文件, - -该命令的主要目的是**翻译ABP框架**的本地化文件(因为[abp库](https://github.com/abpframework/abp)在不同的文件中含有成千上万个本地化文件需要翻译). - -非常感谢如果你使用这个命令将框架资源翻译**为你的母语**. - -查看[文档](https://docs.abp.io/en/abp/latest/CLI#translate)来学习如何使用它.也可查看[贡献指南](https://docs.abp.io/en/abp/latest/Contribution/Index). - -### 新的虚拟文件系统浏览器模块 - -感谢[@liangshiw](https://github.com/liangshiw)创建并贡献了一个新的模块用来浏览[虚拟文件系统](https://docs.abp.io/en/abp/latest/Virtual-File-System)中的文件.它适用于MVC UI并显示所有应用程序中的虚拟文件.示例截图: - -![virtual-file-explorer-1](virtual-file-explorer-1.png) - -![virtual-file-explorer-2](virtual-file-explorer-2.png) - -[查看文档](https://docs.abp.io/en/abp/latest/Modules/Virtual-File-Explorer)学习如何使用它. - -### 示例应用程序:SignalR与分层架构 - -在分布式/分层架构中实施SignalR是具有挑战性的.我们创建了一个示例应用程序演示如何轻松地使用[SignalR集成](https://docs.abp.io/en/abp/latest/SignalR-Integration)和[分布式事件总线](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus)系统 - -查看示例解决方案的[源代码](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo). - -**一篇正在路上的文章**将深入地解释该解决方案.关注[@abpframework](https://twitter.com/abpframework)的Twitter帐号. - -![signalr-tiered-demo](signalr-tiered-demo.png) - -*一张文章中的图片,显示了该解决方案的通信图* - -### 关于gRPC - -我们创建了一个示例应用程序来说明如何在基于ABP的应用程序中创建和使用gRPC端点. - -查看GitHub上的[源码](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo). - -我们本来计划为所有预构建的应用程序模块创建gRPC端点,但我们发现ASP.NET Core gRPC集成还不够成熟,不支持一些常见的部署场景.所以推迟到了下一个版本(更多内容[查看此评论](https://github.com/abpframework/abp/issues/2882#issuecomment-633080242)).但是,在你的应用程序中使用gRPC是非常标准的. ABP框架与gRPC没有问题.看一下[示例应用程序](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo). - -### 其它 - -* [时区系统](https://github.com/abpframework/abp/pull/3933)为应用程序支持不同的时区. -* 在IIS上支持[虚拟路径部署](https://github.com/abpframework/abp/issues/4089). -* 为Angular UI支持RTL. - -其它更新请查看[GitHub发行说明](https://github.com/abpframework/abp/releases/tag/2.9.0). - -## ABP商业版2.9有哪些新增内容 - -与往常一样, 除了ABP框架所有这些功能以外,ABP商业版在本次发布还有一些额外的功能.本节介绍[ABP商业版](https://commercial.abp.io/)在2.9版本中的亮点. - -### 组织单元管理UI - -我们为组织单元创建了UI,管理ABP商业版[Identity模块](https://commercial.abp.io/modules/Volo.Identity.Pro)的成员和角色: - -![organization-units](organization-units.png) - -OU管理适用于MVC(Razor Pages)和Angular用户界面. - -### 聊天模块Angular UI - -我们在前一个版本介绍了新的[聊天模块](https://commercial.abp.io/modules/Volo.Chat), 当时它只有ASP.NET Core MVC / Razor Pages UI. 现在它也包含了一个Angular UI选项. - -![abp-chat-module](abp-chat-module.png) - -*聊天模块的截图 - 两个用户互相发消息* - -### Easy CRM Angular UI - -Easy CRM是建立在ABP商业版上的一个示例应用程序, 用来为ABP商业版客户提供一个相对复杂的应用程序.在2.7版本中,我们已经发布了MVC / Razor Pages UI. 这次2.9版中, 我们为Easy CRM应用程序发布了Angular UI. - -![easy-crm](easy-crm.png) - -*Easy CRM应用程序中"订单详细"的截图.* - -查看[Easy CRM文档](https://docs.abp.io/en/commercial/latest/samples/easy-crm)学习如何下载并运行它. - -### ABP Suite模块代码生成 - -[ABP Suite](https://commercial.abp.io/tools/suite)是一个工具,主要功能是用来为一个实体[生成代码](https://docs.abp.io/en/commercial/latest/abp-suite/generating-crud-page), 从数据库到UI层具有完整的CRUD功能. - -![suite](suite.png) - -*ABP Suite的截图: 定义新实体的属性并且为你生成应用程序代码!* - -在本次发布之前它只工作于[应用程序模板](https://docs.abp.io/en/commercial/latest/startup-templates/application/index).现在,它支持为[模块项目](https://docs.abp.io/en/commercial/latest/startup-templates/module/index)生成代码.利用代码生成的威力来创建可复用应用程序模块是很棒的一个做法. - -除了这个主要功能,我们在这个版本中向ABP Suite添加了许多细微的改进. - ->注意:模块模板代码生成目前处于测试阶段.如果你发现任何bug,请告知我们. - -### Lepton主题 - -[Lepton主题](https://commercial.abp.io/themes)是我们为ABP商业版开发的一个商业主题. - -* 与Bootstrap 100%兼容 - 让你不写主题特定的HTML! -* 提供不同类型的风格 - 看一下下图中的Material风格. -* 提供不同类型的布局(侧/顶部菜单,流式/盒式布局...). -* 轻量化,响应式和现代化. -* 还有...它是可升级的,没有成本!你只需更新NuGet / NPM包来获得新的功能. - -我们创建了它的专属网站:[http://leptontheme.com/](http://leptontheme.com/) - -在这里你可以查看所有的组件, 无需单独的应用程序. - -![lepton-theme](lepton-theme.png) - -这个网站目前正处于一个非常早期的阶段.我们将创建文档和和改进网站, 来为你的开发提供参考和探索主题的功能. - -### 即将推出:文件管理模块 - -基于新的blob存储系统(上面介绍的),我们已经开始构建一个文件管理模块用来管理(浏览/上传/下载)你应用程序中分层文件系统并在用户与客户之间分享文件. - -我们计划在ABP商业版v3.0中发行最初版本,并继续进行后续版本的改进. - -## 关于下一个版本:3.0 - -我们在[v2.8](https://blog.abp.io/abp/ABP-v2.8.0-Releases-%26-Road-Map)和v2.9中增加了许多新的功能.在下一个版本中,我们将完全专注于**文档,性能优化**和其它改进,如bug修复. - -长期以来,我们每2周发布一个新功能版本.我们在v3.0以后继续这种方式.但是,v3.0是一个例外,开发周期大概为4周.**v3.0的计划发布日期是2020年7月1日**. - -## 彩蛋:文章! - -除了开发我们的产品,我们的团队都在不断地撰写各种主题的文章/教程.你可以看一下最新的文章: - -* [ASP.NET Core 3.1使用Pub/Sub实现WebHook](https://volosoft.com/blog/ASP.NET-CORE-3.1-Webhook-Implementation-Using-Pub-Sub) -* [ASP.NET Core使用Azure Key Vault](https://volosoft.com/blog/Using-Azure-Key-Vault-with-ASP.NET-Core) \ No newline at end of file diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/abp-chat-module.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/abp-chat-module.png deleted file mode 100644 index 5ae351ebba..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/abp-chat-module.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/easy-crm.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/easy-crm.png deleted file mode 100644 index 07d7a2b54d..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/easy-crm.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/lepton-theme.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/lepton-theme.png deleted file mode 100644 index f6a85115e0..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/lepton-theme.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/organization-units.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/organization-units.png deleted file mode 100644 index 2a6ad72ffb..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/organization-units.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/signalr-tiered-demo.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/signalr-tiered-demo.png deleted file mode 100644 index c4ca5e2b14..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/signalr-tiered-demo.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/suite.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/suite.png deleted file mode 100644 index 7e08cb3ef3..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/suite.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-1.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-1.png deleted file mode 100644 index 0db07459ca..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-1.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-2.png b/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-2.png deleted file mode 100644 index 5d4d85755d..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-06-05 v2_9_Release/virtual-file-explorer-2.png and /dev/null differ diff --git a/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/Post.md deleted file mode 100644 index 1175875d16..0000000000 --- a/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/Post.md +++ /dev/null @@ -1,177 +0,0 @@ -# ABP框架 v3.0 已发布 - -我们很高兴地宣布,**ABP框架**和**ABP商业版**3.0版已经发布.与常规的2周发布一个版本不同的是, 这个版本用了4周的时间.关闭了**119个[issue](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3A3.0)**,合并了**89个[pull request](https://github.com/abpframework/abp/pulls?q=is%3Aopen+is%3Apr+milestone%3A3.0)** 和主框架[仓库](https://github.com/abpframework/abp)中的**798次提交**. - -由于这是一个**主要版本**,它也包括了一些**重大更改**.不要害怕,这些变化都容易对应,并且下面会详细解释. - -> 详细更改日志请查看[GitHub的发布说明](https://github.com/abpframework/abp/releases/tag/3.0.0). - -## ABP框架3.0有哪些新增内容? - -这篇文章将只包括重要功能/更改.你可在[GitHub的发布说明](https://github.com/abpframework/abp/releases/tag/3.0.0)上看到所有的更改. - -### Angular 10! - -Angular 10刚刚[发布](https://blog.angular.io/version-10-of-angular-now-available-78960babd41),我们就立即迁移了[启动模板](https://docs.abp.io/en/abp/latest/Startup-Templates/Application)到Angular 10!所以,当你以Angular UI[创建一个新的解决方案](https://abp.io/get-started)时, 你将获得新Angular的优点. - -我们为想要迁移到Angular 10的老版本项目准备了一份[迁移指南](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md). - -### Oracle集成包 - -之前我们为使用Devart库, 基于EF Core for Oracle的应用程序创建了[一个集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart), 因为官方的Oracle EF Core包还未支持EF Core 3.1. 现在它以[beta发布](https://www.nuget.org/packages/Oracle.EntityFrameworkCore/3.19.0-beta1).虽然是beta版,但我们已经创建了它的[集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle),这样你就可以在你的应用程序中使用它. - -更多细节请查看[文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle). - -### Azure BLOB存储提供器 - -在上一个版本中我们创建了一个[Blob存储系统](https://docs.abp.io/en/abp/latest/Blob-Storing), 包含一个文件系统和数据库存储提供器. 本次发布我们引入了Azure BLOB存储提供器.请查看[文档](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure). - -### 分布式缓存批量操作和新的Redis缓存包 - -ASP.NET Core的[标准IDistributeCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)接口不包含**批量操作**,如使用单独方法/服务器调用设置多个项目. ABP框架引入了新的方法可用于在ABP的`IDistributedCache`接口中进行批量操作: - -* GetManyAsync / GetMany -* SetManyAsync / SetMany - -然后,我们需要为Redis缓存实现这些新方法并且[必须创建](https://github.com/abpframework/abp/issues/4483)一个Redis集成包用于扩展微软的实现. - -这些方法也被ABP框架所使用, 用来缓存某个用户/角色/租户的设置,功能和权限,并且带来了**显著的性能提升**. - -更多细节请查看[缓存文档](https://docs.abp.io/en/abp/latest/Caching). - -### 为虚拟文件系统支持嵌入文件清单 - -虚拟文件系统现在支持在项目中使用`GenerateEmbeddedFilesManifest`来添加编译程序集中嵌入资源的**真实的文件/目录结构**.所以,你现在可以访问文件,而没有任何文件名的限制(以前,目录名称中的一些特殊字符,比如`.`在某些情况会有问题) - -查看[文档](https://docs.abp.io/en/abp/latest/Virtual-File-System)学习如何利用新系统的优点. - -### 新示例 - -基于社区的需求,我们准备了两个新的示例应用程序: - -* [StoredProcedureDemo](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo)演示了如何在自定义仓储库中调用存储过程,视图和函数. -* [OrganizationUnitSample](https://github.com/abpframework/abp-samples/tree/master/OrganizationUnitSample)演示了如何为你的实体使用[Identity模块](https://docs.abp.io/en/abp/latest/Modules/Identity)的组织单元系统. - -### DynamicStringLength 和 DynamicMaxLength 属性 - -当类被用作模型或[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)时, 标准的`StringLength`和`MaxLength`数据注解属性用来验证类中的属性是很有用的. 但是, 就像任何其它的属性一样, 长度的值应该是一个**编译时**已知的字面值(常量). - -**示例: 使用`StringLength`** - -````csharp -public class CreateBookDto -{ - public const int MaxNameLength = 128; //常量! - - [StringLength(MaxNameLength)] - public string Name { get; set; } -} -```` - -ABP框架现在有`DynamicStringLength` 和 `DynamicMaxLength`属性允许在**运行时**确定长度. - -**示例: 使用`DynamicStringLength`** - -````csharp -public class CreateBookDto -{ - public static int MaxNameLength { get; set; } = 128; - - [DynamicStringLength(typeof(CreateBookDto), nameof(MaxNameLength))] - public string Name { get; set; } -} -```` - -`DynamicStringLength`通过类的**类型**和这个类的一个静态属性的**名字**来读取最大长度(也有一个最小长度的选项, 就像`StringLength`). - -这样就允许你从配置中读取最大值并且在应用程序启动时设置(一般在[模块](https://docs.abp.io/en/abp/latest/Module-Development-Basics)的`PreConfigureServices`方法) - -````csharp -CreateBookDto.MaxNameLength = 200; -```` - -[预建应用程序模块](https://docs.abp.io/en/abp/latest/Modules/Index)使用了这个功能,所以你现在可以覆盖这些模块中定义的属性的最大长度了. - -### 自动分布式事件 - -ABP可以为所有的实体在创建,更新和删除时**自动发布分布式事件**. 这很有用因为在分布式系统中你通常会对这些基础事件感兴趣. - -这个功能在v3.0中**已成熟并[文档化了](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus#pre-defined-events)**. 你可以轻松地配置一部分或全部要发布事件的实体. - -### IAsyncQueryableExecuter - -使用LINQ扩展方法,当你在查询时需要调用`ToListAsync()`,`FirstOrDefaultAsync()`... 不幸的是,这些方法并不是**标准的**LINQ扩展方法.它们定义在[Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)包中(或在[MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver/),如果你使用MongoDB的话). - -所以,如果你想使用异步扩展方法, 你需要依赖这个包.这样做破坏了分层,并且使你的应用程序或领域层依赖于EF Core / MongoDB的包. - -`IAsyncQueryableExecuter`是ABP框架定义的服务, 用来**不依赖特定提供器地执行异步查询**(EF Core / MongoDB包). - -查看[文档](https://docs.abp.io/en/abp/latest/Repositories#iqueryable-async-operations)来获取细节和了解我们的建议. - -### API 文档 - -现在我们为每次发布ABP框架和模块都公开[API文档](https://docs.abp.io/api-docs/abp/3.0/api/index.html).所以,你可以更容易地查看ABP框架的类.点击[文档](https://docs.abp.io/en/abp/latest/)导航菜单中的**API文档**的链接. - -### 包列表 - -我们[创建了一个页面](http://abp.io/packages)列出所有ABP相关的官方NuGet和NPM包. - -### 其他 - -* 为Identity Server实现了前端通道的服务端客户[登出](https://identityserver4.readthedocs.io/en/latest/topics/signout.html) -* `abp.currentUser`(Angular UI中的`CurrentUser`服务)现在有一个包含当前用户的角色名的`roles`数组. -* 升级所有的Nu​​Get和NPM软件包的依赖. -* 引入了`ReadOnlyAppService`基类(只包含get操作), 做为`CrudAppService`基类(其中有get,create,update和delete操作)的补充. - -其他更新请查看[GitHub发行说明](https://github.com/abpframework/abp/releases/tag/3.0.0). - -## ABP商业版3.0有哪些新增内容? - -除了所有ABP框架的功能以外,ABP商业版同样在本次发布中有一些额外的功能.本节介绍3.0版本中[ABP商业版](https://commercial.abp.io/)的亮点. - -### 新的文件管理模块 - -我们创建了一个用于在应用程序中存储和管理文件的新模块.这个新模块基于[BLOB存储系统](https://docs.abp.io/en/abp/latest/Blob-Storing),因此它可以使用不同的存储提供器来存储文件内容. - -**示例截图** - -![file-management-ui](file-management-ui.png) - -您可以上传,下载和以分层文件夹结构组织文件.它也兼容多租户,你可以为租户限制总文件大小.在未来的版本中,我们将致力于“共享”系统, 用于以一个更可控的方式在用户之间或通过公开链接与你的客户共享文件. - -> 文件管理模块是目前仅适用于MVC / Razor Pages UI.我们正工作于Angular UI,它会在未来的版本中发布. - -## 重大更改 - -由于这是一个主要版本,我们重新设计了一些API,并引入了一些"容易解决"的重大更改. - -### ABP框架 - -* 更改了一些预建应用程序模块的**常量**为静态属性, 这样你就可在你的代码中对它们修改. 如果你在属性中使用了这些常量,那么请使用上面介绍的`DynamicStringLength`. -* 更改`ConcurrencyStamp`最大长度为40. 你需要在更新ABP框架后**添加一个数据库迁移**并且更新数据库. -* NPM软件包的依赖关系使用`~`代替`^`,为了更加稳定. - -### ABP商业版 - -* 更改了应用程序标识的文件名.此前,它为每个主题使用了单独的标识文件,如`theme1.png`,`theme1-reverse.png`,`theme2.png`,`theme2-reverse.png`(...`6`).现在,我们只有两个标识文件:`logo-light.png`和`logo-dark.png`.所以,重命名MVC UI的`wwwroot/images/logo/`和Angular UI的`/src/assets/images/logo/`下的标识. -* 我们也增加了ABP商业版的[API文档](https://docs.abp.io/api-docs/commercial/3.0/api/index.html). - -> **此外,Angular UI请查看[迁移指南](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md)**. - -## 已知问题 - -* 3.0.0版本有一个分层架构的问题.查看[这个issue](https://github.com/abpframework/abp/pull/4564)为你的应用程序解决它,直到我们发布V3.0.1. - -## 关于未来的版本 - -我们将继续在每两个星期发布一个新的小功能版本.所以,下一个预计发布日期是**2020年7月16日**的**3.1**版本. - -在接下来的几个版本中,我们将专注于**Blazor UI**,如[路线图](https://docs.abp.io/en/abp/latest/Road-Map)中承诺的.我们将继续改进文档,创建示例,添加其他新功能和改善.关注[ABP框架Twitter账号](https://twitter.com/abpframework)获取最新消息... - -## 彩蛋: 文章! - -除了开发我们的产品,我们的团队一直在不断书写各种主题的文章/教程.你可能感兴趣的最新文章: - -* [Angular 10有哪些新增内容?](https://volosoft.com/blog/what-is-new-in-angular-10) -* [在分布式架构中使用ABP, SignalR和RabbitMQ构建实时消息系统](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) -* [如何在Angular模板中使用Attribute Directives来避免重复](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) diff --git a/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/file-management-ui.png b/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/file-management-ui.png deleted file mode 100644 index 3dd42f950d..0000000000 Binary files a/docs/zh-Hans/Blog-Posts/2020-07-01 v3_0_Release/file-management-ui.png and /dev/null differ diff --git a/docs/zh-Hans/CLI-New-Command-Samples.md b/docs/zh-Hans/CLI-New-Command-Samples.md deleted file mode 100644 index dd537682c1..0000000000 --- a/docs/zh-Hans/CLI-New-Command-Samples.md +++ /dev/null @@ -1,248 +0,0 @@ -# ABP CLI - 新解决方案命令示例 - -`abp new`命令基于abp模板创建abp解决方案或其他组件. [ABP CLI](CLI.md)有一些参数可以用于创建新的ABP解决方案. 在本文档中, 我们将向你展示一些创建新的解决方案的命令示例. 所有的项目名称都是`Acme.BookStore`. 目前, 唯一可用的移动端项目是`React Native`移动端应用程序. 可用的数据库提供程序有`Entity Framework Core`和`MongoDB`. 所有命令都以`abp new`开头. - -## Angular - -以下命令用于创建Angular UI项目: - -* 在新文件夹中创建项目, **Entity Framework Core**, 非移动端应用程序: - - ````bash - abp new Acme.BookStore -u angular --mobile none --database-provider ef -csf - ```` - -* 在新文件夹中创建项目, **Entity Framework Core**, 默认应用程序模板, **拆分Auth Server**: - - ```bash - abp new Acme.BookStore -t app -u angular -m none --separate-auth-server --database-provider ef -csf - ``` - -* 在新文件夹中创建项目, **Entity Framework Core**, **自定义连接字符串**: - - ```bash - abp new Acme.BookStore -u angular -csf --connection-string Server=localhost;Database=MyDatabase;Trusted_Connection=True - ``` - -* 在`C:\MyProjects\Acme.BookStore`中创建解决方案, **MongoDB**, 默认应用程序模板, 包含移动端项目: - - ```bash - abp new Acme.BookStore -u angular --database-provider mongodb --output-folder C:\MyProjects\Acme.BookStore - ``` - -* 在新文件夹中创建项目, **MongoDB**, 默认应用程序模板, 不创建移动端应用程序, **拆分Auth Server**: - - ```bash - abp new Acme.BookStore -t app -u angular -m none --separate-auth-server --database-provider mongodb -csf - ``` - -## MVC - -以下命令用于创建MVC UI项目: - -* 在新文件夹中创建项目, **Entity Framework Core**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef -csf - ``` - -* 在新文件夹中创建项目, **Entity Framework Core**, **分层结构** (*Web和HTTP API层是分开的*), 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -u mvc --mobile none --tiered --database-provider ef -csf - ``` - -* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider mongodb -csf - ``` - -* 在新文件夹中创建项目, **MongoDB**, **分层结构**: - - ```bash - abp new Acme.BookStore -u mvc --tiered --database-provider mongodb -csf - ``` - - -## Blazor - -以下命令用于创建Blazor项目: - -* **Entity Framework Core**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -t app -u blazor --mobile none - ``` - -* **Entity Framework Core**, **拆分Auth Server**, 包含移动端应用程序: - - ```bash - abp new Acme.BookStore -u blazor --separate-auth-server - ``` - -* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf - ``` - -## Blazor Server - -以下命令用于创建Blazor项目: - -* **Entity Framework Core**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -t app -u blazor-server --mobile none - ``` - -* **Entity Framework Core**, **拆分Auth Server**, **拆分API Host**, 包含移动端应用程序: - - ```bash - abp new Acme.BookStore -u blazor-server --tiered - ``` - -* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf - ``` - -## 无UI - -在默认应用程序模板中, 始终有一个前端项目. 在这个选项中没有前端项目. 它有一个`HttpApi.Host`项目为你的HTTP WebAPI提供服务. 这个选项适合在你想创建一个WebAPI服务时使用. - -* 在新文件夹中创建项目, **Entity Framework Core**, 拆分Auth Server: - - ```bash - abp new Acme.BookStore -u none --separate-auth-server -csf - ``` -* **MongoDB**, 不创建移动端应用程序: - - ```bash - abp new Acme.BookStore -u none --mobile none --database-provider mongodb - ``` - - - -## 控制台应用程序 - -这是一个基于.NET控制台应用程序的模板, 集成了ABP模块架构. 要创建控制台应用程序, 请使用以下命令: - -* 项目由以下文件组成: `Acme.BookStore.csproj`, `appsettings.json`, `BookStoreHostedService.cs`, `BookStoreModule.cs`, `HelloWorldService.cs` 和 `Program.cs`. - - ```bash - abp new Acme.BookStore -t console -csf - ``` - -## 模块 - -模块是主项目使用的可重用子应用程序. 如果你正在构建微服务解决方案, 使用ABP模块是最佳方案. 由于模块不是最终的应用程序, 每个模块都有前端UI项目和数据库提供程序. 模块模板带有MVC UI, 可以在没有最终解决方案的情况下进行开发. 但是, 如果要在最终解决方案下开发模块, 可以添加`--no-ui`参数来去除MVC UI项目. - -* 包含前端: `MVC`, `Angular`, `Blazor`. 包含数据库提供程序: `Entity Framework Core`, `MongoDB`. 包含MVC启动项目. - - ```bash - abp new Acme.IssueManagement -t module - ``` -* 与上面相同, 但不包括MVC启动项目. - - ```bash - abp new Acme.IssueManagement -t module --no-ui - ``` - -* 创建模块并将其添加到解决方案中 - - ```bash - abp new Acme.IssueManagement -t module --add-to-solution-file - ``` - -## 从特定版本创建解决方案 - -创建解决方案时, 它总是使用最新版本创建. 要从旧版本创建项目, 可以使用`--version`参数. - -* 使用v3.3.0版本创建解决方案, 包含Angular UI和Entity Framework Core. - - ```bash - abp new Acme.BookStore -t app -u angular -m none --database-provider ef -csf --version 3.3.0 - ``` - -要获取ABP版本列表, 请查看以下链接: https://www.nuget.org/packages/Volo.Abp.Core/ - -## 从自定义模板创建 - -ABP CLI使用默认的[应用程序模板](https://github.com/abpframework/abp/tree/dev/templates/app)创建项目. 如果要从自定义模板创建新的解决方案, 可以使用参数`--template-source`. - -* 在`c:\MyProjects\templates\app`目录中使用模板, MVC UI, Entity Framework Core, 不创建移动端应用程序. - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source "c:\MyProjects\templates\app" - ``` - -* 除了此命令从URL `https://myabp.com/app-template.zip` 检索模板之外, 与上一个命令相同. - - ```bash - abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source https://myabp.com/app-template.zip - ``` - -## 创建预览版本 - -ABP CLI始终使用最新版本. 要从预览(RC)版本创建解决方案, 请添加`--preview`参数. - -* 在新文件夹中创建项目, Blazor UI, Entity Framework Core, 不创建移动端应用程序, **使用最新版本**: - - ```bash - abp new Acme.BookStore -t app -u blazor --mobile none -csf --preview - ``` - -## 选择数据库管理系统 - -默认的数据库管理系统是 `Entity Framework Core` / ` SQL Server`. 你可以通过使用`--database-management-system`参数选择DBMS. [可用的值](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/DatabaseManagementSystem.cs) 包括 `SqlServer`, `MySQL`, `SQLite`, `Oracle`, `Oracle-Devart`, `PostgreSQL`. 默认值是 `SqlServer`. - -* 在新文件夹中创建项目, Angular UI, **PostgreSQL** 数据库: - - ```bash - abp new Acme.BookStore -u angular --database-management-system PostgreSQL -csf - ``` - -## 使用静态HTTP端口 - -ABP CLI始终为项目分配随机端口. 如果需要保留默认端口并且创建解决方案始终使用相同的HTTP端口, 请添加参数`--no-random-port`. - -* 在新文件夹中创建项目, MVC UI, Entity Framework Core, **静态端口**: - - ```bash - abp new Acme.BookStore --no-random-port -csf - ``` - -## 引用本地ABP框架 - -在ABP解决方案中, 默认情况下从NuGet引用ABP库. 有时, 你需要在本地将ABP库引用到你的解决方案中. 这利于调试框架本身. 本地ABP框架的根目录必须有`Volo.Abp.sln`文件. 你可以将以下目录的内容复制到你的文件系统中 - -* MVC UI, Entity Framework Core, **引用本地的ABP库**: - -本地路径必须是ABP存储库的根目录. -如果`C:\source\abp\framework\Volo.Abp.sln`是你的框架解决方案的路径, 那么你必须设置`--abp-path`参数值为`C:\source\abp`. - - ```bash - abp new Acme.BookStore --local-framework-ref --abp-path C:\source\abp - ``` - -**输出**: - -如下所示, 引用本地ABP框架库项目. - -```xml - - - - - - - - -``` - -## 另请参阅 - -* [ABP CLI文档](CLI.md) diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md deleted file mode 100644 index ef412b7348..0000000000 --- a/docs/zh-Hans/CLI.md +++ /dev/null @@ -1,474 +0,0 @@ -# ABP CLI - -ABP CLI (命令行接口) 是一个命令行工具,用来执行基于ABP解决方案的一些常见操作. - -## Installation - -ABP CLI 是一个 [dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). 使用命令行窗口安装: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -更新最新版本: - -````bash -dotnet tool update -g Volo.Abp.Cli -```` - -## 全局选项 - -虽然每个命令可能都有一组选项,但有些全局选项可以与任何命令一起使用: - -* `--skip-cli-version-check`: 跳过检查最新版本的ABP CLI. 如果没有指定,它会检查最新版本,如果检查到ABP CLI的新版本,会显示一条警告消息. - -## Commands - -这里是所有可用的命令列表: - -* **`help`**: 展示ABP CLI的用法帮助信息. -* **`cli`**: 更新或删除ABP CLI. -* **`new`**:生成基于ABP的[启动模板](Startup-Templates/Index.md). -* **`update`**:自动更新的ABP解决方案ABP相关的NuGet和NPM包. -* **`clean`**: 删除当前目录下所有的 `BIN` 和 `OBJ` 子目录. -* **`add-package`**: 添加ABP包到项目. -* **`add-module`**: 添加[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index)到解决方案. -* **`generate-proxy`**: 生成客户端代理以使用HTTP API端点. -* **`remove-proxy`**: 移除以前生成的客户端代理. -* **`switch-to-preview`**: 切换到ABP框架的最新预览版本。 -* **`switch-to-nightly`**: 切换解决方案所有ABP相关包为[夜间构建](Nightly-Builds.md)版本. -* **`switch-to-stable`**: 切换解决方案所有ABP相关包为最新的稳定版本. -* **`translate`**: 当源代码控制存储库中有多个JSON[本地化](Localization.md文件时,可简化翻译本地化文件的过程. -* **`login`**: 使用你在[abp.io](https://abp.io/)的用户名和密码在你的计算机上认证. -* **`login-info`**: 展示当前登录用户信息. -* **`logout`**: 在你的计算机注销认证. -* **`bundle`**: 为 ABP Blazor 和 MAUI Blazor 项目生成引用的脚本和样式. -* **`install-libs`**: 为 MVC / Razor Pages 和 Blazor Server UI 类型安装NPM包. -* **`clear-download-cache`** 删除下载的模版缓存. - -### help - -展示ABP CLI的基本用法: - -用法: - -````bash -abp help [command-name] -```` - -示例: - -````bash -abp help # Shows a general help. -abp help new # Shows help about the "new" command. -```` - -### cli - -更新或删除ABP CLI - -用法: - -````bash -abp cli [command-name] -```` - -示例: - -````bash -abp cli update -abp cli update --preview -abp cli update --version 5.0.0 -abp cli remove -```` - -### new - -生成基于ABP[启动模板](Startup-Templates/Index.md)的新解决方案. - -用法: - -````bash -abp new <解决方案名称> [options] -```` - -示例: - -````bash -abp new Acme.BookStore -```` - -* Acme.BookStore是解决方案的名称. -* 常见的命名方式类似于 *YourCompany.YourProject*. 不过你可以使用自己喜欢的方式,如 *YourProject* (单级命名空间) 或 *YourCompany.YourProduct.YourModule* (三级命名空间). - -参阅[ABP CLI 创建新解决方案示例](CLI-New-Command-Samples.md)查看更多示例. - -#### Options - -* `--template` 或者 `-t`: 指定模板. 默认的模板是 `app`,会生成web项目.可用的模板有: - * `app` (default): [应用程序模板](Startup-Templates/Application.md). 其他选项: - * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: - * `mvc`: ASP.NET Core MVC.此模板的其他选项: - * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景. - * `angular`: Angular. 这个模板还有一些额外的选项: - * `--separate-auth-server`: 将Auth Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. - * `blazor`: Blazor. 这个模板还有一些额外的选项: - * `--separate-auth-server`: 将Auth Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. - * `none`: 无UI. 这个模板还有一些额外的选项: - * `--separate-auth-server`: 将Auth Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. - * `--mobile` 或者 `-m`: 指定移动应用程序框架. 如果未指定,则不会创建任何移动应用程序,其他选项: - * `none`: 不包含移动应用程序. - * `react-native`: React Native. - * `--database-provider` 或者 `-d`: 指定数据库提供程序.默认是 `ef`.其他选项: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `module`: [Module template](Startup-Templates/Module.md). 其他选项: - * `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI). - * **`console`**: [Console template](Startup-Templates/Console.md). - * **`maui`**: [Maui template](Startup-Templates/MAUI.md). - * **`app-nolayers`**: 应用程序单层模板 - * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: - * `mvc`: ASP.NET Core MVC. - * `angular`: Angular. - * `blazor`: Blazor UI. - * `blazor-server`: Blazor Server. - * `none`: 不包含UI. - * `--database-provider` 或 `-d`: 或者 `-d`: 指定数据库提供程序.默认是 `ef`.其他选项: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. -* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录. -* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,你会希望使用最新的版本. -* `--preview`: 使用最新的预览版本. -* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D:\local-templat` 或 `https://.../my-template-file.zip`). -* `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. -* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后). -* `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用. -* `--skip-cache` or `-sc`: 从服务器下载最新的模版并更新本地模版缓存. - -### update - -更新所有ABP相关的包可能会很繁琐,框架和模块都有很多包. 此命令自动将解决方案或项目中所有ABP相关的包更新到最新版本. - -用法: - -````bash -abp update [options] -```` - -* 如果你的文件夹中有.sln文件,运行命令会将解决方案中所有项目ABP相关的包更新到最新版本. -* 如果你的文件夹中有.csproj文件,运行命令会将项目中所有ABP相关的包更新到最新版本. - -#### Options - -* `--npm`: 仅更新NPM包 -* `--nuget`: 仅更新的NuGet包 -* `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 -* `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. -* `--check-all`: 分别检查每个包的新版本. 默认是 `false`. -* `--version` or `-v`: 指定用于升级的版本. 如果没有指定,则使用最新版本. - -### clean - -删除当前目录下所有的 `BIN` 和 `OBJ` 子目录. - -用法: - -````bash -abp clean -```` - -### add-package - -通过以下方式将ABP包添加到项目中 - -* 添加nuget包做为项目的依赖项目. -* 添加 `[DependsOn(...)]` attribute到项目的模块类 (请参阅 [模块开发文档](Module-Development-Basics.md)). - -> 需要注意的是添加的模块可能需要额外的配置,通常会在包的文档中指出. - -用法: - -````bash -abp add-package <包名> [options] -```` - -示例: - -```` -abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic -```` - -* 示例中将`Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic`包添加到项目中. - -#### Options - -* `--project` 或 `-p`: 指定项目 (.csproj) 路径. 如果未指定,Cli会尝试在当前目录查找.csproj文件. -* `--with-source-code`: 下载包的源码到你的解决方案文件夹,而不是NuGet/NPM软件包. -* `--add-to-solution-file`: 添加下载/创建的包添加到解决方案文件中,你在IDE中打开解决方案时也会看到包的项目. (仅当 `--with-source-code` 为 `True` 时可用.) - -> 当前只有基本主题包([MVC](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Basic-Theme) 和 [Blazor](https://docs.abp.io/zh-Hans/abp/latest/UI/Blazor/Basic-Theme)) 可以下载. -> - Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic -> - Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme -> - Volo.Abp.AspNetCore.Components.Web.BasicTheme -> - Volo.Abp.AspNetCore.Components.Server.BasicTheme - -### add-module - -通过查找模块的所有包,查找解决方案中的相关项目,并将每个包添加到解决方案中的相应项目,从而将[多包应用程序模块](Modules/Index)添加到解决方案中. - -> 由于分层,不同的数据库提供程序选项或其他原因,业务模块通常由多个包组成. 使用`add-module`命令可以大大简化向模块添加模块的过程. 但是每个模块可能需要一些其他配置,这些配置通常在相关模块的文档中指出. - -用法: - -````bash -abp add-module <模块名称> [options] -```` - -示例: - -```bash -abp add-module Volo.Blogging -``` - -* 示例中将Volo.Blogging模块添加到解决方案中. - -#### Options - -* `--solution` 或 `-s`: 指定解决方案 (.sln) 路径. 如果未指定,CLI会尝试在当前目录中寻找.sln文件. -* `--skip-db-migrations`: 对于EF Core 数据库提供程序,它会自动添加新代码的第一次迁移 (`Add-Migration`) 并且在需要时更新数据库 (`Update-Database`). 指定此选项可跳过此操作. -* `-sp` 或 `--startup-project`: 启动项目的项目文件夹的相对路径. 默认值是当前文件夹. -* `--with-source-code`: 添加模块的源代码,而不是NuGet/NPM软件包. -* `--add-to-solution-file`: 添加下载/创建的模块添加到解决方案文件中,你在IDE中打开解决方案时也会看到模块的项目. (仅当 `--with-source-code` 为 `True` 时可用.) - -### generate-proxy - -为你的HTTP API生成Angular, C# 或 JavaScript服务代理,简化从客户端使用服务的成本. 在运行此命令之前,你的host必须启动正在运行. - -用法: - -````bash -abp generate-proxy -t [options] -```` - -示例: - -````bash -abp generate-proxy -t ng -abp generate-proxy -t js -url https://localhost:44302/ -abp generate-proxy -t csharp -url https://localhost:44302/ -```` - -#### Options - -* `--type` 或 `-t`: 客户端类型的名称. 可用的客户端有: - * `csharp`: C#, 工作在 `*.HttpApi.Client` 项目目录. 此客户端有一些可选选项: - * `--without-contracts`: 取消生成应用程序服务接口,类,枚举和DTO. - * `--folder`: 放置生成的 CSharp 代码的文件夹名称. 默认值: `ClientProxies`. - * `ng`: Angular. 此客户端有一些可选选项: - * `--api-name` 或 `-a`: 在 `/src/environments/environment.ts` 中定义的API端点名称。. 默认值: `default`. - * `--source` 或 `-s`: 指定解析根名称空间和API定义URL的Angular项目名称. 默认值: `defaultProject` - * `--target` 或 `-t`: 指定放置生成的代码的Angular项目名称. 默认值: `defaultProject`. - * `--prompt` 或 `-p`: 在命令行提示符下询问选项(未指定的选项). - * `js`: JavaScript. 工作在 `*.Web` 项目目录. 此客户端有一些可选选项: - * `--output` or `-o`: 放置生成的 JavaScript 代码的文件夹名称. -* `--module` 或 `-m`: 指定要为其生成代理的后端模块的名称. 默认值: `app`. -* `--working-directory` or `-wd`: 执行目录. 用于 `csharp` 和 `js` 客户端类型. -* `--url` or `-u`: API定义的URL. 用于 `csharp` 和 `js` 客户端类型. - -> 参阅 [Angular服务代理文档](UI/Angular/Service-Proxies.md) 了解更多. - -### remove-proxy - -从Angular, CSharp 或 JavaScript应用程序中删除以前生成的代理代码. 在运行此命令之前,你的host必须启动正在运行. - -这在你之前为多个模块生成代理并且需要删除其中一个模块时特别有用. - -用法: - -````bash -abp remove-proxy -t [options] -```` - -示例: - -````bash -abp remove-proxy -t ng -abp remove-proxy -t js -m identity -o Pages/Identity/client-proxies.js -abp remove-proxy -t csharp --folder MyProxies/InnerFolder -```` - -#### Options - -* `--type` 或 `-t`: 客户端类型的名称. 可用的客户端有: - * `csharp`: C#, 工作在 `*.HttpApi.Client` 项目目录. 此客户端有一些可选选项: - * `--folder`: 放置生成的 CSharp 代码的文件夹名称. 默认值: `ClientProxies`. - * `ng`: Angular. 此客户端有一些可选选项: - * `--api-name` 或 `-a`: 在 `/src/environments/environment.ts` 中定义的API端点名称。. 默认值: `default`. - * `--source` 或 `-s`: 指定解析根名称空间和API定义URL的Angular项目名称. 默认值: `defaultProject` - * `--target` 或 `-t`: 指定放置生成的代码的Angular项目名称. 默认值: `defaultProject`. - * `--prompt` 或 `-p`: 在命令行提示符下询问选项(未指定的选项). - * `js`: JavaScript. 工作在 `*.Web` 项目目录. 此客户端有一些可选选项: - * `--output` or `-o`: 放置生成的 JavaScript 代码的文件夹名称. -* `--module` 或 `-m`: 指定要为其生成代理的后端模块的名称. 默认值: `app`. -* `--working-directory` or `-wd`: 执行目录. 用于 `csharp` 和 `js` 客户端类型. -* `--url` or `-u`: API定义的URL. 用于 `csharp` 和 `js` 客户端类型. - -> 参阅 [Angular服务代理文档](UI/Angular/Service-Proxies.md) 了解更多. - -### switch-to-preview - -你可以使用此命令将项目切换到ABP框架的最新预览版本. - -用法: - -````bash -abp switch-to-preview [options] -```` - -#### Options - -* `--solution-directory` 或 `-sd`: 指定目录. 解决方案应该在该目录或其子目录中. 如果未指定默认为当前目录. - -### switch-to-nightly - -想要切换到ABP框架的最新[每晚构建](Nightly-Builds.md)预览版可以使用此命令. - -用法: - -````bash -abp switch-to-nightly [options] -```` - -#### Options - -`--solution-directory` 或 `-sd`: 指定目录. 解决方案应该在该目录或其子目录中. 如果未指定默认为当前目录. - -### switch-to-stable - -如果你使用的是ABP框架预览包(包括每晚构建),可以使用此命令切换回最新的稳定版本. - -用法: - -````bash -abp switch-to-stable [options] -```` -#### Options - -`--solution-directory` 或 `-sd`: 指定目录. 解决方案应该在该目录或其子目录中. 如果未指定默认为当前目录. - -### translate - - -源代码控制存储库中有多个JSON[本地化](Localization.md)文件时,用于简化翻译[本地化](Localization.md)文件的过程. - -* 该命令将基于参考文化创建一个统一的json文件 -* 它搜索当前目录和所有子目录中的所有本地化"JSON"文件(递归). 然后创建一个包含所有需要翻译的条目的文件(默认情况下名为 "abp-translation.json"). -* 翻译了此文件中的条目后,你就可以使用 `--apply` 命令将更改应用于原始本地化文件. - -> 该命令的主要目的是翻译ABP框架本地化文件(因为[abp仓库](https://github.com/abpframework/abp)包括数十个要在不同目录中转换的本地化文件). - -#### 创建翻译文件 - -第一步是创建统一的翻译文件: - -````bash -abp translate -c [options] -```` - -示例: - -````bash -abp translate -c de -```` - -该命令为 `de-DE` (德语)文化创建了统一的翻译文件. - -##### 附加选项 - -* `--reference-culture` 或 `-r`: 默认值 `en`. 指定参考文化. -* `--output` 或 `-o`: 输出文件名. 默认值 `abp-translation.json`. -* `--all-values` 或 `-all`: 包括所有要翻译的键. 默认情况下,统一翻译文件仅包含目标文化的缺失文本. 如果你可能需要修改之前已经翻译的值,请指定此参数. - -#### 应用更改 - - -翻译完统一翻译文件中的条目后,你可以使用 `--apply` 参数将更改应用于原始本地化文件: - -````bash -abp translate --apply # apply all changes -abp translate -a # shortcut for --apply -```` - -然后,检查源代码控制系统上的更改,以确保它已更改了正确的文件. 如果你翻译了ABP框架资源, 请发送 "Pull Request". 提前感谢你的贡献. - -##### 附加选项 - -* `--file` 或 `-f`: 默认值: `abp-translation.json`. 翻译文件(仅在之前使用过 `--output` 选项时使用). - -### login - -CLI的一些功能需要登录到abp.io平台. 使用你的用户名登录 - -```bash -abp login # Allows you to enter your password hidden -abp login -p # Specify the password as a parameter (password is visible) -abp login --organization # If you have multiple organizations, you need set your active organization -abp login -p -o # You can enter both your password and organization in the same command -abp login --device # Use device login flow -``` - -> 当使用-p参数,请注意,因为你的密码是可见的. 它对于CI / CD自动化管道很有用. - -请注意,新的登录将终止先前的会话并创建一个新的会话. - -### login-info - -展示你的登录信息, 如 **名称** , **用户名** , **地址** 和 **组织**. - -```bash -abp login-info -``` - -### logout - -通过从计算机中删除会话令牌来注销. - -``` -abp logout -``` - -### bundle - -这个命令为ABP Blazor WebAssembly 和 MAUI Blazor 项目生成引用的脚本和样式并且更新 **index.html** 文件, 它帮助开发者轻松的管理ABP模块的依赖. 为了使 ```bundle``` 命令工作, 它的**执行目录**或者传递 ```--working-directory``` 参数的目录必须包含Blazor或MAUI Blazor项目文件(*.csproj) - -用法: - -````bash -abp bundle [options] -```` - -#### Options - -* ```--working-directory``` 或 ```-wd```: 指定当前执行目录, 这个命令在当前目录不包含项目文件时非常有用. -* ```--force``` 或 ```-f```: 在生成引用之前强制构建项目. -* ```--project-type``` 或 ```-t```: 指定项目类型. 默认类型是 `webassembly`. 可用的类型有: - * `webassembly` - * `maui-blazor` - -`bundle` command reads the `appsettings.json` file inside the Blazor and MAUI Blazor project for bundling options. For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) - -### install-libs - -为 MVC / Razor Pages 和 Blazor Server UI 类型安装NPM包, 它的 **执行目录** 或者传递的 ```--working-directory``` 目录必须包含一个项目文件(*.csproj). - -`install-libs` 命令读取 `abp.resourcemapping.js` 来管理包. 参阅[客户端包管理](UI/AspNetCore/Client-Side-Package-Management.md)了解更多细节. - -用法: - -````bash -abp install-libs [options] -```` - -#### Options - -* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. diff --git a/docs/zh-Hans/Caching.md b/docs/zh-Hans/Caching.md deleted file mode 100644 index d8f064a24e..0000000000 --- a/docs/zh-Hans/Caching.md +++ /dev/null @@ -1,274 +0,0 @@ -# 缓存 - -ABP框架扩展了 [ASP.NET Core的分布式缓存系统](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). - -## 安装 - -> 默认情况下 [启动模板](Startup-Templates/Application.md) 已经安装了这个包. 所以大部分情况下你不需要手动安装. - -[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching)是缓存系统的核心包. 可以使用 [ABP CLI](CLI.md) 的add-package命令将其安装到项目中: - -``` -abp add-package Volo.Abp.Caching -``` -你需要在包含 `csproj` 文件的文件夹中的命令行终端上运行此命令(请参阅 [其他选项](https://abp.io/package-detail/Volo.Abp.Caching) 安装). - -## 使用方式 - -### `IDistributedCache` 接口 - -ASP.NET Core 定义了 `IDistributedCache` 接口用于 get/set 缓存值. 但是会有以下问题: - -* 它适用于 **byte 数组** 而不是 .NET 对象. 因此你需要对缓存的对象进行**序列化/反序列化**. -* 它为所有的缓存项提供了 **单个 key 池** , 因此; - * 你需要注意键区分 **不同类型的对象**. - * 你需要注意**不同租户**(参见[多租户](Multi-Tenancy.md))的缓存项. - -> `IDistributedCache` 定义在 `Microsoft.Extensions.Caching.Abstractions` 包中. 这使它不仅适用于ASP.NET Core应用程序, 也可用于**任何类型的程序**. - -> `IDistributedCache` 接口的默认实现是 `MemoryDistributedCache` 它使用**内存**工作. 参见 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed) 了解如何切换到 **Redis** 或其他缓存提供程序. 此外, 如果要将Redis用作分布式缓存服务器, [Redis缓存](Redis-Cache.md) 文档. - -有关更多信息, 参见 [ASP.NET Core 分布式缓存文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed). - -### `IDistributedCache` 接口 - -ABP框架在[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching/)包定义了通用的泛型 `IDistributedCache` 接口. `TCacheItem` 是存储在缓存中的对象类型. - -`IDistributedCache` 接口解决了上述中的问题; - -* 它在内部 **序列化/反序列化** 缓存对象. 默认使用 **JSON** 序列化, 但可以替换[依赖注入](Dependency-Injection.md)系统中 `IDistributedCacheSerializer` 服务的实现来覆盖默认的处理. -* 它根据缓存中对象类型自动向缓存key添加 **缓存名称** 前缀. 默认缓存名是缓存对象类的全名(如果你的类名以`CacheItem` 结尾, 那么`CacheItem` 会被忽略,不应用到缓存名称上). 你也可以在缓存类上使用 **`CacheName` 特性** 设置缓存的名称. -* 它自动将**当前的租户id**添加到缓存键中, 以区分不同租户的缓存项 (只有在你的应用程序是[多租户](Multi-Tenancy.md)的情况下生效). 如果要在多租户应用程序中的所有租户之间共享缓存对象, 请在缓存项类上定义`IgnoreMultiTenancy`特性以禁用此选项. -* 允许为每个应用程序定义 **全局缓存键前缀** , 不同的应用程序可以在共享的分布式缓存中拥有自己的隔离池. -* 它可以在任何可能绕过缓存的情况下 **容忍错误** 的发生. 这在缓存服务器出现临时问题时非常有用. -* 它有 `GetManyAsync` 和 `SetManyAsync` 等方法, 可以显著提高**批处理**的性能. - -**示例: 在缓存中存储图书名称和价格** - -````csharp -namespace MyProject -{ - public class BookCacheItem - { - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -你可以注入 `IDistributedCache` 服务用于 get/set `BookCacheItem` 对象. - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Volo.Abp.Caching; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class BookService : ITransientDependency - { - private readonly IDistributedCache _cache; - - public BookService(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId.ToString(), //缓存键 - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } - - private Task GetBookFromDatabaseAsync(Guid bookId) - { - //TODO: 从数据库获取数据 - } - } -} -```` - -* 示例服务代码中的 `GetOrAddAsync()` 方法从缓存中获取图书项. `GetOrAddAsync`是ABP框架在 ASP.NET Core 分布式缓存方法中添增的附加方法. -* 如果没有在缓存中找到图书,它会调用工厂方法 (本示例中是 `GetBookFromDatabaseAsync`)从原始数据源中获取图书项. -* `GetOrAddAsync` 有一个可选参数 `DistributedCacheEntryOptions` , 可用于设置缓存的生命周期. - -`IDistributedCache` 与ASP.NET Core的`IDistributedCache` 接口拥有相同的方法, 你可以参考 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed). - -### `IDistributedCache` 接口 - -`IDistributedCache` 接口默认了**缓存键**是 `string` 类型 (如果你的键不是string类型需要进行手动类型转换). 但当缓存键的类型不是`string`时, 可以使用`IDistributedCache`. - -**示例: 在缓存中存储图书名称和价格** - -示例缓存项 - -````csharp -using Volo.Abp.Caching; - -namespace MyProject -{ - [CacheName("Books")] - public class BookCacheItem - { - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -* 在本例中使用`CacheName`特性给`BookCacheItem`类设置缓存名称. - -你可以注入 `IDistributedCache` 服务用于 get/set `BookCacheItem` 对象. - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Volo.Abp.Caching; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class BookService : ITransientDependency - { - private readonly IDistributedCache _cache; - - public BookService(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId, //Guid类型作为缓存键 - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } - private Task GetBookFromDatabaseAsync(Guid bookId) - { - //TODO: 从数据库获取数据 - } - } -} -```` - -* 示例服务中 `GetOrAddAsync()` 方法获取缓存的图书项. -* 我们采用了 `Guid` 做为键, 在 `_cache_GetOrAddAsync()` 方法中传入 `Guid` 类型的bookid. - -#### 复杂类型的缓存键 - -`IDistributedCache` 在内部使用键对象的 `ToString()` 方法转换类型为string. 如果你的将复杂对象做为缓存键,那么需要重写类的 `ToString` 方法. - -举例一个作为缓存键的类: - -````csharp -public class UserInOrganizationCacheKey -{ - public Guid UserId { get; set; } - - public Guid OrganizationId { get; set; } - - //构建缓存key - public override string ToString() - { - return $"{UserId}_{OrganizationId}"; - } -} -```` - -用法示例: - -````csharp -public class BookService : ITransientDependency -{ - private readonly IDistributedCache _cache; - - public BookService( - IDistributedCache cache) - { - _cache = cache; - } - - ... -} -```` - -## 配置 - -### AbpDistributedCacheOptions -`AbpDistributedCacheOptions` 是配置缓存的主要[Option类](Options.md). - -**示例:为应用程序设置缓存键前缀** - -```csharp -Configure(options => -{ - options.KeyPrefix = "MyApp1"; -}); -``` -> 在[模块类](Module-Development-Basics.md)的`ConfigureServices`方法中添加代码. - -#### 可用选项 - -* `HideErrors` (`bool`, 默认: `true`): 启用/禁用隐藏从缓存服务器写入/读取值时的错误. -* `KeyPrefix` (`string`, 默认: `null`): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容. -* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如 `AbsoluteExpiration` 和 `SlidingExpiration`). `SlidingExpiration`的默认值设置为20分钟. - -## 错误处理 - -当为你的对象设计缓存时, 通常会首先尝试从缓存中获取值. 如果在缓存中找不到该值, 则从**来源**查询对象. 它可能在**数据库**中, 或者可能需要通过HTTP调用远程服务器. - -在大多数情况下, 你希望**容忍缓存错误**; 如果缓存服务器出现错误, 也不希望取消该操作. 相反, 你可以默默地隐藏(并记录)错误并**从来源查询**. 这就是ABP框架默认的功能. - -ABP的分布式缓存 [异常处理](Exception-Handling.md), 默认记录并隐藏错误. 有一个全局修改该功能的选项(参见下面的选项内容). - -所有的`IDistributedCache` (和 `IDistributedCache`)方法都有一个可选的参数`hideErrors`, 默认值为`null`. 如果此参数设置为`null`, 则全局生效, 否则你可以选择单个方法调用时隐藏或者抛出异常. - -## 批量操作 - -ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能 - -* `SetManyAsync` 和 `SetMany` 方法可以用来向缓存中设置多个值. -* `GetManyAsync` 和 `GetMany` 方法可以用来从缓存中获取多个值. -* `GetOrAddManyAsync` 和 `GetOrAddMany` 方法可以用来从缓存中获取并添加缺少的值. -* `RefreshManyAsync` 和 `RefreshMany` 方法可以来用重置多个值的滚动过期时间. -* `RemoveManyAsync` 和 `RemoveMany` 方法可以用来从缓存中删除多个值. - -> 这些不是标准的ASP.NET Core缓存方法, 所以某些提供程序可能不支持. [ABP Redis集成包](Redis-Cache.md)实现了它们. 如果提供程序不支持,会回退到 `SetAsync` 和 `GetAsync` ... 方法(循环调用). - -## 高级主题 - -### 工作单元级别的缓存 - -分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的**事务被回滚了**, 该怎么办?在这种情况下, 缓存值是错误的. - -`IDistributedCache<..>`方法提供一个可选参数, `considerUow`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效. - -### IDistributedCacheSerializer - -`IDistributedCacheSerializer`服务用于序列化和反序列化缓存内容. 默认实现是`Utf8JsonDistributedCacheSerializer`类, 它使用`IJsonSerializer`服务将对象转换为[JSON](Json-Serialization.md), 反之亦然. 然后, 它使用UTC8编码将JSON字符串转换为分布式缓存接受的字节数组. - -如果你想实现自己的序列化逻辑, 可以自己实现并[替换](Dependency-Injection.md) 此服务. - -### IDistributedCacheKeyNormalizer - -默认情况下, `IDistributedCacheKeyNormalizer`是由`DistributedCacheKeyNormalizer`类实现的. 它将缓存名称、应用程序缓存前缀和当前租户id添加到缓存键中. 如果需要更高级的键规范化, 可以自己实现并[替换](Dependency-Injection.md)此服务. - -## 另请参阅 - -* [Redis 缓存](Redis-Cache.md) diff --git a/docs/zh-Hans/Configuration.md b/docs/zh-Hans/Configuration.md deleted file mode 100644 index 9d40af46c0..0000000000 --- a/docs/zh-Hans/Configuration.md +++ /dev/null @@ -1,3 +0,0 @@ -# 配置 - -ASP.NET Core 提供了一个灵活可扩展,基于键值的配置系统. 但是配置系统独立于ASP.NET Core是`Microsoft.Extensions` 类库的部分. 它可以用于任何类型的应用程序 . 参见 [微软文档](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) 学习配置的更多内容. ABP 框架与配置系统百分百兼容. \ No newline at end of file diff --git a/docs/zh-Hans/Connection-Strings.md b/docs/zh-Hans/Connection-Strings.md deleted file mode 100644 index 290cce0436..0000000000 --- a/docs/zh-Hans/Connection-Strings.md +++ /dev/null @@ -1,89 +0,0 @@ -# 连接字符串 - -ABP框架的设计是[模块化](Module-Development-Basics.md), [微服务兼容](Microservice-Architecture.md) 和 [多租户](Multi-Tenancy.md). 同时设计了连接字符串管理来支持这些场景; - -* 允许为每个模块设置单独的连接字符串,这样每个模块都可以有自己的物理数据库. 甚至可以将模块配置为使用不同的DBMS. -* 允许为每个租户设置单独的连接字符串使用单独的数据库(在SaaS应用程序中). - -它还支持混合场景; - -* 允许将模块分组到数据库 (所有的模块分组到一个共享数据库, 两个模块使用数据库A, 三个模块使用数据库B, 一个模块使用数据库C其余的模块使用数据库D...等.) -* 允许将租户分组到数据库中,像模块一样. -* 允许为每个租户每个模块分离数据库 (数据库过多会增加维护成本,但ABP框架支持这种需求). - -所有[预构建应用模块](Modules/Index.md)已设计为与以上场景兼容. - -## 配置连接字符串 - -参见以下配置: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MyMainDb;Trusted_Connection=True;", - "AbpIdentityServer": "Server=localhost;Database=MyIdsDb;Trusted_Connection=True;", - "AbpPermissionManagement": "Server=localhost;Database=MyPermissionDb;Trusted_Connection=True;" -} -```` - -> ABP使用 `IConfiguration` 服务获取应用程序配置. 虽然在 `appsettings.json` 文件中写入配置是最简单的方法, 但它不仅限于此文件. 你可以使用环境变量, user secrets, Azure Key Vault... 等. 更多信息参阅 [配置](Configuration.md) 文档. - -以上配置定义了三个不同的连接字符串: - -* `MyMainDb` (`Default` 连接字符串)是应用程序的主连接字符串. 如果没有为模块指定连接字符串,则回退到 `Default` 连接字符串. [应用程序启动模板](Startup-Templates/Application.md) 配置为使用单个字符串, 所以所有的模块都使用单个数据库. -* `MyIdsDb` 由 [IdentityServer](Modules/IdentityServer.md) 模块使用. -* `MyPermissionDb` 由 [权限管理](Modules/Permission-Management.md) 模块使用. - -[预构建的应用程序模块](Modules/Index.md) 为连接字符串名称定义常量. 例如IdentityServer模块在 `AbpIdentityServerDbProperties` 类(位于 `Volo.Abp.IdentityServer` 命名空间)定义了 `ConnectionStringName` 常量 . 其他的模块类似的定义常量,你可以查看连接字符串的名称. - -### AbpDbConnectionOptions - -ABP实际上使用 `AbpDbConnectionOptions` 获取连接字符串. 如果如上所述设置了连接字符串, `AbpDbConnectionOptions` 会被自动填充. 但是你也可以使用[选项模式](Options.md)设置或覆盖连接字符串. 你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置`AbpDbConnectionOptions`). -如下所示: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.ConnectionStrings.Default = "..."; - options.ConnectionStrings["AbpPermissionManagement"] = "..."; - }); -} -```` - -## 设置连接字符串名称 - -模块通常使用 `ConnectionStringName` attribute 为 `DbContext` 类关联一个唯一的连接字符串名称. 示例: - -````csharp -[ConnectionStringName("AbpIdentityServer")] -public class IdentityServerDbContext - : AbpDbContext, IIdentityServerDbContext -{ -} -```` - -对于 [Entity Framework Core](Entity-Framework-Core.md) 和 [MongoDB](MongoDB.md), 写入到 `DbContext` 类 (和接口,如果有的话). - -> 如果你开发的是与数据库提供程序无关的可重用模块, 请参见 [最佳实践指南](Best-Practices/Index.md). - -## Entity Framework Core的数据库迁移 - -关系数据库需要在使用数据库之前创建数据库和数据库架构 (表, 视图...等). - -启动模板(使用 EF Core ORM) 带有一个数据库和一个 `.EntityFrameworkCore.DbMigrations` 项目,其中包含数据库的迁移文件. 该项目主要定义了一个*YourProjectName*MigrationsDbContext,它调用所有模块的 `Configure...()` 方法,例如 `builder.ConfigurePermissionManagement()`. - -一旦要分离模块的数据库,通常需要创建第二个迁移路径. 请参阅[EF Core迁移文档](Entity-Framework-Core-Migrations.md)了解如何为所需模块创建和使用其他数据库. - -## 多租户 - -参阅 [多租户文档](Multi-Tenancy.md)了解如何为租户使用单独的数据库. - -## 替换连接字符串解析器 - -ABP定义了 `IConnectionStringResolver`,并在需要连接字符串时使用它. 有两个预构建的实现: - -* `DefaultConnectionStringResolver` 根据上面"配置连接字符串"一节中定义的规则,使用 `AbpDbConnectionOptions` 选择连接字符串. -* `MultiTenantConnectionStringResolver` 用于多租户应用程序,并尝试获取当前租户的已配置连接字符串(如果有). 它使用 `ITenantStore` 查找连接字符串. 它继承了 `DefaultConnectionStringResolver`, 如果没有为当前租户指定连接字符串则回退到基本逻辑. - -如果需要自定义逻辑来确定连接字符串,可以实现 `IConnectionStringResolver` 接口(也可以从现有类派生)并使用[依赖注入](Dependency-Injection.md)系统替换现有实现. diff --git a/docs/zh-Hans/Contribution/Index.md b/docs/zh-Hans/Contribution/Index.md deleted file mode 100644 index 7b1c3544ef..0000000000 --- a/docs/zh-Hans/Contribution/Index.md +++ /dev/null @@ -1,67 +0,0 @@ -# 贡献指南 - -ABP是[开源](https://github.com/abpframework)和社区驱动项目. 本指南旨在帮助任何想要为项目做出贡献的人. - -## community.abp.io - -如果你可编写文章或关于ASP框架和ASP.NET Core的 "如何" 指南,请提交你的文章到[community.abp.io](https://community.abp.io/)网站. - -## 贡献代码 - -你可以将Pull request(拉取请求)发送到Github存储库. - -- 从Github克隆[ABP存储库](https://github.com/abpframework/abp/). -- 进行必要的更改. -- 发送Pull request(拉取请求). - -在进行任何更改之前,请在[Github问题](https://github.com/abpframework/abp/issues)上进行讨论. 通过这种方式, 其他开发人员将不会处理同一个问题, 你的PR将有更好的机会被接受. - -### Bug修复 & 增强功能 - -你可能希望修复已知Bug或处理计划的增强功能. 请参阅Github上的[问题列表](https://github.com/abpframework/abp/issues). - -### 功能请求 - -如果你对框架或模块有功能的想法, 请在Github上[创建一个问题](https://github.com/abpframework/abp/issues/new)或参加现有的讨论. 如果它被社区所接受你就可以实现它. - -## 文档翻译 - -你可能希望将完整的[文档](https://abp.io/documents/)(包括本文)翻译成你的母语. 请按照下列步骤操作: - -* 从Github克隆[ABP存储库](https://github.com/abpframework/abp/). -* 要添加新语言,请在[docs](https://github.com/abpframework/abp/tree/master/docs)文件夹中创建一个新文件夹. 文件夹名称可以是" en","es","fr","tr"等(参见[所有文化代码](https://msdn.microsoft.com/en-us/library/hh441729.aspx)). -* 获取["en"文件夹](https://github.com/abpframework/abp/tree/master/docs/en)作为文件名和文件夹结构的参考. 如果要翻译相同的文档, 请保持相同的命名. -* 翻译任何文档后发送拉取请求(PR). 请翻译文件后及时发送PR. 不要等到完成所有文件的翻译. - -在[ABP文档网站](https://docs.abp.io)上新添加语言之前,需要翻译一些基本文档: - -* 入门文档 -* 教程 -* CLI - -完成了这些基本的翻译后,将添加一种新的语言 - -## 资源本地化 - -ABP框架具有灵活的[本地化系统](../Localization.md). 你可以为自己的应用程序创建本地化用户界面. - -除此之外,框架和预构建模块已经本地化了文本.请参阅[Volo.Abp.UI包的本地化文本](https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json). - -### 使用 "abp translate" 命令 - -这是推荐的方法,因为它会自动查找所有缺少的文本的特定文化,让你在一个地方翻译. - -* 从Github克隆[ABP存储库](https://github.com/abpframework/abp/). -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI). -* 在abp仓储的根文件夹为你的语言运行`abp translate -c `命令. 例如对法语使用 `abp translate -c fr`, 检查[文档](https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/supported-culture-codes)找到你所用语言的文化代码. -* 命令会在同一文件夹下创建 `abp-translation.json` 文件, 使用你喜欢的编辑器打开这个文件并填写缺少的文本值. -* 一旦你完成了翻译,使用 `abp translate -a` 命令应用更改到相关的文件. -* 在GitHub上发送PR. - -### 手动翻译 - -如果你想更改特定的资源文件,你可以自己找到这个文件进行必要的更改(或为你的语言创建新文件),并在GitHub上发送PR. - -## Bug 报告 - -如果你发现任何Bug, 请[在Github存储库上创建一个问题](https://github.com/abpframework/abp/issues/new). \ No newline at end of file diff --git a/docs/zh-Hans/CurrentUser.md b/docs/zh-Hans/CurrentUser.md deleted file mode 100644 index 8d930b0911..0000000000 --- a/docs/zh-Hans/CurrentUser.md +++ /dev/null @@ -1,167 +0,0 @@ -# 当前用户 - -在Web应用程序中检索有关已登录用户的信息是很常见的. 当前用户是与Web应用程序中的当前请求相关的活动用户. - -## ICurrentUser - -`ICurrentUser` 是主要的服务,用于获取有关当前活动的用户信息. - -示例: [注入](Dependency-Injection.md) `ICurrentUser` 到服务中: - -````csharp -using System; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Users; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly ICurrentUser _currentUser; - - public MyService(ICurrentUser currentUser) - { - _currentUser = currentUser; - } - - public void Foo() - { - Guid? userId = _currentUser.Id; - } - } -} -```` - -公共基类已经将此服务作为基本属性注入. 例如你可以直接在[应用服务](Application-Services.md)中使用 `CurrentUser` 属性: - -````csharp -using System; -using Volo.Abp.Application.Services; - -namespace AbpDemo -{ - public class MyAppService : ApplicationService - { - public void Foo() - { - Guid? userId = CurrentUser.Id; - } - } -} -```` - -### 属性 - -以下是 `ICurrentUser` 接口的基本属性: - -* **IsAuthenticated** 如果当前用户已登录(已认证),则返回 `true`. 如果用户尚未登录,则 `Id` 和 `UserName` 将返回 `null`. -* **Id** (Guid?): 当前用户的Id,如果用户未登录,返回 `null`. -* **UserName** (string): 当前用户的用户名称. 如果用户未登录,返回 `null`. -* **TenantId** (Guid?): 当前用户的租户Id. 对于[多租户](Multi-Tenancy.md) 应用程序很有用. 如果当前用户未分配给租户,返回 `null`. -* **Email** (string): 当前用户的电子邮件地址. 如果当前用户尚未登录或未设置电子邮件地址,返回 `null`. -* **EmailVerified** (bool): 如果当前用户的电子邮件地址已经过验证,返回 `true`. -* **PhoneNumber** (string): 当前用户的电话号码. 如果当前用户尚未登录或未设置电话号码,返回 `null`. -* **PhoneNumberVerified** (bool): 如果当前用户的电话号码已经过验证,返回 `true`. -* **Roles** (string[]): 当前用户的角色. 返回当前用户角色名称的字符串数组. - -### Methods - -`ICurrentUser` 是在 `ICurrentPrincipalAccessor` 上实现的(请参阅以下部分),并可以处理声明. 实际上所有上述属性都是从当前经过身份验证的用户的声明中检索的. - -如果你有自定义声明或获取其他非常见声明类型, `ICurrentUser` 有一些直接使用声明的方法. - -* **FindClaim**: 获取给定名称的声明,如果未找到返回 `null`. -* **FindClaims**: 获取具有给定名称的所有声明(允许具有相同名称的多个声明值). -* **GetAllClaims**: 获取所有声明. -* **IsInRole**: 一种检查当前用户是否在指定角色中的简化方法. - -除了这些标准方法,还有一些扩展方法: - -* **FindClaimValue**: 获取具有给定名称的声明的值,如果未找到返回 `null`. 它有一个泛型重载将值强制转换为特定类型. -* **GetId**: 返回当前用户的 `Id`. 如果当前用户没有登录它会抛出一个异常(而不是返回`null`). 仅在你确定用户已经在你的代码上下文中进行了身份验证时才使用此选项. - -### 验证和授权 - -`ICurrentUser` 的工作方式与用户的身份验证或授权方式无关. 它可以与使用当前主体的任何身份验证系统无缝地配合使用(请参阅下面的部分). - -## ICurrentPrincipalAccessor - -`ICurrentPrincipalAccessor` 是当需要当前用户的Principal时使用的服务(由ABP框架和你的应用程序代码使用). - -对于Web应用程序, 它获取当前 `HttpContext` 的 `User` 属性,对于非Web应用程序它将返回 `Thread.CurrentPrincipal`. - -> 通常你不需要这种低级别的 `ICurrentPrincipalAccessor` 服务,直接使用上述的 `ICurrentUser` 即可. - -### 基本用法 - -你可以注入 `ICurrentPrincipalAccessor` 并且使用 `Principal` 属性获取当前principal: - -````csharp -public class MyService : ITransientDependency -{ - private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; - - public MyService(ICurrentPrincipalAccessor currentPrincipalAccessor) - { - _currentPrincipalAccessor = currentPrincipalAccessor; - } - - public void Foo() - { - var allClaims = _currentPrincipalAccessor.Principal.Claims.ToList(); - //... - } -} -```` - -### 更改当前Principal - -除了某些高级场景外,你不需要设置或更改当前Principal. 如果需要可以使用 `ICurrentPrincipalAccessor` 的 `Change` 方法. 它接受一个 `ClaimsPrincipal` 对象并使其成为作用域的"当前"对象. - -示例: - -````csharp -public class MyAppService : ApplicationService -{ - private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; - - public MyAppService(ICurrentPrincipalAccessor currentPrincipalAccessor) - { - _currentPrincipalAccessor = currentPrincipalAccessor; - } - - public void Foo() - { - var newPrincipal = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] - { - new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString()), - new Claim(AbpClaimTypes.UserName, "john"), - new Claim("MyCustomCliam", "42") - } - ) - ); - - using (_currentPrincipalAccessor.Change(newPrincipal)) - { - var userName = CurrentUser.UserName; //returns "john" - //... - } - } -} -```` - -始终在 `using` 语句中使用 `Change` 方法,在 `using` 范围结束后它将恢复为原始值. - -这可以是一种模拟用户登录的应用程序代码范围的方法,但是请尝试谨慎使用它. - -## AbpClaimTypes - -`AbpClaimTypes` 是一个静态类它定义了标准声明的名称被ABP框架使用. - -* `UserName`, `UserId`, `Role` 和 `Email` 属性的默认值是通常[System.Security.Claims.ClaimTypes](https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes)类设置的, 但你可以改变它们. - -* 其他属性,如 `EmailVerified`, `PhoneNumber`, `TenantId` ...是由ABP框架通过尽可能遵循标准名称来定义的. - -建议使用这个类的属性来代替声明名称的魔术字符串. diff --git a/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md b/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md deleted file mode 100644 index 8fd0921a83..0000000000 --- a/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md +++ /dev/null @@ -1,178 +0,0 @@ -# 自定义应用模块: 扩展实体 - -在某些情况下你可能希望为依赖模块中定义的实体添加一些额外的属性(和数据库字段). 本节将介绍一些实现这一目标的不同方法. - -## Extra Properties - -[Extra properties](Entities.md)是一种存储实体的一些额外数据但不用更改实体的方式. 实体应该实现 `IHasExtraProperties` 接口. 所有预构建模块定义的聚合根实体都实现了 `IHasExtraProperties` 接口,所以你可以在这些实体中存储额外的属性. - -示例: - -````csharp -//SET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -user.SetProperty("Title", "My custom title value!"); -await _identityUserRepository.UpdateAsync(user); - -//GET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -return user.GetProperty("Title"); -```` - -这种方法开箱即用并且非常简单,你可以使用不同的属性名称(如这里的`Title`)在同一时间存储多个属性. - -对于EF Core额外的属性被格式化成单个 `JSON` 字符值串存储在数据库中. 对于MongoDB它们做为单独的字段存储. - -参阅[实体文档](Entities.md)了解更多关于额外系统. - -> 可以基于额外的属性执行**业务逻辑**. 你可以[重写服务方法](Customizing-Application-Modules-Overriding-Services.md). 然后获取或设置如上所示的值. - -## 实体扩展 (EF Core) - -如上所述,实体所有的额外属性都作为单个JSON对象存储在数据库表中. 它不适用复杂的场景,特别是在你需要的时候. - -* 使用额外属性创建**索引**和**外键**. -* 使用额外属性编写**SQL**或**LINQ**(例如根据属性值搜索). -* 创建你**自己的实体**映射到相同的表,但在实体中定义一个额外属性做为 **常规属性**(参阅 [EF Core迁移文档](Entity-Framework-Core-Migrations.md)了解更多). - -为了解决上面的问题,用于EF Core的ABP框架实体扩展系统允许你使用上面定义相同的额外属性API,但将所需的属性存储在单独的数据库表字段中. - -假设你想要添加 `SocialSecurityNumber` 到[身份模块](Modules/Identity.md)的 `IdentityUser` 实体. 你可以使用 `ObjectExtensionManager` 类: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreProperty( - "SocialSecurityNumber", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(32); - } - ); -```` - -* 你提供了 `IdentityUser` 作为实体名(泛型参数), `string` 做为新属性的类型, `SocialSecurityNumber` 做为属性名(也是数据库表的字段名). -* 你还需要提供一个使用[EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)定义数据库映射属性的操作. - -> 必须在使用相关的 `DbContext` 之前执行此代码. 应用程序启动模板定义了一个名为 `YourProjectNameEfCoreEntityExtensionMappings` 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理. - -定义实体扩展后你需要使用EF Core的[Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration)和[Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database)命令来创建code first迁移类并更新数据库. - -然后你可以使用上一部分中定义的相同额外属性系统来操纵实体上的属性. - -## 创建新实体映射到同一个数据库表/Collection - -另一个方法是**创建你自己的实体**映射到**同一个数据库库**(对于MongoDB数据库是collection) - -[应用程序启动模板](Startup-Templates/Application.md)的 `AppUser` 已经实现了这种方法. [EF Core迁移文档](Entity-Framework-Core-Migrations.md)描述了在这些情况下如何实现和管理**EF Core数据库迁移**. 这种方法同样适用于MongoDB,但你不需要处理数据库迁移问题. - -## 创建一个拥有自己数据库表/Collection的新实体 - -映射你的实体到依赖模块的**已存在的表**有一些缺点; - -* 你需要处理EF Core的**数据库迁移架构**. 需要特别注意迁移代码,特别是当你需要在实体间添加**关系**时. -* 你的应用程序数据库和模块数据库将是 **同一个物理数据库**. 通常需要时可以将模块数据库分开,但使用相同的表会对其进行限制. - -如果你想要使你的实体或模块定义的实体**低耦合**,那么可以创建自己的数据库表/collection并且将你的实体映射到自己的数据库表. - -在这种情况下你需要处理**同步问题**,尤其是你要**复制**相关实体的某些属性/字段时,有一些解决方案; - -* 如果你构建的是一个 **单体** 应用程序(或者在同一进程管理你的实体和依赖模块的实体),那么你可以使用[本地事件总线](Local-Event-Bus.md)监听实体更改. -* 如果你构建的是一个 **分布式** 系统,模块的实体和你的实体在不同的 进程/服务 管理(创建/更新/删除),那么你可以使用[分布式事件总线](Distributed-Event-Bus.md)订阅实体的更改事件. - -在你处理事件时,你可以在自己的数据库中更改自己的实体. - -### 订阅本地事件总线 - -[本地事件总线](Local-Event-Bus.md)系统是发布和订阅同一应用程序中发生的事件的方法. - -假设你想要获取 `IdentityUser` 实体的更改信息(创建,更改或删除). 你可以创建一个类实现 `ILocalEventHandler>` 接口. - -````csharp -public class MyLocalIdentityUserChangeEventHandler : - ILocalEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityChangedEventData eventData) - { - var userId = eventData.Entity.Id; - var userName = eventData.Entity.UserName; - //... - } -} -```` - -* `EntityChangedEventData` 涵盖了给定实体的创建,更新或删除事件. 如果你需要你可以分别订阅创建,更新或删除事件(在同一个类或不同的类中). -* 这里的代码在**本地事务之外执行**,因为它监听 `EntityChanged` 事件. 如果当前[工作单元](Unit-Of-Work.md)是事务性的,你可以订阅 `EntityChangingEventData` 事件,它在**同一本地(进行)事务**中执行事件处理. - -> 提醒:这些方法需要在包含处理类的同一进程中更改 `IdentityUser` 实体. 即使在集群环境(同一应用程序的多个实例在不同的服务器进行),它也完美工作. - -### 订阅分布式事件总线 - -[分布式事件总线](Distributed-Event-Bus.md)是在一个应用程序中发布事件,并在相同服务器或不同服务器运行的相同应用程序或不同应用程序中接收事件的方法. - -假设你想要获取 `IdentityUser` 实体的创建,更改或删除信息. 你可以像以下一样创建一个类: - -````csharp -public class MyDistributedIdentityUserChangeEventHandler : - IDistributedEventHandler>, - IDistributedEventHandler>, - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "created" event - } - } - - public async Task HandleEventAsync(EntityUpdatedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "updated" event - } - } - - public async Task HandleEventAsync(EntityDeletedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "deleted" event - } - } -} -```` - -* 它实现了多个 `IDistributedEventHandler` 接口: **创建**,**更改**和**删除**,因为分布式事件总线单独发布事件,没有本地事件总线那样的"Changed"事件. -* 它订阅了 `EntityEto`, 这是一个通用的事件类,ABP框架针对所有类型的实体**自动发布**. 这就是为什么它检查**实体类型**(因为我们没有假设有对 `IdentityUser` 实体有安全的类型引用,所以它是字符串类型的). - -预构建应用模块没有定义专门的事件类型(如`UserEto` - "ETO" 意思是 "事件传输对象"). 此功能在路线图上([关注这个issue](https://github.com/abpframework/abp/issues/3033)),一旦完成后,你就可以订阅独立的实体类型: - -````csharp -public class MyDistributedIdentityUserCreatedEventHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - var userId = eventData.Entity.Id; - var userName = eventData.Entity.UserName; - //...handle the "created" event - } - - //... -} -```` - -* 这个处理程序只会在新用户创建时执行. - -> 唯一预定义的专门事件类是 `UserEto`, 你可以订阅 `EntityCreatedEto` 获取用户创建时的通知. 此事件也适用于身份模块. - -## 另请参阅 - -* [自定义已存在的模块](Customizing-Application-Modules-Guide.md) \ No newline at end of file diff --git a/docs/zh-Hans/Customizing-Application-Modules-Guide.md b/docs/zh-Hans/Customizing-Application-Modules-Guide.md deleted file mode 100644 index bf3404ddb7..0000000000 --- a/docs/zh-Hans/Customizing-Application-Modules-Guide.md +++ /dev/null @@ -1,62 +0,0 @@ -# 自定义现有模块 - -ABP框架提供的设计旨在支持构建完全[模块化的应用程序](Module-Development-Basics.md)和系统. 它还提供了一些可以在任何类型的应用程序中**使用**的[预构建应用模块](Modules/Index.md) - -例如,你可以在你的应用程序中**重用**[身份管理模块](Modules/Identity.md)去添加用户,角色和权限管理. [应用程序启动模板](Startup-Templates/Application.md)已经**预装**了Identity和其他模块. - -## 复用应用模块 - -你有两个选项去复用应用模块: - -### 添加包引用 - -你可以添加相关模块的 **NuGet** 和 **NPM** 包引用到你的应用程序,并配置模块(根据它的文档)集成到你的应用程序中. - -正如前面提到,[应用程序启动模板](Startup-Templates/Application.md)已经**预装了一些基本模块**,它引用模块的NuGet和NPM包. - -这种方法具有以下优点: - -* 你的解决方案会非常**干净**,只包含你**自己的应用程序代码**. -* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bug修复**. - -然而有一个缺点: - -* 你可能无法**自定义**模块,因为模块源码没有在你的解决方案中. - -本文档介绍了 **或者自定义或扩展** 依赖模块并且无需更改其源码,尽快与更改完整的源码比起是有限的,但仍有一些好的方法可以自定义. - -如果你不认为自己会对预构建的模块进行重大更改,那么使用包引用的方法复用模块是推荐的方法. - -### 包含源码 - -如果你想要在预构建的模块上进行**重大**更改或添加**主要功能**,但是可用的扩展点不够使用,那么可以考虑直接使用依赖模块的源码. - -这种情况下,你通常**添加模块源码**到你的解决方案中,并将**包引用替换**为本地项目引用. **[ABP CLI](CLI.md)** 可以为你自动化这一过程. - -#### 分离模块解决方案 - -你可能不希望将模块源代码**直接包含在解决方案**中. 每个模块都包含十多个项目文件,添加**多个模块**会使解决方案变的臃肿可能还会影响**开发时的加载速度**,另外你可能有不同的开发团队维护不同模块. - -无论如何,你都可以为需要的模块创建**单独的解决方案**,将依赖模块做为解决方案中的项目引用. 比如在[abp仓库](https://github.com/abpframework/abp/),我们就是这样做的. - -> 我们看到的一个问题是Visual Studio在这种方式下不能很好的工作(解决方案目录之外对本地项目的引用不能很好地支持). 如果在开发过程中出错(对于外部模块),请在Visual Studio打开应用程序的解决方案后,在命令行运行 `dotnet restore`命令. - -#### 发布的自定义模块的包 - -一个备选方案是将重新打包模块的源代码(NuGet/NPM包),使用包引用. 你可以为公司使用本地私人的Nuget/NPM服务器. - -## 模块自定义/扩展途径 - -如果你决定使用预构建模块的NuGet/NPM包引用方式. 下面的文档详细解释了如何自定义/扩展现有模块的方法: - -* [扩展实体](Customizing-Application-Modules-Extending-Entities.md) -* [重写服务](Customizing-Application-Modules-Overriding-Services.md) -* [重写界面](Customizing-Application-Modules-Overriding-User-Interface.md) - -### 另请参阅 - -另外,请参阅以下文档: - -* 参阅 [本地化文档](Localization.md) 学习如何扩展已存在的本地化资源. -* 参阅 [设置文档](Settings.md) 学习如何更改依赖模块的设置定义. -* 参阅 [授权文档](Authorization.md) 学习如何更改依赖模块的权限定义. \ No newline at end of file diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md deleted file mode 100644 index 1e9ae77f36..0000000000 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ /dev/null @@ -1,269 +0,0 @@ -# 自定义应用模块: 重写服务 - -你可能想要**更改**依赖模块的**行为(业务逻辑)**. 在这种情况下,你可以使用[依赖注入](Dependency-Injection.md)的能力替换服务,控制器甚至页面模型到你自己的实现. - -注册到依赖注入的任何类,包括ABP框架的服务都可以被**替换**. - -你可以根据自己的需求使用不同的选项,下面的章节中将介绍这些选项. - -> 请注意,某些服务方法可能不是virtual,你可能无法override,我们会通过设计将其virtual,如果你发现任何方法不可以被覆盖,请[创建一个issue](https://github.com/abpframework/abp/issues/new)或者你直接修改后并发送**pull request**到GitHub. - -## 替换接口 - -如果给定的服务定义了接口,像 `IdentityUserAppService` 类实现了 `IIdentityUserAppService` 接口,你可以为这个接口创建自己的实现并且替换当前的实现. 例如: - -````csharp -public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -`MyIdentityUserAppService` 通过命名约定替换了 `IIdentityUserAppService` 的当前实现. 如果你的类名不匹配,你需要手动公开服务接口: - -````csharp -[ExposeServices(typeof(IIdentityUserAppService))] -public class TestAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -依赖注入系统允许为一个接口注册多个服务. 注入接口时会解析最后一个注入的服务. 显式的替换服务是一个好习惯. - -示例: - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IIdentityUserAppService))] -public class TestAppService : IIdentityUserAppService, ITransientDependency -{ - //... -} -```` - -使用这种方法, `IIdentityUserAppService` 接口将只会有一个实现. 也可以使用以下方法替换服务: - -````csharp -context.Services.Replace( - ServiceDescriptor.Transient() -); -```` - -你可以在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法编写替换服务代码. - -## 重写一个服务类 - -大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法. - -### 示例: 重写服务方法 - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))] -public class MyIdentityUserAppService : IdentityUserAppService -{ - //... - public MyIdentityUserAppService( - IdentityUserManager userManager, - IIdentityUserRepository userRepository, - IGuidGenerator guidGenerator - ) : base( - userManager, - userRepository, - guidGenerator) - { - } - - public async override Task CreateAsync(IdentityUserCreateDto input) - { - if (input.PhoneNumber.IsNullOrWhiteSpace()) - { - throw new AbpValidationException( - "Phone number is required for new users!", - new List - { - new ValidationResult( - "Phone number can not be empty!", - new []{"PhoneNumber"} - ) - } - ); } - - return await base.CreateAsync(input); - } -} -```` - -示例中**重写**了 `IdentityUserAppService` [应用程序](Application-Services.md) `CreateAsync` 方法检查手机号码. 然后调用了基类方法继续**基本业务逻辑**. 通过这种方法你可以在基本业务逻辑**之前**和**之后**执行其他业务逻辑. - -你也可以完全**重写**整个业务逻辑去创建用户,而不是调用基类方法. - -### 示例: 重写领域服务 - -````csharp -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IdentityUserManager))] -public class MyIdentityUserManager : IdentityUserManager -{ - public MyIdentityUserManager( - IdentityUserStore store, - IIdentityRoleRepository roleRepository, - IIdentityUserRepository userRepository, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger logger, - ICancellationTokenProvider cancellationTokenProvider) : - base(store, - roleRepository, - userRepository, - optionsAccessor, - passwordHasher, - userValidators, - passwordValidators, - keyNormalizer, - errors, - services, - logger, - cancellationTokenProvider) - { - } - - public async override Task CreateAsync(IdentityUser user) - { - if (user.PhoneNumber.IsNullOrWhiteSpace()) - { - throw new AbpValidationException( - "Phone number is required for new users!", - new List - { - new ValidationResult( - "Phone number can not be empty!", - new []{"PhoneNumber"} - ) - } - ); - } - - return await base.CreateAsync(user); - } -} -```` - -示例中类继承了 `IdentityUserManager` [领域服务](Domain-Services.md),并且重写了 `CreateAsync` 方法进行了与之前相同的手机号码检查. 结果也是一样的,但是这次我们在领域服务实现了它,假设这是我们系统的**核心领域逻辑**. - -> 这里需要 `[ExposeServices(typeof(IdentityUserManager))]` attribute,因为 `IdentityUserManager` 没有定义接口 (像 `IIdentityUserManager`) ,依赖注入系统并不会按照约定公开继承类的服务(如已实现的接口). - -参阅[本地化系统](Localization.md)了解如何自定义错误消息. - -### 重写其他服务 - -控制器,框架服务,视图组件类以及其他类型注册到依赖注入的类都可以像上面的示例那样被重写. - -## 扩展数据传输对象 - -你可以如[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md)所述扩展实体. 并使用上面介绍的重写相关服务**使用自定义属性****执行其他业务逻辑**. - -应用程序使用的数据传输对象(**DTO**)同样可扩展. 这样你可以使服务返回其他属性并在UI(或其他客户端)得到其他属性. - -### 示例 - -假设你已经按照[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md)中的说明添加了 `SocialSecurityNumber` 并希望从 `IdentityUserAppService的GetListAsync` 方法获取用户列表时包括此属性. - -你可以使用[对象扩展系统](Object-Extensions.md)将属性添加到 `IdentityUserDto`. 在应用程序启动模板带有的 `YourProjectNameDtoExtensions` 类中编写以下代码: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber" - ); -```` - -这段代码为 `IdentityUserDto` 类添加了 `string` 类型的 `SocialSecurityNumber` 属性. 现在你可以在RREST API客户端调用 `/api/identity/users` HTTP API(内部使用 `IdentityUserAppService`),你会在 `extraProperties` 部分看到 `SocialSecurityNumber` 值. - -````json -{ - "totalCount": 1, - "items": [{ - "tenantId": null, - "userName": "admin", - "name": "admin", - "surname": null, - "email": "admin@abp.io", - "emailConfirmed": false, - "phoneNumber": null, - "phoneNumberConfirmed": false, - "twoFactorEnabled": false, - "lockoutEnabled": true, - "lockoutEnd": null, - "concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c", - "isDeleted": false, - "deleterId": null, - "deletionTime": null, - "lastModificationTime": "2020-04-09T21:25:47.0740706", - "lastModifierId": null, - "creationTime": "2020-04-09T21:25:46.8308744", - "creatorId": null, - "id": "8edecb8f-1894-a9b1-833b-39f4725db2a3", - "extraProperties": { - "SocialSecurityNumber": "123456789" - } - }] -} -```` - -手动添加了 `123456789` 值到数据库中. - -所有预构建的模块都在DTO中支持额外属性,你可以对其轻松的配置. - -### 定义检查 - -当为实体[定义](Customizing-Application-Modules-Extending-Entities.md)额外的属性时,由于安全性它不会自动出现在所有相关的DTO中. 额外属性可能包含敏感数据并且你可能不想默认公开给客户端. - -因此如果要用于DTO,需要为相应的DTO显式定义相同的属性(如上所述). 如果要允许在用户创建时进行设置还需要为 `IdentityUserCreateDto` 定义. - -如果属性并不是安全敏感,这可能会很枯燥. 对象扩展系统允许你忽略检查定义的属性. 参阅示例: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.MapEfCore(b => b.HasMaxLength(32)); - options.CheckPairDefinitionOnMapping = false; - } - ); -```` - -这是定义实体属性的另一种方法( 有关 `ObjectExtensionManager` 更多信息,请参阅[文档](Object-Extensions.md)). 这次我们设置了 `CheckPairDefinitionOnMapping` 为false,在将实体映射到DTO时会跳过定义检查. - -如果你不喜欢这种方法,但想简单的向多个对象(DTO)添加单个属性, `AddOrUpdateProperty` 可以使用类型数组添加额外的属性: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - new[] - { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), - typeof(IdentityUserUpdateDto) - }, - "SocialSecurityNumber" - ); -```` - -### 关于用户界面 - -该系统允许你向实体和DTO添加额外的属性并执行自定义业务代码,但它与用户界面无关. - -参阅 [重写用户界面](Customizing-Application-Modules-Overriding-User-Interface.md) 指南了解关于UI部分. - -## 如何找到服务? - -[模块文档](Modules/Index.md) 包含了定义的主要服务列表. 另外 你也可以查看[源码](https://github.com/abpframework/abp/tree/dev/modules)找到所有的服务. diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md deleted file mode 100644 index eed56807ea..0000000000 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md +++ /dev/null @@ -1,6 +0,0 @@ -# 重写用户界面 - -你可以想要重写页面,组件,JavaScript,CSS或你依赖模块的图片文件. 重写UI取决于你使用的UI框架. 选择UI框架以继续: - -* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) -* [Angular](UI/Angular/Customization-User-Interface.md) \ No newline at end of file diff --git a/docs/zh-Hans/Dapper.md b/docs/zh-Hans/Dapper.md deleted file mode 100644 index 1f6f1aee61..0000000000 --- a/docs/zh-Hans/Dapper.md +++ /dev/null @@ -1,61 +0,0 @@ -# Dapper 集成 - -Dapper 是轻量化的数据库提供者,使用Dapper主要的好处是编写T-SQL查询,它为 `IDbConnection` 接口提供了一些扩展方法. - -ABP没有为Dapper封装许多功能, ABP Dapper库在ABP EntityFrameworkCore的基础上提供了 `DapperRepository` 基类,在其中提供了Dapper需要的`IDbConnection`和`IDbTransaction`属性,这两个属性可以和[工作单元](Unit-Of-Work.md)很好的配合. - -## 安装 - -请先根据[EF Core的集成文档](Entity-Framework-Core.md)安装并配置好EF Core. - -`Volo.Abp.Dapper`是Dapper集成的主要[nuget包](https://www.nuget.org/packages/Volo.Abp.Dapper). - -将其安装到你的项目中(在分层应用程序中适用于 数据访问/基础设施层): - -```shell -Install-Package Volo.Abp.Dapper -``` - -然后添加 `AbpDapperModule` 模块依赖项(`DependsOn` Attribute) 到 [module](Module-Development-Basics.cn.md)(项目中的Mudole类): - -````C# -using Volo.Abp.Dapper; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpDapperModule))] - public class MyModule : AbpModule - { - //... - } -} -```` - -## 实现Dapper仓储 - -下面的代码实现了`Person`仓储, 它需要EF Core的`DbContext`(MyAppDbContext). 你可以注入`PersonDapperRepository`来调用它的方法. - -`DbConnection`和`DbTransaction`来自于`DapperRepository`基类. - -```C# -public class PersonDapperRepository : DapperRepository, ITransientDependency -{ - public PersonDapperRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public virtual async Task> GetAllPersonNames() - { - return (await DbConnection.QueryAsync("select Name from People", transaction: DbTransaction)) - .ToList(); - } - - public virtual async Task UpdatePersonNames(string name) - { - return await DbConnection.ExecuteAsync("update People set Name = @NewName", new { NewName = name }, - DbTransaction); - } -} -``` diff --git a/docs/zh-Hans/Dapr/Index.md b/docs/zh-Hans/Dapr/Index.md deleted file mode 100644 index a5237cbb71..0000000000 --- a/docs/zh-Hans/Dapr/Index.md +++ /dev/null @@ -1,443 +0,0 @@ -# ABP Dpar 集成 - -> 这个文档假设你已经熟悉[Dapr](https://dapr.io/)并且想在你的ABP应用中使用它. - -[Dapr](https://dapr.io/) (分布式应用运行时)提供了简化微服务连接的API.它是一个开源项目,主要由微软支持.它也是CNCF(云原生计算基金会)项目,受到社区的信任. - -ABP和Dapr有一些相似的特性,如服务到服务通信,分布式消息总线和分布式锁.然而,它们的目的完全不同.ABP的目标是通过提供自以为是的架构并提供必要的基础架构库,可重用模块和工具来正确实现该架构来提供端到端的开发人员体验.另一方面,Dapr的目的是提供一个运行时,将常见的微服务通信模式与应用程序逻辑解耦. - -ABP和Dapr可以完美地在同一个应用程序中一起工作.ABP提供了一些包来提供更好的集成,其中Dapr功能与ABP相似.你可以根据[Dapr文档](https://docs.dapr.io/)使用其他Dapr功能,而不需要ABP集成包. - -## ABP Dpar 集成包 - -ABP提供了以下NuGet包用于Dapr集成: - -* [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr): 主要的Dapr集成包.所有其他包都依赖于此包. -* [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr): 与Dapr的[服务调用](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/)集成的ABP的[动态](../API/Dynamic-CSharp-API-Clients.md)和[静态](../API/Static-CSharp-API-Clients.md)C# API客户端代理系统集成包. -* [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr): 使用Dapr的[发布和订阅](https://docs.dapr.io/developing-applications/building-blocks/pubsub/)构建块实现ABP的分布式事件总线.使用此包,你可以发送事件,但不能接收. -* [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus): 提供从Dapr的[发布和订阅](https://docs.dapr.io/developing-applications/building-blocks/pubsub/)构建块接收事件的端点.使用此包发送和接收事件. -* [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr): 使用Dapr的[分布式锁](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/)构建块为ABP框架的[分布式锁定](../Distributed-Locking.md)服务. - -在以下部分中,我们将看到如何使用这些包在ABP基础解决方案中使用Dapr. - -## 基础 - -### 安装 - -> 这个部分解释了如何将[Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr)添加到你的项目中.如果你使用的是其他Dapr集成包,你可以跳过这个部分,因为这个包会被间接添加. - -使用ABP CLI将[Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet包添加到你的项目中: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)如果你之前没有安装过. -* 在你想要添加`Volo.Abp.Dapr`包的`.csproj`文件所在的目录中打开命令行(终端). -* 运行`abp add-package Volo.Abp.Dapr`命令. - -如果你想手动添加,安装 [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet包到你的项目中,并在项目内的[ABP模块](../Module-Development-Basics.md)类中添加`[DependsOn(typeof(AbpDaprModule))]`. - -### AbpDaprOptions - -`AbpDaprOptions` 是配置全局Dapr设置的主要[选项类](../Options.md).**所有设置都是可选的,你大多数情况下不需要配置它们.** 如果你需要,你可以在[模块类](../Module-Development-Basics.md)的`ConfigureServices`方法中配置它: - -````csharp -Configure(options => -{ - // ... -}); -```` - -可用的`AbpDaprOptions`类属性: - -* `HttpEndpoint` (可选):创建`DaprClient`对象时使用的HTTP端点.如果你没有指定,将使用默认值. -* `GrpcEndpoint` (可选):创建`DaprClient`对象时使用的gRPC端点.如果你没有指定,将使用默认值. -* `DaprApiToken` (可选):应用程序向Dapr发送请求时使用的[Dapr API token](https://docs.dapr.io/operations/security/api-token/).默认情况下,它从`DAPR_API_TOKEN`环境变量中填充(配置后由 Dapr 设置).有关详细信息,请参阅本文档的*安全*部分. -* `AppApiToken` (可选):用于验证来自Dapr的请求的[应用程序API token](https://docs.dapr.io/operations/security/app-api-token/).默认情况下,它从`APP_API_TOKEN`环境变量中填充(配置后由 Dapr 设置).有关详细信息,请参阅本文档的*安全*部分. - -或者, 你可以在 `appsettings.json` 文件的 `Dapr` 部分中配置选项.示例: - -````csharp -"Dapr": { - "HttpEndpoint": "http://localhost:3500/" -} -```` - -### IAbpDaprClientFactory - -`IAbpDaprClientFactory` 可用于创建 `DaprClient` 或 `HttpClient` 对象来执行对 Dapr 的操作.它使用 `AbpDaprOptions`,因此你可以配置设置. - -**示例用法:** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAbpDaprClientFactory _daprClientFactory; - - public MyService(IAbpDaprClientFactory daprClientFactory) - { - _daprClientFactory = daprClientFactory; - } - - public async Task DoItAsync() - { - // Create a DaprClient object with default options - DaprClient daprClient = await _daprClientFactory.CreateAsync(); - - /* Create a DaprClient object with configuring - * the DaprClientBuilder object */ - DaprClient daprClient2 = await _daprClientFactory - .CreateAsync(builder => - { - builder.UseDaprApiToken("..."); - }); - - // Create an HttpClient object - HttpClient httpClient = await _daprClientFactory.CreateHttpClientAsync("target-app-id"); - } -} -```` - -`CreateHttpClientAsync` 方法还获取可选的 `daprEndpoint` 和 `daprApiToken` 参数. - -> 你可以在应用程序中使用Dapr API创建客户端对象.推荐使用`IAbpDaprClientFactory`,但不是必需的. - -## C# API 客户端代理集成 - -ABP可以[动态](../API/Dynamic-CSharp-API-Clients.md)或[静态](../API/Static-CSharp-API-Clients.md)生成代理类,以便从Dotnet客户端应用程序调用HTTP API.在分布式系统中使用HTTP API是非常合理的.[Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr)包配置了客户端代理系统,因此它使用Dapr的服务调用构建块进行应用程序之间的通信. - -### 安装 - -使用ABP CLI将[Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet包添加到项目(客户端): - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)如果你之前没有安装过. -* 在你想要添加`Volo.Abp.Http.Client.Dapr`包的`.csproj`文件所在的目录中打开命令行(终端). -* 运行`abp add-package Volo.Abp.Http.Client.Dapr`命令. - -如果你想手动添加,安装 [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet包到你的项目中,并在项目内的[ABP模块](../Module-Development-Basics.md)类中添加`[DependsOn(typeof(AbpHttpClientDaprModule))]`. - -### 配置 - -当你安装了[Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet 包,所有你需要做的就是在`appsettings.json`或使用`AbpRemoteServiceOptions`[选项类](../Options.md)中配置ABP的远程服务选项. - -**示例:** - -````csharp -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://dapr-httpapi/" - } - } -} -```` - -`dapr-httpapi` 在这个例子中是你的Dapr配置中服务器应用程序的应用程序ID. - -远程服务名称(示例中是`Default`)应该匹配动态客户端代理中`AddHttpClientProxies`调用或静态客户端代理中`AddStaticHttpClientProxies`调用中指定的远程服务名称.如果你的客户端只与一个服务器通信,使用`Default`是可以的.但是,如果你的客户端使用多个服务器,你通常在`RemoteServices`配置中有多个键. 你将远程服务端点配置为Dapr应用程序ID,在你使用ABP的客户端代理系统时它将自动工作并通过Dapr进行HTTP调用, - -> 参阅[动态](../API/Dynamic-CSharp-API-Clients.md) 和 [static](../API/Static-CSharp-API-Clients.md)客户端代理文档,了解ABP的客户端代理系统的详细信息. - -## 分布式事件总线集成 - -[ABP的分布式事件总线](../Distributed-Event-Bus.md)系统提供了一个方便的抽象,允许应用程序通过事件异步通信.ABP提供了各种分布式消息系统(如RabbitMQ,Kafka和Azure)的集成包.Dapr也有一个[发布和订阅构建块](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/),用于相同的目的:分布式消息/事件. - -ABP的[Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr)和[Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus)包可以使用Dapr基础设施来实现ABP的分布式事件总线. - -任何类型的应用程序(例如,控制台或ASP.NET Core应用程序)都可以使用[Volo.Abp.EventBus.Dapr]包通过Dapr发布事件.为了能够接收消息(通过订阅事件),你需要安装[Volo.Abp.AspNetCore.Mvc.Dapr.EventBus]包,并且你的应用程序应该是ASP.NET Core应用程序. - -### 安装 - -如果你的应用程序是ASP.NET Core应用程序并且你想发送和接收事件,你需要按照下面的描述安装[Volo.Abp.AspNetCore.Mvc.Dapr.EventBus]包: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)如果你之前没有安装过. -* 在你想要添加`Volo.Abp.AspNetCore.Mvc.Dapr.EventBus`包的`.csproj`文件所在的目录中打开命令行(终端). -* 运行`abp add-package Volo.Abp.AspNetCore.Mvc.Dapr.EventBus`命令. - -如果你想手动添加,安装 [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) NuGet包到你的项目中,并在项目内的[ABP模块](../Module-Development-Basics.md)类中添加`[DependsOn(typeof(AbpAspNetCoreMvcDaprEventBusModule))]`. - -> **如果你安装了[Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus)包, 那么你不需要安装[Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr)包,因为它已经由第一个包引用** - -如果你的应用程序不是ASP.NET Core应用程序,你不能从Dapr接收事件,至少使用ABP的集成包(如果你想在不同类型的应用程序中接收事件,请参阅[Dapr的文档](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/)).但是你仍然可以使用[Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr)包发布消息.在这种情况下,请按照下面的步骤将该包安装到你的项目中: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)如果你之前没有安装过. -* 在你想要添加`Volo.Abp.EventBus.Dapr`包的`.csproj`文件所在的目录中打开命令行(终端). -* 运行`abp add-package Volo.Abp.EventBus.Daprs`命令. - -如果你想手动添加,安装 [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) NuGet包到你的项目中,并在项目内的[ABP模块](../Module-Development-Basics.md)类中添加`[DependsOn(typeof(AbpEventBusDaprModule))]`. - -### 配置 - -你可以为Dapr配置`AbpDaprEventBusOptions`[选项类](../Options.md): - -````csharp -Configure(options => -{ - options.PubSubName = "pubsub"; -}); -```` - -可用的`AbpDaprEventBusOptions`类的属性: - -* `PubSubName` (可选): 通过`DaprClient.PublishEventAsync`方法发布消息时的`pubsubName`参数.默认值:`pubsub`. - -### ABP订阅端点 - -ABP提供了以下端点来接收来自Dapr的事件: - -* `dapr/subscribe`: Dapr使用此端点从应用程序获取订阅列表.ABP会自动返回所有分布式事件处理程序类和具有`Topic`属性的自定义控制器操作的订阅. -* `api/abp/dapr/event`: 用于接收来自Dapr的所有事件的统一端点.ABP根据主题名称将事件分派给您的事件处理程序. - -> **由于ABP会在内部调用`MapSubscribeHandler` 方法,所以你不应该手动调用了.** 如果你想支持[CloudEvents](https://cloudevents.io/)标准,你可以在你的ASP.NET Core管道中使用`app.UseCloudEvents()`中间件. - -### 用法 - -#### ABP的方式 - -你可以按照[ABP的分布式事件总线文档](../Distributed-Event-Bus.md)来学习如何以ABP的方式发布和订阅事件.你的应用程序代码不需要做任何改变就可以使用Dapr的发布-订阅功能.ABP将自动为你的事件处理程序类(实现`IDistributedEventHandler`接口)订阅Dapr. - -ABP提供了 `api/abp/dapr/event` - -**示例:使用`IDistributedEventBus`服务发布事件** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IDistributedEventBus _distributedEventBus; - - public MyService(IDistributedEventBus distributedEventBus) - { - _distributedEventBus = distributedEventBus; - } - - public async Task DoItAsync() - { - await _distributedEventBus.PublishAsync(new StockCountChangedEto - { - ProductCode = "AT837234", - NewStockCount = 42 - }); - } -} -```` - -**示例:通过实现`IDistributedEventHandler`接口来订阅事件** - -````csharp -public class MyHandler : - IDistributedEventHandler, - ITransientDependency -{ - public async Task HandleEventAsync(StockCountChangedEto eventData) - { - var productCode = eventData.ProductCode; - // ... - } -} -```` - -参阅[ABP的分布式事件总线文档](../Distributed-Event-Bus.md)来了解细节. - -#### 使用Dapr API - -在ABP的标准分布式事件总线系统之外,你还可以使用Dapr的API来发布事件. - -> 如果你直接使用Dapr API来发布事件,你可能无法从ABP的标准分布式事件总线功能中受益,比如outbox/inbox模式的实现. - -**示例:使用`DaprClient`发布事件** - -````csharp -public class MyService : ITransientDependency -{ - private readonly DaprClient _daprClient; - - public MyService(DaprClient daprClient) - { - _daprClient = daprClient; - } - - public async Task DoItAsync() - { - await _daprClient.PublishEventAsync( - "pubsub", // pubsub name - "StockChanged", // topic name - new StockCountChangedEto // event data - { - ProductCode = "AT837234", - NewStockCount = 42 - } - ); - } -} -```` - -**示例:通过创建ASP.NET Core控制器来订阅事件** - -````csharp -public class MyController : AbpController -{ - [HttpPost("/stock-changed")] - [Topic("pubsub", "StockChanged")] - public async Task TestRouteAsync([FromBody] StockCountChangedEto model) - { - HttpContext.ValidateDaprAppApiToken(); - - // Do something with the event - return Ok(); - } -} -```` - -`HttpContext.ValidateDaprAppApiToken()` 扩展方法由ABP提供,用于检查请求是否来自Dapr.这是可选的.如果你想启用验证,你应该配置Dapr将App API令牌发送到你的应用程序.如果没有配置,`ValidateDaprAppApiToken()`不会执行任何操作.参阅[Dapr的App API令牌文档](https://docs.dapr.io/operations/security/app-api-token/)了解更多信息.还可以参阅本文档中的**AbpDaprOptions**和**安全**部分. - -参阅[Dapr的文档](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/publish-subscribe)来了解使用Dapr API发送和接收事件的细节. - -## 分布式锁 - -> Dapr的分布式锁功能目前处于Alpha阶段,可能还不稳定.在这一点上,不建议用Dapr来替换ABP的分布式锁. - -ABP提供了一个[分布式锁](../Distributed-Locking.md)抽象来控制多个应用程序对共享资源的访问.Dapr也有一个[分布式锁构建块](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/).[Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr)包使ABP使用Dapr的分布式锁系统. - -### 安装 - -使用ABP CLI将[Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr)NuGet包添加到项目(客户端): - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)如果你之前没有安装过. -* 在你想要添加`Volo.Abp.DistributedLocking.Dapr`包的`.csproj`文件所在的目录中打开命令行(终端). -* 运行`abp add-package Volo.Abp.DistributedLocking.Dapr`命令. - -如果你想手动添加,安装 [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) NuGet包到你的项目中,并在项目内的[ABP模块](../Module-Development-Basics.md)类中添加`[DependsOn(typeof(AbpDistributedLockingDaprModule))]`. - -### 配置 - -你可以在[你的模块](../Module-Development-Basics.md)的`ConfigureServices`方法中使用`AbpDistributedLockDaprOptions`选项类来配置Dapr分布式锁: - -````csharp -Configure(options => -{ - options.StoreName = "mystore"; -}); -```` - -以下选项可用: - -* **`StoreName`** (必需):Dapr使用的存储库名称.锁键名称在同一存储库中范围内.这意味着不同的应用程序可以在不同的存储库中获取相同的锁名称.对于要控制访问的相同资源,请使用相同的存储库名称. -* `Owner` (可选):`DaprClient.Lock`方法使用的`owner`值.如果你不指定,ABP使用一个随机值,这在一般情况下是可以的. -* `DefaultExpirationTimeout` (可选):锁过期后的默认值.默认值:2分钟. - -### 用法 - -你可以注入并使用`IAbpDistributedLock`服务,就像在[分布式锁文档](../Distributed-Locking.md)中解释的那样. - -**示例:** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IAbpDistributedLock _distributedLock; - - public MyService(IAbpDistributedLock distributedLock) - { - _distributedLock = distributedLock; - } - - public async Task MyMethodAsync() - { - await using (var handle = - await _distributedLock.TryAcquireAsync("MyLockName")) - { - if (handle != null) - { - // your code that access the shared resource - } - } - } -} -```` - -这里有两点关于`TryAcquireAsync`方法我们应该提到,与ABP的标准用法不同: - -* `timeout` 参数目前没有使用(即使你指定了它),因为Dapr不支持等待获取锁. -* Dapr 使用过期超时系统(这意味着即使你不通过释放处理程序来释放锁,锁也会在超时后自动释放).但是,ABP的`TryAcquireAsync`方法没有这样的参数.目前,你可以在应用程序中将`AbpDistributedLockDaprOptions.DefaultExpirationTimeout`设置为全局值. - -Dapr的分布式锁功能目前处于Alpha阶段,其API是可能会改变的候选者.如果你想要使用它,你可以这样做,但是要准备好未来的变化.目前,我们建议使用ABP的[分布式锁文档](../Distributed-Locking.md)中提到的[DistributedLock](https://github.com/madelson/DistributedLock)库. - -## 安全 - -如果你使用Dapr,你的应用程序中的大部分或全部传入和传出请求都会通过Dapr.Dapr使用两种API令牌来保护应用程序与Dapr之间的通信. - -### Dapr API Token - -> 这个令牌默认情况下是自动设置的,通常你不需要关心它. - -[在Dapr中启用API令牌身份验证](https://docs.dapr.io/operations/security/api-token/)文档描述了Dapr API令牌是什么以及如何配置.如果你想为你的应用程序启用它,请阅读该文档. - -如果你启用了Dapr API令牌,你应该在你的应用程序中向Dapr发送该令牌.`AbpDaprOptions`定义了一个`DaprApiToken`属性,作为在你的应用程序中配置Dapr API令牌的中心点. - -`DaprApiToken`属性的默认值是从`DAPR_API_TOKEN`环境变量设置的,并且该环境变量是在Dapr运行时设置的.所以,大多数情况下,你不需要在你的应用程序中配置`AbpDaprOptions.DaprApiToken`.但是,如果你需要配置(或覆盖它),你可以在模块类的`ConfigureServices`方法中这样做,如下面的代码块所示: - -````csharp -Configure(options => -{ - options.DaprApiToken = "..."; -}); -```` - -或者你可以在`appsettings.json`文件中设置它: - -````json -"Dapr": { - "DaprApiToken": "..." -} -```` - -一旦你设置了它,它就会在使用`IAbpDaprClientFactory`时使用.如果你需要在应用程序中使用该值,你可以注入`IDaprApiTokenProvider`并使用其`GetDaprApiToken()`方法. - -### App API Token - -> 启用App API令牌验证是强烈推荐的.否则,例如,任何客户端都可以直接调用你的事件订阅端点,你的应用程序就像发生了事件一样(如果你的事件订阅端点中没有其他安全策略). - -[在Dapr中使用令牌身份验证请求身份验证](https://docs.dapr.io/operations/security/app-api-token/)文档描述了App API令牌是什么以及如何配置.如果你想为你的应用程序启用它,请阅读该文档. - -如果你启用了App API令牌,你可以验证它以确保请求来自Dapr.ABP提供了有用的快捷方式来验证它. - -**示例:在事件处理HTTP API中验证App API令牌** - -````csharp -public class MyController : AbpController -{ - [HttpPost("/stock-changed")] - [Topic("pubsub", "StockChanged")] - public async Task TestRouteAsync([FromBody] StockCountChangedEto model) - { - // Validate the App API token! - HttpContext.ValidateDaprAppApiToken(); - - // Do something with the event - return Ok(); - } -} -```` - -`HttpContext.ValidateDaprAppApiToken()` 是ABP框架提供的扩展方法.如果HTTP头中缺少或错误的令牌,则会抛出`AbpAuthorizationException`(头名称为`dapr-api-token`).你也可以注入`IDaprAppApiTokenValidator`并使用其方法在任何服务中验证令牌(不仅仅是在控制器类中). - -你可以配置`AbpDaprOptions.AppApiToken`,如果你想设置(或覆盖)App API令牌值.默认值由`APP_API_TOKEN`环境变量设置.你可以在模块类的`ConfigureServices`方法中这样做,如下面的代码块所示: - -````csharp -Configure(options => -{ - options.AppApiToken = "..."; -}); -```` - -或者你可以在`appsettings.json`文件中设置它: - -````json -"Dapr": { - "AppApiToken": "..." -} -```` - -如果你需要在应用程序中使用该值,你可以注入`IDaprApiTokenProvider`并使用其`GetAppApiToken()`方法. - -## 另请参阅 - -* [Dapr for .NET Developers](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/) -* [Dapr官方文档](https://docs.dapr.io/) diff --git a/docs/zh-Hans/Data-Access.md b/docs/zh-Hans/Data-Access.md deleted file mode 100644 index bf3eb479c8..0000000000 --- a/docs/zh-Hans/Data-Access.md +++ /dev/null @@ -1,15 +0,0 @@ -# 数据访问 - -ABP框架被设计为与数据库无关, 它通过[仓储](Repositories.md)和[工作单元](Unit-Of-Work.md)抽象处理来自任何类型的数据源. - -目前实现了以下数据库访问提供程序: - -* [Entity Framework Core](Entity-Framework-Core.md) (与各种[DBMS和提供程序](https://docs.microsoft.com/zh-cn/ef/core/providers/?tabs=dotnet-core-cli)一起使用.) -* [MongoDB](MongoDB.md) -* [Dapper](Dapper.md) - -## 另请参阅 - -* [连接字符串](Connection-Strings.md) -* [种子数据](Data-Seeding.md) -* [数据过滤](Data-Filtering.md) \ No newline at end of file diff --git a/docs/zh-Hans/Data-Filtering.md b/docs/zh-Hans/Data-Filtering.md deleted file mode 100644 index 8a43ad93b1..0000000000 --- a/docs/zh-Hans/Data-Filtering.md +++ /dev/null @@ -1,255 +0,0 @@ -# 数据过滤 - -[Volo.Abp.Data](https://www.nuget.org/packages/Volo.Abp.Data) 包定义了在查询数据库时自动过滤数据的服务. - -## 预定义的过滤 - -ABP定义了一些开箱即用的过滤. - -### ISoftDelete - -将[实体](Entities.md)标记为已删除,并不是物理删除. 实现 `ISoftDelete` 接口将你的实体"软删除". - -示例: - -````csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities; - -namespace Acme.BookStore -{ - public class Book : AggregateRoot, ISoftDelete - { - public string Name { get; set; } - - public bool IsDeleted { get; set; } //Defined by ISoftDelete - } -} -```` - -`ISoftDelete` 定义了 `IsDeleted` 属性. 当你使用[仓储](Repositories.md)删除一条记录时, ABP会自动将 `IsDeleted` 设置为true,并将删除操作替换为修改操作(如果需要,也可以手动将 `IsDeleted` 设置为true). 在查询数据库时会**自动过滤软删除的实体**. - -> `ISoftDelete` 过滤默认启用, 想要真正的从数据库删除实体需要显示的禁用过滤. 参见下面提到的 `IDataFilter` 服务. - -### IMultiTenant - -[多租户](Multi-Tenancy.md) 是创建 SaaS 应用程序的有效方法. 多租户应用程序通常需要在租户间隔离数据. 实现 `IMultiTenant` 接口使你的实体支持 "多租户". - -示例: - -````csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities; -using Volo.Abp.MultiTenancy; - -namespace Acme.BookStore -{ - public class Book : AggregateRoot, ISoftDelete, IMultiTenant - { - public string Name { get; set; } - - public bool IsDeleted { get; set; } //Defined by ISoftDelete - - public Guid? TenantId { get; set; } //Defined by IMultiTenant - } -} -```` - -`IMultiTenant` 接口定义了 `TenantId` 属性用于自动过滤当前租户实体. 更多信息参见[多租户](Multi-Tenancy.md)文档. - -## IDataFilter 服务: 启用/禁用 数据过滤 - -你可以使用 `IDataFilter` 服务控制数据过滤. - -示例: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class MyBookService : ITransientDependency - { - private readonly IDataFilter _dataFilter; - private readonly IRepository _bookRepository; - - public MyBookService( - IDataFilter dataFilter, - IRepository bookRepository) - { - _dataFilter = dataFilter; - _bookRepository = bookRepository; - } - - public async Task> GetAllBooksIncludingDeletedAsync() - { - //Temporary disable the ISoftDelete filter - using (_dataFilter.Disable()) - { - return await _bookRepository.GetListAsync(); - } - } - } -} -```` - -* [注入](Dependency-Injection.md) `IDataFilter` 服务到你的类中. -* 在 `using` 语句中使用 `Disable` 方法创建一个代码块,其中禁用了 `ISoftDelete` 过滤器(始终与 `using` 搭配使用,确保代码块执行后将过滤重置为之前的状态). - -`IDataFilter.Enable` 方法可以启用过滤. 可以嵌套使用 `Enable` 和 `Disable` 方法定义内部作用域. - -## AbpDataFilterOptions - -`AbpDataFilterOptions` 用于设置数据过滤系统[选项](Options.md). - -下面的示例代码在默认情况下禁用了 `ISoftDelete` 过滤,除非显示启用,在查询数据库时会包含标记为已删除的实体: - -````csharp -Configure(options => -{ - options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false); -}); -```` - -> 更改全局过滤的默认值需要小心,特别是在你使用预构建的模块时该模块可能是在默认启用软删除过滤的情况下开发的. 但你可以安全的为自己定义的数据过滤执行此操作. - -## 自定义数据过滤 - -定义和实现新的过滤很大程序上取决与数据库提供者. ABP为所有的数据库提供者实现了预构建的过滤. - -首先为过滤定义一个接口 (如 `ISoftDelete` 和 `IMultiTenant`) 然后用实体实现它. - -示例: - -````csharp -public interface IIsActive -{ - bool IsActive { get; } -} -```` - -`IIsActive` 接口可以过滤活跃/消极数据,任何[实体](Entities.md)都可以实现它: - -````csharp -public class Book : AggregateRoot, IIsActive -{ - public string Name { get; set; } - - public bool IsActive { get; set; } //Defined by IIsActive -} -```` - -### EntityFramework Core - -ABP使用[EF Core的全局过滤](https://docs.microsoft.com/en-us/ef/core/querying/filters)系统用于[EF Core 集成](Entity-Framework-Core.md). 所以它很好的集成到EF Core中,即使你直接使用 `DbContext` 它也可以正常工作. - -实现自定义过滤的最佳方法是为重写你的 `DbContext` 的 `ShouldFilterEntity` 和 `CreateFilterExpression` 方法. 示例: - -````csharp -protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; - -protected override bool ShouldFilterEntity(IMutableEntityType entityType) -{ - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) - { - return true; - } - - return base.ShouldFilterEntity(entityType); -} - -protected override Expression> CreateFilterExpression() -{ - var expression = base.CreateFilterExpression(); - - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) - { - Expression> isActiveFilter = - e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); - expression = expression == null - ? isActiveFilter - : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); - } - - return expression; -} -```` - -* 添加 `IsActiveFilterEnabled` 属性用于检查是否启用了 `IIsActive` . 内部使用了之前介绍到的 `IDataFilter` 服务. -* 重写 `ShouldFilterEntity` 和 `CreateFilterExpression` 方法检查给定实体是否实现 `IIsActive` 接口,在必要时组合表达式. - -### MongoDB - -ABP抽象了 `IMongoDbRepositoryFilterer` 接口为[MongoDB 集成](MongoDB.md)实现数据过滤, 只有正确的使用仓储,它才会工作. 否则你需要手动过滤数据. - -目前为MongoDB集成实现数据过滤的最佳方法是重写派生自 `MongoDbRepositoryFilterer` 基类的 `FilterQueryable` 方法: - -````csharp -[ExposeServices(typeof(IMongoDbRepositoryFilterer))] -public class BookMongoDbRepositoryFilterer : MongoDbRepositoryFilterer , ITransientDependency -{ - public BookMongoDbRepositoryFilterer( - IDataFilter dataFilter, - ICurrentTenant currentTenant) : - base(dataFilter, currentTenant) - { - } - - public override TQueryable FilterQueryable(TQueryable query) - { - if (DataFilter.IsEnabled()) - { - return (TQueryable)query.Where(x => x.IsActive); - } - - return base.FilterQueryable(query); - } -} -```` - -示例中仅为 `Book` 实体实现了过滤. 如果你想要为所有的实体实现过滤 (实现了 `IIsActive` 接口的实体),可以创建自己的 `MongoDbRepositoryFilterer` 基类并重 写 `AddGlobalFilters` 方法. 如下所示: - -````csharp -public abstract class MyMongoRepository : MongoDbRepository - where TMongoDbContext : IAbpMongoDbContext - where TEntity : class, IEntity -{ - protected MyMongoRepository(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - protected override void AddGlobalFilters(List> filters) - { - base.AddGlobalFilters(filters); - - if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)) - && DataFilter.IsEnabled()) - { - filters.Add(Builders.Filter.Eq(e => ((IIsActive)e).IsActive, true)); - } - } -} - - -public class MyMongoDbModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //....... - context.Services - .Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>))); - } -} -```` - -> 参阅[MongoDb集成文档](MongoDB.md)的 "设置默认仓储" 部分了解如何使用自定义类替换默认仓储. \ No newline at end of file diff --git a/docs/zh-Hans/Data-Seeding.md b/docs/zh-Hans/Data-Seeding.md deleted file mode 100644 index 97e7c7915c..0000000000 --- a/docs/zh-Hans/Data-Seeding.md +++ /dev/null @@ -1,166 +0,0 @@ -# 种子数据 - -## 介绍 - -使用数据库的某些应用程序(或模块),可能需要有一些**初始数据**才能​​够正常启动和运行. 例如**管理员用户**和角色必须在一开始就可用. 否则你就无法**登录**到应用程序创建新用户和角色. - -数据种子也可用于[测试](Testing.md)的目的,你的自动测试可以假定数据库中有一些可用的初始数据. - -### 为什么要有种子数据系统? - -尽管EF Core Data Seeding系统提供了一种方法,但它非常有限,不包括生产场景. 此外它仅适用于EF Core. - -ABP框架提供了种子数据系统; - -* **模块化**: 任何[模块](Module-Development-Basics.md)都可以无声地参与数据播种过程,而不相互了解和影响. 通过这种方式模块将种子化自己的初始数据. -* **数据库独立**: 它不仅适用于 EF Core, 也使用其他数据库提供程序(如 [MongoDB](MongoDB.md)). -* **生产准备**: 它解决了生产环境中的问题. 参见下面的*On Production*部分. -* **依赖注入**: 它充分利用了依赖项注入,你可以在播种初始数据时使用任何内部或外部服务. 实际上你可以做的不仅仅是数据播种. - -## IDataSeedContributor - -将数据种子化到数据库需要实现 `IDataSeedContributor` 接口. - -**示例: 如果没有图书,则向数据库播种一个初始图书** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - private readonly ICurrentTenant _currentTenant; - - public BookStoreDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator, - ICurrentTenant currentTenant) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - _currentTenant = currentTenant; - } - - public async Task SeedAsync(DataSeedContext context) - { - using (_currentTenant.Change(context?.TenantId)) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - var book = new Book( - id: _guidGenerator.Create(), - name: "The Hitchhiker's Guide to the Galaxy", - type: BookType.ScienceFiction, - publishDate: new DateTime(1979, 10, 12), - price: 42 - ); - - await _bookRepository.InsertAsync(book); - } - } - } -} -```` - -* `IDataSeedContributor` 定义了 `SeedAsync` 方法用于执行 **数据种子逻辑**. -* 通常**检查数据库**是否已经存在种子数据. -* 你可以**注入**服务,检查数据播种所需的任何逻辑. - -> 数据种子贡献者由ABP框架自动发现,并作为数据播种过程的一部分执行. - -### DataSeedContext - -如果你的应用程序是[多租户](Multi-Tenancy.md), `DataSeedContext` 包含 `TenantId`,因此你可以在插入数据或基于租户执行自定义逻辑时使用该值. - -`DataSeedContext` 还包含用于从 `IDataSeeder` 传递到种子提供者的name-value配置参数. - -## 模块化 - -一个应用程序可以具有多个种子数据贡献者(`IDataSeedContributor`)类. 任何可重用模块也可以实现此接口播种其自己的初始数据. - -例如[Identity模块](Modules/Identity.md)有一个种子数据贡献者,它创建一个管理角色和管理用户并分配所有权限. - -## IDataSeeder - -> 通常你不需要直接使用 `IDataSeeder` 服务,因为如果你从[应用程序启动模板](Startup-Templates/Application.md)开始,该服务已经完成. 但是建议阅读以了解种子数据系统背后的设计. - -`IDataSeeder` 是用于生成初始数据的主要服务. 使用它很容易; - -````csharp -public class MyService : ITransientDependency -{ - private readonly IDataSeeder _dataSeeder; - - public MyService(IDataSeeder dataSeeder) - { - _dataSeeder = dataSeeder; - } - - public async Task FooAsync() - { - await _dataSeeder.SeedAsync(); - } -} -```` - -你可以[注入](Dependency-Injection.md) `IDataSeeder` 并且在你需要时使用它初始化种子数据. 它内部调用 `IDataSeedContributor` 的实现去完成数据播种. - -可以将命名的配置参数发送到 `SeedAsync` 方法,如下所示: - -````csharp -await _dataSeeder.SeedAsync( - new DataSeedContext() - .WithProperty("MyProperty1", "MyValue1") - .WithProperty("MyProperty2", 42) -); -```` - -然后种子数据提供者可以通过前面解释的 `DataSeedContext` 访问这些属性. - -如果模块需要参数,应该在[模块文档](Modules/Index.md)中声明它. 例如[Identity Module](Modules/Identity.md)使用 `AdminEmail` 和 `AdminPassword` 参数,如果你提供了(默认使用默认值). - -### 在何处以及如何播种数据? - -重要的是要了解在何处以及如何执行 `IDataSeeder.SeedAsync()`. - -#### On Production - -[应用程序启动模板](Startup-Templates/Application.md)带有一个*YourProjectName***.DbMigrator** 项目(图中的Acme.BookStore.DbMigrator). 这是一个**控制台应用程序**,负责**迁移**数据库架构(关系数据库)和初始种子数据: - -![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) - -控制台应用程序已经为你正确配置,它甚至支持**多租户**场景,其中每个租户拥有自己的数据库(迁移和必须的数据库). - -当你将解决方案的**新版本部署到服务器**时,都需要运行这个DbMigrator应用程序. 它会迁移你的**数据库架构**(创建新的表/字段…)和播种正确运行解决方案的新版本所需的**新初始数据**. 然后就可以部署/启动实际的应用程序了. - -即使你使用的是MongoDB或其他NoSQL数据库(不需要进行架构迁移),也建议使用DbMigrator应用程序为你的数据添加种子或执行数据迁移. - -有这样一个单独的控制台应用程序有几个优点; - -* 你可以在更新你的应用程序**之前运行它**,所以你的应用程序可以在准备就绪的数据库上运行. -* 与本身初始化种子数据相比,你的应用程序**启动速度更快**. -* 应用程序可以在**集群环境**中正确运行(其中应用程序的多个实例并发运行). 在这种情况下如果在应用程序启动时播种数据就会有冲突. - -#### On Development - -我们建议以相同的方式进行开发. 每当你[创建数据库迁移](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/)(例如使用EF Core `Add-Migration` 命令)或更改数据种子代码(稍后说明)时,请运行DbMigrator控制台应用程序. - -> 你可以使用EF Core继续执行标准的 `Update-Database` 命令,但是它不会初始化种子数据. - -#### On Testing - -你可能想为自动[测试](Testing.md)初始化数据种子, 这需要使用 `IDataSeeder.SeedAsync()`. 在[应用程序启动模板](Startup-Templates/Application.md)中,它在TestBase项目的*YourProjectName*TestBaseModule类的[OnApplicationInitialization](Module-Development-Basics.md)方法中完成. - -除了标准种子数据(也在生产中使用)之外,你可能还希望为自动测试添加其他种子数据. 你可以在测试项目中创建一个新的数据种子贡献者以处理更多数据. \ No newline at end of file diff --git a/docs/zh-Hans/Data-Transfer-Objects.md b/docs/zh-Hans/Data-Transfer-Objects.md deleted file mode 100644 index 5e3a18bb06..0000000000 --- a/docs/zh-Hans/Data-Transfer-Objects.md +++ /dev/null @@ -1,281 +0,0 @@ -# 数据传输对象 - -## 介绍 - -**数据传输对象**(DTO)用于在**应用层**和**表示层**或其他类型的客户端之间传输数据. - -通常用**DTO**作为参数在表示层(可选)调用[应用服务](Application-Services.md). 它使用领域对象执行某些**特定的业务逻辑**,并(可选)将DTO返回到表示层.因此表示层与领域层完全**隔离**. - -### DTO的需求 - -> 如果你感觉你已经知道并确认使用DTO的好处,你可以**跳过这一节**. - -首先为每个应用程序服务方法创建DTO类可能被看作是一项冗长而耗时的工作. 但是如果正确使用它们,它们可以保存在应用程序. 为什么和如何> - -#### 领域层的抽象 - -DTO提供了一种从表示层**抽象领域对象**的有效方法. 实际上你的**层**被正确地分开了. 如果希望完全更改表示层,可以继续使用现有的应用程序层和领域层. 或者你可以重写领域层完全更改数据库架构,实体和O/RM框架,而无需更改表示层. 当然前提是应用程序服务的契约(方法签名和dto)保持不变. - -#### 数据隐藏 - -假设你有一个具有属性Id,名称,电子邮件地址和密码的 `User` 实体. 如果 `UserAppService` 的 `GetAllUsers()` 方法返回 `List`,任何人都可以访问你所有用户的密码,即使你没有在屏幕上显示它. 这不仅关乎安全,还关乎数据隐藏. 应用程序服务应该只返回表示层(或客户端)所需要的内容,不多也不少. - -#### 序列化和延迟加载问题 - -当你将数据(一个对象)返回到表示层时,它很可能是序列化的. 例如在返回JSON的REST API中,你的对象将被序列化为JSON并发送给客户端. 在这方面将实体返回到表示层可能会有问题,尤其是在使用关系数据库和像Entity Framework Core这样的ORM提供者时. - -在真实的应用程序中你的实体可以相互引用. `User` 实体可以引用它的角色. 如果你想序列化用户,它的角色也必须是序列化的. `Role` 类可以有 `List `,而 `Permission` 类可以有一个对 `PermissionGroup` 类的引用,依此类推...想象一下所有这些对象都被立即序列化了. 你可能会意外地序列化整个数据库! 同样,如果你的对象具有循环引用,则它们可能根本**不会**序列化成功. - -有什么解决方案? 将属性标记为 `NonSerialized` 吗? 不,你不知道什么时候应该序列化什么时候应该序列化. 一个应用程序服务方法可能需要它,而另一个则不需要. 在这种情况下返回安全,可序列化且经过特殊设计的DTO是一个不错的选择. - -几乎所有的O/RM框架都支持延迟加载. 此功能可在需要时从数据库加载实体. 假设 `User` 类具有对 `Role` 类的引用. 当你从数据库中获取用户时,`Role` 属性(或集合)不会被立即填充. 首次读取 `Role` 属性时,它是从数据库加载的. 因此如果将这样的实体返回到表示层,它将通过执行额外的查询从数据库中检索额外的实体. 如果序列化工具读取实体,它会递归读取所有属性,并且可以再次检索整个数据库(如果实体之间存在关系). - -如果在表示层中使用实体,可能会出现更多问题.**最好不要在表示层中引用领域/业务层程序集**. - -如果你确定使用DTO,我们可以继续讨论ABP框架提供的关于dto的建议. - -> ABP并不强迫你使用DTO,但是**强烈建议将DTO作为最佳实践**. - -## 标准接口和基类 - -DTO是一个没有依赖性的简单类,你可以用任何方式进行设计. 但是ABP引入了一些**接口**来确定命名**标准属性**和**基类**的**约定**,以免在声明**公共属性**时**重复工作**. - -**它们都不是必需的**,但是使用它们可以**简化和标准化**应用程序代码. - -### 实体相关DTO - -通常你需要创建与你的实体相对应的DTO,从而生成与实体类似的类. ABP框架在创建DTO时提供了一些基类来简化. - -#### EntityDto - -`IEntityDto` 是一个只定义 `Id` 属性的简单接口. 你可以实现它或从 `EntityDto` 继承. - -**Example:** - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace AbpDemo -{ - public class ProductDto : EntityDto - { - public string Name { get; set; } - //... - } -} -```` - -#### 审计DTO - -如果你的实体继承自被审计的实体类(或实现审计接口)可以使用以下基类来创建DTO: - -* `CreationAuditedEntityDto` -* `CreationAuditedEntityWithUserDto` -* `AuditedEntityDto` -* `AuditedEntityWithUserDto` -* `FullAuditedEntityDto` -* `FullAuditedEntityWithUserDto` - -#### 可扩展的DTO - -如果你想为你的DTO使用[对象扩展系统](Object-Extensions.md),你可以使用或继承以下DTO类: - -* `ExtensibleObject` 实现 `IHasExtraProperties` (其它类继承这个类). -* `ExtensibleEntityDto` -* `ExtensibleCreationAuditedEntityDto` -* `ExtensibleCreationAuditedEntityWithUserDto` -* `ExtensibleAuditedEntityDto` -* `ExtensibleAuditedEntityWithUserDto` -* `ExtensibleFullAuditedEntityDto` -* `ExtensibleFullAuditedEntityWithUserDto` - -### 列表结果 - -通常将DTO列表返回给客户端. `IListResult` 接口和 `ListResultDto` 类用于使其成为标准. - -`IListResult` 接口的定义: - -````csharp -public interface IListResult -{ - IReadOnlyList Items { get; set; } -} -```` - -**示例: 返回产品列表** - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - - public ProductAppService(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task> GetListAsync() - { - //Get entities from the repository - List products = await _productRepository.GetListAsync(); - - //Map entities to DTOs - List productDtos = - ObjectMapper.Map, List>(products); - - //Return the result - return new ListResultDto(productDtos); - } - } -} -```` - -你可以简单地返回 `productDtos` 对象(并更改方法的返回类型), 这也没有错. 返回一个 `ListResultDto` 会使`List` 做为 `Item` 属性包装到另一个对象中. 这具有一个优点:以后可以在不破坏远程客户端的情况下(当它们作为JSON结果获得值时)在返回值中添加更多属性. 在开发可重用的应用程序模块时特别建议使用这种方式. - -### 分页 & 排序列表结果 - -从服务器请求分页列表并将分页列表返回给客户端是更常见的情况. ABP定义了一些接口和类来对其进行标准化: - -#### 输入 (请求) 类型 - -以下接口和类用于标准化客户端发送的输入. - -* `ILimitedResultRequest`: 定义 `MaxResultCount`(`int`) 属性从服务器请求指定数量的结果. -* `IPagedResultRequest`: 继承自 `ILimitedResultRequest` (所以它具有 `MaxResultCount` 属性)并且定义了 `SkipCount` (`int`)用于请求服务器的分页结果时跳过计数. -* `ISortedResultRequest`: 定义 `Sorting` (`string`)属性以请求服务器的排序结果. 排序值可以是“名称”,"*Name*", "*Name DESC*", "*Name ASC, Age DESC*"... 等. -* `IPagedAndSortedResultRequest` 继承自 `IPagedResultRequest` 和 `ISortedResultRequest`,所以它有上述所有属性. - -建议你继承以下基类DTO类之一,而不是手动实现接口: - -* `LimitedResultRequestDto` 实现了 `ILimitedResultRequest`. -* `PagedResultRequestDto` 实现了 `IPagedResultRequest` (和继承自 `LimitedResultRequestDto`). -* `PagedAndSortedResultRequestDto` 实现了 `IPagedAndSortedResultRequest` (和继承自 `PagedResultRequestDto`). - -##### 最大返回数量 - -`LimitedResultRequestDto`(和其它固有的)通过以下规则限制和验证 `MaxResultCount`; - -* 如果客户端未设置 `MaxResultCount`,则假定为**10**(默认页面大小). 可以通过设置 `LimitedResultRequestDto.DefaultMaxResultCoun` t静态属性来更改此值. -* 如果客户端发送的 `MaxResultCount` 大于*1,000**,则会产生**验证错误**. 保护服务器免受滥用服务很重要. 如果需要可以通过设置 `LimitedResultRequestDto.MaxMaxResultCount` 静态属性来更改此值. - -建议在应用程序启动时设置静态属性,因为它们是静态的(全局). - -#### 输出 (响应) 类型 - -以下接口和类用于标准化发送给客户端的输出. - -* `IHasTotalCount` 定义 `TotalCount`(`long`)属性以在分页的情况下返回记录的总数. -* `IPagedResult` 集成自 `IListResult` 和 `IHasTotalCount`, 所以它有 `Items` 和 `TotalCount` 属性. - -建议你继承以下基类DTO类之一,而不是手动实现接口: - -* `PagedResultDto` 继承自 `ListResultDto` 和实现了 `IPagedResult`. - -**示例: 从服务器请求分页和排序的结果并返回分页列表** - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - - public ProductAppService(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task> GetListAsync( - PagedAndSortedResultRequestDto input) - { - //Create the query - var query = _productRepository - .OrderBy(input.Sorting); - - //Get total count from the repository - var totalCount = await query.CountAsync(); - - //Get entities from the repository - List products = await query - .Skip(input.SkipCount) - .Take(input.MaxResultCount) - .ToListAsync(); - - //Map entities to DTOs - List productDtos = - ObjectMapper.Map, List>(products); - - //Return the result - return new PagedResultDto(totalCount, productDtos); - } - } -} -```` - -ABP框架还定义了一种 `PageBy` 扩展方法(与`IPagedResultRequest`兼容),可用于代替 `Skip` + `Take`调用: - -````csharp -var query = _productRepository - .OrderBy(input.Sorting) - .PageBy(input); -```` - -> 注意我们将`Volo.Abp.EntityFrameworkCore`包添加到项目中以使用 `ToListAsync` 和 `CountAsync` 方法,因为它们不包含在标准LINQ中,而是由Entity Framework Core定义. - -如果你不了解示例代码,另请参阅[仓储文档](Repositories.md). - -## 相关话题 - -### 验证 - -[应用服务](Application-Services.md)方法,控制器操作,页面模型输入...的输入会自动验证. 你可以使用标准数据注释属性或自定义验证方法来执行验证. - -参阅[验证文档](Validation.md)了解更多. - -### 对象到对象的映射 - -创建与实体相关的DTO时通常需要映射这些对象. ABP提供了一个对象到对象的映射系统简化映射过程. 请参阅以下文档: - -* [对象到对象映射文档](Object-To-Object-Mapping.md)介绍了这些功能. -* [应用服务文档](Application-Services.md)提供了完整的示例. - -## 最佳实践 - -你可以自由设计DTO类,然而这里有一些你可能想要遵循的最佳实践和建议. - -### 共同原则 - -* DTO应该是**可序列化的**,因为它们通常是序列化和反序列化的(JSON或其他格式). 如果你有另一个带参数的构造函数,建议使用空(无参数)的公共构造函数. -* 除某些[验证](Validation.md)代码外,DTO**不应包含任何业务逻辑**. -* DTO不要继承实体,也**不要引用实体**. [应用程序启动模板](Startup-Templates/Application.md)已经通过分隔项目来阻止它. -* 如果你使用自动[对象到对象](Object-To-Object-Mapping.md)映射库,如AutoMapper,请启用**映射配置验证**以防止潜在的错误. - -### 输入DTO原则 - -* 只定义用例**所需的属性**. 不要包含不用于用例的属性,这样做会使开发人员感到困惑. - -* **不要在**不同的应用程序服务方法之间重用输入DTO. 因为不同的用例将需要和使用DTO的不同属性,从而导致某些属性在某些情况下没有使用,这使得理解和使用服务更加困难,并在将来导致潜在的错误. - -### 输出DTO原则 - -* 如果在所有情况下填充**所有属性**,就可以**重用输出DTO**. diff --git a/docs/zh-Hans/Dependency-Injection.md b/docs/zh-Hans/Dependency-Injection.md deleted file mode 100644 index 4cc6616f52..0000000000 --- a/docs/zh-Hans/Dependency-Injection.md +++ /dev/null @@ -1,340 +0,0 @@ -## 依赖注入 - -ABP的依赖注入系统是基于Microsoft的[依赖注入扩展](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection)库(Microsoft.Extensions.DependencyInjection nuget包)开发的.因此,它的文档在ABP中也是有效的. - -> 虽然ABP框架没有对任何第三方DI提供程序的核心依赖, 但它必须使用一个提供程序来支持动态代理(dynamic proxying)和一些高级特性以便ABP特性能正常工作.启动模板中已安装了Autofac. 更多信息请参阅 [Autofac 集成](Autofac-Integration.md) 文档. - -### 模块化 - -由于ABP是一个模块化框架,因此每个模块都定义它自己的服务并在它自己的单独[模块类](Module-Development-Basics.md)中通过依赖注入进行注册.例: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //在此处注入依赖项 - } -} -```` - -### 依照约定的注册 - -ABP引入了依照约定的服务注册.依照约定你无需做任何事,它会自动完成.如果要禁用它,你可以通过重写`PreConfigureServices`方法,设置`SkipAutoServiceRegistration`为`true`. - -````C# -public class BlogModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - SkipAutoServiceRegistration = true; - } -} -```` - -一旦跳过自动注册,你应该手动注册你的服务.在这种情况下,`AddAssemblyOf`扩展方法可以帮助你依照约定注册所有服务.例: - -````c# -public class BlogModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - SkipAutoServiceRegistration = true; - } - - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAssemblyOf(); - } -} -```` - -以下部分解释了约定和配置. - -#### 固有的注册类型 - -一些特定类型会默认注册到依赖注入.例子: - -* 模块类注册为singleton. -* MVC控制器(继承``Controller``或``AbpController``)被注册为transient. -* MVC页面模型(继承``PageModel``或``AbpPageModel``)被注册为transient. -* MVC视图组件(继承``ViewComponent``或``AbpViewComponent``)被注册为transient. -* 应用程序服务(实现``IApplicationService``接口或继承``ApplicationService``类)注册为transient. -* 存储库(实现``IRepository``接口)注册为transient. -* 域服务(实现``IDomainService``接口)注册为transient. - -示例: - -````C# -public class BlogPostAppService : ApplicationService -{ -} -```` - -``BlogPostAppService`` 由于它是从已知的基类派生的,因此会自动注册为transient生命周期. - -#### 依赖接口 - -如果实现这些接口,则会自动将类注册到依赖注入: - -* ``ITransientDependency`` 注册为transient生命周期. -* ``ISingletonDependency`` 注册为singleton生命周期. -* ``IScopedDependency`` 注册为scoped生命周期. - -示例: - -````C# -public class TaxCalculator : ITransientDependency -{ -} -```` - -``TaxCalculator``因为实现了``ITransientDependency``,所以它会自动注册为transient生命周期. - -#### Dependency 特性 - -配置依赖注入服务的另一种方法是使用``DependencyAttribute``.它具有以下属性: - -* ``Lifetime``: 注册的生命周期:Singleton,Transient或Scoped. -* ``TryRegister``: 设置``true``则只注册以前未注册的服务.使用IServiceCollection的TryAdd ... 扩展方法. -* ``ReplaceServices``: 设置``true``则替换之前已经注册过的服务.使用IServiceCollection的Replace扩展方法. - -示例: - -````C# -[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -public class TaxCalculator -{ - -} - -```` - -如果定义了``Lifetime``属性,则``Dependency``特性具有比其他依赖接口更高的优先级. - -#### ExposeServices 特性 - -``ExposeServicesAttribute``用于控制相关类提供了什么服务.例: - -````C# -[ExposeServices(typeof(ITaxCalculator))] -public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency -{ - -} -```` - -``TaxCalculator``类只公开``ITaxCalculator``接口.这意味着你只能注入``ITaxCalculator``,但不能注入``TaxCalculator``或``ICalculator``到你的应用程序中. - -#### 依照约定公开的服务 - -如果你未指定要公开的服务,则ABP依照约定公开服务.以上面定义的``TaxCalculator``为例: - -* 默认情况下,类本身是公开的.这意味着你可以按``TaxCalculator``类注入它. -* 默认情况下,默认接口是公开的.默认接口是由命名约定确定.在这个例子中,``ICalculator``和``ITaxCalculator``是``TaxCalculator``的默认接口,但``ICanCalculate``不是. - -#### 组合到一起 - -只要有意义,特性和接口是可以组合在一起使用的. - -````C# -[Dependency(ReplaceServices = true)] -[ExposeServices(typeof(ITaxCalculator))] -public class TaxCalculator : ITaxCalculator, ITransientDependency -{ - -} -```` - -#### 手动注册 - -在某些情况下,你可能需要向``IServiceCollection``手动注册服务,尤其是在需要使用自定义工厂方法或singleton实例时.在这种情况下,你可以像[Microsoft文档](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection)描述的那样直接添加服务.例: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //注册一个singleton实例 - context.Services.AddSingleton(new TaxCalculator(taxRatio: 0.18)); - - //注册一个从IServiceProvider解析得来的工厂方法 - context.Services.AddScoped(sp => sp.GetRequiredService()); - } -} -```` - -### 注入依赖关系 - -使用已注册的服务有三种常用方法. - -#### 构造方法注入 - -这是将服务注入类的最常用方法.例如: - -````C# -public class TaxAppService : ApplicationService -{ - private readonly ITaxCalculator _taxCalculator; - - public TaxAppService(ITaxCalculator taxCalculator) - { - _taxCalculator = taxCalculator; - } - - public void DoSomething() - { - //...使用 _taxCalculator... - } -} -```` - -``TaxAppService``在构造方法中得到``ITaxCalculator``.依赖注入系统在运行时自动提供所请求的服务. - -构造方法注入是将依赖项注入类的首选方式.这样,除非提供了所有构造方法注入的依赖项,否则无法构造类.因此,该类明确的声明了它必需的服务. - -#### 属性注入 - -Microsoft依赖注入库不支持属性注入.但是,ABP可以与第三方DI提供商(例如[Autofac](https://autofac.org/))集成,以实现属性注入.例: - -````C# -public class MyService : ITransientDependency -{ - public ILogger Logger { get; set; } - - public MyService() - { - Logger = NullLogger.Instance; - } - - public void DoSomething() - { - //...使用 Logger 写日志... - } -} -```` - -对于属性注入依赖项,使用公开的setter声明公共属性.这允许DI框架在创建类之后设置它. - -属性注入依赖项通常被视为可选依赖项.这意味着没有它们,服务也可以正常工作.``Logger``就是这样的依赖项,``MyService``可以继续工作而无需日志记录. - -为了使依赖项成为可选的,我们通常会为依赖项设置默认/后备(fallback)值.在此示例中,NullLogger用作后备.因此,如果DI框架或你在创建``MyService``后未设置Logger属性,则``MyService``依然可以工作但不写日志. - -属性注入的一个限制是你不能在构造函数中使用依赖项,因为它是在对象构造之后设置的. - -当你想要设计一个默认注入了一些公共服务的基类时,属性注入也很有用.如果你打算使用构造方法注入,那么所有派生类也应该将依赖的服务注入到它们自己的构造方法中,这使得开发更加困难.但是,对于非可选服务使用属性注入要非常小心,因为它使得类的要求难以清楚地看到. - -#### 从IServiceProvider解析服务 - -你可能希望直接从``IServiceProvider``解析服务.在这种情况下,你可以将``IServiceProvider``注入到你的类并使用``GetService``方法,如下所示: - -````C# -public class MyService : ITransientDependency -{ - private readonly IServiceProvider _serviceProvider; - - public MyService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public void DoSomething() - { - var taxCalculator = _serviceProvider.GetService(); - //... - } -} -```` - -#### 释放/处理(Releasing/Disposing)服务 - -如果你使用了构造函数或属性注入,则无需担心释放服务的资源.但是,如果你从``IServiceProvider``解析了服务,在某些情况下,你可能需要注意释放服务. - -ASP.NET Core会在当前HTTP请求结束时释放所有服务,即使你直接从``IServiceProvider``解析了服务(假设你注入了IServiceProvider).但是,在某些情况下,你可能希望释放/处理手动解析的服务: - -* 你的代码在AspNet Core请求之外执行,执行者没有处理服务范围. -* 你只有对根服务提供者的引用. -* 你可能希望立即释放和处理服务(例如,你可能会创建太多具有大量内存占用且不想过度使用内存的服务). - -在任何情况下,你都可以使用这样的`using`代码块来安全地立即释放服务: - -````C# -using (var scope = _serviceProvider.CreateScope()) -{ - var service1 = scope.ServiceProvider.GetService(); - var service2 = scope.ServiceProvider.GetService(); -} -```` - -两个服务在创建的scope被处理时(在using块的末尾)释放. - -## 高级特性 - -### IServiceCollection.OnRegistered 事件 - -你可能想在注册到依赖注入的每个服务上执行一个操作, 在你的模块的 `PreConfigureServices` 方法中, 使用 `OnRegistered` 方法注册一个回调(callback) , 如下所示: - -````csharp -public class AppModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - context.Services.OnRegistered(ctx => - { - var type = ctx.ImplementationType; - //... - }); - } -} -```` - -`ImplementationType` 提供了服务类型. 该回调(callback)通常用于向服务添加拦截器. 例如: - -````csharp -public class AppModule : AbpModule -{ - public override void PreConfigureServices(ServiceConfigurationContext context) - { - context.Services.OnRegistered(ctx => - { - if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true)) - { - ctx.Interceptors.TryAdd(); - } - }); - } -} -```` - -这个示例判断一个服务类是否具有 `MyLogAttribute` 特性, 如果有的话就添加一个 `MyLogInterceptor` 到拦截器集合中. - -> 注意, 如果服务类公开了多于一个服务或接口, `OnRegistered` 回调(callback)可能被同一服务类多次调用. 因此, 较安全的方法是使用 `Interceptors.TryAdd` 方法而不是 `Interceptors.Add` 方法. 请参阅动态代理(dynamic proxying)/拦截器 [文档](Dynamic-Proxying-Interceptors.md). - -### IServiceCollection.OnActivated 事件 - -一旦服务完全构建完成`OnActivated`事件就会触发. 你可以执行依赖于服务已完全构建的的一些任务, 虽然这种情况可能很少见. - -````csharp -var serviceDescriptor = ServiceDescriptor.Transient(); -services.Add(serviceDescriptor); -if (setIsReadOnly) -{ - services.OnActivated(serviceDescriptor, x => - { - x.Instance.As().IsReadOnly = true; - }); -} -```` - -> 注意,`OnActivated`事件可以为一个`ServiceDescriptor`注册多次. - -## 第三方提供程序 - -虽然ABP框架没有对任何第三方DI提供程序的核心依赖, 但它必须使用一个提供程序来支持动态代理(dynamic proxying)和一些高级特性以便ABP特性能正常工作. - -启动模板中已安装了Autofac. 更多信息请参阅 [Autofac 集成](Autofac-Integration.md) 文档. - - -### 请参阅 - -* [ASP.NET Core依赖注入最佳实践,提示和技巧](https://blog.abp.io/asp-net-core-dependency-injection-best-practices-tips-tricks) diff --git a/docs/zh-Hans/Deployment/Clustered-Environment.md b/docs/zh-Hans/Deployment/Clustered-Environment.md deleted file mode 100644 index a32b9b6ceb..0000000000 --- a/docs/zh-Hans/Deployment/Clustered-Environment.md +++ /dev/null @@ -1,99 +0,0 @@ -# 部署到集群环境 - -本文档介绍了在将应用程序部署到**多个应用程序实例同时运行**的集群环境中时应注意的内容, 并解释了如何在基于ABP的应用程序中处理这些内容. - -> 无论你使用的是单体式应用程序还是微服务解决方案, 本文档均有效. 适用于一个流程. 应用程序可以是单体式web应用程序、微服务解决方案中的服务、控制台应用程序或其他类型的可执行进程. -> -> 例如, 如果你将应用程序部署到Kubernetes并把应用程序或服务在多个POD中运行, 那么应用程序或服务将在集群环境中运行. - -## 了解集群环境 - -> 如果你已经熟悉集群部署和负载均衡器, 可以跳过本节. - -### 单实例部署 - -考虑作为**单个实例**部署的应用程序, 如下图所示: - -![deployment-single-instance](../images/deployment-single-instance.png) - -浏览器和其他客户端应用程序可以直接向应用程序发出HTTP请求. 你可以在客户端和应用程序之间放置一个web服务器(例如IIS或NGINX), 但仍有一个应用程序实例在单个服务器或容器中运行. 单实例的配置**限于规模**, 因为它在一台服务器上运行, 并且你受到服务器容量的限制. - -### 集群部署 - -**集群部署**是在一台或多台服务器上**同时运行**应用程序**多个实例**的方式. 通过这种方式, 不同的实例可以满足不同的请求, 并且可以通过在系统中添加新服务器来扩展. 下图显示了集群使用**负载均衡器**的典型实现: - -![deployment-clustered](../images/deployment-clustered.png) - -### 负载均衡器 - -[负载均衡器](https://en.wikipedia.org/wiki/Load_balancing_(computing)) 有很多特性, 但它们基本上会将**传入的HTTP请求转发**给应用程序的实例, 并将响应返回给客户端应用程序. - -负载平衡器可以使用不同的算法来选择应用程序实例, 同时确定用于传递传入请求的应用程序实例. **循环**是最简单、最常用的算法之一. 请求被轮流传递到应用程序实例. 第一个实例得到第一个请求, 第二个实例得到第二个请求, 依此类推. 在所有实例都被使用之后, 它返回到第一个实例, 并且下一个请求的算法也是类似的. - -### 潜在问题 - -一旦应用程序的多个实例并行运行, 你应该仔细考虑以下内容: - -* 当你有多个实例时, 存储在应用程序 **内存中的任何状态(数据)** 都将成为问题. 存储在应用程序实例内存中的状态可能在下一个请求中不可用, 因为下一个请求将由不同的应用程序实例处理. 虽然有一些解决方案(比如粘性会话)可以解决这个问题, 但如果你想在集群、容器或云中运行应用程序, **最好将其设计为无状态**. -* **内存缓存** 是一种内存状态, 不应在集群应用程序中使用. 你应该使用**分布式缓存**. -* 你不应该在**本地文件系统**中存储应用程序所有实例都可以使用的数据. 不同的应用程序实例可能在不同的容器或服务器中运行, 并且它们可能无法访问同一个文件系统. 你可以使用**云或外部存储提供商**作为解决方案. -* 如果你有**后台工作者**或**作业队列管理器**, 则应小心, 因为多个实例可能会尝试执行同一作业或同时执行同一工作. 因此, 你可能会多次完成相同的工作, 或者在尝试访问和更改相同的资源时可能会出现很多错误. - -集群部署可能会有更多问题, 但这些是最常见的问题. ABP被设计为与集群部署场景兼容. 以下各节介绍了将基于ABP的应用程序部署到集群环境时应执行的操作. - -## 切换分布式缓存 - -ASP.NET Core提供了不同类型的缓存功能. [内存缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory)将对象存储在本地服务器的内存中, 并且仅对存储该对象的应用程序可用. 集群环境中的非粘性会话应使用[分布式缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed), 除了一些特定场景(例如, 你可以将本地CSS文件缓存到内存中. 它是只读数据, 在所有应用程序实例中都是相同的. 出于性能原因, 你可以将其缓存到内存中, 而不会出现任何问题). - -[ABP的分布式缓存](../Caching.md)扩展了[ASP.NET Core的分布式缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)的基础设施. 默认情况下, 它在内存中工作. 当你要将应用程序部署到集群环境时, 应该配置实际的分布式缓存提供程序. - -> 即使应用程序不直接使用`IDistributedCache`, 也应该为集群部署配置缓存提供程序. 因为ABP框架和预构建的[应用程序模块](../Modules/Index.md)正在使用分布式缓存. - -ASP.NET Core提供了可以用作分布式缓存提供程序的多种集成, 如[Redis](https://redis.io/)和[NCache](https://www.alachisoft.com/ncache/). 你可以按照[微软文档](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)了解如何在应用程序中使用它们. - -如果你决定使用Redis作为分布式缓存提供程序, **请遵循[ABP的Redis缓存集成文档](../Redis-Cache.md)** 了解将其安装到应用程序并配置Redis所需遵循的步骤. - -> 根据你在创建新ABP解决方案时的偏好, Redis缓存可能会预先安装在你的解决方案中. 例如, 如果你在MVC UI中选择了*Tiered*选项, Redis缓存将进行预装. 因为, 在这种情况下, 解决方案中有两个应用程序, 它们应该使用相同的缓存源来保持一致. - -## 使用合适的BLOB存储提供程序 - -如果你在[文件系统提供程序](../Blob-Storing-File-System.md)中使用了ABP的[BLOB存储](../Blob-Storing.md)功能, 则应该在集群环境中使用另一个提供程序, 因为文件系统提供程序使用应用程序的本地文件系统. - -[数据库BLOB提供程序](../Blob-Storing-Database)是最简单的方法, 因为它使用应用程序的主数据库(或另一个数据库, 如果你配置的话)来存储BLOB. 但是, 你应该记住, BLOB是大型对象, 可能会迅速增加数据库的大小. - -> [ABP商业版](https://commercial.abp.io/)启动解决方案模板预装了数据库BLOB提供程序, 并将BLOB存储在应用程序的数据库中. - -查看[BLOB Storing](../Blob-Storing.md)文档以查看所有可用的BLOB存储提供程序. - -## 配置后台作业 - -ABP的[后台作业系统](../Background-Jobs.md)将要在后台执行的任务进行排队. 后台作业队列是持久性的, 排队的任务能够保证执行(如果失败, 将重新尝试). - -ABP的默认后台作业管理器与集群环境兼容. 它使用[分布式锁](../Distributed-Locking.md)来确保一次只能在单个应用程序实例中执行作业. 请参阅下面的*配置分布式锁提供程序*部分, 了解如何为应用程序配置分布式锁提供程序, 以便默认后台作业管理器在集群环境中正常工作. - -如果不想使用分布式锁提供程序, 可以使用以下选项: - -* 停止所有应用程序实例中的后台作业管理器(将`AbpBackgroundJobOptions.IsJobExecutionEnabled`设置为`false`)只保留其中一个应用程序实例, 以便只有单个实例执行作业(而其他应用程序实例仍可以对作业进行排队). -* 在所有应用程序实例中停止后台作业管理器(将`AbpBackgroundJobOptions.IsJobExecutionEnabled`设置为`false`), 并创建一个专用的应用程序(可能是在自己的容器中运行的控制台应用程序或在后台运行的Windows服务)来执行所有后台作业. 如果你的后台作业占用大量系统资源(CPU、RAM或磁盘), 那么这是一个不错的选择, 这样你就可以将该后台应用程序部署到专用服务器上, 并且后台作业不会影响应用程序的性能. - -> 如果你使用的是外部后台作业集成(例如[Hangfire](../Background-Workers-Hangfire.md)或[Quartz](../Background-Workers-Quartz.md))而不是默认的后台作业管理器, 请参阅提供程序的文档, 了解如何为集群环境配置它. - -## 配置分布式锁提供程序 - -ABP通过[分布式锁](https://github.com/madelson/DistributedLock)库实现了一个抽象的分布式锁. 分布式锁用于控制多个应用程序对共享资源的并发访问, 以防止由于并发写入而导致资源损坏. ABP框架和一些预构建的[应用程序模块](../Modules/Index.md)出于一些原因正在使用分布式锁. - -但是, 分布式锁系统默认在进程中工作. 这意味着它实际上不是分布式的, 除非配置分布式锁提供程序. 因此, 如果尚未配置应用程序的提供程序, 请按照[分布式锁](../Distributed-Locking.md)文档为其配置提供程序. - -## 实现后台工作者 - -ASP.NET Core[托管服务](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services), ABP提供[后台工作者](../Background-Workers.md)在应用程序的后台线程中执行任务. - -如果你的应用程序有在后台运行的任务, 你应该注意它们在集群环境中的行为, 尤其是在后台任务使用相同资源的情况下. 你应该设计后台任务, 以便它们在集群环境中继续正常工作. - -假设SaaS应用程序中的后台工作者检查用户订阅, 并在订阅续订日期临近时发送电子邮件. 如果后台任务在多个应用程序实例中运行, 可能会多次向某些用户发送同一封电子邮件, 这会影响他们. - -我们建议你使用以下方法之一来解决此问题: - -* 实现你的后台工作者, 以便他们在集群环境中工作时不会出现任何问题. 使用[分布式锁](../Distributed-Locking.md)来确保并发控制是一种方法. 应用程序实例中的后台工作者可能会处理分布式锁, 因此其他应用程序实例中的工作者将等待该锁. 这样, 只有一个工作者在实际工作, 而其他的则在等待. 如果你实现了这一点, 你的后台工作者就可以安全地运行, 不必关心应用程序是如何部署的. -* 停止所有应用程序实例中的后台工作者(将`AbpBackgroundWorkerOptions.IsEnabled`设置为`false`), 只保留其中一个应用程序实例, 因此只有单个实例运行这些后台工作者. -* 停止所有应用程序实例中的后台工作者(将`AbpBackgroundWorkerOptions.IsEnabled`设置为`false`), 并创建一个专用的应用程序(可能是在自己的容器中运行的控制台应用程序或在后台运行的Windows服务)来执行所有后台任务. 如果你的后台工作者消耗大量系统资源(CPU、RAM或磁盘), 那么这是一个不错的选择, 这样你就可以将该后台应用程序部署到专用服务器上, 并且你的后台任务不会影响应用程序的性能. diff --git a/docs/zh-Hans/Deployment/Index.md b/docs/zh-Hans/Deployment/Index.md deleted file mode 100644 index 6c58358eee..0000000000 --- a/docs/zh-Hans/Deployment/Index.md +++ /dev/null @@ -1,9 +0,0 @@ -# 部署 - -部署ABP应用程序与部署其他.NET或ASP.NET Core应用程序并没有什么不同. 你可以将其部署到云服务提供商(例如Azure、AWS、Google)或内部部署服务器、IIS或任何其他web服务器. ABP的文档中没有太多关于部署的信息. 你可以参考提供商的文档. - -但是, 在部署应用程序时, 有些主题是你应该注意的. 其中大多数是一般的软件部署注意事项, 但你应该了解如何在基于ABP的应用程序中处理它们. 我们为此准备了指南, 建议你在设计部署配置之前仔细阅读这些指南. - -## 指南 - -* [部署到集群环境](Clustered-Environment.md): 讲解了当你希望同时运行应用程序的多个实例时, 如何来配置应用程序. diff --git a/docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md b/docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md deleted file mode 100644 index 79e65db414..0000000000 --- a/docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md +++ /dev/null @@ -1,163 +0,0 @@ -# 分布式事件总线Kafka集成 - -> 本文解释了 **如何配置[Kafka](https://kafka.apache.org/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统. - -## 安装 - -使用ABP CLI添加[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka)NuGet包到你的项目: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. -* 在你想要安装 `Volo.Abp.EventBus.Kafka` 包的 `.csproj` 文件目录打开命令行(终端). -* 运行 `abp add-package Volo.Abp.EventBus.Kafka` 命令. - -如果你想要手动安装,安装[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusKafkaModule))]` 到你的项目[模块](Module-Development-Basics.md)类. - -## 配置 - -可以使用配置使用标准的[配置系统](Configuration.md),如 `appsettings.json` 文件,或[选项](Options.md)类. - -### `appsettings.json` 文件配置 - -这是配置Kafka设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量). - -**示例:使用默认配置连接到本地Kafka服务器的最小配置** - - -````json -{ - "Kafka": { - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName" - } - } -} -```` - -* `MyGroupId` 是应用程序的名称,用于Kafka的**GroupId**. -* `MyTopicName` 是**topic名称**. - -参阅[Kafka文档](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html)更好的了解这些选项. - -#### 连接 - -如果需要连接到本地主机以外的另一台服务器,需要配置连接属性. - -**示例: 指定主机名 (如IP地址)** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092" - } - }, - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName" - } - } -} -```` - -允许定义多个连接. 在这种情况下,你可以指定用于事件总线的连接. - -**示例: 声明两个连接并将其中一个用于事件总线** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092" - }, - "SecondConnection": { - "BootstrapServers": "321.321.321.321:9092" - } - }, - "EventBus": { - "GroupId": "MyGroupId", - "TopicName": "MyTopicName", - "ConnectionName": "SecondConnection" - } - } -} -```` - -这允许你可以在你的应用程序使用多个Kafka服务器,但将其中一个做为事件总线. - -你可以使用任何[ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html)属性作为连接属性. - -**示例: 指定socket超时时间** - -````json -{ - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "123.123.123.123:9092", - "SocketTimeoutMs": 60000 - } - } - } -} -```` - -### 选项类 - -`AbpKafkaOptions` 和 `AbpKafkaEventBusOptions` 类用于配置Kafka的连接字符串和事件总线选项. - -你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项. - -**示例: 配置连接** - -````csharp -Configure(options => -{ - options.Connections.Default.BootstrapServers = "123.123.123.123:9092"; - options.Connections.Default.SaslUsername = "user"; - options.Connections.Default.SaslPassword = "pwd"; -}); -```` - -**示例: 配置 consumer config** - -````csharp -Configure(options => -{ - options.ConfigureConsumer = config => - { - config.GroupId = "MyGroupId"; - config.EnableAutoCommit = false; - }; -}); -```` - -**示例: 配置 producer config** - -````csharp -Configure(options => -{ - options.ConfigureProducer = config => - { - config.MessageTimeoutMs = 6000; - config.Acks = Acks.All; - }; -}); -```` - -**示例: 配置 topic specification** - -````csharp -Configure(options => -{ - options.ConfigureTopic = specification => - { - specification.ReplicationFactor = 3; - specification.NumPartitions = 3; - }; -}); -```` - -使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值. diff --git a/docs/zh-Hans/Distributed-Event-Bus-RabbitMQ-Integration.md b/docs/zh-Hans/Distributed-Event-Bus-RabbitMQ-Integration.md deleted file mode 100644 index 57530e1d6f..0000000000 --- a/docs/zh-Hans/Distributed-Event-Bus-RabbitMQ-Integration.md +++ /dev/null @@ -1,165 +0,0 @@ -# 分布式事件总线RabbitMQ集成 - -> 本文解释了 **如何配置[RabbitMQ](https://www.rabbitmq.com/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统. - -## 安装 - -使用ABP CLI添加[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)NuGet包到你的项目: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. -* 在你想要安装 `Volo.Abp.EventBus.RabbitMQ` 包的 `.csproj` 文件目录打开命令行(终端). -* 运行 `abp add-package Volo.Abp.EventBus.RabbitMQ` 命令. - -如果你想要手动安装,安装[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusRabbitMqModule))]` 到你的项目[模块](Module-Development-Basics.md)类. - -## 配置 - -可以使用配置使用标准的[配置系统](Configuration.md),如 `appsettings.json` 文件,或[选项](Options.md)类. - -### `appsettings.json` 文件配置 - -这是配置RabbitMQ设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量). - -**示例:最小化配置与默认配置连接到本地的RabbitMQ服务器** - -````json -{ - "RabbitMQ": { - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -```` - -* `ClientName` 是应用程序的名称,用于RabbitMQ的**队列名称**. -* `ExchangeName` 是 **交换机名称**. - -参阅[RabbitMQ文档](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues)更好的了解这些选项. - -#### 连接 - -如果需要连接到本地主机以外的另一台服务器,需要配置连接属性. - -**示例: 指定主机名 (如IP地址)** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -```` - -允许定义多个连接. 在这种情况下,你可以指定用于事件总线的连接. - -**示例: 声明两个连接并将其中一个用于事件总线** - -````json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123" - }, - "SecondConnection": { - "HostName": "321.321.321.321" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName", - "ConnectionName": "SecondConnection" - } - } -} -```` - -这允许你可以在你的应用程序使用多个RabbitMQ服务器,但将其中一个做为事件总线. - -你可以使用任何[ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties)属性作为连接属性. - -**示例: 指定连接端口** - -````csharp -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123", - "Port": "5672" - } - } - } -} -```` - -如果需要连接到 RabbitMQ 集群,你可以指定多个 HostName。 - -**示例: 连接到 RabbitMQ 集群** - -```json -{ - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "123.123.123.123;234.234.234.234" - } - }, - "EventBus": { - "ClientName": "MyClientName", - "ExchangeName": "MyExchangeName" - } - } -} -``` - -### 选项类 - -`AbpRabbitMqOptions` 和 `AbpRabbitMqEventBusOptions` 类用于配置RabbitMQ的连接字符串和事件总线选项. - -你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项. - -**示例: 配置连接** - -````csharp -Configure(options => -{ - options.Connections.Default.UserName = "user"; - options.Connections.Default.Password = "pass"; - options.Connections.Default.HostName = "123.123.123.123"; - options.Connections.Default.Port = 5672; -}); -```` - -**示例: 配置客户端,交换机名称和预取数量** - -````csharp -Configure(options => -{ - options.ClientName = "TestApp1"; - options.ExchangeName = "TestMessages"; - options.PrefetchCount = 1; -}); -```` - -**示例:配置队列和交换机的额外参数** - -```csharp -Configure(options => -{ - options.ExchangeArguments["x-delayed-type"] = "direct"; - options.QueueArguments["x-message-ttl"] = 60000; -}); -``` - -使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值. diff --git a/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md b/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md deleted file mode 100644 index 72806038b3..0000000000 --- a/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md +++ /dev/null @@ -1,65 +0,0 @@ -# 分布式事件总线Rebus集成 - - -> 本文解释了 **如何配置[Rebus](http://mookid.dk/category/rebus/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统. - - -## 安装 - -使用ABP CLI添加[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus)NuGet包到你的项目: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. -* 在你想要安装 `Volo.Abp.EventBus.Rebus` 包的 `.csproj` 文件目录打开命令行(终端). -* 运行 `abp add-package Volo.Abp.EventBus.Rebus` 命令. - -如果你想要手动安装,安装[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusRebusModule))]` 到你的项目[模块](Module-Development-Basics.md)类. - -## 配置 - -可以使用配置使用标准的[配置系统](Configuration.md),如[选项](Options.md)类. - -`AbpRebusEventBusOptions` 类用于配置事件总线选项. - -你可以在你的[模块](Module-Development-Basics.md)的 `PreConfigureServices` 方法配置选项. - -**示例: 最小化配置** - -```csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; -}); -``` - -Rebus 有很多选项,你可以使用 `AbpRebusEventBusOptions` 的 `Configurer` 属性来配置. - -默认事件**存储在内存中**. 参阅[rebus文档](https://github.com/rebus-org/Rebus/wiki/Transport)了解更多信息. - -**示例: 配置存储** - -````csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; - options.Configurer = rebusConfigurer => - { - rebusConfigurer.Transport(t => t.UseMsmq("eventbus")); - rebusConfigurer.Subscriptions(s => s.UseJsonFile(@"subscriptions.json")); - }; -}); -```` - -你可以使用 `AbpRebusEventBusOptions` 的 `Publish` 属性来更改发布方法. - -**示例: 配置事件发布** - -````csharp -PreConfigure(options => -{ - options.InputQueueName = "eventbus"; - options.Publish = async (bus, type, data) => - { - await bus.Publish(data); - }; -}); -```` diff --git a/docs/zh-Hans/Distributed-Event-Bus.md b/docs/zh-Hans/Distributed-Event-Bus.md deleted file mode 100644 index 8c32e6294b..0000000000 --- a/docs/zh-Hans/Distributed-Event-Bus.md +++ /dev/null @@ -1,302 +0,0 @@ -# 分布式事件总线 - -分布式事件总线系统允许**发布**和**订阅跨应用/服务边界**传输的事件. 你可以使用分布式事件总线在**微服务**或**应用程序**之间异步发送和接收消息. - -## 提供程序 - -分布式事件总线系统提供了一个可以被任何提供程序实现的**抽象**. 有四种开箱即用的提供程序: - -* `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**. -* `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-Integration.md)了解如何配置它. -* `KafkaDistributedEventBus` 通过[Kafka](https://kafka.apache.org/)实现分布式事件总线. 请参阅[Kafka集成文档](Distributed-Event-Bus-Kafka-Integration.md)了解如何配置它. -* `RebusDistributedEventBus` 通过[Rebus](http://mookid.dk/category/rebus/)实现分布式事件总线. 请参阅[Rebus集成文档](Distributed-Event-Bus-Rebus-Integration.md)了解如何配置它. - -使用本地事件总线作为默认具有一些重要的优点. 最重要的是:它允许你编写与分布式体系结构兼容的代码. 您现在可以编写一个整体应用程序,以后可以拆分成微服务. 最好通过分布式事件而不是本地事件在边界上下文之间(或在应用程序模块之间)进行通信. - -例如,[预构建的应用模块](Modules/Index.md)被设计成在分布式系统中作为服务工作,同时它们也可以在独立应用程序中作为模块工作,而不依赖于外部消息代理. - -## 发布事件 - -以下介绍了两种发布分布式事件的方法. - -### IDistributedEventBus - -可以[注入](Dependency-Injection.md) `IDistributedEventBus` 并且使用发布分布式事件. - -**示例: 产品的存货数量发生变化时发布分布式事件** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IDistributedEventBus _distributedEventBus; - - public MyService(IDistributedEventBus distributedEventBus) - { - _distributedEventBus = distributedEventBus; - } - - public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) - { - await _distributedEventBus.PublishAsync( - new StockCountChangedEto - { - ProductId = productId, - NewCount = newCount - } - ); - } - } -} -```` - -`PublishAsync` 方法需要一个参数:事件对象,它负责保持与事件相关的数据,是一个简单的普通类: - - -````csharp -using System; - -namespace AbpDemo -{ - [EventName("MyApp.Product.StockChange")] - public class StockCountChangedEto - { - public Guid ProductId { get; set; } - - public int NewCount { get; set; } - } -} -```` - -即使你不需要传输任何数据也需要创建一个类(在这种情况下为空类). - -> `Eto` 是我们按照约定使用的**E**vent **T**ransfer **O**bjects(事件传输对象)的后缀. s虽然这不是必需的,但我们发现识别这样的事件类很有用(就像应用层上的[DTO](Data-Transfer-Objects.md) 一样). - -#### 事件名称 - -`EventName`attribute是可选的,但建议使用. 如果不声明,事件名将事件名称将是事件类的全名. 这里是 `AbpDemo.StockCountChangedEto`. - -#### 关于序列化的事件对象 - -事件传输对象**必须是可序列化**的,因为将其传输到进程外时,它们将被序列化/反序列化为JSON或其他格式. - -避免循环引用,多态,私有setter,并提供默认(空)构造函数,如果你有其他的构造函数.(虽然某些序列化器可能会正常工作),就像DTO一样. - -### 实体/聚合根类 - -[实体](Entities.md)不能通过依赖注入注入服务,但是在实体/聚合根类中发布分布式事件是非常常见的. - -**示例: 在聚合根方法内发布分布式事件** - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - public int StockCount { get; private set; } - - private Product() { } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - - public void ChangeStockCount(int newCount) - { - StockCount = newCount; - - //ADD an EVENT TO BE PUBLISHED - AddDistributedEvent( - new StockCountChangedEto - { - ProductId = Id, - NewCount = newCount - } - ); - } - } -} -```` - -`AggregateRoot` 类定义了 `AddDistributedEvent` 来添加一个新的分布式事件,事件在聚合根对象保存(创建,更新或删除)到数据库时发布. - -> 如果实体发布这样的事件,以可控的方式更改相关属性是一个好的实践,就像上面的示例一样 - `StockCount`只能由保证发布事件的 `ChangeStockCount` 方法来更改. - -#### IGeneratesDomainEvents 接口 - -实际上添加分布式事件并不是 `AggregateRoot` 类独有的. 你可以为任何实体类实现 `IGeneratesDomainEvents`. 但是 `AggregateRoot` 默认实现了它简化你的工作. - -> 不建议为不是聚合根的实体实现此接口,因为它可能不适用于此类实体的某些数据库提供程序. 例如它适用于EF Core,但不适用于MongoDB. - -#### 它是如何实现的? - -调用 `AddDistributedEvent` 不会立即发布事件. 当你将更改保存到数据库时发布该事件; - -* 对于 EF Core, 它在 `DbContext.SaveChanges` 中发布. -* 对于 MongoDB, 它在你调用仓储的 `InsertAsync`, `UpdateAsync` 或 `DeleteAsync` 方法时发由 (因为MongoDB没有更改跟踪系统). - -## 订阅事件 - -一个服务可以实现 `IDistributedEventHandler` 来处理事件. - -**示例: 处理上面定义的`StockCountChangedEto`** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyHandler - : IDistributedEventHandler, - ITransientDependency - { - public async Task HandleEventAsync(StockCountChangedEto eventData) - { - var productId = eventData.ProductId; - } - } -} -```` - -这就是全部. - -* `MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEto` 事件时调用 `HandleEventAsync`. -* 如果你使用的是分布式消息代理,比如RabbitMQ,ABP会自动**订阅消息代理上的事件**,获取消息执行处理程序. -* 如果事件处理程序成功执行(没有抛出任何异常),它将向消息代理发送**确认(ACK)**. - -你可以在处理程序注入任何服务来执行所需的逻辑. 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `IDistributedEventHandler` 接口. - -> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项. - -## 预定义的事件 - -如果你配置,ABP框架会为[实体](Entities.md)**自动发布创建,更新和删除**分布式事件. - -### 事件类型 - -有三种预定义的事件类型: - -* `EntityCreatedEto` 是实体 `T` 创建后发布. -* `EntityUpdatedEto` 是实体 `T` 更新后发布. -* `EntityDeletedEto` 是实体 `T` 删除后发布. - -这些都是泛型的, `T` 实际上是**E**vent **T**ransfer **O**bject (ETO)的类型,而不是实体的类型,因为实体对象不能做为事件数据传输,所以通常会为实体类定义一个ETO类,如为 `Product` 实体定义 `ProductEto`. - -### 订阅事件 - -订阅自动事件与订阅常规分布式事件相同. - -**示例: 产品更新后获取通知** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.EventBus.Distributed; - -namespace AbpDemo -{ - public class MyHandler : - IDistributedEventHandler>, - ITransientDependency - { - public async Task HandleEventAsync(EntityUpdatedEto eventData) - { - var productId = eventData.Entity.Id; - //TODO - } - } -} -```` - -* `MyHandler` 实现了 `IDistributedEventHandler>`. - -### 配置 - -你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 中配置 `AbpDistributedEntityEventOptions`添加选择器. - -**示例: 配置示例** - -````csharp -Configure(options => -{ - //Enable for all entities - options.AutoEventSelectors.AddAll(); - - //Enable for a single entity - options.AutoEventSelectors.Add(); - - //Enable for all entities in a namespace (and child namespaces) - options.AutoEventSelectors.AddNamespace("Volo.Abp.Identity"); - - //Custom predicate expression that should return true to select a type - options.AutoEventSelectors.Add( - type => type.Namespace.StartsWith("MyProject.") - ); -}); -```` - -* 最后一个提供了灵活性来决定是否应该针对给定的实体类型发布事件. 返回 `true` 代表为该 `Type` 发布事件. - -你可以添加多个选择器. 如果选择器之一与实体类型匹配,则将其选中. - -### 事件传输对象 - -一旦你为一个实体启用了**自动事件**,ABP框架就会为实体上的更改发布事件. 如果你没有为实体指定对应的**E**vent **T**ransfer **O**bject(ETO), ABP框架会使用一个标准类型 `EntityEto`,它只有两个属性: - -* `EntityType` (`string`): 实体类的全名(包括命令空间). -* `KeysAsString` (`string`): 已更改实体的主键.如果它只有一个主键,这个属性将是主键值. 对于复合键,它包含所有用`,`(逗号)分隔的键. - -因此可以实现 `IDistributedEventHandler>` 订阅事件. 但是订阅这样的通用事件不是一个好方法,你可以为实体类型定义对应的ETO. - -**示例: 为 `Product` 声明使用 `ProductEto`** - -````csharp -Configure(options => -{ - options.AutoEventSelectors.Add(); - options.EtoMappings.Add(); -}); -```` - -在这个示例中; - -* 添加选择器允许发布 `Product` 实体的创建,更新和删除事件. -* 配置为使用 `ProductEto` 作为事件传输对象来发布与 `Product` 相关的事件. - -分布式事件系统使用[对象到对象的映射](Object-To-Object-Mapping.md)系统来映射 `Product` 对象到 `ProductEto` 对象,你需要配置映射. 请参阅可以对象到对象映射文档了解所有选项,下面的示例展示了如何使用[AutoMapper](https://automapper.org/)库配置它. - -**示例: 使用AutoMapper配置 `Product` 到 `ProductEto` 映射** - -````csharp -using System; -using AutoMapper; -using Volo.Abp.Domain.Entities.Events.Distributed; - -namespace AbpDemo -{ - [AutoMap(typeof(Product))] - public class ProductEto : EntityEto - { - public Guid Id { get; set; } - public string Name { get; set; } - } -} -```` - -此示例使用AutoMapper的 `AutoMap` 属性配置的映射. 你可以创建一个配置文件类代替. 请参阅AutoMapper文档了解更多选项. diff --git a/docs/zh-Hans/Distributed-Locking.md b/docs/zh-Hans/Distributed-Locking.md deleted file mode 100644 index 02228beaa5..0000000000 --- a/docs/zh-Hans/Distributed-Locking.md +++ /dev/null @@ -1,131 +0,0 @@ -# 分布式锁 -分布式锁是一种管理多个应用程序访问同一资源的技术. 主要目的是同一时间只允许多个应用程序中的一个访问资源. 否则, 从不同的应用程序访问同一对象可能会破坏资源. - -> ABP当前的分布式锁实现基于[DistributedLock](https://github.com/madelson/DistributedLock)库. - -## 安装 - -你可以打开一个命令行终端并输入以下命令来安装[Volo.Abp.DistributedLocking](https://www.nuget.org/packages/Volo.Abp.DistributedLocking)到你的项目中: - -````bash -abp add-package Volo.Abp.DistributedLocking -```` - -这个库提供了使用分布式锁系统所需的API, 但是, 在使用它之前, 你应该配置一个提供程序. - -### 配置一个提供程序 - -[DistributedLock](https://github.com/madelson/DistributedLock)库对[Redis](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md)和[ZooKeeper](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.ZooKeeper.md)提供[多种实现](https://github.com/madelson/DistributedLock#implementations). - -例如, 如果你想使用[Redis provider](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md), 你应该将[DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet包添加到项目中, 然后将以下代码添加到ABP[模块](Module-Development-Basics.md)类的`ConfigureServices`方法中: - -````csharp -using Medallion.Threading; -using Medallion.Threading.Redis; - -namespace AbpDemo -{ - [DependsOn( - typeof(AbpDistributedLockingModule) - //If you have the other dependencies, you should do here - )] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - context.Services.AddSingleton(sp => - { - var connection = ConnectionMultiplexer - .Connect(configuration["Redis:Configuration"]); - return new - RedisDistributedSynchronizationProvider(connection.GetDatabase()); - }); - } - } -} -```` - -此代码从[配置](Configuration.md)获取Redis连接字符串, 因此你可以将以下行添加到`appsettings.json`文件: - -````json -"Redis": { - "Configuration": "127.0.0.1" -} -```` - -## 使用 - -有两种方法可以使用分布式锁API: ABP的`IAbpDistributedLock`抽象和[DistributedLock](https://github.com/madelson/DistributedLock)库的API. - -### 使用IAbpDistributedLock服务 - -`IAbpDistributedLock`是ABP框架提供的一个用于简单使用分布式锁的服务. - -**实例: 使用`IAbpDistributedLock.TryAcquireAsync`方法** - -````csharp -using Volo.Abp.DistributedLocking; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IAbpDistributedLock _distributedLock; - public MyService(IAbpDistributedLock distributedLock) - { - _distributedLock = distributedLock; - } - - public async Task MyMethodAsync() - { - await using (var handle = - await _distributedLock.TryAcquireAsync("MyLockName")) - { - if (handle != null) - { - // your code that access the shared resource - } - } - } - } -} -```` - -`TryAcquireAsync`可能无法获取锁. 如果无法获取锁, 则返回`null`. 在这种情况下, 你不应该访问资源. 如果句柄不为`null`, 则表示你已获得锁, 并且可以安全地访问资源. - -`TryAcquireAsync`方法拥有以下参数: - -* `name` (`string`, 必须): 锁的唯一名称. 不同的锁命名用于访问不同的资源. -* `timeout` (`TimeSpan`): 等待获取锁的超时值. 默认值为`TimeSpan.Zero`, 这意味着如果锁已经被另一个应用程序拥有, 它不会等待. -* `cancellationToken`: 取消令牌可在触发后取消操作. - -### 配置 - -#### AbpDistributedLockOptions - -`AbpDistributedLockOptions` 是配置分布式锁的主要选项类. - -**示例: 设置应用程序的分布式锁Key前缀** - -```csharp -Configure(options => -{ - options.KeyPrefix = "MyApp1"; -}); -``` - -> 在你的[模块类](Module-Development-Basics.md)中的 `ConfigureServices` 方法进行配置. - -##### 可用选项 - -* KeyPrefix (string, 默认值: null): 指定分布式锁名称前缀. - -### 使用DistributedLock库的API - -ABP的`IAbpDistributedLock`服务非常有限, 主要用于ABP框架的内部使用. 对于你自己的应用程序, 可以使用DistributedLock库自己的API. 参见[文档](https://github.com/madelson/DistributedLock)详细信息. - -## Volo.Abp.DistributedLocking.Abstractions库 - -如果你正在构建一个可重用的库或应用程序模块, 那么对于作为单个实例运行的简单应用程序, 你可能不希望为模块带来额外的依赖关系. 在这种情况下, 你的库可以依赖于[Volo.Abp.DistributedLocking.Abstractions](https://nuget.org/packages/Volo.Abp.DistributedLocking.Abstractions)库, 它定义了`IAbpDistributedLock`服务, 并将其在进程内实现(实际上不是分布式的). 通过这种方式, 你的库可以在作为单个实例运行的应用程序中正常运行(没有分布式锁提供程序依赖项). 如果应用程序部署到[集群环境](Deployment/Clustered-Environment.md), 那么应用程序开发人员应该安装一个真正的分布式提供程序, 如*安装*部分所述. diff --git a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md deleted file mode 100644 index 95bce60bfc..0000000000 --- a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md +++ /dev/null @@ -1,1979 +0,0 @@ -# 实现领域驱动设计 - -## 总述 - -本文是实现领域驱动设计(DDD)的**实用指南**.虽然在实现中依赖了ABP框架,但是本文中的概念,理论和设计模式同样适用于其它类型的项目,不仅限于.Net项目. - -### 目标 - -本文的目标是: - -* **介绍并解释**DDD的架构,概念,原理及构建. -* **解释**ABP框架的分层架构及解决方案结构. -* 通过**案例**,介绍实现DDD的一些**规则**及最佳实践. -* 展示**ABP框架**为DDD的实现提供了哪些基础设施. -* 最后,基于软件开发**最佳实践**和我们的经验提供**建议**来创建一个**可维护的代码库**. - -### 简单的代码 - -> **踢足球**非常**简单**,但是**踢简单的足球**却**非常难**.— 约翰·克鲁伊夫(Johan Cruyff) - -在编码的世界中,引用此名言: - -> **写代码**非常**简单**,但是**写简单的代码**却**非常难** — ??? - -在本文中,我们将介绍一些容易实现的规则. - -随着**应用程序的变化**,有时候,为了节省开发时间会**违反一些本应遵守的规则**,使得代码变得**复杂**且难以维护.短期来看确实节省了开发时间,但是后期可能需要花费更多的时间为之前的偷懒而**买单**.**无法对原有的代码进行维护**,导致大量的逻辑都需要进行**重写**. - -如果你**遵循规则并按最佳实践的方式**进行编码,那么你的代码将易于维护,你的业务逻辑将**更快的满足**需求的变化. - -## 什么是领域驱动设计? - -领域驱动设计(DDD)是一种将实现与**持续进化**的模型连接在一起来满足**复杂**需求的软件开发方法. - -DDD适用于**复杂领域**或**较大规模**的系统,而不是简单的CRUD程序.它着重于**核心领域逻辑**,而不是基础架构.这样有助于构建一个**灵活**,模块化,**可维护**的代码库. - -### OOP & SOLID - -实现DDD高度依赖面向对象编程思想(OOP)和[SOLID](https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))原则.事实上,DDD已经**实现**并**延伸**了这些原则,因此,**深入了解**OOP和SOLID对实施DDD十分有利. - -### DDD分层与整洁架构 - -基于DDD的架构分为四个基础层 - -![domain-driven-design-layers](images/domain-driven-design-layers.png) - -**业务逻辑**分为两层,分别为 *领域(Domain)* 层和 *应用(Application)* 层,它们包含不同类型的业务逻辑. - -* **领域层**:只实现领域业务逻辑,与用例无关. -* **应用层**:基于领域层来实现满足用例的业务逻辑.用例可以看作是用户界面(UI)或外部应用程序的交互. -* **展现层**:包含应用程序的UI元素. -* **基础设施层**:通过对第三方库的集成或抽象,来满足其它层的非核心业务逻辑的实现. - -同样的分层架构也可以如下图所示:被称为 **整洁架构**, 又或者称为 **洋葱架构**: - -![domain-driven-design-clean-architecture](images/domain-driven-design-clean-architecture.png) - -在整洁架构中,**每层只依赖内部的层**,独立的层在圆圈的最中心,也就是领域层. - -### 核心构建组成 - -DDD的关注点在**领域层**和**应用层**上,而展现层和基础设施层则视为*细节*(这个词原文太抽象,自己体会吧),业务层不应依赖它们. - -这并不意味着展现层和基础设施层不重要.它们非常重要,但*UI框架* 和 *数据库提供程序* 需要你自己定义规则和总结最佳实践.这些不在DDD的讨论范围中. - -本节将介绍领域层和应用层的基本构建组件. - -#### 领域层构建组成 - -* **实体(Entity)**: [实体](Entities.md)是种领域对象,它有自己的属性(状态,数据)和执行业务逻辑的方法.实体由唯一标识符(Id)表示,不同ID的两个实体被视为不同的实体. -* **值对象(Value Object)**: [值对象](Value-Objects.md)是另外一种类型的领域对象,使用值对象的属性来判断两个值对象是否相同,而非使用ID判断.如果两个值对象的属性值全部相同就被视为同一对象.值对象通常是不可变的,大多数情况下它比实体简单. -* **聚合(Aggregate) 和 聚合根(Aggregate Root)**: [聚合](Entities.md)是由**聚合根**包裹在一起的一组对象(实体和值对象).聚合根是一种具有特定职责的实体. -* **仓储(Repository)** (接口): [仓储](Repositories.md)是被领域层或应用层调用的数据库持久化接口.它隐藏了DBMS的复杂性,领域层中只定义仓储接口,而非实现. -* **领域服务(Domain Service)**: [领域服务](Domain-Services.md)是一种无状态的服务,它依赖多个聚合(实体)或外部服务来实现该领域的核心业务逻辑. -* **规约(Specification)**: [规约](Specifications.md)是一种**强命名**,**可重用**,**可组合**,**可测试**的实体过滤器. -* **领域事件(Domain Event)**: [领域事件](Event-Bus.md)是当领域某个事件发生时,通知其它领域服务的方式,为了解耦领域服务间的依赖. - -#### 应用层构建组成 - -* **应用服务(Application Service)**: [应用服务](Application-Services.md)是为实现用例的无状态服务.展现层调用应用服务获取DTO.应用服务调用多个领域服务实现用例.用例通常被视为一个工作单元. -* **数据传输对象(DTO)**: [DTO](Data-Transfer-Objects.md)是一个不含业务逻辑的简单对象,用于应用服务层与展现层间的数据传输. -* **工作单元(UOW)**: [工作单元](Unit-Of-Work.md)是事务的原子操作.UOW内所有操作,当成功时全部提交,失败时全部回滚. - -## 实现领域驱动:重点 - -### .NET解决方案分层 - -下图是使用[ABP的启动模板](Startup-Templates/Application.md)创建的解决方案: - -![domain-driven-design-vs-solution](images/domain-driven-design-vs-solution.png) - -解决方案名为 `IssueTracking` ,它包含多个项目.该解决方案出于**DDD原则**及**开发**和**部署**的实践来进行分层.后面会在各小节中介绍解决方案中的项目. - -> 在使用启动模板时,如果选择了其它类型的UI或*数据库提供程序*,解决方案的结构会略有不同.但是领域层和应用层是一样的,这才是DDD的重点.如果你想了解有关解决方案的更多信息,请参见[启动模板](Startup-Templates/Application.md). - -#### 领域层 - -领域层分为两个项目: - -* `IssueTracking.Domain`是**领域层中必需**的,它包含之前介绍的**构建组成**(实体,值对象,领域服务,规约,仓储接口等). -* `IssueTracking.Domain.Shared`是领域层中**很薄的项目**,它只包含领域层与其它层共享的数据类型的定义.例如,枚举,常量等. - -#### 应用层 - -应用层也被分为了两个项目: - -* `IssueTracking.Application.Contracts`包含**接口**的定义及接口依赖的**DTO**,此项目可以被展现层或其它客户端应用程序引用. -* `IssueTracking.Application`是**应用层中必需**的,它实现了`IssueTracking.Application.Contracts`项目中定义的接口. - -#### 展现层 - -* `IssueTracking.Web`是一个ASP.NET Core MVC / Razor Pages应用程序.它是提供UI元素及API服务的可执行程序. - -> ABP框架还支持其它类型的UI框架,包括[Angular](UI/Angular/Quick-Start.md)和[Blazor](UI/Blazor/Overall.md).当选择*Angular*或*Blazor*时,解决方案不会有`IssueTracking.Web`项目.但是会添加`IssueTracking.HttpApi.Host`在解决方案中,此项目会提供HTTP API供UI调用. - -#### 远程服务层 - -* `IssueTracking.HttpApi`包含了HTTP API的定义.它通常包含*MVC Controller* 和 *Model*(如果有).因此,你可以在此项目中提供HTTP API. - -> 大多数情况下,通过使用*API Controller* 包装应用服务,供客户端远程调用.ABP框架的[API自发现系统](API/Auto-API-Controllers.md)可以自动将应用服务公开为API,因此,通常不需要在此项目中再创建Controller.出于需要手动添加额外Controller的情况,也包含在模板解决方案中. - -* `IssueTracking.HttpApi.Client`当C#客户端应用程序需要调用`IssueTracking.HttpApi`的API时,这个项目非常有用.客户端程序仅需引用此项目就可以通过依赖注入方式,远程调用应用服务.它是通过ABP框架的[动态C#客户端API代理系统](API/Dynamic-CSharp-API-Clients.md)来实现的. - -> 在解决方案文件夹`test`下,有一个名为`IssueTracking.HttpApi.Client.ConsoleTestApp`的控制台程序.它演示了如何使用`IssueTracking.HttpApi.Client`项目来远程调用应用程序公开的API.因为只是演示,你可以删除此项目,再或者你认为`IssueTracking.HttpApi`不需要,同样可以删除. - -#### 基础设施层 - -你可能只创建一个基础设施项目来完成所有抽象类的定义及外部类的集成,又或者为不同的依赖创建多个不同的项目. - -我们建议采用一种平衡的方法:为主要的依赖的库(例如 Entity Framework Core)创建一个独立的项目,为其它的依赖库创建一个公共的基础设施项目. - -ABP的启动解决方案中包含两个用于集成Entity Framework Core的项目: - -* `IssueTracking.EntityFrameworkCore`是必需的,因为需要集成*EF Core*.应用程序的`数据库上下文(DbContext)`,数据库对象映射,仓储接口的实现,以及其它与*EF Core*相关的内容都位于此项目中. -* `IssueTracking.EntityFrameworkCore.DbMigrations`是管理Code First方式数据库迁移记录的特殊项目.此项目定义了一个独立的`DbContext`来追踪迁移记录.只有当添加一个新的数据库迁移记录或添加一个新的[应用模块](Modules/Index.md)时,才会使用此项目,否则,其它情况无需修改此项目内容. - -> 你可能想知道为什么会有两个EF Core项目,主要是因为[模块化](Module-Development-Basics.md).每个应用模块都有自己独立的`DbContext`,你的应用程序也有自己`DbContext`.`DbMigrations`项目包含**合并**所有模块迁移记录的**单个迁移路径**.虽然大多数情况下你无需过多了解,但也可以查看[EF Core迁移](Entity-Framework-Core-Migrations.md)了解更多信息. - -#### 其它项目 - -另外还有一个项目`IssueTracking.DbMigrator`,它是一个简单的控制台程序,用来执行数据库迁移,包括**初始化**数据库及创建**种子数据**.这是一个非常实用的应用程序,你可以在开发环境或生产环境中使用它. - -### 项目间的依赖关系 - -下图展示了解决方案中项目间的依赖关系(有些项目比较简单就未展示): - -![domain-driven-design-project-relations](images/domain-driven-design-project-relations.png) - -之前已介绍了这些项目.现在,我们来解释依赖的原因: - -* `Domain.Shared` 所有项目直接或间接依赖此项目.此项目中的所有类型都可以被其它项目所引用. -* `Domain` 仅依赖`Domain.Shared`项目,因为`Domain.Shared`本就属于领域层的一部分.例如,`Domain.Shared`项目中的枚举类型 `IssueType` 被`Domain`项目中的`Issue`实体所引用. -* `Application.Contracts` 依赖`Domain.Shared`项目,可以在DTO中重用`Domain.Shared`中的类型.例如,`Domain.Shared`项目中的枚举类型 `IssueType` 同样被`Contracts`项目中的`CreateIssueDto`DTO所引用. -* `Application` 依赖`Application.Contracts`项目,因为此项目需要实现应用服务的接口及接口使用的DTO.另外也依赖`Domain`项目,因为应用服务的实现必须依赖领域层中的对象. -* `EntityFrameworkCore` 依赖`Domain`项目,因为此项目需要将领域对象(实体或值对象)映射到数据库的表,另外还需要实现`Domain`项目中的仓储接口. -* `HttpApi` 依赖`Application.Contracts`项目,因为Controllers需要注入应用服务. -* `HttpApi.Client` 依赖`Application.Contracts`项目,因为此项目需要使用应用服务. -* `Web` 依赖`HttpApi`项目,因为此项目对外提供HTTP APIs.另外Pages或Components 需要使用应用服务,所以还间接依赖了`Application.Contracts`项目 - -#### 虚线依赖 - -你在上图中会发现用虚线表示了另外两个依赖.`Web`项目依赖了 `Application` and `EntityFrameworkCore`,理论上`Web`不应该依赖这两个项目,但实际上依赖了.原因如下: - -`Web`是最终的运行程序,是负责托管Web的宿主,它在运行时需要**应用服务和仓储的实现类**. - -这种依赖关系的设计,可能会让你有机会在展现层直接使用到EF Core的对象,**应该严格禁止这样的做法**.如果想在解决方案分层上规避这种问题,有下面两种方式,相对复杂一些: - -* 将`Web`项目类型改为razor类库,并创建一个新项目,比如`Web.Host`,`Web.Host`依赖`Web`,`Application`,`EntityFrameworkCore`三个项目,并作为Web宿主程序运行.注意,不要写任何与UI相关的代码,只是作为**宿主运行**. -* 在`Web`项目中移除对`Application`和`EntityFrameworkCore`的引用,`Web`在启动时,再动态加载程序集`IssueTracking.Application.dll`和`IssueTracking.EntityFrameworkCore.dll`.可以使用ABP框架的[插件模块](PlugIn-Modules.md)来动态加载程序集. - -### DDD模式的应用程序执行顺序 - -下图展示了基于DDD模式下的Web应用程序执行顺序: - -![](images/domain-driven-design-web-request-flow.png) - -* 通常由UI(用例)发起一个HTTP请求到服务器. -* 由展现层(或分布式服务层)中的一个*MVC Controller*或*Razor Page Handler*处理请求,在这个阶段可以执行一些AOP逻辑([授权](Authorization.md),[验证](Validation.md),[异常处理](Exception-Handling.md)等),*MVC Controller*或*Razor Page Handler*调用注入的应用服务接口,并返回其调用后的结果(DTO).. -* 应用服务使用领域层的对象(实体,仓储接口,领域服务等)来实现UI(用例)交互.此阶段同样可以执行一些AOP逻辑(授权,验证等).应用服务中的每个方法应该是一个[工作单元](Unit-Of-Work.md),代表它是一次原子性操作. - -跨域问题大多数由**ABP框架自动实现**,通常不需要为此额外编码. - -### 通用原则 - -在详细介绍之前,我们先来看一些DDD的总体原则. - -#### 数据库提供程序 / ORM 独立原则 - -领域层和应用层应该与*数据库提供程序 / ORM*无关.领域层和应用层仅依赖仓储接口,并且仓储接口不依赖特定的ORM对象. - -原因如下: - -1. 未来领域层或应用层的基础设施会发生改变,例如,需要支持另外一种数据库类型,因此需要保持**领域层或应用层的基础设施是独立的**. -2. 将基础设施的实现隐藏在仓储中,使得领域层或应用层更**专注于业务逻辑代码**. -3. 可以通过模拟仓储接口,使得自动化测试更为方便. - -> 关于此原则, `EntityFrameworkCore`项目只被启动程序项目所引用,解决方案中其它项目均未引用. - -##### 关于数据库独立原则的讨论 - -**原因1**会非常影响你**领域对象的建模**(特别是实体间的关系)及**应用程序的代码**.假如,开始选择了关系型数据库,并使用了[Entity Framework Core](Entity-Framework-Core.md),后面尝试切换到[MongoDB](MongoDB.md),那么 **EF Core 中一些非常有用的特性**你就不能使用了,例如: - -* 无法使用[变更追踪](https://docs.microsoft.com/zh-cn/ef/core/querying/tracking) ,因为*MongoDB provider*没有提供此功能,因此,你始终需要显式的更新已变更的实体. -* 无法在不同的聚合间使用[导航属性](https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships),因为文档型数据库是不支持的.有关更多信息,请参见"规则:聚合间仅通过Id关联". - -如果你认为这些功能对你很**重要**,并且你永远都不会**离开** *EF Core*,那么我们认为你可以忽略这一原则.假如你在设计实体关系时使用了*EF Core*,你甚至可以在应用层引用*EF Core Nuget*包,并直接使用异步的LINQ扩展方法,例如 `ToListAsync()`(有关更多信息,请参见[仓储](Repositories.md)文档中的*IQueryable*和*Async Operations*). - -但是我们仍然建议采用仓储模式来隐藏基础设施中实现过程. - -#### 展现层技术无关原则 - -展现层技术(UI框架)是现代应用程序中最多变的部分之一.**领域层和应用层**应该对展现层所采用的技术或框架**一无所知**.使用ABP启动模板就非常容易实现此原则. - -在某些情况下,你可能需要在应用层和展现层中写重复的逻辑,例如,参数验证和授权检查.展现层检查出于**用户体验**,应用层或领域层检查出于**数据安全性**和**数据完整性**. - -#### 关注状态的变化,而不是报表/查询 - -DDD关注领域对象的**变化和相互作用**,如何创建或修改一个具有数据**完整性,有效性**,符合**业务规则**的实体对象. - -DDD忽略**领域对象的数据展示**,这并不意味着它们并不重要,如果应用程序没有精美的看板和报表,谁会愿意用呢?但是报表是另外一个讨论话题,你可以通过使用SQL Server报表功能或ElasticSearch来提供数据展示,又或者使用优化后的SQL查询语句,创建数据库索引或存储过程.唯一的原则是不要将这些内容混入领域的业务逻辑中. - -## 实现领域驱动:构建组成 - -这是本指南的重要部分,我们将通过示例介绍并解释一些**明确的规则**,在实现领域驱动设计时,你可以遵循这些规则并将其应用于解决方案中. - -### 领域 - -示例中会使用一些概念,这些概念在Github中被使用,例如, `Issue`(问题), `Repository`(仓库), `Label`(标签) 和`User`(用户).下图中展示了一些聚合,聚合根,实体,值对象以及它们之间的关系: - -![domain driven design example schema](images/domain-driven-design-example-domain-schema.png) - -**问题聚合** 由`Issue` 聚合根,及其包含的 `Comment` 和`IssueLabel` 集合组成.我们将重要讨论 `Issue` 聚合根: - -![domain-driven-design-issue-aggregate-diagram](images/domain-driven-design-issue-aggregate-diagram.png) - -### Aggregates - -如前所述, [聚合](Entities.md)是由**聚合根**包裹在一起的一组对象(实体和值对象).本节将介绍于聚合的有关原理和规则. - -> 后面的文档中,我们将使用 *实体* 替代 *聚合根* 或 *子集合实体* ,除非我们明确指明使用 *聚合根* 或 *子集合实体* . - -#### 聚合 / 聚合根 原则 - -##### 业务规则 - -实体负责实现与其自身属性相关的业务规则.同时*聚合根实体*还负责它们的子集合实体. - -聚合应该通过领域规则和约束来保证自身的**完整性**和**有效性**.这意味着,实体与DTO是不同的,实体应该比DTO多了些**实现业务逻辑的方法**.我们应该尽可能地在实体上实现业务规则. - -##### 独立单元 - -应该在一个独立的单元中完成**一个聚合的获取及保存**,包括自身属性及其子集合.假如我们需要在`Issue`中添加一个新的`Comment`,步骤如下: - -* 从数据库中获取一个 `Issue` 对象,包括所有子集合(`Comments`和`IssueLabels`). -* 使用 `Issue` 实体上的添加新`Comment`的方法,例如 `Issue.AddComment(...);`. -* 在数据库的单次操作中完成整个 `Issue`对象(包括子集合)的保存. - -对于在关系型数据库上用过 **EF Core** 的开发人员会认为,获取`Issue`的同时加载子集合没有必要并且还影响性能.为什么不使用SQL语句`Insert`来直接插入记录呢? - -这样做的原因是我们需要执行业务规则来保证数据的一致性和完整性.假如有一个业务规则:"用户不能对已锁定的问题进行评论".那如何在不查询数据库的情况下,获取问题是否已被锁定?所以,只有关联的对象都被加载了的时候,我们才可以执行业务规则. - -另外,使用**MongoDB**的开发人员就认为此原则很好理解.在MongoDB中,聚合对象(包含子集合)会被保存到一个`collection`中.因而,无需任何其它配置,就可以实现查询一个聚合,同时包含所有子对象. - -ABP框架有助于你实现这一原则 - -**例子: 问题追加评论** - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IRepository _issueRepository; - - public IssueAppService(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - [Authorize] - public async Task CreateCommentAsync(CreateCommentDto input) - { - var issue = await _issueRepository.GetAsync(input.IssueId); - issue.AddComment(CurrentUser.GetId(), input.Text); - await _issueRepository.UpdateAsync(issue); - } -} -```` - -通过`_issueRepository.GetAsync`方法来获取`Issue`对象时,默认就已经加载了所有子集合.对于MongoDB很简单,EF Core 则需要额外配置,一旦配置,ABP仓储类会自动处理.`_issueRepository.GetAsync`方法还有个可选参数`includeDetails`,可以传`false`,手动禁止加载子集合. - -> 如何配置及替代方案,请参考[EF Core document](Entity-Framework-Core.md)的*加载关联实体* 章节. - -`Issue.AddComment`接收两个参数,分别是`userId`和`text`,再执行自己的业务规则,最终将评论添加到`Issue`的评论集合中. - -最后,我们使用`_issueRepository.UpdateAsync`方法,将对象保存到数据库中. - -> EF Core 具有**变更追踪**的功能,因此,不需要调用`_issueRepository.UpdateAsync`方法.ABP的工作单元会在方法结束时,自动执行`DbContext.SaveChanges()`的.如果使用MongoDB则需要显式手动调用. -> -> 因此,当需要额外编写仓储层的实现,应该在实体变化时始终调用 `UpdateAsync` 方法. - -##### 事务边界 - -通常认为一个聚合就是一个事务边界.如果用例只涉及单个聚合,那么读取及修改就是一个操作单元.对聚合内所有对象的修改都将作为原子操作一起保存,无需显式创建数据库事务. - -但是,实际上,可能需要在一个用例中更改**多个聚合对象的实例**,并且还要求创建事务来保证**原子更新**和**数据一致性**.因此,ABP框架提供了为每个用例(应用服务中的方法),可以创建显式事务的功能.有关更多信息,请参见文档[工作单元](Unit-Of-Work.md). - -##### 序列化 - -一个聚合(包含聚合根及子集合)可以被序列化或反序列化.例如,MongoDB在保存对象到数据库时,会将聚合序列化为JSON文件,读取时再进行反序列化. - -> 使用关系型数据库+ORM时,这个原则不是必须的,但是,这是领域驱动设计的重要实践. - -以下规则遵循序列化原则 - -#### 聚合/聚合根规则及最佳实践 - -以下规则是遵循上述原则. - -##### 聚合间只通过ID相互引用 - -聚合应该只引用其它聚合的ID,也就是说,不允许定义导航属性关联至其它聚合. - -* 该规则遵循了可序列化原则. -* 该规则还可以避免不同聚合彼此间的相互操作以及业务逻辑的暴露. - -下图中,可以看到两个聚合根,`GitRepository` 和`Issue` : - -![domain-driven-design-reference-by-id-sample](images/domain-driven-design-reference-by-id-sample.png) - -* `GitRepository` 不应该包含 `Issue`的集合,因为`Issue`属于不同的聚合. -* `Issue` 不应该包含导航属性至 `GitRepository` .因为 `GitRepository`属于不同的聚合. -* `Issue` 可以有 `RepositoryId` 的引用. - -因此,若要获取`Issue`关联的 `GitRepository`对象,需要使用`Issue`的`RepositoryId`在数据库中进行一次查询. - -###### 对于EF Core和关系型数据库 - -MongoDB中不适合使用导航属性或集合的,原因是:当前源聚合对象会被序列化为JSON,其中会保存导航目标聚合的副本. - -在使用EF Core在关系型数据库上进行操作时,开发者可能认为此规则没必要.但是我们认为这是一条非常重要的规则,有助于降**低领域的复杂性**减少风险.我们强烈建议遵守此规则.如果你确定要忽略此规则,请参见上面的"关于数据库独立原理的讨论"章节. - -##### 保持聚合尽量的小 - -保持聚合简单而小巧是一个比较好的做法.因为聚合的读取与保存是一个整体,当处理较大对象时会出现性能问题,如下所示: - -![domain-driven-design-aggregate-keep-small](images/domain-driven-design-aggregate-keep-small.png) - -角色聚合包含`UserRole`值对象集合,方便查询该角色下有哪些用户.注意,`UserRole`不是聚合,并且也遵守了*聚合间只通过ID相互引用*的规则.但是在现实场景中,一个角色可能被给成千上万个用户,当从数据库中加载一个角色时,会关联加载数千个用户对象,这里会有 严重的性能问题. - -反过来看,`User`也可以有`Roles` 集合,现实中一个用户不会具有太多的角色,因此采用`User`关联`Roles`这种方式比较合适. - -当使用**非关系型数据库时**,`User`和`Role` 同时都有关联子集合,会出现另外一个问题.相同的记录会被重复记录在不同的集合中,并且难以保证数据一致性(需要添加记录到`User.Roles`和`Role.Users`中) - -因此,请根据以下注意事项来确定聚合的边界: - -* 同时被使用的对象. -* 查询(读取/保存)性能和内存消耗. -* 数据完整性,有效性,一致性. - -现实情况: - -* 大多数聚合根**没有子集合**. -* 子集合的数量控制在**100-150个**.如果集合数量超过150个,考虑将子对象改成聚合根. - -##### 聚合根 / 实体的主键 - -* 聚合根通常具有唯一的标识符ID (主键: PK).我们建议使用 `Guid`作为聚合根的主键类型. (原因请参见[Guid生成文档](Guid-Generation.md)). -* 聚合中的实体(非聚合根)可以使用联合主键. - -如图所示: - -![domain-driven-design-entity-primary-keys](images/domain-driven-design-entity-primary-keys.png) - -* `Organization`有一个`Guid`的标识符 (`Id`). -* `OrganizationUser`是一个子集合,使用 `OrganizationId` 和`UserId`作为联合主键. - -并不是所有的子集合的主键都是联合主键,有些情况下,可以使用单独的`Id`作为主键. - -> 联合主键实际上是关系型数据库中的概念,因为子集合对象有与之对应的数据库表,而表也要有主键.但是在非关系型数据库中,无需为子集合实体定义主键,因为它们本身就已属于一个聚合根. - -##### 聚合根 / 实体的构造函数 - -构造函数是实体生命周期的开始被执行.以下是构造函数的编写建议: - -* 将实体的**必填属性**作为构造函数参数,这样可以创建一个**有效(符合规则)的实体**.另外,将非必填属性作为构造函数的可选参数. -* 参数必须**检查有效性**. -* 所有**子集合**对象必须被初始化. - -**示例: `Issue` (聚合根) 构造函数** - -````csharp -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Volo.Abp; -using Volo.Abp.Domain.Entities; - -namespace IssueTracking.Issues -{ - public class Issue : AggregateRoot - { - public Guid RepositoryId { get; set; } - public string Title { get; set; } - public string Text { get; set; } - public Guid? AssignedUserId { get; set; } - public bool IsClosed { get; set; } - public IssueCloseReason? CloseReason { get; set; } //enum - - public ICollection Labels { get; set; } - - public Issue( - Guid id, - Guid repositoryId, - string title, - string text = null, - Guid? assignedUserId = null - ) : base(id) - { - RepositoryId = repositoryId; - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - - Text = text; - AssignedUserId = assignedUserId; - - Labels = new Collection(); - } - - private Issue() { /* for deserialization & ORMs */ } - } -} -```` - -* `Issue` 通过构造函数的参数,对必填属性进行赋值,而从创建一个有效的实体对象. -* 构造函数对参数进行**验证**(`Check.NotNullOrWhiteSpace(...)`必填项为空,抛出异常). -* 初始化子集合.在实例化`Issue`对象后,访问 `Labels` ,不会出现空指针异常. -* 构造函数还将 `id`传递给父类,不要在构造函数内生成 `Guid`(参阅 [Guid生成](Guid-Generation.md)). -* 为ORM保留**私有的无参构造函数**.防止编写代码时,意外使用了无参构造函数. - -> 参见 [实体](Entities.md) 文档,了解更多使用ABP框架创建实体的信息. - -##### 实体属性访问器和方法 - -上面的示例中,我们在构造函数中对 `Title` 进行了非空检查.但是开发人员可以再次对`Title`进行赋值. - -如果我们使用**公开的属性**,则无法在实体控制数据的**有效性**和**完整性**.建议: - -* 如果某个属性具有业务逻辑,则将该属性的**setter**改为私有. -* 定义公开的方法来修改属性. - -**示例:提供方法修改属性** - -````csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities; - -namespace IssueTracking.Issues -{ - public class Issue : AggregateRoot - { - public Guid RepositoryId { get; private set; } //Never changes - public string Title { get; private set; } //Needs validation - public string Text { get; set; } //No validation - public Guid? AssignedUserId { get; set; } //No validation - public bool IsClosed { get; private set; } //Should change with CloseReason - public IssueCloseReason? CloseReason { get; private set; } //Should change with IsClosed - - //... - - public void SetTitle(string title) - { - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - } - - public void Close(IssueCloseReason reason) - { - IsClosed = true; - CloseReason = reason; - } - - public void ReOpen() - { - IsClosed = false; - CloseReason = null; - } - } -} -```` - -* `RepositoryId` setter是私有的.创建后无法变更,业务规则不允许将已有的问题移到其它仓库. -* `Title` setter是私有的.它的修改时需要加以验证. -* `Text` 和 `AssignedUserId` setter 是公开的.因为业务规则允许它们为空或任意值.我们认为没必要将它们改为私有的,如果将来业务发生变化,再将setter改为私有,并提供公开的方法进行修改.另外实体属于领域层,不会直接暴露属性给应用层(或其它层),目前将其公开不是什么大问题. -* `IsClosed` 和 `IssueCloseReason` 它们是一组属性.定义 `Close` 和 `ReOpen` 方法来同时对这两个属性进行赋值.这样可以防止问题被无故关闭. - -##### 业务逻辑与实体异常 - -对实体进行验证,或执行业务逻辑时,通常需要抛出异常: - -* 领域中定义的**特定的异常**. -* 实体方法中**抛出的异常**. - -**示例** - -````csharp -public class Issue : AggregateRoot -{ - //... - - public bool IsLocked { get; private set; } - public bool IsClosed { get; private set; } - public IssueCloseReason? CloseReason { get; private set; } - - public void Close(IssueCloseReason reason) - { - IsClosed = true; - CloseReason = reason; - } - - public void ReOpen() - { - if (IsLocked) - { - throw new IssueStateException( - "Can not open a locked issue! Unlock it first." - ); - } - - IsClosed = false; - CloseReason = null; - } - - public void Lock() - { - if (!IsClosed) - { - throw new IssueStateException( - "Can not open a locked issue! Unlock it first." - ); - } - - IsLocked = true; - } - - public void Unlock() - { - IsLocked = false; - } -} -```` - -这里有两个业务规则: - -* 已锁定的问题无法重新开启. -* 无法锁定已开打的问题. - -当违反业务规则时,`Issue` 会抛出 `IssueStateException` 异常: - -````csharp -using System; - -namespace IssueTracking.Issues -{ - public class IssueStateException : Exception - { - public IssueStateException(string message) - : base(message) - { - - } - } -} -```` - -抛出异常会引发两个问题: - -1. 当异常发生时,**用户**应该看到异常(错误)信息吗?如果需要看到,异常消息如何实现本地化? 实体中无法注入[本地化](Localization.md)的 `IStringLocalizer` 接口. -2. 对于Web应用或HTTP API,应向客户端返回什么**HTTP状态代码**. - -ABP框架的 [异常处理](Exception-Handling.md) 可以解决上述问题. - -**示例:使用异常编码** - -````csharp -using Volo.Abp; - -namespace IssueTracking.Issues -{ - public class IssueStateException : BusinessException - { - public IssueStateException(string code) - : base(code) - { - - } - } -} -```` - -* `IssueStateException` 继承至 `BusinessException` .对于`BusinessException`的派生类,ABP框架默认返回的HTTP状态码是403 (默认是服务器内部错误 状态码 500) -* 将`code` 作为Key,在本地化资源中查找对应的文字. - -现在,我们修改 `ReOpen` 方法: - -````csharp -public void ReOpen() -{ - if (IsLocked) - { - throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue"); - } - - IsClosed = false; - CloseReason = null; -} -```` - -> 使用常量而不是魔法字符串. - -在本地化资源文件中添加对应的记录: - -````json -"IssueTracking:CanNotOpenLockedIssue": "Can not open a locked issue! Unlock it first." -```` - -* 异常发生时,ABP将自动使用本地化消息(基于当前语言). -* 异常编码 (`IssueTracking:CanNotOpenLockedIssue` )会被发送到客户端.同样可以以编程方式处理此异常. - -> 你可以无需定义 `IssueStateException`,直接抛出`BusinessException`异常.详细信息,参见[异常处理文档](Exception-Handling.md) - -##### 实体中业务逻辑依赖外部服务时 - -仅依赖实体本身的属性执行的业务规非常简单.但是有时候,复杂的业务逻辑会**查询数据库**或使用[依赖注入](Dependency-Injection.md)中的其它服务,这该怎么办?注意:**实体是无法注入服务的**. - -实现这种业务逻辑有两种方式: - -* 将依赖的服务以**方法的参数**,传递到实体的业务逻辑方法中. -* 定义一个**领域服务**. - -领域服务我们后面再说.我们先看看在实体类中如何实现: - -**示例:业务规则: 不允许将3个以上未解决的问题关联到一个用户** - -````csharp -public class Issue : AggregateRoot -{ - //... - public Guid? AssignedUserId { get; private set; } - - public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) - { - var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); - - if (openIssueCount >= 3) - { - throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); - } - - AssignedUserId = user.Id; - } - - public void CleanAssignment() - { - AssignedUserId = null; - } -} -```` - -* `AssignedUserId` 私有的属性setter.此属性只能通过`AssignToAsync` 和`CleanAssignment` 方法来修改. -* `AssignToAsync` 通过 `user.Id` 属性获取一个 `AppUser` 实体. -* `IUserIssueService` 是获取用户未解决问题的服务. -* `AssignToAsync` 当不满足业务规则时抛出异常. -* 最后,符合规则,就对属性`AssignedUserId` 进行赋值. - -这样就解决了将问题关联到用户时,需要调用外部服务的问题,但是它也存在几个问题: - -* 实体**依赖了外部服务**,实体变得**复杂**. -* 实体调用变的复杂.在调用`AssignToAsync` 方法时还需要传递 `IUserIssueService` 服务. - -实现这种业务逻辑另外一种方案,使用领域服务,后面将详细说明. - -### 仓储 - -[仓储](Repositories.md)是一个类集合的接口,它通常被领域层或应用层调用,负责访问持久化系统(数据库),读取写入业务对象(聚合). - -仓储的原则: - -* 在**领域层**中定义仓储接口,因为仓储会被领域层或应用层调用,在**基础设施层中实现**(*EntityFrameworkCore* 项目). -* 仓储中**不要写任何业务逻辑**. -* 仓储接口不依赖 **数据库提供程序 / ORM**.例如,不要在仓储中返回 `DbSet` 类型,因为 `DbSet`是EF Core中的对象. -* **仅为聚合根定义仓储**,非聚合根对象不要提供仓储,因为子集合可以通过聚合根来进行持久化. - -#### 仓储中不要写任何业务逻辑 - -我们经常不小心把业务逻辑编写到了仓储层. - -**示例:从仓储中获取非活动的问题** - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; - -namespace IssueTracking.Issues -{ - public interface IIssueRepository : IRepository - { - Task> GetInActiveIssuesAsync(); - } -} -```` - -`IIssueRepository`继承至`IRepository<...>`接口,并新增一个新接口`GetInActiveIssuesAsync`.此仓储为聚合根`Issue`提供查询的实现. - -````csharp -public class Issue : AggregateRoot, IHasCreationTime -{ - public bool IsClosed { get; private set; } - public Guid? AssignedUserId { get; private set; } - public DateTime CreationTime { get; private set; } - public DateTime? LastCommentTime { get; private set; } - //... -} -```` - -(上面的属性仅为了演示此示例) - -原则要求仓储不包含业务逻辑,上面的示例"**什么是非活动的问题?**"这个属于业务规则吗? - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using IssueTracking.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -namespace IssueTracking.Issues -{ - public class EfCoreIssueRepository : - EfCoreRepository, - IIssueRepository - { - public EfCoreIssueRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task> GetInActiveIssuesAsync() - { - var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); - - return await DbSet.Where(i => - - //Open - !i.IsClosed && - - //Assigned to nobody - i.AssignedUserId == null && - - //Created 30+ days ago - i.CreationTime < daysAgo30 && - - //No comment or the last comment was 30+ days ago - (i.LastCommentTime == null || i.LastCommentTime < daysAgo30) - - ).ToListAsync(); - } - } -} -```` - -(使用EF Core来实现. 如何使用EF Core实现仓储,请参见[EF Core集成文档](Entity-Framework-Core.md) ) - -来看一下`GetInActiveIssuesAsync`的实现,可以看到定义了一个**非活动问题的业务规则**: - -- 是*open*的(非*IsClosed* ) -- 没有关联到任何人 -- 创建时间大于30天 -- 最近30天没有评论 - -这个业务逻辑就被实现在了仓储内部,当我们需要重用这个业务规则时就会出现问题. - -例如:我们需要再实体`Issue`上添加一个方法来判断是否非活动`bool IsInActive()`,以方便我们在`Issue`实例上获取. - -代码如下: - -````csharp -public class Issue : AggregateRoot, IHasCreationTime -{ - public bool IsClosed { get; private set; } - public Guid? AssignedUserId { get; private set; } - public DateTime CreationTime { get; private set; } - public DateTime? LastCommentTime { get; private set; } - //... - - public bool IsInActive() - { - var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); - return - //Open - !IsClosed && - - //Assigned to nobody - AssignedUserId == null && - - //Created 30+ days ago - CreationTime < daysAgo30 && - - //No comment or the last comment was 30+ days ago - (LastCommentTime == null || LastCommentTime < daysAgo30); - } -} -```` - -我们需要拷贝代码来实现,如果将来业务规则发送变化,我们就必须修改这两处的代码,这样做非常危险. - -这里有一个很好的解决方案,就是使用*规约模式*. - -### 规约模式 - -[规约](Specifications.md)是一种**强命名**,**可重用**,**可组合**,**可测试**的实体过滤器. - -ABP框架提供了基础设施来轻松定义规约类,你可以在代码中方便使用.我们来将非活动问题使用规约方式实现: - -````csharp -using System; -using System.Linq.Expressions; -using Volo.Abp.Specifications; - -namespace IssueTracking.Issues -{ - public class InActiveIssueSpecification : Specification - { - public override Expression> ToExpression() - { - var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); - return i => - - //Open - !i.IsClosed && - - //Assigned to nobody - i.AssignedUserId == null && - - //Created 30+ days ago - i.CreationTime < daysAgo30 && - - //No comment or the last comment was 30+ days ago - (i.LastCommentTime == null || i.LastCommentTime < daysAgo30); - } - } -} -```` - -基类`Specification`通过表达式简化了创建规约的过程,仅需要将仓储中的表达式迁移至规约中. - -现在,我们可以在`Issue` 和 `EfCoreIssueRepository`中重用规约`InActiveIssueSpecification`了. - -#### 在实体内使用规约 - -`Specification` 类提供了一个`IsSatisfiedBy`方法,在实例对象上应用规约检查,判断是否满足规约的要求.代码如下: - -````csharp -public class Issue : AggregateRoot, IHasCreationTime -{ - public bool IsClosed { get; private set; } - public Guid? AssignedUserId { get; private set; } - public DateTime CreationTime { get; private set; } - public DateTime? LastCommentTime { get; private set; } - //... - - public bool IsInActive() - { - return new InActiveIssueSpecification().IsSatisfiedBy(this); - } -} -```` - -实例化一个新的规约`InActiveIssueSpecification`实例,并通过`IsSatisfiedBy` 方法进行规约检查. - -#### 在仓储内使用规约 - -首先,我们先修改一下仓储接口: - -````csharp -public interface IIssueRepository : IRepository -{ - Task> GetIssuesAsync(ISpecification spec); -} -```` - -先将`GetInActiveIssuesAsync` 方法改名为`GetIssuesAsync` ,因为我们改为使用规约方式,现在就无需为不同的查询条件创建不同的接口方法(例如:`GetAssignedIssues(...)`,`GetLockedIssues(...)`) - -再修改下仓储实现: - -````csharp -public class EfCoreIssueRepository : - EfCoreRepository, - IIssueRepository -{ - public EfCoreIssueRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task> GetIssuesAsync(ISpecification spec) - { - return await DbSet - .Where(spec.ToExpression()) - .ToListAsync(); - } -} -```` - -由于`ToExpression()`方法返回一个表达式,因此可以直接将其传递给`Where`方法来过滤实体. - -我们可以在调用`GetIssuesAsync`方法时,传递任何规约的实例. - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IIssueRepository _issueRepository; - - public IssueAppService(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task DoItAsync() - { - var issues = await _issueRepository.GetIssuesAsync( - new InActiveIssueSpecification() - ); - } -} -```` - -##### 默认仓储使用规约 - -实际上,我们不必非要创建一个自定义仓储来使用规约方式,泛型仓储`IRepository`同样可以使用规约,因为`IRepository`已扩展了`IQueryable`对象,因此可以在泛型仓储上使用,代码如下: - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IRepository _issueRepository; - - public IssueAppService(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task DoItAsync() - { - var issues = AsyncExecuter.ToListAsync( - _issueRepository.Where(new InActiveIssueSpecification()) - ); - } -} -```` - -`AsyncExecuter`是ABP框架提供的一个异步LINQ扩展方法(与`ToListAsync`类似),这个方法不依赖依赖EF Core,请参见[仓储文档](Repositories.md). - -#### 组合规约 - -规约强大的能力就是可组合.假设我们还有一个业务规则:`Issue` 仅在里程碑中才返回`true`: - -````csharp -public class MilestoneSpecification : Specification -{ - public Guid MilestoneId { get; } - - public MilestoneSpecification(Guid milestoneId) - { - MilestoneId = milestoneId; - } - - public override Expression> ToExpression() - { - return i => i.MilestoneId == MilestoneId; - } -} -```` - -此规约与`InActiveIssueSpecification`的区别,它是有参数的.我们可以组合两种规约来实现获取指定里程碑下的非活动问题列表 - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IRepository _issueRepository; - - public IssueAppService(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task DoItAsync(Guid milestoneId) - { - var issues = AsyncExecuter.ToListAsync( - _issueRepository - .Where( - new InActiveIssueSpecification() - .And(new MilestoneSpecification(milestoneId)) - .ToExpression() - ) - ); - } -} -```` - -上面的示例使用了`And`扩展方法来组合规约.还有更多的组合方法,如:`Or(...)`和`AndNot(...)`. - -> 有关ABP框架提供的规约更多信息,请参见[规约文档](Specifications.md). - -### 领域服务 - -领域服务主要用来实现本领域的逻辑: - -* 依赖**服务和仓储**. -* 需要使用多个聚合. - -领域服务和领域对象一起使用.领域服务可以获取并返回**实体**,**值对象**等,它们不返回**DTO**.DTO属于应用层的一部分. - -**示例:用户关联一个问题** - -需要在`Issue`实体中实现问题的关联: - -````csharp -public class Issue : AggregateRoot -{ - //... - public Guid? AssignedUserId { get; private set; } - - public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) - { - var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); - - if (openIssueCount >= 3) - { - throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); - } - - AssignedUserId = user.Id; - } - - public void CleanAssignment() - { - AssignedUserId = null; - } -} -```` - -现在我们把逻辑迁移到领域服务中实现. - -首先,修改一下 `Issue` 类: - -````csharp -public class Issue : AggregateRoot -{ - //... - public Guid? AssignedUserId { get; internal set; } -} -```` - -* 删除关联的相关方法. -* 修改属性 `AssignedUserId` 的 setter 为 `internal`,以允许领域服务可以修改. - -下一步是创建一个名为`IssueManager`的领域服务,此领域服务的`AssignToAsync`方法负责将问题关联至指定的用户. - -````csharp -public class IssueManager : DomainService -{ - private readonly IRepository _issueRepository; - - public IssueManager(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task AssignToAsync(Issue issue, AppUser user) - { - var openIssueCount = await _issueRepository.CountAsync( - i => i.AssignedUserId == user.Id && !i.IsClosed - ); - - if (openIssueCount >= 3) - { - throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); - } - - issue.AssignedUserId = user.Id; - } -} -```` - -`IssueManager`可以注入其它服务,来查询指定用户已经关联的未解决问题数量. - -> 我们建议使用 `Manager` 后缀来命名领域服务. - -这种设计的唯一缺陷是可以在类`Issue`外部修改`Issue.AssignedUserId`属性.但是它的访问级别是`internal`而非`public`,在`IssueTracking.Domain`项目内部才能被修改,我们认为这样是合理的: - -* 开发人员清楚领域层的开发规则,他们会使用`IssueManager`来执行业务逻辑. -* 应用层开发人员只能使用`IssueManager`,因此他们无法直接修改实体属性. - -尽管两种方式有各自的优势,但我们更喜欢创建领域服务并注入其它服务来执行业务逻辑这种方式. - -### 应用服务 - -应用服务是实现**用例**的无状态服务.应用服务通常**获取并返回DTO**.应用服务被展现层所使用,应用服务**调用领域对象**(实体,仓储等)来实现用例. - -应用服务的通用原则: - -* 实现特定用例的**应用程序逻辑**,不要在应用服务内实现核心领域的逻辑. -* 应用服务的方法**不要返回实体**.始终只返回DTO. - -**示例:用户关联一个问题** - -````csharp -using System; -using System.Threading.Tasks; -using IssueTracking.Users; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace IssueTracking.Issues -{ - public class IssueAppService : ApplicationService, IIssueAppService - { - private readonly IssueManager _issueManager; - private readonly IRepository _issueRepository; - private readonly IRepository _userRepository; - - public IssueAppService( - IssueManager issueManager, - IRepository issueRepository, - IRepository userRepository) - { - _issueManager = issueManager; - _issueRepository = issueRepository; - _userRepository = userRepository; - } - - [Authorize] - public async Task AssignAsync(IssueAssignDto input) - { - var issue = await _issueRepository.GetAsync(input.IssueId); - var user = await _userRepository.GetAsync(input.UserId); - - await _issueManager.AssignToAsync(issue, user); - - await _issueRepository.UpdateAsync(issue); - } - } -} -```` - -应用服务的方法通常包含三个步骤: - -1. 从数据库获取用例所需的领域对象. -2. 使用领域对象(领域服务,实体等)执行业务逻辑. -3. 将实体的变更持久化至数据库. - -> 如果使用的是EF Core,第三步不是必须的,因为EF Core有追踪实体变化的功能.如果要利用此功能,请参阅上面的"关于数据库独立原则的讨论"章节. - -`IssueAssignDto` 是本示例中一个简单的DTO对象: - -````csharp -using System; - -namespace IssueTracking.Issues -{ - public class IssueAssignDto - { - public Guid IssueId { get; set; } - public Guid UserId { get; set; } - } -} -```` - -### 数据传输对象 - -[DTO](Data-Transfer-Objects.md)是应用层与展现层间传输数据的简单对象.应用服务方法获取并返回Dto. - -#### DTO通用原则和最佳实践 - -* DTO应该是**可被序列化**的.因为大所数情况下,DTO是通过网络传输的,因此它应该具有**无参的构造函数**. -* 不应该包含任何**业务逻辑**. -* **切勿**继承或引用**实体**. - -**输入DTO**(应用服务方法的参数)与 **输出DTO** (应用服务方法的返回对象)具有不同的作用,因此,它们应该区别对待. - -#### 输入DTO 最佳实践 - -##### 不要在输入DTO中定义不使用的属性 - -**仅**在输入DTO中定义用例**所需要的属性**!否则,会造成调用应用服务的客户端产生困惑. - -这个规则好像没什么必要,因为没人会在方法参数(输入DTO)中添加无用的属性.但是,有时候,特别是在重用DTO时,输入DTO会包含无用的属性. - -##### 不要重用输入DTO - -**为每个用例**(应用服务的方法)单独定义一个**专属的输入DTO**.否则,在一些情况下,会添加一些不被使用的属性,这样就违反上面的规则:不要在输入DTO中定义不使用的属性. - -在两个用例中重用相同的DTO似乎很有吸引力,因为它们的属性是一模一样的.现阶段它们是一样的,但是随着业务变化,可能它们会产生差异,届时你可能还是需要进行拆分.**和用例间的耦合相比,代码的复制可能是更好的做法**. - -重用输入DTO的另外一种方式是**继承**DTO,这同样会产生上面描述的问题. - -**示例:用户应用服务** - -````csharp -public interface IUserAppService : IApplicationService -{ - Task CreateAsync(UserDto input); - Task UpdateAsync(UserDto input); - Task ChangePasswordAsync(UserDto input); -} -```` - -`UserDto`作为`IUserAppService`所有方法的输入DTO,代码如下: - -````csharp -public class UserDto -{ - public Guid Id { get; set; } - public string UserName { get; set; } - public string Email { get; set; } - public string Password { get; set; } - public DateTime CreationTime { get; set; } -} -```` - -对于上面的示例: - -* `Id` 属性在 *Create* 方法中,没有被使用,因为`Id`由服务器生成. -* `Password` 属性在 *Update* 方法中,没有被使用.因为有修改密码的单独方法. -* `CreationTime` 属性未被使用,因为不允许客户端发送创建时间属性,这个应该由服务器生成. - -较好的做法应该这样: - -````csharp -public interface IUserAppService : IApplicationService -{ - Task CreateAsync(UserCreationDto input); - Task UpdateAsync(UserUpdateDto input); - Task ChangePasswordAsync(UserChangePasswordDto input); -} -```` - -下面是输入DTO的定义: - -````csharp -public class UserCreationDto -{ - public string UserName { get; set; } - public string Email { get; set; } - public string Password { get; set; } -} - -public class UserUpdateDto -{ - public Guid Id { get; set; } - public string UserName { get; set; } - public string Email { get; set; } -} - -public class UserChangePasswordDto -{ - public Guid Id { get; set; } - public string Password { get; set; } -} -```` - -虽然编写了更多的代码,但是这样可维护性更高. - -**例外情况:** 该规则有一些例外的情况,例如,你想开发两个方法,它们共用相同的输入DTO(通过继承或重用),有一个报表页面有多个过滤条件,多个应用服务使用相同的输入参数返回不同的结果(如,大屏展示数据,Excel报表,csv报表).这种情况下,你是需要修改一个参数,多个应用服务都应该一起被修改. - -##### 输入DTO中验证逻辑 - -- 仅在DTO内执行**简单验证**.使用数据注解验证属性或通过`IValidatableObject` 方式. -- **不要执行领域验证**.例如,不要在DTO中检查用户名是否唯一的验证. - -**示例:使用注解方式** - -````csharp -using System.ComponentModel.DataAnnotations; - -namespace IssueTracking.Users -{ - public class UserCreationDto - { - [Required] - [StringLength(UserConsts.MaxUserNameLength)] - public string UserName { get; set; } - - [Required] - [EmailAddress] - [StringLength(UserConsts.MaxEmailLength)] - public string Email { get; set; } - - [Required] - [StringLength(UserConsts.MaxEmailLength, - MinimumLength = UserConsts.MinPasswordLength)] - public string Password { get; set; } - } -} -```` - -当输入无效时,ABP框架会自动验证输入DTO,抛出`AbpValidationException`异常,并向客户返回`400`的HTTP状态码. - -> 一些开发人员认为最好将验证规则和DTO分离.我们认为声明性(数据注解)方式是比较实用的,不会引起任何设计问题.如果你喜欢其它方式,ABP还支持[FluentValidation继承](FluentValidation.md).有关所有验证的详细文档,请参见[验证文档](Validation.md). - -#### 输出DTO最佳实践 - -* 保持**数量较少**的输出DTO,尽可能**重用输出DTO**(例外:不要将输入DTO作为输出DTO). -* 输出DTO可以包含比用例需要的属性**更多**的属性. -* 针对 **Create** 和 **Update** 方法,返回实体的DTO. - -以上建议的原因是: - -* 使客户端代码易于开发和扩展: - * 客户端处理**相似但不相同**的DTO是有问题的. - * 将来UI或客户端通常会使用到DTO上的**其它属性**.返回实体的所有属性,可以在无需修改服务端代码的情况下,只修改客户端代码. - * 在开放API给**第三方客户端**时,避免不同需求的返回不同的DTO. -* 使服务器端代码易于开发和扩展: - * 你需要**维护**的类的数量较少. - * 你可以重用Entity->DTO**对象映射**的代码. - * 不同的方法返回相同的类型,可以使得在**添加新方法**时变的简单明了. - -**示例:不同的方法返回不同的DTO** - -````csharp -public interface IUserAppService : IApplicationService -{ - UserDto Get(Guid id); - List GetUserNameAndEmail(Guid id); - List GetRoles(Guid id); - List GetList(); - UserCreateResultDto Create(UserCreationDto input); - UserUpdateResultDto Update(UserUpdateDto input); -} -```` - -> 这里我们没有使用异步方式,是为了示例更清晰,你实际代码中应该使用异步方式) - -上面的示例代码中,每个方法都返回了不同的DTO类型,这样处理,会导致查询数据,映射对象都会有很多重复的代码. - -应用服务`IUserAppService` 可以简化成如下代码: - -````csharp -public interface IUserAppService : IApplicationService -{ - UserDto Get(Guid id); - List GetList(); - UserDto Create(UserCreationDto input); - UserDto Update(UserUpdateDto input); -} -```` - -只需使用一个DTO对象 - -````csharp -public class UserDto -{ - public Guid Id { get; set; } - public string UserName { get; set; } - public string Email { get; set; } - public DateTime CreationTime { get; set; } - public List Roles { get; set; } -} -```` - -* 删除 `GetUserNameAndEmail`和`GetRoles` 方法,因为,返回的DTO中已经包含了对应的信息. -* `GetList`方法的返回的泛型类型与`Get`方法的返回类型一致. -* `Create` 与 `Update`的返回类型都是 `UserDto`. - -如上所述,使用相同的DTO有很多优点.例如,我们在UI上使用**表格**展现用户集合,再用户数据更新后,我们可以获取到返回对象,并对**表格数据源进行更新**.因此,我们无需再次调用`GetList`来获取全部数据.这就是我们为什么建议`Create` 与 `Update`方法都返回相同`UserDto`的原因. - -##### 讨论 - -输出DTO的建议并不适用于所有情况.出于**性能**原因,我们可以忽略这些建议,尤其是在返回**大量数据**,为UI定制,**并发量较高**时. - -在这些情况下,你可以定制仅包含**必要信息的DTO**.上面的建议只适用于额外多些属性并**不会损失太多性能**,并关注代码**可维护**的应用系统. - -#### 对象映射到对象 - -当两个对象具有相同或相似的属性,自动将[对象映射到对象](Object-To-Object-Mapping.md)是一种将值从一个对象复制到另外一个对象非常有用的方法. - -DTO和实体通常具有相同或相似的属性,你经常需要从一个实体创建一个DTO对象.相较于手动映射,基于[AutoMapper](http://automapper.org/)的ABP[对象映射系统](Object-To-Object-Mapping.md),更加方便简单. - -* **仅**在**实体=>输出DTO**的时候使用自动映射. -* 不要在**输入DTO=>Entity**的时候使用自动映射. - -因为以下原因,你不应该在输入DTO=>Entity的时候使用自动映射: - -1. 实体类通常具有一个**构造函数**,该构造函数带有参数,确保创建有效的对象,而自动对象映射通常需要一个无参构造函数. -2. 大多数实体中的属性setter是**私有的**,你只能调用实体上的方法来修改属性. -3. 另外,需要进行对用户或客户端的**输入参数进行验证**,而不是盲目映射到实体属性上. - -尽管其中一些问题可以额外配置映射来解决(如,AutoMapper允许自定义映射规则),但这样会使业务逻辑被**耦合到基础设施代码中**.我们认为业务代码应该明确,清晰且易于理解. - -有关此部分的建议,请参加下面的"*实体创建*"部分 - -## 用例 - -本节将演示一些用例,并讨论替代方案 - -### 实体创建 - -实体或聚合根的创建,是实体生命周期的开始."*聚合/聚合根规则及最佳实践*"章节中建议为Entity类定义**一个主构造函数**,以确保创建一个**有效的实体**.因此,需要创建该实体对象实例时,都应该**使用该构造函数**. - - `Issue` 聚合根的代码如下: - -````csharp -public class Issue : AggregateRoot -{ - public Guid RepositoryId { get; private set; } - public string Title { get; private set; } - public string Text { get; set; } - public Guid? AssignedUserId { get; internal set; } - - public Issue( - Guid id, - Guid repositoryId, - string title, - string text = null - ) : base(id) - { - RepositoryId = repositoryId; - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - Text = text; //Allow empty/null - } - - private Issue() { /* Empty constructor is for ORMs */ } - - public void SetTitle(string title) - { - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - } - - //... -} -```` - -* 通过其非空参数的构造函数创建有效的实体. -* 如需修改 `Title` 属性,必须通过`SetTitle` 方法,来确保被设置值的有效性. -* 如需将此问题关联至用户,则需要使用`IssueManager`(关联前需要执行一些业务逻辑,相关逻辑参见上面的"*领域服务*"部分) -* `Text` 属性setter是公开的,因为它可以为null,并且本示例中也没有验证规则,它在构造函数中也是可选的. - -创建问题的应用服务代码: - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IssueManager _issueManager; - private readonly IRepository _issueRepository; - private readonly IRepository _userRepository; - - public IssueAppService( - IssueManager issueManager, - IRepository issueRepository, - IRepository userRepository) - { - _issueManager = issueManager; - _issueRepository = issueRepository; - _userRepository = userRepository; - } - - public async Task CreateAsync(IssueCreationDto input) - { - // Create a valid entity - var issue = new Issue( - GuidGenerator.Create(), - input.RepositoryId, - input.Title, - input.Text - ); - - // Apply additional domain actions - if (input.AssignedUserId.HasValue) - { - var user = await _userRepository.GetAsync(input.AssignedUserId.Value); - await _issueManager.AssignToAsync(issue, user); - } - - // Save - await _issueRepository.InsertAsync(issue); - - // Return a DTO represents the new Issue - return ObjectMapper.Map(issue); - } -} -```` - -`CreateAsync` 方法; - -* 使用 `Issue` **构造函数** 创建一个有效的问题.`Id` 属性通过[IGuidGenerator](Guid-Generation.md)服务生成.此处没有使用对象自动映射. -* 如果需要将**问题关联至用户**,则通过 `IssueManager`来执行关联逻辑. -* **保存** 实体至数据库. -* 最后,使用 `IObjectMapper` 将`Issue`实体**映射**为 `IssueDto` 并返回. - -#### 在创建实体时执行领域规则 - -`Issue`除了在构造函数中进行了一些简单验证外,示例中没其它业务验证.在有些情况下,在创建实体时会有一些其它业务规则. - -假如,已经存在一个完全相同的问题,那么就不要再创建问题.这个规则应该在哪里执行?在**应用服务中执行是不对的**,因为它是**核心业务(领域)的规则**,应该将此规则在领域服务中执行.在这种情况下,我们应该在`IssueManager`中执行此规则,因此应该强制应用服务调用领域服务`IssueManager`来新建`Issue`. - -首先修改 `Issue` 构造函数的访问级别为 `internal`: - -````csharp -public class Issue : AggregateRoot -{ - //... - - internal Issue( - Guid id, - Guid repositoryId, - string title, - string text = null - ) : base(id) - { - RepositoryId = repositoryId; - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - Text = text; //Allow empty/null - } - - //... -} -```` - -这样可以防止,应用服务直接使用`Issue` 的构造函数去创建`Issue` 实例,必须使用 `IssueManager`来创建.然后我们再添加一个`CreateAsync`方法: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace IssueTracking.Issues -{ - public class IssueManager : DomainService - { - private readonly IRepository _issueRepository; - - public IssueManager(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task CreateAsync( - Guid repositoryId, - string title, - string text = null) - { - if (await _issueRepository.AnyAsync(i => i.Title == title)) - { - throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); - } - - return new Issue( - GuidGenerator.Create(), - repositoryId, - title, - text - ); - } - } -} -```` - -* `CreateAsync` 方法会检查标题是否已经存在,当有相同标题的问题时,会抛出业务异常. -* 如果标题没有重复的,则创建并返回一个新的 `Issue`对象. - -再修改`IssueAppService` 的代码,来调用 `IssueManager`的 `CreateAsync` 方法: - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IssueManager _issueManager; - private readonly IRepository _issueRepository; - private readonly IRepository _userRepository; - - public IssueAppService( - IssueManager issueManager, - IRepository issueRepository, - IRepository userRepository) - { - _issueManager = issueManager; - _issueRepository = issueRepository; - _userRepository = userRepository; - } - - public async Task CreateAsync(IssueCreationDto input) - { - // Create a valid entity using the IssueManager - var issue = await _issueManager.CreateAsync( - input.RepositoryId, - input.Title, - input.Text - ); - - // Apply additional domain actions - if (input.AssignedUserId.HasValue) - { - var user = await _userRepository.GetAsync(input.AssignedUserId.Value); - await _issueManager.AssignToAsync(issue, user); - } - - // Save - await _issueRepository.InsertAsync(issue); - - // Return a DTO represents the new Issue - return ObjectMapper.Map(issue); - } -} - -// *** IssueCreationDto class *** -public class IssueCreationDto -{ - public Guid RepositoryId { get; set; } - [Required] - public string Title { get; set; } - public Guid? AssignedUserId { get; set; } - public string Text { get; set; } -} -```` - -##### 讨论:为什么`IssueManager`中没有执行`Issue`的保存? - -你可能会问"**为什么`IssueManager`中没有执行`Issue`的保存?**".我们认为这是应用服务的职责. - -因为,应用服务可能在保存`Issue`对象之前,需要对其它对象进行修改.如果领域服务执行了保存,那么*保存*操作就是重复的. - -* 会触发两次数据库交互,这会导致性能损失. -* 需要额外添加显式的事务来包含这两个操作,才能保证数据一致性. -* 如果因为业务规则取消了实体的创建,则应该在数据库事务中回滚事务,取消所有操作. - -假如在`IssueManager.CreateAsync`中先保存一次数据,那么数据会先执行一次*Insert*操作,后面关联用户的逻辑执行后,又会再执行一次*Update*操作. - -如果不在`IssueManager.CreateAsync`中保存数据,那么,新建`Issue`和关联用户,只会执行一次*Insert*操作. - -##### 讨论:为什么没有在应用服务中执行标题是否重复的检查? - -简单地说"因为它是**核心领域逻辑**,应该在领域层实现".这又带来一个新问题,"**如何确定**是领域层逻辑,还是应用层逻辑"?(这个我们后面再详细讨论) - -对于此示例,可以用一个简单的问题来判断到底是领域逻辑还是应用逻辑:"如果还有另外一种创建`Issue`的方式(用例),我们是否还需要执行?如果需要执行,就属于领域层逻辑,不需要执行就是应用层逻辑".你可能认为为什么还有别的用例来创建`Issue`呢? - -* 应用程序的**最终用户**可能会在UI上创建`Issue`. -* 系统内部人员,可以在**后台管理**端采用另外一种方式创建`Issue`(这种情况下,可能使用不同的业务规则). -* 对**第三方客户端**开放的API,它们的规则又有所不同. -* 还有**后台作业系统**会执行某些操作时创建`Issue`,这样,它是在没有任何用户交互情况下创建`Issue`. -* 还有可能是UI上某个按钮,可以将某些内容(例如,讨论)转为`Issue`. - -我们还可以举更多例子.所有这些都应该通过**不同的应用服务方法来实现**(请参见下面的"*多个应用服务层*"部分),但是它们**始终遵循**以下的规则: - -新的问题标题不能与任何已有的问题标题相同.这就是为什么说的"*标题是否重复的检查*"属于核心领域逻辑的原因,这个逻辑应该在领域层,而**不应该**在应用层的所有方法中**去重复**定义. - -### 修改实体 - -创建实体后,将根据用例对实体进行修改,直到将其从系统中删除.可以有不同的用例直接或间接的修改实体. - -在本节中,我们将讨论一种典型的修改操作,该操作会修改`Issue`的多个属性. - -从*Update* DTO开始: - -````csharp -public class UpdateIssueDto -{ - [Required] - public string Title { get; set; } - public string Text { get; set; } - public Guid? AssignedUserId { get; set; } -} -```` - -对比`IssueCreationDto`,可以发现,缺少了`RepositoryId`属性,因为我们不允许跨仓库移动`Issue`.仅`Title`属性是必填的. - -`IssueAppService`中*Update*的实现如下:: - -````csharp -public class IssueAppService : ApplicationService, IIssueAppService -{ - private readonly IssueManager _issueManager; - private readonly IRepository _issueRepository; - private readonly IRepository _userRepository; - - public IssueAppService( - IssueManager issueManager, - IRepository issueRepository, - IRepository userRepository) - { - _issueManager = issueManager; - _issueRepository = issueRepository; - _userRepository = userRepository; - } - - public async Task UpdateAsync(Guid id, UpdateIssueDto input) - { - // Get entity from database - var issue = await _issueRepository.GetAsync(id); - - // Change Title - await _issueManager.ChangeTitleAsync(issue, input.Title); - - // Change Assigned User - if (input.AssignedUserId.HasValue) - { - var user = await _userRepository.GetAsync(input.AssignedUserId.Value); - await _issueManager.AssignToAsync(issue, user); - } - - // Change Text (no business rule, all values accepted) - issue.Text = input.Text; - - // Update entity in the database - await _issueRepository.UpdateAsync(issue); - - // Return a DTO represents the new Issue - return ObjectMapper.Map(issue); - } -} -```` - -* `UpdateAsync` 方法参数 `id`被作为独立参数,放置在`UpdateIssueDto`之外.这是一项设计决策,当你将此应用服务[自动导出](API/Auto-API-Controllers.md)为HTTP API时,API端点时帮助ABP正确定义HTTP路由,这与DDD无关. -* 首先从数据库中**获取** `Issue` 实体. -* 通过 `IssueManager`的 `ChangeTitleAsync`方法修改标题,而非直接通过 `Issue.SetTitle(...)`直接修改.因为我们需要像创建时那样,**执行标题的重复检查逻辑**.这需要对`Issue`类和`IssueManager`类进行一些调整(将在下面说明). -* 通过 `IssueManager`的 `AssignToAsync` 方法来**关联用户**. -* 直接设置 `Issue.Text`属性,因为它本身没有任何业务逻辑需要执行.如果以后需要可以再进行重构. -* **保存修改**至数据库.同样,保存修改后的实体属于应用服务的职责,它可以协调业务对象和事务.如果在`IssueManager`内部的 `ChangeTitleAsync` 和 `AssignToAsync` 方法中进行保存,则会导致两次数据库操作(请参见上面的*讨论:为什么`IssueManager`中没有执行`Issue`的保存?*) -* 最后,使用 `IObjectMapper` 将`Issue`实体**映射**为 `IssueDto` 并返回. - -如前所述,我们需要对`Issue`类和`IssueManager`类进行一些调整: - -首先,修改 `SetTitle`方法的访问级别为internal: - -````csharp -internal void SetTitle(string title) -{ - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); -} -```` - -再在`IssueManager`中添加一个新方法来修改标题: - -````csharp -public async Task ChangeTitleAsync(Issue issue, string title) -{ - if (issue.Title == title) - { - return; - } - - if (await _issueRepository.AnyAsync(i => i.Title == title)) - { - throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); - } - - issue.SetTitle(title); -} -```` - -## 领域逻辑和应用逻辑 - -如前所述,领域驱动设计中的*业务逻辑*分为两部分(各层):领域逻辑和应用逻辑 - -![domain-driven-design-domain-vs-application-logic](images/domain-driven-design-domain-vs-application-logic.png) - -领域逻辑是系统的*核心领域规则*组成,而应用逻辑则满足特定的*用例*. - -虽然定义很明确,但是实施起来却并不容易.你可能无法确定哪些代码应该属于领域层,哪些代码应该属于应用层,本节会尝试解释差异. - -### 多应用层 - -当你的系统很大时,DDD有助于**处理复杂问题**.尤其是,**单个领域**需要多个**应用程序运行**,那么**领域逻辑与应用逻辑分离**就变的非常重要. - -假设你正在构建一个具有多个应用程序的系统: - -* 一个**公开的应用网站**,使用ASP.NET Core MVC构建,展示商品给来访者.这样的网站不需要身份验证即可查看商品.来访者只有执行了某些操作(例如,将商品添加到购物车)后,才需要登录网站. -* 一个**后台管理系统**,UI使用Angular,通过REST API请求数据.内部员工使用这个系统来维护数据(例如,编辑商品说明). -* 一个**移动端应用程序**,它比公开的网站UI上更加简洁.它通过REST API或其它技术(例如,TCP sockets)请求数据. - -![domain-driven-design-multiple-applications](images/domain-driven-design-multiple-applications.png) - -每个应用程序都有不同的**需求**,不同的**用例**(应用服务方法),不同的DTO,不同的**验证**和**授权**规则等. - -将所有这些逻辑都集中到一个应用层中,会使你的服务包含太多的`if`条件分支及**复杂的业务逻辑**,从而使你的代码开发,**维护**,测试,引发各种问题. - -如果你在一个领域中有多个应用程序 - -- 为每种应用程序或客户端创建独立的应用层,并在这些单独层中执行特定于应用业务逻辑. -- 使用共享的核心领域逻辑 - -为了实现这样的设计,首先我们需要区分领域逻辑和应用逻辑. - -为了更清楚的实现,你可以为不同的应用类型创建不同的项目(`.csproj`): - -* `IssueTracker.Admin.Application` 和 `IssueTracker.Admin.Application.Contracts` 为后台管理系统提供服务. -* `IssueTracker.Public.Application` 和 `IssueTracker.Public.Application.Contracts` 为公开网站提供服务. -* `IssueTracker.Mobile.Application` 和 `IssueTracker.Mobile.Application.Contracts` 为移动端应用提供服务. - -### 示例 - -本节包含一些应用服务及领域服务的示例,讨论业务逻辑应该放置在哪一层 - -**示例:在领域服务中创建`Organization`** - -````csharp -public class OrganizationManager : DomainService -{ - private readonly IRepository _organizationRepository; - private readonly ICurrentUser _currentUser; - private readonly IAuthorizationService _authorizationService; - private readonly IEmailSender _emailSender; - - public OrganizationManager( - IRepository organizationRepository, - ICurrentUser currentUser, - IAuthorizationService authorizationService, - IEmailSender emailSender) - { - _organizationRepository = organizationRepository; - _currentUser = currentUser; - _authorizationService = authorizationService; - _emailSender = emailSender; - } - - public async Task CreateAsync(string name) - { - if (await _organizationRepository.AnyAsync(x => x.Name == name)) - { - throw new BusinessException("IssueTracking:DuplicateOrganizationName"); - } - - await _authorizationService.CheckAsync("OrganizationCreationPermission"); - - Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}"); - - var organization = new Organization(); - - await _emailSender.SendAsync( - "systemadmin@issuetracking.com", - "New Organization", - "A new organization created with name: " + name - ); - - return organization; - } -} -```` - -我们来逐个检查`CreateAsync`方法中的代码,讨论是否应该在领域服务中 - -* **正确**:首先检查有**无重复的组织名称**,并抛出异常.这与核心领域规则有关,因为我们绝对不允许重复的名称. -* **错误**:领域服务不应该执行**授权检查**,[授权](Authorization.md)应该在应用层处理. -* **错误**:它记录了日志,包括[当前用户](CurrentUser.md)的`UserName`.领域服务不应该依赖当前用户,即便系统中没有用户,领域服务也应可用.当前用户应该是与展现层或应用层有关的概念. -* **错误**:它发送了有关新组织被创建的[邮件](Emailing.md),我们认为这也是特定用例的业务逻辑,你可能像在不同的用例中创建不同的邮件,又或者某些情况无需发送邮件. - -**示例:应用服务中创建`Organization`** - -````csharp -public class OrganizationAppService : ApplicationService -{ - private readonly OrganizationManager _organizationManager; - private readonly IPaymentService _paymentService; - private readonly IEmailSender _emailSender; - - public OrganizationAppService( - OrganizationManager organizationManager, - IPaymentService paymentService, - IEmailSender emailSender) - { - _organizationManager = organizationManager; - _paymentService = paymentService; - _emailSender = emailSender; - } - - [UnitOfWork] - [Authorize("OrganizationCreationPermission")] - public async Task CreateAsync(CreateOrganizationDto input) - { - await _paymentService.ChargeAsync( - CurrentUser.Id, - GetOrganizationPrice() - ); - - var organization = await _organizationManager.CreateAsync(input.Name); - - await _organizationManager.InsertAsync(organization); - - await _emailSender.SendAsync( - "systemadmin@issuetracking.com", - "New Organization", - "A new organization created with name: " + input.Name - ); - - return organization; // !!! - } - - private double GetOrganizationPrice() - { - return 42; //Gets from somewhere else... - } -} -```` - -我们来逐个检查`CreateAsync`方法中的代码,讨论是否应该在应用服务中 - -* **正确**:应用服务的方法应该是一个工作单元(事务).ABP的[工作单元](Unit-Of-Work.md)系统可以使得此工作自动进行(甚至无需`[UnitOfWork]`注解). -* **正确**: [授权](Authorization.md)应该在应用层处理.这里通过使用`[Authorize]`来完成. -* **正确**:调用付款(基础设施服务)为此操作收取费用(创建组织是我们业务中的付费服务). -* **正确**:应用服务负责将变更的数据保存到数据库. -* **正确**:我们可以将[邮件](Emailing.md)作为通知发送给管理员. -* **错误**:请勿从应用服务中返回实体,应该返回DTO. - -**讨论:为什么不将支付逻辑移到领域服务中?** - -你可能想知道为什么付款逻辑代码不在`OrganizationManager`中.付款是非常**重要的事情**,我们不能**遗漏任何一次付款**. - -它确实非常重要,但是,它不能放到领域服务中.我们可能还有**其它用例**来创建组织但不收取任何费用.例如: - -* 管理员可以在后台管理系统创建新组织,而无需支付任何费用. -* 后台作业系统导入,集成,同步组织而无需支付费用. - -如你所见,**付款不是创建有效组织的必要操作**.它是特定的应用服务逻辑. - -**示例:CRUD操作** - -````csharp -public class IssueAppService -{ - private readonly IssueManager _issueManager; - - public IssueAppService(IssueManager issueManager) - { - _issueManager = issueManager; - } - - public async Task GetAsync(Guid id) - { - return await _issueManager.GetAsync(id); - } - - public async Task CreateAsync(IssueCreationDto input) - { - await _issueManager.CreateAsync(input); - } - - public async Task UpdateAsync(UpdateIssueDto input) - { - await _issueManager.UpdateAsync(input); - } - - public async Task DeleteAsync(Guid id) - { - await _issueManager.DeleteAsync(id); - } -} -```` - -该应用服务本身**不执行任何操作**,并将所有**操作转发给** *领域服务*.它甚至将DTO传递给`IssueManager` - -* 如果没有**任何业务逻辑**,只有简单的**CRUD**操作,**请勿**创建领域服务. -* **切勿**将**DTO**传递给领域服务,或从领域服务返回**DTO**. - -可以在应用服务中直接注入仓储,实现查询,创建,更新及删除操作.除非在这些操作过程中需要执行某些领域逻辑,在这种情况下,请创建领域服务. - -> 不要创建"将来可能需要"这种CRUD领域服务方法([YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it)),在需要时重构它并重构现有代码. 由于应用层优雅地抽象了领域层,因此重构过程不会影响UI层和其他客户端. - -## 相关书籍 - -如果你对领域驱动设计和构建大型系统有兴趣,建议将以下书籍作为参考书籍: - -* "*Domain Driven Design*" by Eric Evans -* "*Implementing Domain Driven Design*" by Vaughn Vernon -* "*Clean Architecture*" by Robert C. Martin diff --git a/docs/zh-Hans/Domain-Driven-Design.md b/docs/zh-Hans/Domain-Driven-Design.md deleted file mode 100644 index d088f7596b..0000000000 --- a/docs/zh-Hans/Domain-Driven-Design.md +++ /dev/null @@ -1,33 +0,0 @@ -# 领域驱动设计 - -## 什么是DDD? - -ABP框架提供了**基础设施**使基于**领域驱动设计**的开发更易实现. DDD在[维基百科中的定义](https://zh.wikipedia.org/wiki/%E5%9F%9F%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91)如下: - -> **领域驱动设计(DDD)** 是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法. 领域驱动设计的前提是: -> -> - 把项目的主要重点放在核心领域和领域逻辑上 -> - 把复杂的设计放在领域模型上 -> - 发起技术专家和领域专家之间的创造性协作,以迭代方式完善解决特定领域问题的概念模型 - -### 分层 - -ABP框架遵循DDD原则和模式去实现分层应用程序模型,该模型由四个基本层组成: - -- **表示层**: 为用户提供接口. 使用*应用层*实现与用户交互. -- **应用层**: 表示层与领域层的中介,编排业务对象执行特定的应用程序任务. 使用应用程序逻辑实现用例. -- **领域层**: 包含业务对象以及业务规则. 是应用程序的核心. -- **基础设施层**: 提供通用的技术功能,支持更高的层,主要使用第三方类库. - -## 内容 - -* **领域层** - * [实体与聚合根](Entities.md) - * 值对象 - * [仓储](Repositories.md) - * 领域服务 - * 规约 -* **应用层** - * [应用服务](Application-Services.md) - * [数据传输对象(DTO)](Data-Transfer-Objects.md) - * 工作单元 \ No newline at end of file diff --git a/docs/zh-Hans/Domain-Services.md b/docs/zh-Hans/Domain-Services.md deleted file mode 100644 index 0c61dd2330..0000000000 --- a/docs/zh-Hans/Domain-Services.md +++ /dev/null @@ -1,127 +0,0 @@ -# 领域服务 - -## 介绍 - -在 [领域驱动设计](Domain-Driven-Design.md) (DDD) 解决方案中,核心业务逻辑通常在聚合 ([实体](Entities.md)) 和领域服务中实现. 在以下情况下特别需要创建领域服务 - -* 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑. -* 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合. - -## ABP 领域服务基础设施 - -领域服务是简单的无状态类. 虽然你不必从任何服务或接口派生,但 ABP 框架提供了一些有用的基类和约定. - -### DomainService 和 IDomainService - -从 `DomainService` 基类派生领域服务或直接实现 `IDomainService` 接口. - -**示例: 创建从 `DomainService` 基类派生的领域服务.** - -````csharp -using Volo.Abp.Domain.Services; -namespace MyProject.Issues -{ - public class IssueManager : DomainService - { - - } -} -```` - -当你这样做时: - -* ABP 框架自动将类注册为瞬态生命周期到依赖注入系统. -* 你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如 [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)). - -> 建议使用 `Manager` 或 `Service` 后缀命名领域服务. 我们通常使用如上面示例中的 `Manager` 后缀. -**示例: 实现将问题分配给用户的领域逻辑** - -````csharp -public class IssueManager : DomainService -{ - private readonly IRepository _issueRepository; - public IssueManager(IRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task AssignAsync(Issue issue, AppUser user) - { - var currentIssueCount = await _issueRepository - .CountAsync(i => i.AssignedUserId == user.Id); - - //Implementing a core business validation - if (currentIssueCount >= 3) - { - throw new IssueAssignmentException(user.UserName); - } - issue.AssignedUserId = user.Id; - } -} -```` - -问题是定义如下所示的 [聚合根](Entities.md): - -````csharp -public class Issue : AggregateRoot -{ - public Guid? AssignedUserId { get; internal set; } - - //... -} -```` - -* 使用 `internal` 的 set 确保外层调用者不能直接在调用 set ,并强制始终使用 `IssueManager` 为 `User` 分配 `Issue`. - -### 使用领域服务 - -领域服务通常用于 [应用程序服务](Application-Services.md). - -**示例: 使用 `IssueManager` 将问题分配给用户** - -````csharp -using System; -using System.Threading.Tasks; -using MyProject.Users; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -namespace MyProject.Issues -{ - public class IssueAppService : ApplicationService, IIssueAppService - { - private readonly IssueManager _issueManager; - private readonly IRepository _userRepository; - private readonly IRepository _issueRepository; - public IssueAppService( - IssueManager issueManager, - IRepository userRepository, - IRepository issueRepository) - { - _issueManager = issueManager; - _userRepository = userRepository; - _issueRepository = issueRepository; - } - public async Task AssignAsync(Guid id, Guid userId) - { - var issue = await _issueRepository.GetAsync(id); - var user = await _userRepository.GetAsync(userId); - await _issueManager.AssignAsync(issue, user); - await _issueRepository.UpdateAsync(issue); - } - } -} -```` - -由于 `IssueAppService` 在应用层, 它不能直接将问题分配给用户.因此,它使用 `IssueManager`. - -## 应用程序服务与领域服务 - -虽然 [应用服务](Application-Services.md) 和领域服务都实现了业务规则,但存在根本的逻辑和形式差异: - -* 应用程序服务实现应用程序的 **用例** (典型 Web 应用程序中的用户交互), 而领域服务实现 **核心的、用例独立的领域逻辑**. -* 应用程序服务获取/返回 [数据传输对象](Data-Transfer-Objects.md), 领域服务方法通常获取和返回 **领域对象** ([实体](Entities.md), [值对象](Value-Objects.md)). -* 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用. - -## 生命周期 - -领域服务的生命周期是 [瞬态](https://docs.abp.io/en/abp/latest/Dependency-Injection) 的,它们会自动注册到依赖注入服务. diff --git a/docs/zh-Hans/Dynamic-Proxying-Interceptors.md b/docs/zh-Hans/Dynamic-Proxying-Interceptors.md deleted file mode 100644 index 6657f1ca33..0000000000 --- a/docs/zh-Hans/Dynamic-Proxying-Interceptors.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic Proxying / Interceptors - -待添加 diff --git a/docs/zh-Hans/Emailing.md b/docs/zh-Hans/Emailing.md deleted file mode 100644 index 2ca01b5cb3..0000000000 --- a/docs/zh-Hans/Emailing.md +++ /dev/null @@ -1,257 +0,0 @@ -# 邮件发送系统 - -ABP 框架为发送电子邮件提供各种服务、设置和集成; - -* 提供用于发送电子邮件的`IEmailSender`服务. -* 定义 [settings](Settings.md)来配置电子邮件发送. -* 集成到[后台作业系统](Background-Jobs.md)以通过后台作业发送电子邮件. -* 提供[MailKit 集成](MailKit.md)包. - -## 安装 - -> 如果你使用的是[应用程序启动模板](Startup-Templates/Application.md),则该软件包已安装。 -> -建议使用 [ABP CLI](CLI.md) 安装此包。在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令: - -````bash -abp add-package Volo.Abp.Emailing -```` -如果你还没有做到这一点,你首先需要安装 ABP CLI。有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.Emailing)。 - -## 发送电子邮件 - -### IEmailSender - -[注入](Dependency-Injection.md) `IEmailSender` 到任何服务并使用 `SendAsync` 方法发送电子邮件。 - -**示例** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; - -namespace MyProject -{ - public class MyService : ITransientDependency - { - private readonly IEmailSender _emailSender; - - public MyService(IEmailSender emailSender) - { - _emailSender = emailSender; - } - - public async Task DoItAsync() - { - await _emailSender.SendAsync( - "target@domain.com", // target email address - "Email subject", // subject - "This is email body..." // email body - ); - } - } -} -```` - -`SendAsync` 方法具有重载以提供更多参数,例如; - -* **from**: 你可以将其设置为设置发件人电子邮件地址。如果未提供,则使用默认发件人地址(请参阅下面的电子邮件设置) -* **isBodyHtml**: 表示邮件正文是否可以包含HTML标签。**默认:true**。 - -> `IEmailSender` 是建议的发送邮件的方式,因为它使你的代码提供者独立。 - -#### 邮件消息 - -除了原始参数之外,你还可以传递一个**标准的 `MailMessage` 对象**([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage) ) 到 `SendAsync` 方法以设置更多选项,例如添加附件。 - -### ISmtpEmailSender - -默认情况下,发送电子邮件由标准的 `SmtpClient` 类([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient))实现。实现类是`SmtpEmailSender`。此类还公开了 `ISmtpEmailSender` 服务(除了 `IEmailSender`)。 - -大多数时候你想直接使用`IEmailSender`来让你的代码提供者独立。但是,如果要创建具有相同电子邮件设置的 `SmtpClient` 对象,可以注入 `ISmtpEmailSender` 并使用其 `BuildClientAsync` 方法获取 `SmtpClient` 对象并自己发送电子邮件。 - -## 发送邮件任务队列/后台作业 - -`IEmailSender`有一个`QueueAsync`方法,可以用来将邮件添加到后台作业队列中,在后台线程中发送。通过这种方式,你不会因为等待发送邮件而占用用户的时间。`QueueAsync`方法得到的参数与`SendAsync`方法相同。 - -发送邮件任务队列可以容忍错误,因为后台作业系统具有重试机制来克服临时网络/服务器问题。 - -有关后台作业系统的更多信息,请参阅[后台作业文档](Background-Jobs.md)。 - -## 电子邮件设置 - -电子邮件发送使用 [设置系统](Settings.md) 来定义设置并在运行时获取这些设置的值。 `Volo.Abp.Emailing.EmailSettingNames` 定义了设置名称的常量,如下所示: - -* **Abp.Mailing.DefaultFromAddress**: 当你在发送电子邮件时未指定发件人时,用作发件人的电子邮件地址(就像上面的示例一样). -* **Abp.Mailing.DefaultFromDisplayName**: 当你在发送电子邮件时未指定发件人时,用作发件人的显示名称(就像在上面的示例中一样). -* **Abp.Mailing.Smtp.Host**: SMTP 服务器的 IP/域(默认值:127.0.0.1)。 -* **Abp.Mailing.Smtp.Port**: SMTP 服务器的端口(默认值:25). -* **Abp.Mailing.Smtp.UserName**: 用户名,如果 SMTP 服务器需要身份验证需要。 -* **Abp.Mailing.Smtp.Password**: 密码,如果 SMTP 服务器需要身份验证需要。 **此值已加密**(请参阅下面的部分). -* **Abp.Mailing.Smtp.Domain**: 账号域,如果 SMTP 服务器需要身份验证需要. -* **Abp.Mailing.Smtp.EnableSsl**: 指示 SMTP 服务器是否使用 SSL 的值(“true”或“false”。默认值:“false”). -* **Abp.Mailing.Smtp.UseDefaultCredentials**:如果为 true,则使用默认凭据,而不是提供的用户名和密码(“true”或“false”。默认值:“true”)。. - -可以从[设置管理](Modules/Setting-Management.md)模块的*设置页面*管理电子邮件设置: - -![email-settings](images/email-settings.png) - ->如果你已从 ABP 启动模板创建解决方案,则已安装设置管理模块。 - -如果你不使用设置管理模块,你可以简单地在 `appsettings.json` 文件中定义设置: - -````json -"Settings": { - "Abp.Mailing.Smtp.Host": "127.0.0.1", - "Abp.Mailing.Smtp.Port": "25", - "Abp.Mailing.Smtp.UserName": "", - "Abp.Mailing.Smtp.Password": "", - "Abp.Mailing.Smtp.Domain": "", - "Abp.Mailing.Smtp.EnableSsl": "false", - "Abp.Mailing.Smtp.UseDefaultCredentials": "true", - "Abp.Mailing.DefaultFromAddress": "noreply@abp.io", - "Abp.Mailing.DefaultFromDisplayName": "ABP application" -} -```` - -You can set/change these settings programmatically using the `ISettingManager` and store values in a database. See the [setting system document](Settings.md) to understand the setting system better. -你可以使用 `ISettingManager` 以编程方式设置/更改这些设置,并将值存储在数据库中。请参阅 [设置系统文档](Settings.md)更好地了解设置系统。 - -### 加密 SMTP 密码 - -*Abp.Mailing.Smtp.Password* 必须是一个**加密**值。如果你使用 `ISettingManager` 设置密码,你不必担心。它在内部加密 set 上的值并在 get 上解密。 - -如果使用 `appsettings.json` 存储密码,则应手动注入 `ISettingEncryptionService` 并使用其 `Encrypt` 方法获取加密值。这可以通过在你的应用程序中创建一个简单的代码来完成。然后你可以删除代码。更好的是,你可以在应用程序中创建一个 UI 来配置电子邮件设置。在这种情况下,你可以直接使用 `ISettingManager` 而不用担心加密。 - -### ISmtpEmailSenderConfiguration - -如果你不想使用设置系统来存储电子邮件发送配置,你可以将 `ISmtpEmailSenderConfiguration` 服务替换为你自己的实现,以从任何其他来源获取配置。 `ISmtpEmailSenderConfiguration` 默认由 `SmtpEmailSenderConfiguration` 实现,如上所述,它从设置系统中获取配置。 - -## 文本模板集成 - -ABP 框架提供了一个强大而灵活的[文本模板系统](Text-Templating.md)。你可以使用文本模板系统来创建动态电子邮件内容。注入 `ITemplateRenderer` 并使用 `RenderAsync` 渲染模板。然后将结果用作电子邮件正文。 - -虽然你可以定义和使用自己的文本模板,但电子邮件发送系统提供了两个简单的内置文本模板。 - -**示例:使用标准和简单的消息模板发送电子邮件** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace Acme.BookStore.Web -{ - public class MyService : ITransientDependency - { - private readonly IEmailSender _emailSender; - private readonly ITemplateRenderer _templateRenderer; - - public MyService( - IEmailSender emailSender, - ITemplateRenderer templateRenderer) - { - _emailSender = emailSender; - _templateRenderer = templateRenderer; - } - - public async Task DoItAsync() - { - var body = await _templateRenderer.RenderAsync( - StandardEmailTemplates.Message, - new - { - message = "This is email body..." - } - ); - - await _emailSender.SendAsync( - "target-address@domain.com", - "Email subject", - body - ); - } - } -} -```` - -生成的电子邮件正文将如下所示: - -````html - - - - - - - This is email body... - - -```` - -电子邮件系统定义了具有给定名称的内置文本模板: - -"**Abp.StandardEmailTemplates.Message**" 是最简单的带有文本消息的模板: - -````html -{%{{{model.message}}}%} -```` - -此模板使用“Abp.StandardEmailTemplates.Layout”作为其布局. - -"**Abp.StandardEmailTemplates.Layout**" 是一个提供 HTML 文档布局的简单模板: - -````html - - - - - - - {%{{{content}}}%} - - -```` - -最终呈现的消息如上所示。 - -> 这些模板名称是在 `Volo.Abp.Emailing.Templates.StandardEmailTemplates` 类中定义的。 - -### 覆盖/替换标准模板 - -You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](Virtual-File-System.md) (VFS) or replace them in your own template definition provider. -你希望用自己的模板替换标准模板,这样你就可以准备电子邮件模板文件。你可以使用 [虚拟文件系统](Virtual-File-System.md) (VFS),或在你自己的模板定义提供程序中替换它们。 - -虚拟文件系统中模板的路径如下图所示: - -* `/Volo/Abp/Emailing/Templates/Layout.tpl` -* `/Volo/Abp/Emailing/Templates/Message.tpl` - -如果你将文件添加到虚拟文件系统中的相同位置,你的文件将覆盖它们。 - -模板是内联本地化的,这意味着你可以利用 [本地化系统](Localization.md) 的强大功能使你的模板具有多元文化。 - -详见[文本模板系统](Text-Templating.md) 文档。 - -> 请注意,你可以为应用程序定义和使用自己的模板,而不是使用标准的简单模板。这些标准模板主要用于可重用的模块,它们不定义自己的模板,而是依赖内置的模板。只需覆盖标准的电子邮件布局模板,就可以轻松自定义使用的模块发送的电子邮件。 - -## NullEmailSender - -`NullEmailSender` 是实现 `IEmailSender` 的内置类,但将电子邮件内容写入 [标准日志系统](Logging.md),而不是实际发送电子邮件。 - -这个类特别有用,尤其是在你开发是不想发送真实电子邮件。 [应用启动模板](Startup-Templates/Application.md)已经在**DEBUG模式**中使用了这个类,在领域层配置如下: - -````csharp -#if DEBUG - context.Services.Replace(ServiceDescriptor.Singleton()); -#endif -```` - -因此,你在 DEBUG 模式下不会收到电子邮件。电子邮件将在生产时按预期发送(RELEASE 模式)。如果你也想在 DEBUG 上发送电子邮件,请删除这些行。 - -## 其他 - -* [用于发送电子邮件的MailKit集成](MailKit.md) diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md deleted file mode 100644 index b9c236e346..0000000000 --- a/docs/zh-Hans/Entities.md +++ /dev/null @@ -1,410 +0,0 @@ -# 实体 - -实体是DDD(Domain Driven Design)中核心概念.Eric Evans是这样描述实体的 "一个没有从其属性,而是通过连续性和身份的线索来定义的对象" - -实体通常映射到关系型数据库的表中. - -## 实体类 - -实体都继承自`Entity`类,如下所示: - -```C# -public class Book : Entity -{ - public string Name { get; set; } - - public float Price { get; set; } -} -``` - -> 如果你不想继承基类`Entity`,也可以直接实现`IEntity`接口 - -`Entity`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`Guid`类型.可以是其他类型如`string`, `int`, `long`或其他你需要的类型. - -### Guid主键的实体 - -如果你的实体Id类型为 `Guid`,有一些好的实践可以实现: - -* 创建一个构造函数,获取ID作为参数传递给基类. - * 如果没有为GUID Id赋值,**ABP框架会在保存时设置它**,但是在将实体保存到数据库之前最好在实体上有一个有效的Id. -* 如果使用带参数的构造函数创建实体,那么还要创建一个 `private` 或 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它. -* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中**使用[`IGuidGenerator`服务](Guid-Generation.md)** 传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. - -示例实体: - -````csharp -public class Book : Entity -{ - public string Name { get; set; } - public float Price { get; set; } - protected Book() - { - } - public Book(Guid id) - : base(id) - { - } -} -```` - -在[应用服务](Application-Services.md)中使用示例: - -````csharp -public class BookAppService : ApplicationService, IBookAppService -{ - private readonly IRepository _bookRepository; - public BookAppService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - public async Task CreateAsync(CreateBookDto input) - { - await _bookRepository.InsertAsync( - new Book(GuidGenerator.Create()) - { - Name = input.Name, - Price = input.Price - } - ); - } -} -```` - -* `BookAppService` 注入图书实体的默认[仓储](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. -* `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md). -* 如果你想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. - -### 具有复合键的实体 - -有些实体可能需要 **复合键** .在这种情况下,可以从非泛型`Entity`类派生实体.如: - -````C# -public class UserRole : Entity -{ - public Guid UserId { get; set; } - - public Guid RoleId { get; set; } - - public DateTime CreationTime { get; set; } - - public UserRole() - { - - } - - public override object[] GetKeys() - { - return new object[] { UserId, RoleId }; - } -} -```` - -上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键. 具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法. - -> 你还需要在**对象关系映射**(ORM)中配置实体的键. 参阅[Entity Framework Core](Entity-Framework-Core.md)集成文档查看示例. - -> 需要注意,复合主键实体不可以使用 `IRepository` 接口,因为它需要一个唯一的Id属性. 但你可以使用 `IRepository`.更多信息请参见[仓储](Repositories.md)的文档. - -### EntityEquals - -`Entity.EntityEquals(...)` 方法用于检查两个实体对象是否相等. - -示例: - -```csharp -Book book1 = ... -Book book2 = ... - -if (book1.EntityEquals(book2)) //Check equality -{ - ... -} -``` - -## 聚合根 - -"*聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的*"( [查看详细介绍](http://martinfowler.com/bliki/DDD_Aggregate.html)) - -`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性 - -> 值得注意的是 ABP 会默认为聚合根创建仓储,当然,ABP也可以为所有的实体创建仓储,详情参见[仓储](Repositories.md). - -ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现[领域驱动设计](Domain-Driven-Design.md)并且创建聚合根,这里有一些最佳实践仅供参考: - -* 聚合根需要维护自身的完整性,所有的实体也是这样.但是聚合根也要维护子实体的完整性.所以,聚合根必须一直有效. -* 使用Id引用聚合根,而不使用导航属性 -* 聚合根被视为一个单元.它是作为一个单元检索和更新的.它通常被认为是一个交易边界. -* 不单独修改聚合根中的子实体 - -如果你想在应用程序中实现DDD,请参阅[实体设计最佳实践指南](Best-Practices/Entities.md). - -### 聚合根例子 - -这是一个具有子实体集合的聚合根例子: - -````C# -public class Order : AggregateRoot -{ - public virtual string ReferenceNo { get; protected set; } - - public virtual int TotalItemCount { get; protected set; } - - public virtual DateTime CreationTime { get; protected set; } - - public virtual List OrderLines { get; protected set; } - - protected Order() - { - - } - - public Order(Guid id, string referenceNo) - { - Check.NotNull(referenceNo, nameof(referenceNo)); - - Id = id; - ReferenceNo = referenceNo; - - OrderLines = new List(); - } - - public void AddProduct(Guid productId, int count) - { - if (count <= 0) - { - throw new ArgumentException( - "You can not add zero or negative count of products!", - nameof(count) - ); - } - - var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId); - - if (existingLine == null) - { - OrderLines.Add(new OrderLine(this.Id, productId, count)); - } - else - { - existingLine.ChangeCount(existingLine.Count + count); - } - - TotalItemCount += count; - } -} - -public class OrderLine : Entity -{ - public virtual Guid OrderId { get; protected set; } - - public virtual Guid ProductId { get; protected set; } - - public virtual int Count { get; protected set; } - - protected OrderLine() - { - - } - - internal OrderLine(Guid orderId, Guid productId, int count) - { - OrderId = orderId; - ProductId = productId; - Count = count; - } - - internal void ChangeCount(int newCount) - { - Count = newCount; - } - - public override object[] GetKeys() - { - return new Object[] {OrderId, ProductId}; - } -} -```` - -> 如果你不想你的聚合根继承`AggregateRoot`类,你可以直接实现`IAggregateRoot`接口 - -`Order`是一个具有`Guid`类型`Id`属性的 **聚合根**.它有一个`OrderLine`实体集合.`OrderLine`是一个具有组合键(`OrderId`和 ` ProductId`)的实体. - -虽然这个示例可能无法实现聚合根的所有最佳实践,但它仍然遵循良好的实践: - -* `Order`有一个公共的构造函数,它需要 **minimal requirements** 来构造一个"订单"实例.因此,在没有`Id`和`referenceNo`的时候是无法创建订单的.**protected/private**的构造函数只有从数据库读取对象时 **反序列化** 才需要. -* `OrderLine`的构造函数是internal的,所以它只能由领域层来创建.在`Order.AddProduct`这个方法的内部被使用. -* `Order.AddProduct`实现了业务规则将商品添加到订单中 -* 所有属性都有`protected`的set.这是为了防止实体在实体外部任意改变.因此,在没有向订单中添加新产品的情况下设置 `TotalItemCount`将是危险的.它的值由`AddProduct`方法维护. - -ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则. - -### 带有组合键的聚合根 - -虽然这种聚合根并不常见(也不建议使用),但实际上可以按照与上面提到的跟实体相同的方式定义复合键.在这种情况下,要使用非泛型的`AggregateRoot`基类. - -### BasicAggregateRoot类 - -`AggregateRoot` 类实现了 `IHasExtraProperties` 和 `IHasConcurrencyStamp` 接口,这为派生类带来了两个属性. `IHasExtraProperties` 使实体可扩展(请参见下面的 *额外的属性*部分) 和 `IHasConcurrencyStamp` 添加了由ABP框架管理的 `ConcurrencyStamp` 属性实现[乐观并发](https://docs.microsoft.com/zh-cn/ef/core/saving/concurrency). 在大多数情况下,这些是聚合根需要的功能. - -但是,如果你不需要这些功能,你的聚合根可以继承 `BasicAggregateRoot`(或`BasicAggregateRoot`). - -## 基类和接口的审计属性 - -有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. - -### 审计接口 - -有很多的审计接口,你可以实现一个你需要的那个. - -> 虽然可以手动实现这些接口,但是可以使用下一节中定义的**基类**简化代码. - -* `IHasCreationTime` 定义了以下属性: - * `CreationTime` -* `IMayHaveCreator` 定义了以下属性: - * `CreatorId` -* `ICreationAuditedObject` 继承 `IHasCreationTime` 和 `IMayHaveCreator`, 所以它定义了以下属性: - * `CreationTime` - * `CreatorId` -* `IHasModificationTime` 定义了以下属性: - * `LastModificationTime` -* `IModificationAuditedObject` 扩展 `IHasModificationTime` 并添加了 `LastModifierId` 属性. 所以它定义了以下属性: - * `LastModificationTime` - * `LastModifierId` -* `IAuditedObject` 扩展 `ICreationAuditedObject` 和 `IModificationAuditedObject`, 所以它定义了以下属性: - * `CreationTime` - * `CreatorId` - * `LastModificationTime` - * `LastModifierId` -* `ISoftDelete` (参阅 [数据过滤文档](Data-Filtering.md)) 定义了以下属性: - * `IsDeleted` -* `IHasDeletionTime` 扩展 `ISoftDelete` 并添加了 `DeletionTime` 属性. 所以它定义了以下属性: - * `IsDeleted` - * `DeletionTime` -* `IDeletionAuditedObject` 扩展 `IHasDeletionTime` 并添加了 `DeleterId` 属性. 所以它定义了以下属性: - * `IsDeleted` - * `DeletionTime` - * `DeleterId` -* `IFullAuditedObject` 继承 `IAuditedObject` 和 `IDeletionAuditedObject`, 所以它定义了以下属性: - * `CreationTime` - * `CreatorId` - * `LastModificationTime` - * `LastModifierId` - * `IsDeleted` - * `DeletionTime` - * `DeleterId` - -当你实现了任意接口,或者从下一节定义的类派生,ABP框架就会尽可能地自动管理这些属性. - -> 实现 `ISoftDelete` , `IDeletionAuditedObject` 或 `IFullAuditedObject` 让你的实体**软删除**. 参阅[数据过滤文档](Data-Filtering.md),了解软删除模式. - -### 审计基类 - -虽然可以手动实现以上定义的任何接口,但建议从这里定义的基类继承: - -* `CreationAuditedEntity` 和 `CreationAuditedAggregateRoot` 实现了 `ICreationAuditedObject` 接口. -* `AuditedEntity` 和 `AuditedAggregateRoot` 实现了 `IAuditedObject` 接口. -* `FullAuditedEntity` and `FullAuditedAggregateRoot` 实现了 `IFullAuditedObject` 接口. - -所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键; - -所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). - -## 额外的属性 - -ABP定义了 `IHasExtraProperties` 接口,可以由实体实现,以便能够动态地设置和获取的实体属性. `AggregateRoot` 基类已经实现了 `IHasExtraProperties` 接口. 如果你从这个类(或者上面定义的一个相关审计类)派生,那么你可以直接使用API​. - -### GetProperty 和 SetProperty 扩展方法 - -这些扩展方法是获取和设置实体数据的推荐方法. 例: - -````csharp -public class ExtraPropertiesDemoService : ITransientDependency -{ - private readonly IIdentityUserRepository _identityUserRepository; - - public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository) - { - _identityUserRepository = identityUserRepository; - } - - public async Task SetTitle(Guid userId, string title) - { - var user = await _identityUserRepository.GetAsync(userId); - - //SET A PROPERTY - user.SetProperty("Title", title); - - await _identityUserRepository.UpdateAsync(user); - } - - public async Task GetTitle(Guid userId) - { - var user = await _identityUserRepository.GetAsync(userId); - - //GET A PROPERTY - return user.GetProperty("Title"); - } -} -```` - -* 属性的**值是object**,可以是任何类型的对象(string,int,bool...等). -* 如果给定的属性未设置值, `GetProperty` 方法会返回 `null`. -* 你可以使用不同的**属性名称**(如这里的`Title`)同时存储多个属性. - -最好为属性名**定义一个常量**防止拼写错误. 最佳方式是定义**扩展方法**来利用智能感知. 例: - -````csharp -public static class IdentityUserExtensions -{ - private const string TitlePropertyName = "Title"; - - public static void SetTitle(this IdentityUser user, string title) - { - user.SetProperty(TitlePropertyName, title); - } - - public static string GetTitle(this IdentityUser user) - { - return user.GetProperty(TitlePropertyName); - } -} -```` - -然后你可以直接使用 `IdentityUser` 对象的 `user.SetTitle("...")` 和 `user.GetTitle()`. - -### HasProperty 和 RemoveProperty 扩展方法 - -* `HasProperty` 用于检查对象是否设置了属性. -* `RemoveProperty` 用于从对象中删除属性. 你可以使用它来替代设置 `null` 值. - -### 它是如何实现的? - -`IHasExtraProperties` 要求实现类定义一个名称为 `ExtraProperties` 的`Dictionary` 属性. - -所以,如果你需要你可以直接使用 `ExtraProperties` 属性来使用字典API,但是推荐使用 `SetProperty` 和 `GetProperty` 方法,因为它们会检查 `null` 值. - -#### 它是如何存储的? - -存储字典的方式取决于你使用的数据库提供程序. - -* 对于 [Entity Framework Core](Entity-Framework-Core.md),这是两种类型的配置; - * 默认它以 `JSON` 字符串形式存储在 `ExtraProperties` 字段中. 序列化到 `JSON` 和反序列化到 `JSON` 由ABP使用EF Core的[值转换](https://docs.microsoft.com/zh-cn/ef/core/modeling/value-conversions)系统自动完成. - * 如果需要,你可以使用 `ObjectExtensionManager` 为所需的额外属性定义一个单独的数据库字段. 未使用 `ObjectExtensionManager` 配置的属性继续使用单个 `JSON` 字段. 当你使用预构建的[应用模块](Modules/Index.md)并且想要[扩展模块的实体](Customizing-Application-Modules-Extending-Entities.md). 参阅[EF Core迁移文档](Entity-Framework-Core.md)了解如何使用 `ObjectExtensionManager`. -* 对于 [MongoDB](MongoDB.md), 它以 **常规字段** 存储, 因为 MongoDB 天生支持这种 [额外](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) 系统. - -### 讨论额外的属性 - -如果你使用**可重复使用的模块**,其中定义了一个实体,你想使用简单的方式get/set此实体相关的一些数据,那么额外的属性系统是非常有用的. -你通常 **不需要** 为自己的实体使用这个系统,是因为它有以下缺点: - -* 它不是**完全类型安全的**,因为它使用字符串用作属性名称. -* 这些属性**不容易[自动映射](Object-To-Object-Mapping.md)到其他对象**. -* 它**不会**为EF Core在数据库表中**创建字段**,因此在数据库中针对这个字段创建索引或搜索/排序并不容易. - -### 额外属性背后的实体 - -`IHasExtraProperties` 不限于与实体一起使用. 你可以为任何类型的类实现这个接口,使用 `GetProperty`,`SetProperty` 和其他相关方法. - -## 另请参阅 - -* [实体设计最佳实践指南](Best-Practices/Entities.md) diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md deleted file mode 100644 index 9750d58612..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ /dev/null @@ -1,886 +0,0 @@ - -# EF Core数据库迁移 - -本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论你可能希望为自己的应用程序实现的**各种场景**. - -> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Part-1.md). - -### 源码 - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo)找到本文引用的示例项目的源代码; 但是为了理解示例项目的源代码,你需要阅读和理解这个文档. - -## 关于EF Core 代码优先迁移 - -Entity Framework Core 提供了一种简单强大[数据库迁移系统](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). ABP框架[启动模板](Startup-Templates/Index.md)使用这个系统,让你以标准的方式开发你的应用程序. - -但是EF Core迁移系统在**模块化环境中不是很好**,在模块化环境中,每个模块都维护**自己的数据库模式**,而实际上两个或多个模块可以**共享一个数据库**. - -由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了**解决方案**. 如果你需要**自定义数据库结构**,那么应当了解这个解决方案. - -> 参阅[EF Core文档](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)充分了解EF Core Code First迁移,以及为什么需要这样的系统. - -## 默认解决方案与数据库配置 - -当你[创建一个新的Web应用程序](https://abp.io/get-started)(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图: - -![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) - -实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的. - -> 本文档将使用 `Acme.BookStore` 示例项目名称来引用项目和类. 你需要在解决方案中找到相应的类/项目. - -### 数据库模式 - -启动模板已预安装了一些[应用程序模块](Modules/Index.md). 解决方案的每一层都有相应的模块包引用. 所以 `.EntityFrameworkCore` 项目含有使用 `EntityFrameworkCore` 模块的Nuget的引用: - -![bookstore-efcore-dependencies](images/bookstore-efcore-dependencies.png) - -通过这种方式,你可以看到所有的 `.EntityFrameworkCore` 项目下的**EF Core的依赖**. - -> 除了模块引用之外,它还引用了 `Volo.Abp.EntityFrameworkCore.SqlServer` 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何[切换到其它DBMS](Entity-Framework-Core-Other-DBMS.md). - -虽然每个模块在设计上有自己的 `DbContext` 类,并且可以使用其自己的**物理数据库**,但解决方案的配置是使用**单个共享数据库**如下图所示: - -![single-database-usage](images/single-database-usage.png) - -这是**最简单的配置**,适用于大部分的应用程序. `appsettings.json` 文件有名为`Default`**单个连接字符串**: - -````json -"ConnectionStrings": { - "Default": "..." -} -```` - -所以你有一个**单一的数据库模式**,其中包含**共享**此数据库的模块的所有表. - -ABP框架的[连接字符串](Connection-Strings.md)系统允许你轻松为所需的模块**设置不同的连接字符串**: - -````json -"ConnectionStrings": { - "Default": "...", - "AbpAuditLogging": "..." -} -```` - -示例配置告诉ABP框架[审计日志模块](Modules/Audit-Logging.md)应使用第二个连接字符串. - -**然而这仅仅只是开始**. 你还需要创建第二个数据库以及里面审计日志表并使用code frist迁移的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景. - -#### 模块表 - -每个模块都使用**自己的数据库表**. 例如[身份模块](Modules/Identity.md)有一些表来管理系统中的用户和角色. - -##### 表前缀 - -由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用**表前缀**来对自己的表进行分组. - -基础模块(如[身份](Modules/Identity.md), [租户管理](Modules/Tenant-Management.md) 和 [审计日志](Modules/Audit-Logging.md))使用 `Abp` 前缀, 其他的模块使用自己的前缀. 如[Identity Server](Modules/IdentityServer.md) 模块使用前缀 `IdentityServer`. - -如果你愿意,你可以为你的应用程序的模块**更改数据库表前缀**. -例: - -````csharp -Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; -```` - -这段代码更改了[Identity Server](Modules/IdentityServer.md)的前缀. 在应用程序的**最开始**编写这段代码. - -> 每个模块还定义了 `DbSchema` 属性,你可以在支持schema的数据库中使用它. - -### 项目 - -从数据库的角度来看.有三个重要的项目将在下一节中解释. - -#### .EntityFrameworkCore 项目 - -这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). - -**每个模块都使用自己的 `DbContext` 类**来访问数据库.同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: - -````csharp -[ConnectionStringName("Default")] -public class BookStoreDbContext : AbpDbContext -{ - public DbSet Users { get; set; } - - /* Add DbSet properties for your Aggregate Roots / Entities here. */ - - public BookStoreDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Configure the shared tables (with included modules) here */ - - builder.Entity(b => - { - //Sharing the same Users table with the IdentityUser - b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); - - b.ConfigureByConvention(); - b.ConfigureAbpUser(); - - /* Configure mappings for your additional properties - * Also see the MyProjectNameEntityExtensions class - */ - }); - - /* Configure your own tables/entities inside the ConfigureBookStore method */ - builder.ConfigureBookStore(); - } -} -```` - -这个简单的 `DbContext` 类仍然需要一些解释: - -* 它定义了一个 `[connectionStringName]` Attribute,它告诉ABP始终为此 `Dbcontext` 使用 `Default` 连接字符串. -* 它从 `AbpDbContext` 而不是标准的 `DbContext` 类继承. 你可以参阅[EF Core集成](Entity-Framework-Core.md)文档了解更多. 现在你需要知道 `AbpDbContext` 基类实现ABP框架的一些约定,为你自动化一些常见的任务. -* 它为 `AppUser` 实体定义了 `DbSet` 属性. `AppUser` 与[身份模块](Modules/Identity.md)的 `IdentityUser` 实体共享同一个表(默认名为 `AbpUsers`). 启动模板在应用程序中提供这个实体,因为我们认为用户实体一般需要应用程序中进行定制. -* 构造函数接受一个 `DbContextOptions` 实例. -* 它覆盖了 `OnModelCreating` 方法定义EF Core 映射. - * 首先调用 `base.OnModelCreating` 方法让ABP框架为我们实现基础映射. - * 然后它配置了 `AppUser` 实体的映射. 这个实体有一个特殊的情况(它与Identity模块共享一个表),在下一节中进行解释. - * 最后它调用 `builder.ConfigureBookStore()` 扩展方法来配置应用程序的其他实体. - -在介绍其他数据库相关项目之后,将更详细地说明这个设计. - -#### .EntityFrameworkCore.DbMigrations 项目 - -正如前面所提到的,每个模块(和你的应用程序)有**它们自己**独立的 `DbContext` 类. 每个 `DbContext` 类只定义了自身模块的实体到表的映射,每个模块(包括你的应用程序)在**运行时**都使用相关的 `DbContext` 类. - -如你所知,EF Core Code First迁移系统依赖于 `DbContext` 类来跟踪和生成Code First迁移. 那么我们应该使用哪个 `DbContext` 进行迁移? 答案是它们都不是. `.EntityFrameworkCore.DbMigrations` 项目中定义了另一个 `DbContext` (示例解决方案中的 `BookStoreMigrationsDbContext`). - -##### MigrationsDbContext - -`MigrationsDbContext` 仅用于创建和应用数据库迁移. **不在运行时使用**. 它将所有使用的模块的所有实体到表的映射以及应用程序的映射**合并**. - -通过这种方式你可以创建和维护**单个数据库迁移路径**. 然而这种方法有一些困难,接下来的章节将解释ABP框架如何克服这些困难. 首先以 `BookStoreMigrationsDbContext` 类为例: - -````csharp -/* This DbContext is only used for database migrations. - * It is not used on runtime. See BookStoreDbContext for the runtime DbContext. - * It is a unified model that includes configuration for - * all used modules and your application. - */ -public class BookStoreMigrationsDbContext : AbpDbContext -{ - public BookStoreMigrationsDbContext( - DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureBackgroundJobs(); - builder.ConfigureAuditLogging(); - builder.ConfigureIdentity(); - builder.ConfigureIdentityServer(); - builder.ConfigureFeatureManagement(); - builder.ConfigureTenantManagement(); - - /* Configure your own tables/entities inside the ConfigureBookStore method */ - builder.ConfigureBookStore(); - } -} -```` - -##### 共享映射代码 - -第一个问题是: 一个模块使用自己的 `DbContext` 这就需要到数据库的映射. 该 `MigrationsDbContext` 也需要相同的映射创建此模块的数据库表. 我们绝对**不希望复制**的映射代码. - -解决方案是定义一个**扩展方法**(在`ModelBuilder`)由两个 `DbContext` 类调用. 所以每个模块都定义了这样的扩展方法. - -例如 `builder.ConfigureBackgroundJobs()` 方法调用[后台作业模块](Modules/Background-Jobs.md)配置数据库表. 扩展方法的定义如下: - -````csharp -public static class BackgroundJobsDbContextModelCreatingExtensions -{ - public static void ConfigureBackgroundJobs( - this ModelBuilder builder, - Action optionsAction = null) - { - var options = new BackgroundJobsModelBuilderConfigurationOptions( - BackgroundJobsDbProperties.DbTablePrefix, - BackgroundJobsDbProperties.DbSchema - ); - - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); - - b.ConfigureCreationTime(); - b.ConfigureExtraProperties(); - - b.Property(x => x.JobName) - .IsRequired() - .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); - - //... - }); - } -} -```` - -此扩展方法还提供了选项用于更改此模块的数据库表前缀和模式,但在这里并不重要. - -最终的应用程序在 `MigrationsDbContext` 类中调用扩展方法, 因此它可以确定此 `MigrationsDbContext` 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个`MigrationsDbContext` 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题. - -同样 `ConfigureBackgroundJobs` 方法也被后台作业模块的 `DbContext` 调用: - -````csharp -[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] -public class BackgroundJobsDbContext - : AbpDbContext, IBackgroundJobsDbContext -{ - public DbSet BackgroundJobs { get; set; } - - public BackgroundJobsDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - //Reuse the same extension method! - builder.ConfigureBackgroundJobs(); - } -} -```` - -通过这种方式,可以在 `DbContext` 类之间共享模块的映射配置. 上面的代码在相关的模块NuGet包中,所以你不需要关心它. - -##### 重用模块的表 - -你可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: - -1. 你可以**直接使用模块定义的实体**(你仍然可以在某种程度上[扩展实体](Customizing-Application-Modules-Extending-Entities.md)). -2. 你可以**创建一个新的实体**映射到同一个数据库表. - -###### 使用由模块定义的实体 - -使用实体定义的模块有标准用法非常简单. 例如身份模块定义了 `IdentityUser` 实体. 你可以为注入 `IdentityUser` 仓储,为此实体执行标准仓储操作. -例: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Identity; - -namespace Acme.BookStore -{ - public class MyService : ITransientDependency - { - private readonly IRepository _identityUserRepository; - - public MyService(IRepository identityUserRepository) - { - _identityUserRepository = identityUserRepository; - } - - public async Task DoItAsync() - { - //Get all users - var users = await _identityUserRepository.GetListAsync(); - } - } -} -```` - -示例注入了 `IRepository`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口. - -另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. - -###### 创建一个新的实体 - -如果你只是按原样使用实体,那么使用模块的实体是很容易的. 但是在下面的情况下你可能需要定义自己的实体并映射到模块实体相同的数据库表. - -* 你想要**添加一个新的字段**到表中并将其映射到实体的属性. 你无法使用模块的实体,因为它没有相关的属性. -* 你想要**使用的表字段的一个子集**. 你不希望访问实体的所有属性并隐藏不相关的属性(从安全的角度或仅出于设计目的). -* 你不希望直接**依赖于**模块实体类. - -无论哪一种情况,流程都是一样的. 假如你想要创建一个名为 `AppRole` 的实体,将其映射到与[身份模块](Modules/Identity.md)的 `IdentityRole` 实体相同的表 - -在这里我们将介绍实现方式, 然后**将讨论这种方法的局限性**. - -首先,在你的 `.Domain` 项目一个新的 `AppRole` 类: - -````csharp -using System; -using Volo.Abp.Domain.Entities; -using Volo.Abp.MultiTenancy; - -namespace Acme.BookStore.Roles -{ - public class AppRole : AggregateRoot, IMultiTenant - { - // Properties shared with the IdentityRole class - - public Guid? TenantId { get; private set; } - public string Name { get; private set; } - - //Additional properties - - public string Title { get; set; } - - private AppRole() - { - - } - } -} -```` - -* 它继承了[`AggregateRoot`类](Entities.md)和实现了[`IMultiTenant`]接口(Multi-Tenancy.md),因为 `IdentityRole` 也做了同样的继承. -* 你可以添加 `IdentityRole` 实体定义的任何属性. 本例只加了 `TenantId` 和 `Name` 属性,因为我们这里只需要它们. 你可以把setters设置为私有(如同本例)以防意外更改身份模块的属性. -* 你可以添加自定义(附加)属性. 本例添加了 `Title` 属性. -* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体.创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). - -现在是时候定义EF Core映射. 打开应用程序的 `DbContext` (此示例中是 `BookStoreDbContext` )添加以下属性: - -````csharp -public DbSet Roles { get; set; } -```` - -然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - /* Configure the shared tables (with included modules) here */ - - //CONFIGURE THE AppRole ENTITY - builder.Entity(b => - { - b.ToTable("AbpRoles"); - b.ConfigureByConvention(); - b.Property(x => x.Title).HasMaxLength(128); - }); - - ... - - /* Configure your own tables/entities inside the ConfigureBookStore method */ - - builder.ConfigureBookStore(); -} -```` - -我们添加了以下几行: - -````csharp -builder.Entity(b => -{ - b.ToTable("AbpRoles"); - b.ConfigureByConvention(); - b.Property(x => x.Title).HasMaxLength(128); -}); -```` - -* 它映射到 `AbpRoles` 表,与 `IdentityRole` 实体共享. -* `ConfigureByConvention()` 配置了标准/基本属性(像`TenantId`),建议总是调用它. - -你已经为你的 `DbContext` 配置自定义属性,该属性在应用程序运行时使用. -与其直接更改 `MigrationsDbContext`,我们应该使用ABP框架的实体扩展系统,在解决方案的 `.EntityFrameworkCore` 项目中找到 `YourProjectNameEntityExtensions` 类(本示例中是 `BookStoreEntityExtensions`)并且进行以下更改: - -````csharp -public static class MyProjectNameEntityExtensions -{ - private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); - - public static void Configure() - { - OneTimeRunner.Run(() => - { - ObjectExtensionManager.Instance - .MapEfCoreProperty( - "Title", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(128); - } - ); - }); - } -} -```` - -> 我们建议使用 `nameof(AppRole.Title)` 而不是硬编码 "Title" 字符串 - -`ObjectExtensionManager` 用于添加属性到现有的实体. 由于 `ObjectExtensionManager.Instance` 是静态实例(单例),因此应调用一次. `OneTimeRunner` 是ABP框架定义简单的工具类. - -参阅[EF Core集成文档](Entity-Framework-Core.md)了解更多关于实体扩展系统. - -我们在两个类中都重复了类似的数据库映射代码,例如 `HasMaxLength(128)`. - -现在你可以在包管理控制台(记得选择 `.EntityFrameworkCore.DbMigrations` 做为PMC的默认项目并将 `.Web` 项目设置为启动项目)使用标准的 `Add-Migration` 命令添加一个新的EF Core数据库迁移. - -![pmc-add-migration-role-title](images/pmc-add-migration-role-title.png) - -命令将创建一个新的Code First迁移类,如下所示: - -````csharp -public partial class Added_Title_To_Roles : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Title", - table: "AbpRoles", - maxLength: 128, - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Title", - table: "AbpRoles"); - } -} -```` - -全部完成,只运行在PMC运行`Update-Database` 命令或运行你解决方案中的`.DbMigrator` 项目应用数据库更改. - -现在你可以像使用应用程序的其他实体一样使用 `AppRole` 实体. 下面是查询和更新角色的[应用服务](Application-Services.md). -示例: - -````csharp -public class AppRoleAppService : ApplicationService, IAppRoleAppService -{ - private readonly IRepository _appRoleRepository; - - public AppRoleAppService(IRepository appRoleRepository) - { - _appRoleRepository = appRoleRepository; - } - - public async Task> GetListAsync() - { - var roles = await _appRoleRepository.GetListAsync(); - - return roles - .Select(r => new AppRoleDto - { - Id = r.Id, - Name = r.Name, - Title = r.Title - }) - .ToList(); - } - - public async Task UpdateTitleAsync(Guid id, string title) - { - var role = await _appRoleRepository.GetAsync(id); - - role.Title = title; - - await _appRoleRepository.UpdateAsync(role); - } -} -```` - -创建新实体并将其映射到依赖模块的表存在一些**限制**: - -* 你的**自定义属性必须是可空的**. 例如 `AppRole.Title` 在这里是可空的. 否则身份模块会抛出异常,因为它在向数据库插入新角色时不知道如何如何填充标题. -* 好的做法是,你不应更新**模块定义的属性**,尤其是在需要业务逻辑的情况下. 通常你可以管理自己(自定义)的属性. - -##### 替代方法 - -你可以使用以下方法替代创建实体添加自定义属性. - -###### 使用ExtraProperties - -所有从 `AggregateRoot` 派生的实体都可以在 `ExtraProperties` 属性(因为它们都实现了 `IHasExtraProperties` 接口)中存储键值对, 它是 `Dictionary` 类型在数据库中被序列化为JSON. 所以你可以在字典中添加值用于查询,无需更改实体. - -例如你可以将查询属性 `Title` 存储在 `IdentityRole` 中,而不是创建一个新的实体. -例: - -````csharp -public class IdentityRoleExtendingService : ITransientDependency -{ - private readonly IIdentityRoleRepository _identityRoleRepository; - - public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) - { - _identityRoleRepository = identityRoleRepository; - } - - public async Task GetTitleAsync(Guid id) - { - var role = await _identityRoleRepository.GetAsync(id); - return role.GetProperty("Title"); - } - - public async Task SetTitleAsync(Guid id, string newTitle) - { - var role = await _identityRoleRepository.GetAsync(id); - role.SetProperty("Title", newTitle); - await _identityRoleRepository.UpdateAsync(role); - } -} -```` - -* `GetProperty` 和 `SetProperty` 方法是在 `role.ExtraProperties` 词典中获取和设置值的简化写法,推荐使用. - -通过这种方式你可以轻松任何类型的值附加到依赖模块的实体. 不过这种方法也有一些缺点: - -* 所有的额外属性都存储在数据库中的一个**JSON对象**,它们不是作为表的字段存储,与简单的表字段相比创建索引和针对此属性使用SQL查询将更加困难. -* 属性名称是字符串,他们**不是类型安全的**. 建议这些类型的属性定义常量,以防止拼写错误. - -###### 使用实体扩展系统 - -实体扩展系统解决了额外属性主要的问题: 它可以将额外属性做为**标准表字段**存储到数据库. - -你需要做的就是如上所诉使用 `ObjectExtensionManager` 定义额外属性, 然后你就可以使得 `GetProperty` 和 `SetProperty` 方法对实体的属性进行get/set,但是这时它存储在数据库表的单独字段中. - -参阅[实体扩展系统](Customizing-Application-Modules-Extending-Entities.md)了解更多. - -###### 创建新表 - -你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本. - -在这种情况下你不需要处理迁移问题,但是需要处理数据复制问题. 当重复的值发生变化时,你应该在表中同步相同的变化. 你可以使用本地或分布式[事件总线](Event-Bus.md)订阅原始实体的更改事件. 这是根据来自另一个微服务的数据推荐的方法,特别是如果它们有单独的物理数据库(你可以在网络中搜索关于微服务设计的数据共享,这是一个广泛的主题). - -#### 讨论另一种场景:每个模块管理自己的迁移路径 - -上面提到, `.EntityFrameworkCore.DbMigrations` 合并所有模块的所有数据库映射(加上应用程序映射)创建统一的迁移路径. - -另一种方法是允许每个模块拥有自己的迁移来维护其数据库表. 虽然它在一开始看起来更模块化,但它有一些重要的缺点: - -* **EF Core迁移系统依赖于数据库DBMS提供程序**. 例如,如果一个模块为SQL Server创建迁移,那么你就不能使用MySQL使用这个迁移代码,对于模块而言为所有可用的DBMS提供程序维护迁移是不切实际的. 将迁移留给应用程序代码(如本文档中所解释的),你就可以在**应用程序代码中选择DBMS**. -* 在模块之间 **共享表** 或在应用程序中 **重用模块的表** 会更加困难. 因为EF Core迁移系统不能处理它,并会抛出异常,例如"数据库中已存在表XXX". -* **自定义/增强** 映射和生成迁移代码会更加困难. -* 当你使用多个模块时更难跟踪和**应用数据库更改**. - -## 使用多个数据库 - -默认的启动模板被组织为所有模块和应用程序使用单个数据库. 但是ABP框架和所有预构建的模块都被设计为**可以使用多个数据库**. 每个模块可以使用自己的数据库,也可以将模块分组到几个数据库中. - -本节将说明如何将审计日志,设置管理和权限管理模块表移至**第二个数据库**,而其余模块继续使用主("Default")数据库. - -结构如下图所示: - -![single-database-usage](images/multiple-database-usage.png) - -### 更改连接字符串部分 - -第一步是更改所有 `appsettings.json` 文件的连接字符串部分. 最初它是这样的: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" -} -```` - -更改如下所示: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True", - "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", - "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", - "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True" -} -```` - -为相关模块添加了**另外三个连接字符串**,以针对 `BookStore_SecondDb` 数据库(它们都是相同的). 例如 `AbpPermissionManagement` 是权限管理模块的连接字符串. - -`AbpPermissionManagement` 是权限管理模块[定义](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11)的一个常量. 如果定义值,ABP框架[连接字符串选择系统](Connection-Strings.md)将为权限管理模块选择此连接字符串. 如果不定义它将回退到 `Default` 连接字符串. - -### 创建第二个迁移项目 - -在**运行时**上定义如上所述的连接字符串就足够了. 但是 `BookStore_SecondDb` 数据库还不存在. 你需要为相关模块创建数据库表. - -就像主数据库我们要使用的EF Core Code Frist迁移系统创建和维护第二个数据库. - -一种简单的方法是为第二个迁移 `DbContext` 创建第二个项目(`.csproj`). - -在你的解决方案中创建一个名为 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 的**类库项目**(如果不喜欢你可以进行其他命名). - -`.csproj` 内容应该和以下类似: - -````xml - - - - - - netcoreapp3.1 - Acme.BookStore.DbMigrationsForSecondDb - - - - - - - - - - - -```` - -你可以仅复制和修改原始 `.DbMigrations` 项目的内容. 该项目引用 `.EntityFrameworkCore` 项目. **唯一的区别**是 `RootNamespace` 值. - -从 `.Web` 项目中添加对该项目的引用(否则,EF Core Tool不允许使用`Add-migration`命令). - -### 创建第二个DbMigrationDbContext - -为迁移创建一个新的 `DbContext` 并调用模块的**扩展方法**为相关模块配置数据库表: - -````csharp -[ConnectionStringName("AbpPermissionManagement")] -public class BookStoreSecondMigrationsDbContext : - AbpDbContext -{ - public BookStoreSecondMigrationsDbContext( - DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureAuditLogging(); - } -} -```` - -> `[ConnectionStringName(...)` Attribute 在这里非常重,它告诉ABP框架应该为这个 `DbContext` 使用哪个连接字符串. 我们使用了 `AbpPermissionManagement`. - -创建一个**Design Time Db Factory**类, EF Core Tool需要使用它(例如`Add-Migration`和`Update-Database` PCM命令): - -````csharp -/* This class is needed for EF Core console commands - * (like Add-Migration and Update-Database commands) */ -public class BookStoreSecondMigrationsDbContextFactory - : IDesignTimeDbContextFactory -{ - public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args) - { - var configuration = BuildConfiguration(); - - var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement")); - - return new BookStoreSecondMigrationsDbContext(builder.Options); - } - - private static IConfigurationRoot BuildConfiguration() - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false); - - return builder.Build(); - } -} -```` - -它与 `.EntityFrameworCore.DbMigrations` 项目中的类相似, 除了它使用 `AbpPermissionManagement` 连接字符串. - -现在你可以打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrationsForSecondDb` 项目为默认项目(确保`.Web`项目是启动项目),运行 `Add-Migration "Initial` 和 `Update-Database` 命令,如下所示: - -![pmc-add-migration-initial-update-database](images/pmc-add-migration-initial-update-database.png) - -现在你应该有一个包含相关数据库表的新数据库. - -![bookstore-second-database](images/bookstore-second-database.png) - -### 从主数据库中删除模块 - -我们 **创建第二个数据库** 包含了审计日志,权限管理和设置管理模块表. 我们应该从**主数据库中删除这些表**,这是很容易的. -S -首先移除从 `MigrationsDbContext` 类以下行(本例中是`BookStoreMigrationsDbContext`): - -````csharp -builder.ConfigurePermissionManagement(); -builder.ConfigureSettingManagement(); -builder.ConfigureAuditLogging(); -```` - -打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrations` 为默认的项目(确保`.Web`项目是启动项目)运行以下命令: - -```` -Add-Migration "Removed_Audit_Setting_Permission_Modules" -```` - -此命令将创建一个如下所示的迁移类: - -````csharp -public partial class Removed_Audit_Setting_Permission_Modules : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AbpAuditLogActions"); - - migrationBuilder.DropTable( - name: "AbpEntityPropertyChanges"); - - migrationBuilder.DropTable( - name: "AbpPermissionGrants"); - - migrationBuilder.DropTable( - name: "AbpSettings"); - - migrationBuilder.DropTable( - name: "AbpEntityChanges"); - - migrationBuilder.DropTable( - name: "AbpAuditLogs"); - } - - ... -} -```` - -在这一步需要小心: - -* 如果你是 **已经在生产的系统**, 那么你应该关心**数据丢失**,在删除表之前需要将表的内容移动到第二个数据库. -* 如果你还 **没有开始** 你的项目, 你可以考虑 **删除所有的迁移** 并且重新生成初始版本,让迁移历史更加清晰. - -运行 `Update-Database` 命令从主数据库中删除表. - -请注意如果你还没有将其复制到新数据库,那么还会**删除了一些初始种子数据**(例如授予管理员角色的权限). 如果你运行应用程序可能无法再登录. 解决方案很简单:在解决方案中重新运行 `.DbMigrator` 控制台应用程序,它会为新数据库添加种子数据. - -### 自动化第二个数据库模式迁移 - -`.DbMigrator` 控制台应用程序可以跨多个数据库运行数据库种子代码无需任何其他配置. 但它不能在第二个数据库迁移项目中运行EF Core First迁移. 下面介绍如何配置控制台迁移应用程序以处理两个数据库. - -#### 实现 IBookStoreDbSchemaMigrator - -`Acme.BookStore.EntityFrameworkCore.DbMigrations` 项目中的 `EntityFrameworkCoreBookStoreDbSchemaMigrator` 类负责为 `BookStoreMigrationsDbContext` 迁移数据库模式. 它应该是这样的: - -````csharp -[Dependency(ReplaceServices = true)] -public class EntityFrameworkCoreBookStoreDbSchemaMigrator - : IBookStoreDbSchemaMigrator, ITransientDependency -{ - private readonly IServiceProvider _serviceProvider; - - public EntityFrameworkCoreBookStoreDbSchemaMigrator( - IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task MigrateAsync() - { - /* We are intentionally resolving the BookStoreMigrationsDbContext - * from IServiceProvider (instead of directly injecting it) - * to properly get the connection string of the current tenant in the - * current scope. - */ - - await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); - } -} -```` - -它实现了 `IBookStoreDbSchemaMigrator` 并**替换现有服务**(见第一行). - -删除 `[Dependency(ReplaceServices = true)]`,因为我们有这个接口的两个实现并且希望同时使用它们. - -在新的迁移项目(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)创建它的一个副本,内部使用 `BookStoreSecondMigrationsDbContext` . 示例实现: - -````csharp -public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator - : IBookStoreDbSchemaMigrator, ITransientDependency -{ - private readonly IServiceProvider _serviceProvider; - - public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator( - IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task MigrateAsync() - { - /* We are intentionally resolving the BookStoreSecondMigrationsDbContext - * from IServiceProvider (instead of directly injecting it) - * to properly get the connection string of the current tenant in the - * current scope. - */ - - await _serviceProvider - .GetRequiredService() - .Database - .MigrateAsync(); - } -} -```` - -> 类的名称对于[依赖注入](Dependency-Injection.md)非常重要. 为了可以使用 `IBookStoreDbSchemaMigrator` 引用注入它的结尾应该是 `BookStoreDbSchemaMigrator`. - -现在我们有 `IBookStoreDbSchemaMigrator` 接口的两个实现,每个实现负责迁移相关的数据库模式. - -#### 为第二个迁移项目定义一个模块类 - -是时候为第二次迁移(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)项目定义[模块](Module-Development-Basics.md)类了: - -````csharp -[DependsOn( - typeof(BookStoreEntityFrameworkCoreModule) - )] -public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(); - } -} -```` - -在 `Acme.BookStore.DbMigrator` 项目中引用 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 项目并且将 `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` 依赖项添加到 `BookStoreDbMigratorModule` 的依赖项列表中. `BookStoreDbMigratorModule` 类应该是这样的: - -````csharp -[DependsOn( - typeof(AbpAutofacModule), - typeof(BookStoreEntityFrameworkCoreDbMigrationsModule), - typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS! - typeof(BookStoreApplicationContractsModule) - )] -public class BookStoreDbMigratorModule : AbpModule -{ - ... -} -```` - -#### 运行数据库迁移程序 - -你可以运行 `.DbMigrator` 应用程序应用迁移和初始化种子数据. 你可以删除这两个数据库进行测试,然后再次运行 `.DbMigrator` 应用程序,它会创建两个数据库. - -## 结论 - -本文档说明了如何拆分数据库以及管理Entity Framework Core解决方案的数据库迁移. 简而言之,你需要为每个不同的数据库创建一个单独的迁移项目. diff --git a/docs/zh-Hans/Entity-Framework-Core-MySQL.md b/docs/zh-Hans/Entity-Framework-Core-MySQL.md deleted file mode 100644 index 75505b883d..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-MySQL.md +++ /dev/null @@ -1,41 +0,0 @@ -# 切换到EF Core MySql提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **MySql** 数据库提供程序 - -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 - -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.MySQL](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.MySQL) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.MySQL;`). - -## UseMySQL() - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关MySQL连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/mysql/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md b/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md deleted file mode 100644 index 150ee51f11..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md +++ /dev/null @@ -1,60 +0,0 @@ -# 切换到 EF Core Oracle Devart 提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 - -> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,有关其他选项,请参见[文档](Entity-Framework-Core-Oracle.md). - -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 - -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的[Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). - -## UseOracle() - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -找到 *YourProjectName*MigrationsDbContextFactory.cs 的 `CreateDbContext()` 方法,将以下代码块 - -```csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -``` - -替换为: - -```csharp -var builder = (DbContextOptionsBuilder) - new DbContextOptionsBuilder().UseOracle - ( - configuration.GetConnectionString("Default") - ); -``` - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关Oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md b/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md deleted file mode 100644 index e5932be3db..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md +++ /dev/null @@ -1,67 +0,0 @@ -# 切换到EF Core Oracle提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 - -> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 - -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 - -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). - -## UseOracle() - -Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. - -In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: - -```csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -``` - -与这个 - -```csharp -var builder = (DbContextOptionsBuilder) - new DbContextOptionsBuilder().UseOracle - ( - configuration.GetConnectionString("Default") - ); -``` - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle.md b/docs/zh-Hans/Entity-Framework-Core-Oracle.md deleted file mode 100644 index cedd4345de..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-Oracle.md +++ /dev/null @@ -1,10 +0,0 @@ -# 切换到EF Core Oracle 提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 - -ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个: - -* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** 使用官方 & 免费的oracle驱动 ( **当前处于 beta**). -* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** 使用[Devart](https://www.devart.com/)公司提供的商业(付费)驱动. - -> 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md deleted file mode 100644 index 56a37396ea..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md +++ /dev/null @@ -1,113 +0,0 @@ -# 切换到EF Core 其它DBMS提供程序 - -**[应用程序启动模板](Startup-Templates/Application.md)** 为EF Core预配置了Sql Server提供程序,EF Core支持许多其它DBMS,你可以在基于ABP的应用程序使用它们. - -ABP框架为一些常见的DMBS提供了简化配置的**集成包**,你可以通过以下文档来学习如何**切换到你喜欢的DBMS**: - -* [MySQL](Entity-Framework-Core-MySQL.md) -* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) -* [Oracle](Entity-Framework-Core-Oracle.md) -* [SQLite](Entity-Framework-Core-SQLite.md) - -你也可以不使用集成包配置DBMS提供程序,虽然总是建议使用集成包(它也使不同模块之间的依赖版本成为标准版本),但是如果没有用于DBMS提供程序的集成包,也可以手动集成. - -本文介绍了如何在不使用[MySQL集成包](Entity-Framework-Core-MySQL.md)的情况下切换到MySQL. - -## 替换SQL Server依赖 - -* 删除 `.EntityFrameworkCore` 项目依赖的 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet 包. -* 添加 [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql/) NuGet 包到 `.EntityFrameworkCore` 项目. - -## 删除模块依赖项 - -从 ***YourProjectName*EntityFrameworkCoreModule** 类的依赖列表中删除`AbpEntityFrameworkCoreSqlServerModule`. - -## 更改UseSqlServer()调用 - -在*YourProjectName*EntityFrameworkCoreModule类中找到以下代码: - -````csharp -Configure(options => -{ - options.UseSqlServer(); -}); -```` - -替换成以下代码: - -````csharp -Configure(options => -{ - options.Configure(ctx => - { - if (ctx.ExistingConnection != null) - { - ctx.DbContextOptions.UseMySql(ctx.ExistingConnection); - } - else - { - ctx.DbContextOptions.UseMySql(ctx.ConnectionString); - } - }); -}); -```` - -* 调用的 `UseMySql` 代码是在 Pomelo.EntityFrameworkCore.MySql 包中定义的,方法还有附加选项,如果需要可以使用它. -* 这段代码首先检查当前请求中是否存在到相同数据库的现有(活动)连接,并在可能的情况下重用它. 这允许在不同的DbContext类型之间共享单个事务. ABP处理其余的事情. -* 如果没有活动的连接,它将把 `ctx.ConnectionString` 传递给UseMySql(这将创建新的数据库连接). 这里使用 `ctx.ConnectionString` 很重要. 不要传递静态连接字符串(或配置中的连接字符串). 因为ABP在多数据库或[多租户](Multi-Tenancy.md)环境中[动态确定正确的连接字符串](Connection-Strings.md). - -## 更改连接连接字符串 - -MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关MySQL连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/mysql/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 更改迁移DbContext Factory - -启动模板包含***YourProjectName*MigrationsDbContextFactory**类,这是EF Core控制台命令所必须的类(比如[Add-Migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)和[Update-Database](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)),在切换到MySql数据库时,我们同时也需要修改`DbContextOptionsBuilder` - -在 *YourProjectName*MigrationsDbContextFactory 类中找到以下代码: - -````csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -```` - -将其替换为: - -````csharp -var builder = new DbContextOptionsBuilder() - .UseMySql(configuration.GetConnectionString("Default")); -```` - -如果在创建或执行数据库迁移时遇到任何问题,请参考相关模块文档 - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. - -## DBMS限制 - -不同的DBMS可能存在一些限制, 如字段名称的最大长度, 索引长度等等. -模块可能提供了一些内置的解决方案. 你可以通过`ModelBuilder`来配置它 如: Identity Server模块 - -```csharp -builder.ConfigureIdentityServer(options => -{ - options.DatabaseProvider = EfCoreDatabaseProvider.MySql; -}); -``` -v2.9+版本无需手动设置 ([版本历史](https://github.com/abpframework/abp/blob/dev/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerModelBuilderConfigurationOptions.cs)) - -相关讨论: https://github.com/abpframework/abp/issues/1920 diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md deleted file mode 100644 index 6e481bb230..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ /dev/null @@ -1,41 +0,0 @@ -# 切换到EF Core PostgreSQL提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **PostgreSQL** 数据库提供程序 - -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 - -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.PostgreSql](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.PostgreSql) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). - -## UseNpgsql() - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -PostgreSql连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关PostgreSql连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/postgresql/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-SQLite.md b/docs/zh-Hans/Entity-Framework-Core-SQLite.md deleted file mode 100644 index 646e3082d5..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core-SQLite.md +++ /dev/null @@ -1,41 +0,0 @@ -# 切换到EF Core SQLite提供程序 - -本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **SQLite** 数据库提供程序. - -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 - -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.SQLite](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SQLite) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Sqlite;`). - -## UseSqlite() - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -SQLite连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关SQLite连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/sqlite/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md deleted file mode 100644 index 036998bc73..0000000000 --- a/docs/zh-Hans/Entity-Framework-Core.md +++ /dev/null @@ -1,923 +0,0 @@ -# Entity Framework Core 集成 - -本文介绍了如何将EF Core作为ORM提供程序集成到基于ABP的应用程序以及如何对其进行配置. - -## 安装 - -`Volo.Abp.EntityFrameworkCore` 是EF Core 集成的主要nuget包. 将其安装到你的项目中(在分层应用程序中适用于 数据访问/基础设施层): - -```shell -Install-Package Volo.Abp.EntityFrameworkCore -``` - -然后添加 `AbpEntityFrameworkCoreModule` 模块依赖项(`DependsOn` Attribute) 到 [module](Module-Development-Basics.md)(项目中的Mudole类): - -````C# -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpEntityFrameworkCoreModule))] - public class MyModule : AbpModule - { - //... - } -} -```` - -> 注: 你可以直接下载预装EF Core的[启动模板](https://abp.io/Templates). - -### 数据库管理系统选择 - -EF Core支持多种数据库管理系统([查看全部](https://docs.microsoft.com/zh-cn/ef/core/providers/)). ABP框架和本文档不依赖于任何特定的DBMS. 如果要创建一个可重用的[应用程序模块](Modules/Index.md),应避免依赖于特定的DBMS包.但在最终的应用程序中,始终会选择一个DBMS. - -参阅[为Entity Framework Core切换到其他DBMS](Entity-Framework-Core-Other-DBMS.md)文档学习如何切换DBMS. - -## 创建 DbContext - -你可以平常一样创建DbContext,它需要继承自 `AbpDbContext`. 如下所示: - -````C# -using Microsoft.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -namespace MyCompany.MyProject -{ - public class MyDbContext : AbpDbContext - { - //...在这里添加 DbSet properties - - public MyDbContext(DbContextOptions options) - : base(options) - { - } - } -} -```` - -### 关于EF Core Fluent Mapping - -[应用程序启动模板](Startup-Templates/Application.md)已配置使用[EF Core fluent configuration API](https://docs.microsoft.com/zh-cn/ef/core/modeling/)映射你的实体到数据库表. - -你依然为你的实体属性使用**data annotation attributes**(像`[Required]`),而ABP文档通常遵循**fluent mapping API** approach方法. 如何使用取决与你. - -ABP框架有一些**实体基类**和**约定**(参阅[实体文档](Entities.md))提供了一些有用的扩展方法来配置从基本实体类继承的属性. - -#### ConfigureByConvention 方法 - -`ConfigureByConvention()` 是主要的扩展方法,它对你的实体**配置所有的基本属性**和约定. 所以在你的流利映射代码中为你所有的实体调用这个方法是 **最佳实践**, - -**示例**: 假设你有一个直接继承 `AggregateRoot` 基类的 `Book` 实体: - -````csharp -public class Book : AuditedAggregateRoot -{ - public string Name { get; set; } -} -```` - -你可以在你的 `DbContext` 重写 `OnModelCreating` 方法并且做以下配置: - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - //Always call the base method - base.OnModelCreating(builder); - - builder.Entity(b => - { - b.ToTable("Books"); - - //Configure the base properties - b.ConfigureByConvention(); - - //Configure other properties (if you are using the fluent API) - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - }); -} -```` - -* 这里调用了 `b.ConfigureByConvention()` 它对于**配置基本属性**非常重要. -* 你可以在这里配置 `Name` 属性或者使用**data annotation attributes**(参阅[EF Core 文档](https://docs.microsoft.com/zh-cn/ef/core/modeling/entity-properties)). - -> 尽管有许多扩展方法可以配置基本属性,但如果需要 `ConfigureByConvention()` 内部会调用它们. 因此仅调用它就足够了. - -### 配置连接字符串选择 - -如果你的应用程序有多个数据库,你可以使用 `[connectionStringName]` Attribute为你的DbContext配置连接字符串名称.例: - -```csharp -[ConnectionStringName("MySecondConnString")] -public class MyDbContext : AbpDbContext -{ - -} -``` - -如果不进行配置,则使用`Default`连接字符串. 如果你配置特定的连接字符串的名称,但在应用程序配置中没有定义这个连接字符串名称,那么它会回退到`Default`连接字符串(参阅[连接字符串文档](Connection-Strings.md)了解更多信息). - -### AbpDbContextOptions - -`AbpDbContextOptions` 用于配置 `DbContext`. 当你使用ABP的应用程序启动模板新建解决方案时, 你会看到一个简单的配置 (在 `EntityFrameworkCore` 集成项目模块类) 如下: - -````csharp -Configure(options => -{ - options.UseSqlServer(); -}); -```` - -上面的配置为应用程序的所有 `DbContext`使用SQL Server作为默认DBMS. 上面的配置是简化的写法, 它也可以使用下面的方法进行配置: - -````csharp -Configure(options => -{ - options.Configure(opts => - { - opts.UseSqlServer(); - }); -}); -```` - -`options.Configure(...)` 方法有更多的选项进行配置. 例如, 你可以设置 `DbContextOptions` (EF Core自有的配置): - -````csharp -Configure(options => -{ - options.Configure(opts => - { - opts.DbContextOptions.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - }); -}); -```` - -如果你有唯一的 `DbContext` 或者有多个 `DbContext`, 但是希望对所有的 `DbContext` 使用相同的DBMS, 你无需更多的配置. 但是, 如果你需要为某个特定的 `DbContext` 配置不同的DBMS或对配置进行定制, 你可以进行如下定制: - -````csharp -Configure(options => -{ - // Default configuration for all DbContexts - options.Configure(opts => - { - opts.UseSqlServer(); - }); - - // Customized configuration for a specific DbContext - options.Configure(opts => - { - opts.UseMySQL(); - }); -}); -```` - -> 参阅 [为Entity Framework Core切换DBMS](Entity-Framework-Core-Other-DBMS.md) 文档学习如何配置DBMS. - -## 将DbContext注册到依赖注入 - -在module中的ConfigureServices方法使用 `AddAbpDbContext` 在[依赖注入](Dependency-Injection.md)系统注册DbContext类. - -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpEntityFrameworkCoreModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(); - - //... - } - } -} -```` - -### 添加默认仓储 - -ABP会自动为DbContext中的实体创建[默认仓储](Repositories.md). 需要在注册的时使用options添加`AddDefaultRepositories()`: - -````C# -services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); -}); -```` - -默认情况下为每个[聚合根实体](Entities.md)(`AggregateRoot`派生的子类)创建一个仓储. 如果想要为其他实体也创建仓储, 请将`includeAllEntities` 设置为 `true`: - -````C# -services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(includeAllEntities: true); -}); -```` - -然后你就可以在服务中注入和使用 `IRepository` 或 `IQueryableRepository`. 假如你有一个主键是Guid名为Book实体(聚合根) - -```csharp -public class Book : AggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } -} -``` - -在[领域服务](Domain-Services.md)中创建一个新的Book实例并且使用仓储持久化到数据库中 - -````csharp -public class BookManager : DomainService -{ - private readonly IRepository _bookRepository; - - //inject default repository to the constructor - public BookManager(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task CreateBook(string name, BookType type) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var book = new Book - { - Id = GuidGenerator.Create(), - Name = name, - Type = type - }; - - //Use a standard repository method - await _bookRepository.InsertAsync(book); - - return book; - } -} -```` - -在这个示例中使用 `InsertAsync` 将新实例插入到数据库中 - -#### 添加自定义仓储 - -默认通用仓储可以满足大多数情况下的需求(它实现了`IQueryable`),但是你可能会需要自定义仓储与仓储方法. 假设你需要根据图书类型删除所有的书籍. - -建议为自定义仓储定义一个接口: - -````csharp -public interface IBookRepository : IRepository -{ - Task DeleteBooksByType(BookType type); -} -```` - -你通常希望从IRepository派生以继承标准存储库方法. 然而,你没有必要这样做. 仓储接口在分层应用程序的领域层中定义,它在数据访问/基础设施层([启动模板](https://abp.io/Templates)中的`EntityFrameworkCore`项目)中实现 - -IBookRepository接口的实现示例: - -````csharp -public class BookRepository - : EfCoreRepository, IBookRepository -{ - public BookRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteBooksByType(BookType type) - { - var dbContext = await GetDbContextAsync(); - await dbContext.Database.ExecuteSqlRawAsync( - $"DELETE FROM Books WHERE Type = {(int)type}" - ); - } -} -```` - -现在可以在需要时[注入](Dependency-Injection.md)`IBookRepository`并使用`DeleteBooksByType`方法. - -#### 覆盖默认通用仓储 - -即使创建了自定义仓储,仍可以注入使用默认通用仓储(在本例中是 `IRepository`). 默认仓储实现不会使用你创建的自定义仓储类. - -如果要将默认仓储实现替换为自定义仓储,请在`AddAbpDbContext`使用options执行: - -````csharp -context.Services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); - - //Replaces IRepository - options.AddRepository(); -}); -```` - -在你想要覆盖默认仓储方法对其自定义时,这一点非常需要. 例如你可能希望自定义`DeleteAsync`方法覆盖默认实现, 以使用更有效的方式删除特定的实体. - -````csharp -public async override Task DeleteAsync( - Guid id, - bool autoSave = false, - CancellationToken cancellationToken = default) -{ - //TODO: Custom implementation of the delete method -} -```` - -## 加载关联实体 - -假设你拥有带有`OrderLine`集合的`Order`,并且`OrderLine`具有`Order`的导航属性: - -````csharp -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Volo.Abp.Auditing; -using Volo.Abp.Domain.Entities; - -namespace MyCrm -{ - public class Order : AggregateRoot, IHasCreationTime - { - public Guid CustomerId { get; set; } - public DateTime CreationTime { get; set; } - - public ICollection Lines { get; set; } //子集合 - - public Order() - { - Lines = new Collection(); - } - } - - public class OrderLine : Entity - { - public Order Order { get; set; } //导航属性 - public Guid OrderId { get; set; } - - public Guid ProductId { get; set; } - public int Count { get; set; } - public double UnitPrice { get; set; } - } -} - -```` - -然后象下面显示的这样定义数据库映射: - -````csharp -builder.Entity(b => -{ - b.ToTable("Orders"); - b.ConfigureByConvention(); - - //定义关系 - b.HasMany(x => x.Lines) - .WithOne(x => x.Order) - .HasForeignKey(x => x.OrderId) - .IsRequired(); -}); - -builder.Entity(b => -{ - b.ToTable("OrderLines"); - b.ConfigureByConvention(); -}); -```` - -当你查询一个 `Order`, 你可能想要在单个查询中**包含**所有的 `OrderLine`s 或根据需要在**以后加载它们**. - -> 实际上这与ABP框架没有直接关系. 你可以按照 [EF Core 文档](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/) 了解全部细节. 本节将涵盖与 ABP 框架相关的一些主题. - -### 预先加载 / 包含子对象的加载 - -当你想加载一个带有关联实体的实体时,可以使用不同的选项. - -#### Repository.WithDetails - -`IRepository.WithDetailsAsync(...)` 可以通过包含一个关系收集/属性来获得 `IQueryable` . - -**示例: 获取一个带有 `lines` 的 `order` 对象** - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace AbpDemo.Orders -{ - public class OrderManager : DomainService - { - private readonly IRepository _orderRepository; - - public OrderManager(IRepository orderRepository) - { - _orderRepository = orderRepository; - } - - public async Task TestWithDetails(Guid id) - { - //通过包含子集合获取一个 IQueryable - var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines); - - //应用其他的 LINQ 扩展方法 - var query = queryable.Where(x => x.Id == id); - - //执行此查询并获取结果 - var order = await AsyncExecuter.FirstOrDefaultAsync(query); - } - } -} -```` - -> `AsyncExecuter` 用于执行异步 LINQ 扩展,而无需依赖 EF Core. 如果你将 EF Core NuGet 包引用添加到你的项目中,则可以直接使用 `await query.FirstOrDefaultAsync()`. 但是, 这次你依赖于域层中的 EF Core. 请参阅 [仓储文档](Repositories.md) 以了解更多. - -**示例: 获取一个包含 `lines` 的 `orders` 列表** - -````csharp -public async Task TestWithDetails() -{ - //通过包含一个子集合获取一个 IQueryable - var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines); - - //执行此查询并获取结果 - var orders = await AsyncExecuter.ToListAsync(queryable); -} -```` - -> 如果你需要包含多个导航属性或集合,`WithDetailsAsync`方法可以获得多个表达参数. - -#### DefaultWithDetailsFunc - -如果你没有将任何表达式传递到 `WithDetailsAsync` 方法,则它包括使用你提供的 `DefaultWithDetailsFunc` 选项的所有详细信息. - -你可以在你的 `EntityFrameworkCore` 项目[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法为一个实体配置 `DefaultWithDetailsFunc`. - -**示例: 在查询一个 `Order` 时包含 `Lines`** - -````csharp -Configure(options => -{ - options.Entity(orderOptions => - { - orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines); - }); -}); -```` - -> 你可以在这里完全使用 EF Core API,因为这位于 EF Core集成项目中. - -然后你可以不带任何参数地调用 `WithDetails` 方法: - -````csharp -public async Task TestWithDetails() -{ - //通过包含一个子集合获取一个 IQueryable - var queryable = await _orderRepository.WithDetailsAsync(); - - //执行此查询并获取结果 - var orders = await AsyncExecuter.ToListAsync(queryable); -} -```` - -`WithDetailsAsync()` 执行你已经在 `DefaultWithDetailsFunc` 中设置的表达式. - -#### 仓储 Get/Find 方法 - -有些标准的 [仓储](Repositories.md) 方法带有可选的 `includeDetails` 参数; - -* `GetAsync` 和 `FindAsync` 方法带有默认值为 `true` 的 `includeDetails`. -* `GetListAsync` 和 `GetPagedListAsync` 方法带有默认值为 `false` 的 `includeDetails`. - -这意味着,默认情况下返回**包含子对象的单个实体**,而列表返回方法则默认不包括子对象信息.你可以明确通过 `includeDetails` 来更改此行为. - -> 这些方法使用上面解释的 `DefaultWithDetailsFunc` 选项. - -**示例:获取一个包含子对象的 `order`** - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id); -} -```` - -**示例:获取一个不包含子对象的 `order`** - -````csharp -public async Task TestWithoutDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id, includeDetails: false); -} -```` - -**示例:获取一个包含子对象的实体列表** - -````csharp -public async Task TestWithDetails() -{ - var orders = await _orderRepository.GetListAsync(includeDetails: true); -} -```` - -#### 更多的替代选择 - -存储库模式尝试封装 EF Core, 因此你的选项是有限的. 如果你需要高级方案,你可以按照其中一个选项执行: - -* 创建自定义存储库方法并使用完整的 EF Core API. -* 在你的项目中引用 `Volo.Abp.EntityFrameworkCore` . 通过这种方式,你可以直接在代码中使用 `Include` 和 `ThenInclude` . - -请参阅 EF Core 的 [预先加载文档](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/eager). - -### 显式 / 延迟加载 - -如果你在查询实体时不包括关系,并且以后需要访问导航属性或集合,则你有不同的选择. - -#### EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync - -仓储提供 `EnsurePropertyLoadedAsync` 和 `EnsureCollectionLoadedAsync` 扩展方法来**显示加载**一个导航属性或子集合. - -**示例: 在需要时加载一个 `Order` 的 `Lines`** - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id, includeDetails: false); - //order.Lines 此时是空的 - - await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines); - //order.Lines 被填充 -} -```` - -如果导航属性或集合已经被加载那么 `EnsurePropertyLoadedAsync` 和 `EnsureCollectionLoadedAsync` 方法不做任何处理. 所以,调用多次也没有问题. - -请参阅 EF Core 的[显示加载文档](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/explicit). - -#### 使用代理的延时加载 - -在某些情况下,可能无法使用显式加载,尤其是当你没有引用 `Repository` 或 `DbContext`时.延时加载是 EF Core 加载关联属性/集合的一个功能, 当你第一次访问它. - -启用延时加载: - -1. 安装 [Microsoft.EntityFrameworkCore.Proxies](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies/) 包到你的项目(通常是 EF Core 集成项目) -2. 为你的 `DbContext` 配置 `UseLazyLoadingProxies` (在 EF Core 项目的模块的 `ConfigureServices` 方法中). 例如: - -````csharp -Configure(options => -{ - options.PreConfigure(opts => - { - opts.DbContextOptions.UseLazyLoadingProxies(); //启用延时加载 - }); - - options.UseSqlServer(); -}); -```` - -3. 使你的导航属性和集合是 `virtual`. 例如: - -````csharp -public virtual ICollection Lines { get; set; } //虚集合 -public virtual Order Order { get; set; } //虚导航属性 -```` - -启用延时加载并整理实体后,你可以自由访问导航属性和集合: - -````csharp -public async Task TestWithDetails(Guid id) -{ - var order = await _orderRepository.GetAsync(id); - //order.Lines 此时是空的 - - var lines = order.Lines; - //order.Lines 被填充 (延时加载) -} -```` - -每当你访问属性/集合时,EF Core 都会自动执行额外的查询,从数据库中加载属性/集合. - -> 应谨慎使用延时加载,因为它可能会在某些特定情况下导致性能问题. - -请参阅 EF Core 的[延时加载文档](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/lazy). - -## 访问 EF Core API - -大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目的). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例如: - -````csharp -public async Task TestAsync() -{ - var dbContext = await _orderRepository.GetDbContextAsync(); - var dbSet = await _orderRepository.GetDbSetAsync(); - //var dbSet = dbContext.Set(); //Alternative, when you have the DbContext -} -```` - -* `GetDbContext` 返回 `DbContext` 引用,而不是 `BookStoreDbContext`. 你可以强制转化它, 但大多数情况下你不会需要它. - -> 要点: 你必须在使用`DbContext`的项目里引用`Volo.Abp.EntityFrameworkCore`包. 这会破坏封装,但在这种情况下,这就是你需要的. - -## 额外属性 & Object Extension Manager - -额外属性系统允许你为实现了 `IHasExtraProperties` 的实体set/get动态属性. 当你想将自定义属性添加到[应用程序模块](Modules/Index.md)中定义的实体时,它特别有用. - -默认情况下, 实体的所有额外属性存储在数据库的一个 `JSON` 对象中. - -实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: - -* [自定义应用模块: 扩展实体](Customizing-Application-Modules-Extending-Entities.md) -* [实体](Entities.md) - -本节只解释了 EF Core相关的 `ObjectExtensionManager` 及其用法. - -### ObjectExtensionManager.Instance - -`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作. - -### MapEfCoreProperty - -`MapEfCoreProperty` 是一种快捷扩展方法,用于定义实体的扩展属性并映射到数据库. - -**示例**: 添加 `Title` 属性 (数据库字段)到 `IdentityRole` 实体: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreProperty( - "Title", - (entityBuilder, propertyBuilder) => - { - propertyBuilder.HasMaxLength(64); - } - ); -```` - -### MapEfCoreEntity - -`MapEfCoreEntity` 一个配置 `Entity` 的快捷扩展方法. - -**示例**: 设置 `IdentityRole` 实体的 `Name` 的最大长度: - -````csharp -ObjectExtensionManager.Instance - .MapEfCoreEntity(builder => - { - builder.As>().Property(x => x.Name).HasMaxLength(200); - }); -```` - -### MapEfCoreDbContext - -`MapEfCoreDbContext` 一个配置 `DbContext` 的快捷扩展方法. - -**示例**: 设置 `IdentityDbContext` 的 `IdentityRole` 实体的 `IdentityRole` 的最大长度: - -````csharp -ObjectExtensionManager.Instance.MapEfCoreDbContext(b => -{ - b.Entity().Property(x => x.Name).HasMaxLength(200); -}); -```` - -如果相关模块已实现此功能(通过使用下面说明的 `ConfigureEfCoreEntity`)则将新属性添加到模型中. 然后你需要运行标准的 `Add-Migration` 和 `Update-Database` 命令更新数据库以添加新字段. - ->`MapEfCoreProperty`, `MapEfCoreEntity` and `MapEfCoreDbContext` 方法必须在使用相关的 `DbContext` 之前调用,它是一个静态方法. 最好的方法是尽早的应用程序中使用它. 应用程序启动模板含有 `YourProjectNameEfCoreEntityExtensionMappings` 类,可以在放心的在此类中使用此方法. - -### ConfigureEfCoreEntity, ApplyObjectExtensionMappings 和 TryConfigureObjectExtensions - -如果你正在开发一个可重用使用的模块,并允许应用程序开发人员将属性添加到你的实体,你可以在实体映射使用 `ConfigureEfCoreEntity` 扩展方法,但是在配置实体映射时可以使用快捷的扩展方法 `ConfigureObjectExtensions`: - -**示例**: -````csharp -public static class QADbContextModelCreatingExtensions -{ - public static void ConfigureQA( - this ModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new QAModelBuilderConfigurationOptions( - QADatabaseDbProperties.DbTablePrefix, - QADatabaseDbProperties.DbSchema - ); - - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "Questions", options.Schema); - b.ConfigureByConvention(); - //... - - //Call this in the end of buildAction. - b.ApplyObjectExtensionMappings(); - }); - - //... - - //Call this in the end of ConfigureQA. - builder.TryConfigureObjectExtensions(); - } -} -```` - -如果你调用 `ConfigureByConvention()` 扩展方法(在此示例中 `b.ConfigureByConvention`),ABP框架内部会调用 `ConfigureObjectExtensions` 和 `ConfigureEfCoreEntity` 方法. 使用 `ConfigureByConvention` 方法是**最佳实践**,因为它还按照约定配置基本属性的数据库映射. - -参阅上面提到的 "*ConfigureByConvention 方法*" 了解更多信息. - -## 高级主题 - -### 控制多租户 - -如果你的方案是基于 [多租户](Multi-Tenancy.md)的, 租户可以拥有 **独立数据库**, 你在解决方案中可以拥有 **多个** `DbContext` 类, 并且其中的一些 `DbContext` 类 **只能在主机端** 可用, 这种情况下建议在 `DbContext` 类上添加 `[IgnoreMultiTenancy]` 属性. ABP 保证相关的 `DbContext` 始终使用主机 [连接字符串](Connection-Strings.md), 即使你在租户上下文中. - -**示例:** - -````csharp -[IgnoreMultiTenancy] -public class MyDbContext : AbpDbContext -{ - ... -} -```` - -不要使用 `[IgnoreMultiTenancy]` 特性如果 `DbContext` 中任何一个实体可以被持久化到多租户数据库中. - -> 当你使用repositories时, ABP 已经为未实现`IMultiTenant`接口的实体使用了主机数据库. 所以, 如果你使用repositories访问数据库, 多数时候你不需要 `[IgnoreMultiTenancy]` 特性. - -### 设置默认仓储类 - -默认的通用仓储的默认实现是`EfCoreRepository`类,你可以创建自己的实现,并将其做为默认实现 - -首先,像这样定义仓储类: - -```csharp -public class MyRepositoryBase - : EfCoreRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} - -public class MyRepositoryBase - : EfCoreRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -第一个用于具有[复合主键的实体](Entities.md),第二个用于具有单个主键的实体 - -建议从`EfCoreRepository`类继承并在需要时重写方法. 否则,你需要手动实现所有标准仓储方法. - -现在,你可以使用SetDefaultRepositoryClasses Options - -```csharp -context.Services.AddAbpDbContext(options => -{ - options.SetDefaultRepositoryClasses( - typeof(MyRepositoryBase<,>), - typeof(MyRepositoryBase<>) - ); - - //... -}); -``` - -### 为默认仓储设置Base DbContext类或接口 - -如果你的DbContext继承了另外一个DbContext或实现了一个接口,你可以使用这个基类或接口作为默认仓储的DbContext. 例: - -````csharp -public interface IBookStoreDbContext : IEfCoreDbContext -{ - DbSet Books { get; } -} -```` - -`IBookStoreDbContext`接口是由`BookStoreDbContext`实现的. 然后你可以使用`AddDefaultRepositories`的泛型重载. - -````csharp -context.Services.AddAbpDbContext(options => -{ - options.AddDefaultRepositories(); - //... -}); -```` - -现在,你的自定义仓储也可以使用`IBookStoreDbContext`接口: - -````csharp -public class BookRepository : EfCoreRepository, IBookRepository -{ - //... -} -```` - -使用DbContext接口的一个优点是它可以被其他实现替换. - -### 替换其他仓储 - -正确定义并使用DbContext接口后,任何其他实现都可以使用以下方法替换它: - -**ReplaceDbContextAttribute** - -```csharp -[ReplaceDbContext(typeof(IBookStoreDbContext))] -public class OtherDbContext : AbpDbContext, IBookStoreDbContext -{ - //... -} -``` - -**ReplaceDbContext option** - -````csharp -context.Services.AddAbpDbContext(options => -{ - //... - options.ReplaceDbContext(); -}); -```` - -在这个例子中,`OtherDbContext`实现了`IBookStoreDbContext`. 此功能允许你在开发时使用多个DbContext(每个模块一个),但在运行时可以使用单个DbContext(实现所有DbContext的所有接口). - -### 拆分查询 - -ABP 为了更好的性能, 默认全局启用 [拆分查询](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries). 你可以按需修改. - -**示例** - -````csharp -Configure(options => -{ - options.UseSqlServer(optionsBuilder => - { - optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); - }); -}); -```` - -### 自定义批量操作 - -如果你有更好的逻辑或使用外部库实现批量操作, 你可以通过实现 `IEfCoreBulkOperationProvider` 覆写这个逻辑. - -- 你可以使用下面的示例模板: - -```csharp -public class MyCustomEfCoreBulkOperationProvider - : IEfCoreBulkOperationProvider, ITransientDependency -{ - public async Task DeleteManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task InsertManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } - - public async Task UpdateManyAsync( - IEfCoreRepository repository, - IEnumerable entities, - bool autoSave, - CancellationToken cancellationToken) - where TDbContext : IEfCoreDbContext - where TEntity : class, IEntity - { - // Your logic here. - } -} -``` - -## 另请参阅 - -* [实体](Entities.md) -* [仓储](Repositories.md) diff --git a/docs/zh-Hans/Event-Bus.md b/docs/zh-Hans/Event-Bus.md deleted file mode 100644 index 07a623499d..0000000000 --- a/docs/zh-Hans/Event-Bus.md +++ /dev/null @@ -1,10 +0,0 @@ -# 事件总线 - -事件总线是将消息从发送方传输到接收方的中介. 它在对象,服务和应用程序之间提供了一种松散耦合的通信方式. - -## 事件总线类型 - -ABP框架提供了两种事件总线类型; - -* **[本地事件总线](Local-Event-Bus.md)** 适合进程内消息传递. -* **[分布式事件总线](Distributed-Event-Bus.md)** 适合进程间消息传递,如微服务发布和订阅分布式事件. \ No newline at end of file diff --git a/docs/zh-Hans/Exception-Handling.md b/docs/zh-Hans/Exception-Handling.md deleted file mode 100644 index 63b95643f0..0000000000 --- a/docs/zh-Hans/Exception-Handling.md +++ /dev/null @@ -1,313 +0,0 @@ -## 异常处理 - -ABP提供了用于处理Web应用程序异常的标准模型. - -* 自动 **处理所有异常** .如果是API/AJAX请求,会向客户端返回一个**标准格式化后的错误消息** . -* 自动隐藏 **内部详细错误** 并返回标准错误消息. -* 为异常消息的 **本地化** 提供一种可配置的方式. -* 自动为标准异常设置 **HTTP状态代码** ,并提供可配置选项,以映射自定义异常. - -### 自动处理异常 - -当满足下面**任意一个条件**时,`AbpExceptionFilter` 会处理此异常: - -* 当**controller action**方法返回类型是**object result**(而不是view result)并有异常抛出时. -* 当一个请求为AJAX(Http请求头中`X-Requested-With`为`XMLHttpRequest`)时. -* 当客户端接受的返回类型为`application/json`(Http请求头中`accept` 为`application/json`)时. - -如果异常被处理过,则会自动**记录日志**并将格式化的**JSON消息**返回给客户端. - -#### 错误消息格式 - -每个错误消息都是`RemoteServiceErrorResponse` 类的实例.最简单的错误JSON只有一个 **Message** 属性,如下所示: - -````json -{ - "error": { - "message": "This topic is locked and can not add a new message" - } -} -```` - -其它**可选字段**可以根据已发生的异常来填充. - -##### 错误代码 - -错误 **代码(code)** 是异常信息中一个有唯一值并可选的字符串值.抛出的异常应实现`IHasErrorCode` 接口来填充该字段.示例JSON如下: - -````json -{ - "error": { - "code": "App:010042", - "message": "This topic is locked and can not add a new message" - } -} -```` - -错误代码同样可用于异常信息的本地化及自定义HTTP状态代码(请参阅下面的相关部分). - -##### 错误详细信息 - -错误的 **详细信息(Details)** 是可选属性.抛出的异常应实现`IHasErrorDetails` 接口来填充该字段.示例JSON如下: - -```json -{ - "error": { - "code": "App:010042", - "message": "This topic is locked and can not add a new message", - "details": "A more detailed info about the error..." - } -} -``` - -##### 验证错误 - -当抛出的异常实现`IHasValidationErrors` 接口时,**validationErrors**是一个可被填充的标准字段.示例JSON如下: - -````json -{ - "error": { - "code": "App:010046", - "message": "Your request is not valid, please correct and try again!", - "validationErrors": [{ - "message": "Username should be minimum length of 3.", - "members": ["userName"] - }, - { - "message": "Password is required", - "members": ["password"] - }] - } -} -```` - -`AbpValidationException`已经实现了`IHasValidationErrors`接口,当请求输入无效时,框架会自动抛出此错误. 因此,除非你有自定义的验证逻辑,否则不需要处理验证错误. - -#### 日志 - -被捕获的异常会被自动记录到日志中. - -##### 日志级别 - -默认情况下,记录异常级别为`Error` .可以通过实现`IHasLogLevel` 接口来指定日志的级别,例如: - -````C# -public class MyException : Exception, IHasLogLevel -{ - public LogLevel LogLevel { get; set; } = LogLevel.Warning; - - //... -} -```` - -##### 异常自定义日志 - -某些异常类型可能需要记录额外日志信息.可以通过实现`IExceptionWithSelfLogging` 接口来记录指定日志,例如: - -````C# -public class MyException : Exception, IExceptionWithSelfLogging -{ - public void Log(ILogger logger) - { - //...log additional info - } -} -```` - -> 扩展方法`ILogger.LogException` 用来记录异常日志. 在需要时可以使用相同的扩展方法. - -### 业务异常 - -大多数异常都是业务异常.可以通过使用`IBusinessException` 接口来标记异常为业务异常. - -`BusinessException` 除了实现`IHasErrorCode`,`IHasErrorDetails` ,`IHasLogLevel` 接口外,还实现了`IBusinessException` 接口.其默认日志级别为`Warning`. - -通常你会将一个错误代码关联至特定的业务异常.例如: - -````C# -throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer); -```` - -`QaErrorCodes.CanNotVoteYourOwnAnswer` 是一个字符串常量. 建议使用下面的错误代码格式: - -```` -: -```` - -**code-namespace**,应在指定的模块/应用层中保证其唯一.例如: - -```` -Volo.Qa:010002 -```` - -`Volo.Qa`在这是作为`code-namespace`. `code-namespace` 同样可以在 **本地化** 异常信息时使用. - -* 你可以直接抛出一个 `BusinessException` 异常,或者需要时可以从该类派生你自己的Exception类型. -* 对于`BusinessException` 类型,其所有属性都是可选的.但是通常会设置`ErrorCode`或`Message`属性. - -### 异常本地化 - -这里有个问题,就是如何在发送错误消息到客户端时,对错误消息进行本地化.ABP提供了2个模型. - -#### 用户友好异常 - -如果异常实现了 `IUserFriendlyException` 接口,那么ABP不会修改 `Message`和`Details`属性,而直接将它发送给客户端. - -`UserFriendlyException` 类是内建的 `IUserFriendlyException` 接口的实现,示例如下: - -````C# -throw new UserFriendlyException( - "Username should be unique!" -); -```` - -采用这种方式是不需要本地化的.如果需要本地化消息,则可以注入**string localizer**( 请参阅[本地化文档](Localization.md) )来实现. 例: - -````C# -throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage"]); -```` - -再在本地化资源中为每种语言添加对应的定义.例如: - -````json -{ - "culture": "en", - "texts": { - "UserNameShouldBeUniqueMessage": "Username should be unique!" - } -} -```` - -**string localizer** 支持参数化信息.例如 - -````C# -throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage", "john"]); -```` - -其本地化文本如下: - -````json -"UserNameShouldBeUniqueMessage": "Username should be unique! '{0}' is already taken!" -```` - -* `IUserFriendlyException`接口派生自`IBusinessException`,而 `UserFriendlyException`类派生自`BusinessException`类. - -#### 使用错误代码 - -`UserFriendlyException`很好用,但是在一些高级用法里面,它存在以下问题: - -* 在抛出异常的地方必须注入**string localizer** 来实现本地化 . -* 但是,在某些情况下,**可能注入不了string localizer**(比如,在静态上下文或实体方法中) - -那么这时就可以通过使用 **错误代码** 的方式来处理本地化,而不是在抛出异常的时候. - -首先,在模块配置代码中将 **code-namespace** 映射至 **本地化资源**: - -````C# -services.Configure(options => -{ - options.MapCodeNamespace("Volo.Qa", typeof(QaResource)); -}); -```` - -然后`Volo.Qa`命名空间下的所有异常都将被对应的本地化资源进行本地化处理. 本地化资源中应包含对应错误代码的文本. 例如: - -````json -{ - "culture": "en", - "texts": { - "Volo.Qa:010002": "You can not vote your own answer!" - } -} -```` - -最后就可以抛出一个包含错误代码的业务异常了: - -````C# -throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer); -```` - -* 抛出所有实现`IHasErrorCode` 接口的异常都具有相同的行为.因此,对错误代码的本地化,并不是`BusinessException`类所特有的. -* 为错误消息定义本地化文本并不是必须的. 如果未定义,ABP会将默认的错误消息发送给客户端. 而不使用异常的`Message`属性. 如果你想要发送异常的`Message`,使用`UserFriendlyException`(或使用实现`IUserFriendlyException`接口的异常类型) - -##### 使用消息的格式化参数 - -如果有参数化的错误消息,则可以使用异常的`Data`属性进行设置.例如: - -````C# -throw new BusinessException("App:010046") -{ - Data = - { - {"UserName", "john"} - } -}; - -```` - -另外有一种更为快捷的方式: - -````C# -throw new BusinessException("App:010046") - .WithData("UserName", "john"); -```` - -下面就是一个包含`UserName` 参数的错误消息: - -````json -{ - "culture": "en", - "texts": { - "App:010046": "Username should be unique. '{UserName}' is already taken!" - } -} -```` - -* `WithData` 支持有多个参数的链式调用 (如`.WithData(...).WithData(...)`). - -### HTTP状态代码映射 - -ABP尝试按照以下规则,自动映射常见的异常类型的HTTP状态代码: - -* 对于 `AbpAuthorizationException`: - * 用户没有登录,返回 `401` (未认证). - * 用户已登录,但是当前访问未授权,返回 `403` (未授权). -* 对于 `AbpValidationException` 返回 `400` (错误的请求) . -* 对于 `EntityNotFoundException`返回 `404` (未找到). -* 对于 `IBusinessException` 和 `IUserFriendlyException` (它是`IBusinessException`的扩展) 返回`403` (未授权) . -* 对于 `NotImplementedException` 返回 `501` (未实现) . -* 对于其他异常 (基础架构中未定义的) 返回 `500` (服务器内部错误) . - -`IHttpExceptionStatusCodeFinder` 是用来自动判断HTTP状态代码.默认的实现是`DefaultHttpExceptionStatusCodeFinder`.可以根据需要对其进行更换或扩展. - -#### 自定义映射 - -可以重写HTTP状态代码的自动映射,示例如下: - -````C# -services.Configure(options => -{ - options.Map("Volo.Qa:010002", HttpStatusCode.Conflict); -}); -```` - -### 内置的异常 - -框架会自动抛出以下异常类型: - -- 当用户没有权限执行操作时,会抛出 `AbpAuthorizationException` 异常. 有关更多信息,请参阅授权文档[authorization](Authorization.md). -- 如果当前请求的输入无效,则抛出`AbpValidationException 异常`. 有关更多信息,请参阅[验证文档](Validation.md). -- 如果请求的实体不存在,则抛出`EntityNotFoundException` 异常. 此异常大多数由 [repositories](Repositories.md) 抛出. - -你同样可以在代码中抛出这些类型的异常(虽然很少需要这样做) - -## 发送异常详情到客户端 - -你可以通过 `AbpExceptionHandlingOptions` 类的 `SendExceptionsDetailsToClients` 属性异常发送到客户端: - -````csharp -services.Configure(options => -{ - options.SendExceptionsDetailsToClients = true; -}); -```` diff --git a/docs/zh-Hans/FluentValidation.md b/docs/zh-Hans/FluentValidation.md deleted file mode 100644 index 265a2cc2ca..0000000000 --- a/docs/zh-Hans/FluentValidation.md +++ /dev/null @@ -1,59 +0,0 @@ -# FluentValidation 集成 - -ABP[验证](Validation.md)基础设施是可扩展的. [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet 包扩展了验证系统使其与[FluentValidation](https://fluentvalidation.net/)库一起工作. - -## 安装 - -建议使用[ABP CLI](CLI.md)安装包. - -### 使用ABP CLI - -在项目(.csproj文件)的文件夹中打开命令行窗口并输入以下命令: - -````bash -abp add-package Volo.Abp.FluentValidation -```` - -### 手动安装 - -如果你想手动安装; - -1. 添加 [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet包到你的项目: - - ```` - Install-Package Volo.Abp.FluentValidation - ```` - -2. 添加 `AbpFluentValidationModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpFluentValidationModule) //Add the FluentValidation module - )] -public class YourModule : AbpModule -{ -} -```` - -## 使用 FluentValidation - -按照 [FluentValidation文档](https://fluentvalidation.net/) 创建验证器类. -例如: - -````csharp -public class CreateUpdateBookDtoValidator : AbstractValidator -{ - public CreateUpdateBookDtoValidator() - { - RuleFor(x => x.Name).Length(3, 10); - RuleFor(x => x.Price).ExclusiveBetween(0.0f, 999.0f); - } -} -```` - -ABP会自动找到这个类并在对象验证时与 `CreateUpdateBookDto` 关联. - -## 另请参阅 - -* [验证系统](Validation.md) \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-Angular-Template.md b/docs/zh-Hans/Getting-Started-Angular-Template.md deleted file mode 100644 index d242b4f39b..0000000000 --- a/docs/zh-Hans/Getting-Started-Angular-Template.md +++ /dev/null @@ -1,8 +0,0 @@ -# 启动模板入门 - -请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: - -* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) -* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) - - \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md deleted file mode 100644 index f55316739c..0000000000 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ /dev/null @@ -1,148 +0,0 @@ -# 在ASP.NET Core MVC Web 应用程序中使用ABP - -本教程将介绍如何开始以最少的依赖关系开始使用ABP开发. - -通常情况下你希望从 **[启动模板](Getting-Started-AspNetCore-MVC-Template.md)** 开始. - -## 创建一个新项目 - -1. 使用Visual Studio 2022 (17.0.0+)创建一个新的ASP.NET Core Web应用程序: - -![](images/create-new-aspnet-core-application-v2.png) - -2. 配置新的项目: - -![](images/select-empty-web-application-v2.png) - -3. 完成创建: - -![](images/create-aspnet-core-application.png) - - -## 安装 Volo.Abp.AspNetCore.Mvc 包 - -Volo.Abp.AspNetCore.Mvc是ABP集成ASP.NET Core MVC的包,请安装它到你项目中: - -```` -Install-Package Volo.Abp.AspNetCore.Mvc -```` - -## 创建ABP模块 - -ABP是一个模块化框架,它需要一个**启动(根)模块**继承自 `AbpModule`: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - // Configure the HTTP request pipeline. - if (env.IsDevelopment()) - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseConfiguredEndpoints(); - } - } -} -```` - -``AppModule`` 是应用程序启动模块的好名称. - -ABP的包定义了模块类,模块可以依赖其它模块.在上面的代码中 ``AppModule`` 依赖于 ``AbpAspNetCoreMvcModule`` (由[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)包定义). 安装新的ABP的包后添加``DependsOn``特性是很常见的做法. - -我们在此模块类中配置ASP.NET Core管道,而不是Startup类中. - -### 启动类 - -接下来修改启动类集成到ABP模块系统: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.ReplaceConfiguration(builder.Configuration); - -builder.Services.AddApplication(); - -var app = builder.Build(); - -app.InitializeApplication(); - -app.Run(); - -```` - -``services.AddApplication()``添加了从``AppModule``中启动的所有模块中定义的所有服务. - -``app.InitializeApplication()`` 初始化并启动应用程序. - -## 运行应用程序! - -启动该应用,它将按预期运行. - -## 使用 Autofac 依赖注入框架 - -虽然ASP.NET Core的依赖注入(DI)系统适用于基本要求,但[Autofac](https://autofac.org/)提供了属性注入和方法拦截等高级功能,这些功能是ABP执行高级应用程序框架功能所必需的. - -用Autofac取代ASP.NET Core的DI系统并集成到ABP非常简单. - -1. 安装 [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) 包 - -```` -Install-Package Volo.Abp.Autofac -```` - -2. 添加 ``AbpAutofacModule`` 依赖 - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] // 在模块上添加依赖AbpAutofacModule -public class AppModule : AbpModule -{ - ... -} -```` - -3. 修改``Program.cs``以使用Autofac: - -````C# -using BasicAspNetCoreApplication; - -var builder = WebApplication.CreateBuilder(args); - -builder.Host.UseAutofac(); //Add this line - -builder.Services.ReplaceConfiguration(builder.Configuration); - -builder.Services.AddApplication(); - -var app = builder.Build(); - -app.InitializeApplication(); - -app.Run(); -```` - -## 源码 - -从[此处](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)获取本教程中创建的示例项目的源代码. diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md deleted file mode 100644 index d242b4f39b..0000000000 --- a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md +++ /dev/null @@ -1,8 +0,0 @@ -# 启动模板入门 - -请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: - -* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) -* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) - - \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-Create-Solution.md b/docs/zh-Hans/Getting-Started-Create-Solution.md deleted file mode 100644 index 5b83f594e9..0000000000 --- a/docs/zh-Hans/Getting-Started-Create-Solution.md +++ /dev/null @@ -1,73 +0,0 @@ -# 入门教程 - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项. - -## 创建新项目 - -我们将使用 ABP CLI 创建一个新的 ABP 项目. - -> 或者, 你可以使用[ABP Framework 网站](https://abp.io/get-started)页面上的选项轻松的 **创建并下载** 项目. - -使用 ABP CLI 的 `new` 命令创建一个新项目: - -````shell -abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{else if UI == "BlazorServer"}} -u blazor-server{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC" || UI == "BlazorServer"}} --tiered{{else}} --separate-auth-server{{end}}{{end}} -```` - -*你可以使用不同级别的命名空间, 例如: BookStore、Acme.BookStore或 Acme.Retail.BookStore.* - -{{ if Tiered == "Yes" }} - -{{ if UI == "MVC" || UI == "BlazorServer" }} - -* `--tified` 参数用于创建认证服务器、 UI 和 API 实际分隔的 N-层解决方案. - -{{ else }} - -* `--separate-auth-server` 参数用于将Auth Server应用程序与API主机应用程序分隔开. 如果未指定, 则服务器上将只有一个端点. - -{{ end }} - -{{ end }} - -> [ABP CLI 文档](./CLI.md) 涵盖了所有可用的命令和选项. - -## 移动端开发 - -如果你想要在你的解决方案中包含 [React Native](https://reactnative.dev/) 项目, 将 `-m react-native` (or `--mobile react-native`) 参数添加到项目创建命令. 这是一个基础的 React Native 启动模板, 用于开发基于你的 ABP 后端的移动应用程序. - -请参阅 [React Native 入门](Getting-Started-React-Native.md) 文档, 了解如何配置和运行 React Native 应用程序. - -### 解决方案结构 - -该解决方案具有分层结构 (基于 [域驱动设计](Domain-Driven-Design.md)), 并包含单元 & 集成测试项目. 请参阅 [应用程序模板文档](Startup-Templates/Application.md) 以详细了解解决方案结构. - -{{ if DB == "Mongo" }} - -#### MongoDB 事务 - -[启动模板](Startup-Templates/Index.md) 默认在`.MongoDB`项目中**禁用**事务. 如果你的MongoDB服务器支持事务, 你可以在*YourProjectMongoDbModule*类中的`ConfigureServices`方法开启它: - - ```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; -}); - ``` - -> 或者你可以删除该代码, 因为 `Auto` 已经是默认行为. - -{{ end }} - -## 下一步 - -* [运行解决方案](Getting-Started-Running-Solution.md) \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-React-Native.md b/docs/zh-Hans/Getting-Started-React-Native.md deleted file mode 100644 index 76581930ce..0000000000 --- a/docs/zh-Hans/Getting-Started-React-Native.md +++ /dev/null @@ -1,70 +0,0 @@ -# React Native入门 - -ABP平台提供了[React Native](https://reactnative.dev/)模板用于开发移动应用程序. - -当你按照[入门文档](Getting-Started.md)中所述**创建新应用程序**时, 你应该使用`-m react-native`选项以在解决方案中包含`react-native`项目. - -## 配置你的本地IP地址 - -运行在Android模拟器或真机上的React Native应用程序无法连接到 `localhost` 上的后.要修复此问题,需要在本地IP上运行后端. - -{{ if Tiered == "No"}} -![React Native host project local IP entry](images/rn-host-local-ip.png) - -* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 和 `Authority` 属性的 `localhost` 替换为你本地的IP地址. -* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. - -{{ else if Tiered == "Yes" }} - -![React Native tiered project local IP entry](images/rn-tiered-local-ip.png) - -* 打开 `.AuthServer` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 属性的 `localhost` 替换为你本地的IP地址. -* 打开 `.AuthServer/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. -* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `Authority` 属性的 `localhost` 替换为你本地的IP地址. -* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. - -{{ end }} - -按照**运行HTTP API Host (服务端口)**那样运行后端. - -> React Native应用程序不信任自动生成的.NET HTTPS证书,你可以在开发期间使用HTTP. - -在 `react-native` 文件夹打开命令行终端,输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). - -```bash -yarn -``` - -* 打开 `react-nativer` 文件夹下的 `Environment.js` 文件. 将 `apiUrl` 和 `issuer` 属性的 `localhost` 替换为你本地的IP地址: - -![react native environment local IP](images/rn-environment-local-ip.png) - -{{ if Tiered == "Yes" }} - -> 确保 `issuer` 与正在运行的 `.AuthServer` 项目匹配, `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配. - -{{else}} - -> 确保 `issuer` 和 `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配 - -{{ end }} - -等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: - -```bash -yarn start -``` - -等待Expo CLI启动后Expo CLI在 `http://localhost:19002/` 地址要开管理页面. - -![expo-interface](images/rn-expo-interface.png) - -在上面的管理界面中,可以通过使用[Expo Client](https://expo.io/tools#client)扫描二维码,使用Android模拟器,iOS模拟器或真机来启动应用程序. - -> 请参阅expo.io上的[Android Studio模拟器](https://docs.expo.io/workflow/android-simulator/)和[iOS模拟器文档](https://docs.expo.io/workflow/ios-simulator/). - -![React Native login screen on iPhone 11](images/rn-login-iphone.png) - -输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. - -应用程序已经启动并执行,你可以基于该启动模板开发应用程序. diff --git a/docs/zh-Hans/Getting-Started-Running-Solution.md b/docs/zh-Hans/Getting-Started-Running-Solution.md deleted file mode 100644 index f0a4942d64..0000000000 --- a/docs/zh-Hans/Getting-Started-Running-Solution.md +++ /dev/null @@ -1,199 +0,0 @@ -# 入门教程 - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项. - -## 创建数据库 - -### 连接字符串 - -检查在 {{if Tiered == "Yes"}}`.AuthServer` 和`.HttpApi.Host` 项目{{else}}{{if UI=="MVC"}}`.Web` 项目{{else if UI=="BlazorServer"}}`.Blazor` 项目{{else}}`.HttpApi.Host` 项目{{end}}{{end}} 中 `appsettings.json` 文件里的**连接字符串**. - -{{ if DB == "EF" }} - -````json -"ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True" -} -```` - -> **关于连接字符串和数据库管理系统** -> -> 解决方案配置为默认使用 **Entity Framework Core** 与 **MS SQL Server**. 但是, 如果在执行ABP CLI 的`new`命令时使用了`-dbms`参数来选择其他DBMS (如`-dbms MySQL`), 那么连接字符串可能不同. -> -> EF Core 支持 [多种](https://docs.microsoft.com/en-us/ef/core/providers/) 据库提供程序, 因此你可以使用任何受支持的DBMS. 你可以需要时候参阅[Entity Framework 集成文档](Entity-Framework-Core.md) 来学习如何[切换到另一个DBMS](Entity-Framework-Core-Other-DBMS.md). - -### 数据库迁移 - -该解决方案使用[Entity Framework Core Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). 带有 `.DbMigrator` 的控制台程序用于 **应用迁移** 和 **初始化种子数据**. 它在**开发**和**生产**环境中都很有用. - -> `.dbMigator` 项目有自己的 `appsettings.json`. 因此, 如果你更改了之前的连接字符串, 那么也应该更改这个连接字符串. - -### 初次迁移 - -`.dbMigator` 应用程序在首次运行时自动**创建初始迁移**. - -**如果你使用的是 Visual Studio, 你可以跳到 *运行 dbMigrator* 部分.** 但是, 其他 IDE (例如 Rider) 在首次运行时可能会遇到问题, 因为它会添加初始迁移并编译项目. 在这种情况下, 请在 `.dbMigration` 项目的文件夹中打开命令行终端, 然后运行以下命令: - -````bash -dotnet run -```` - -下次, 你可以像往常一样在 IDE 中运行它. - -### 运行迁移 - -右键单击 `.dbMigration` 项目, 然后选择 **设置为启动项目** - -![set-as-startup-project](images/set-as-startup-project.png) - - 按F5(或Ctrl + F5) 运行应用程序. 它将具有如下所示的输出: - - ![db-migrator-output](images/db-migrator-output.png) - -> 初始的[种子数据](Data-Seeding.md)在数据库中创建了 `admin` 用户(密码为`1q2w3E*`) 用于登录应用程序. 所以, 对于新数据库至少使用 `.DbMigrator` 一次. - -{{ else if DB == "Mongo" }} - -````json -"ConnectionStrings": { - "Default": "mongodb://localhost:27017/BookStore" -} -```` - -该解决方案配置为在本地计算机中使用 **MongoDB**, 因此你需要启动并运行 MongoDB 服务器实例, 或者将连接字符串更改为另一台 MongoDB 服务器. - -### 种子初始数据 - -该解决方案带有 `.DbMigrator` 的控制台程序用于 **初始化种子数据**. 它在**开发**和**生产**环境中都很有用. - -> `.dbMigator` 项目有自己的 `appsettings.json`. 因此, 如果你更改了之前的连接字符串, 那么也应该更改这个连接字符串. - -右键单击 `.dbMigration` 项目, 然后选择 **设置为启动项目** - -![set-as-startup-project](images/set-as-startup-project.png) - - 按F5(或Ctrl + F5) 运行应用程序. 它将具有如下所示的输出: - - ![db-migrator-output](images/db-migrator-output.png) - -> 初始的[种子数据](Data-Seeding.md)在数据库中创建了 `admin` 用户(密码为`1q2w3E*`) 用于登录应用程序. 所以, 对于新数据库至少使用 `.DbMigrator` 一次. - -{{ end }} - -## 运行应用程序 - -{{ if UI == "MVC" || UI == "BlazorServer" }} - -{{ if Tiered == "Yes" }} - -> 分层解决方案使用 **Redis** 作为分布式缓存. 确保它已安装并在本地计算机上运行. 如果你使用的是远程 Redis 服务器, 请修改项目的 ` appsettings.json` 文件中的配置. - -1. 确保 `.AuthServer` 项目是启动项目. 运行此应用程序, 它将在浏览器中打开 **登录** 页面. - -> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快. - -你可以登录, 但不能在这里进入主应用程序. 这 **只是身份验证服务器**. - -2. 确保 `.httpapi.Host` 项目是启动项目, 然后运行应用将在浏览器中打开 **Swagger UI**. - -![swagger-ui](images/swagger-ui.png) - -这是 Web 应用程序使用的 HTTP API. - -3. 最后, 确保 {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} 项目是启动项目,然后运行应用程序,它将在浏览器中打开 **欢迎** 页面 - -![mvc-tiered-app-home](images/bookstore-home.png) - -单击 **login** 按钮, 它将重定向到 *身份验证服务器* 以登录到应用程序: - -![bookstore-login](images/bookstore-login.png) - -{{ else # Tiered != "Yes" }} - -确保 {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} 项目是启动项目. 运行应用程序将会在浏览器中打开 **login** 页面: - -> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快. - -![bookstore-login](images/bookstore-login.png) - -{{ end # Tiered }} - -{{ else # UI != MVC || BlazorServer }} - -### 运行 HTTP API 主机(服务器端) - -{{ if Tiered == "Yes" }} - -> 分层解决方案使用 Redis 作为分布式缓存. 确保它已安装并在本地计算机上运行. 如果你使用的是远程 Redis 服务器, 请修改项目的 `appsettings.json` 文件中的配置. - -确保 `.AuthServer` 项目是启动项目. 运行此应用程序, 它将在浏览器中打开 **登录** 页面. - -> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快. - -你可以登录, 但不能在这里进入主应用程序. 这 **只是身份验证服务器**. - -确保 `.HttpApi.Host` 项目是启动项目, 然后运行应用程序将打开 Swagger UI 的: - -{{ else # Tiered == "No" }} - -确保 `.HttpApi.Host` 项目是启动项目, 然后运行应用程序将打开 Swagger UI 的: - -> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快. - -{{ end # Tiered }} - -![swagger-ui](images/swagger-ui.png) - -你可以在这里查看应用程序的API并测试它们. 获取Swagger UI的[更多信息](https://swagger.io/tools/swagger-ui/). - -{{ end # UI }} - -{{ if UI == "Blazor" }} - -### 运行 Blazor 应用程序 (客户端) - -确保 `.Blazor` 项目是启动项目并运行应用程序. - -> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快. - -应用程序启动后, 单击页头上的 **Login** 链接, 你将重定向到身份验证服务器以输入用户名和密码: - -![bookstore-login](images/bookstore-login.png) - -{{ else if UI == "NG" }} - -### 运行 Angular 应用程序 (客户端) - -转到 `Angular` 文件夹, 打开命令行终端, 键入 `yarn` 命令(我们建议使用 [yarn](https://yarnpkg.com/) 软件包管理器, 而 `npm install` 也可以使用) - -```bash -yarn -``` - -当所有node模块加载完毕后, 执行 `yarn start` (或 `npm start`) 命令: - -```bash -yarn start -``` - -初次构建可能需要更长的时间. 完成后, 它会在默认浏览器中使用 [localhost:4200](http://localhost:4200/) 地址打开 Angular UI. - -![bookstore-login](images/bookstore-login.png) - -{{ end }} - -输入用户名 **admin** 和 密码 **1q2w3E*** 登录应用程序. 应用程序已启动并正在运行. 你可以根据此启动模板开始开发应用程序. - -## 另请参见 - -* [Web 应用程序开发教程](Tutorials/Part-1.md) -* [应用程序启动模板](Startup-Templates/Application.md) diff --git a/docs/zh-Hans/Getting-Started-Setup-Environment.md b/docs/zh-Hans/Getting-Started-Setup-Environment.md deleted file mode 100644 index 0ba3c2738d..0000000000 --- a/docs/zh-Hans/Getting-Started-Setup-Environment.md +++ /dev/null @@ -1,53 +0,0 @@ -# 入门教程 - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项. - -## 设置你的开发环境 - -第一件事! 在创建项目之前, 让我们先设置你的开发环境. - -### 先决条件 - -开发计算机上应安装以下工具: - -* 一个集成开发环境 (比如: [Visual Studio](https://visualstudio.microsoft.com/vs/)) 它需要支持 [.NET 8.0+](https://dotnet.microsoft.com/download/dotnet) 的开发. -{{ if UI != "Blazor" }} -* [Node 16+](https://nodejs.org/) -* [Yarn v1.20+ (不是v2)](https://classic.yarnpkg.com/en/docs/install) [1](#f-yarn) 或 npm v6+ (已跟随Node一起安装) -{{ end }} -{{ if Tiered == "Yes" }} -* [Redis](https://redis.io/) (启动解决方案使用 Redis 作为 [分布式缓存](Caching.md)). -{{ end }} - -{{ if UI != "Blazor" }} - -1 _Yarn v2 工作方式不同, 不被支持._ [↩](#a-yarn) - -{{ end }} - -### 安装 ABP CLI - -[ABP CLI](./CLI.md) 是一个命令行界面, 用于自动执行基于 ABP 的解决方案的一些常见任务. 首先, 你需要使用以下命令安装 ABP CLI: - -````shell -dotnet tool install -g Volo.Abp.Cli -```` - -如果已安装, 则可以使用以下命令对其进行更新: - -````shell -dotnet tool update -g Volo.Abp.Cli -```` - -## 下一步 - -* [创建新的解决方案](Getting-Started-Create-Solution.md) \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-With-Startup-Templates.md b/docs/zh-Hans/Getting-Started-With-Startup-Templates.md deleted file mode 100644 index d242b4f39b..0000000000 --- a/docs/zh-Hans/Getting-Started-With-Startup-Templates.md +++ /dev/null @@ -1,8 +0,0 @@ -# 启动模板入门 - -请参阅以下教程,了解如何使用预构建的应用程序启动模板开始使用ABP框架: - -* [ASP.NET Core MVC / Razor Pages UI 入门](Getting-Started?UI=MVC&DB=EF&Tiered=No) -* [Angular UI 入门](Getting-Started?UI=NG&DB=EF&Tiered=No) - - \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started.md b/docs/zh-Hans/Getting-Started.md deleted file mode 100644 index 1cf1430a35..0000000000 --- a/docs/zh-Hans/Getting-Started.md +++ /dev/null @@ -1,20 +0,0 @@ -# 入门教程 - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -```` - -> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项. - -## 内容 - -本教程介绍如何使用 ABP 框架 **创建和运行** 新的 Web 应用程序. 请按照以下步骤操作; - -1. [设置你的开发环境](Getting-Started-Setup-Environment.md) -2. [创建新的解决方案](Getting-Started-Create-Solution.md) -3. [运行解决方案](Getting-Started-Running-Solution.md) \ No newline at end of file diff --git a/docs/zh-Hans/Global-Features.md b/docs/zh-Hans/Global-Features.md deleted file mode 100644 index 776d4e0718..0000000000 --- a/docs/zh-Hans/Global-Features.md +++ /dev/null @@ -1,3 +0,0 @@ -# Global Features - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Guid-Generation.md b/docs/zh-Hans/Guid-Generation.md deleted file mode 100644 index 1b097f8b08..0000000000 --- a/docs/zh-Hans/Guid-Generation.md +++ /dev/null @@ -1,111 +0,0 @@ -# GUID 生成 - -GUID是数据库管理系统中使用的常见**主键类型**, ABP框架更喜欢GUID作为预构建[应用模块](Modules/Index.md)的主要对象. `ICurrentUser.Id` 属性([参见文档](CurrentUser.md))是GUID类型,这意味着ABP框架假定用户ID始终是GUID, - -## 为什么偏爱GUID? - -GUID有优缺点. 你可以在网上找到许多与此主题相关的文章,因此我们不再赘述,而是列出了最基本的优点: - -* 它可在所有数据库提供程序中**使用**. -* 它允许在客户端**确定主键**,而不需要通过**数据库往返**来生成Id值. 在向数据库插入新记录时,这可以提高性能并允许我们在与数据库交互之前知道PK. -* GUID是**自然唯一的**在以下情况下有一些优势; - * 你需要与**外部**系统集成, - * 你需要**拆分或合并**不同的表. - * 你正在创建**分布式系统** -* GUID是无法猜测的,因此在某些情况下与自动递增的Id值相比,GUID**更安全**. - -尽管存在一些缺点(只需在Web上搜索),但在设计ABP框架时我们发现这些优点更为重要. - -## IGuidGenerator - -GUID的最重要问题是**默认情况下它不是连续的**. 当你将GUID用作主键并将其设置为表的**聚集索引**(默认设置)时,这会在**插入时带来严重的性能问题**(因为插入新记录可能需要对现有记录进行重新排序). - -所以,**永远不要为你的实体使用 `Guid.NewGuid()` 创建ID**!. - -这个问题的一个好的解决方案是生成**连续的GUID**,由ABP框架提供的开箱即用的. `IGuidGenerator` 服务创建顺序的GUID(默认由 `SequentialGuidGenerator` 实现). 当需要手动设置[实体](Entities.md)的Id时,请使用 `IGuidGenerator.Create()`. - -**示例: 具有GUID主键的实体并创建该实体** - -假设你有一个具有 `Guid` 主键的 `Product` [实体](Entities.md): - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - private Product() { /* This constructor is used by the ORM/database provider */ } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - } -} -```` - -然后你想要创建一个产品: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace AbpDemo -{ - public class MyProductService : ITransientDependency - { - private readonly IRepository _productRepository; - private readonly IGuidGenerator _guidGenerator; - - public MyProductService( - IRepository productRepository, - IGuidGenerator guidGenerator) - { - _productRepository = productRepository; - _guidGenerator = guidGenerator; - } - - public async Task CreateAsync(string productName) - { - var product = new Product(_guidGenerator.Create(), productName); - - await _productRepository.InsertAsync(product); - } - } -} -```` - -该服务将 `IGuidGenerator` 注入构造函数中. 如果你的类是[应用服务](Application-Services.md)或派生自其他基类之一,可以直接使用 `GuidGenerator` 基类属性,该属性是预先注入的 `IGuidGenerator` 实例. - -## Options - -### AbpSequentialGuidGeneratorOptions - -`AbpSequentialGuidGeneratorOptions` 是用于配置顺序生成GUID的[选项类](Options.md). 它只有一个属性: - -* `DefaultSequentialGuidType` (`SequentialGuidType` 类型的枚举): 生成GUID值时使用的策略. - -数据库提供程序在处理GUID时的行为有所不同,你应根据数据库提供程序进行设置. `SequentialGuidType` 有以下枚举成员: - -* `SequentialAtEnd` (**default**) 用于[SQL Server](Entity-Framework-Core.md). -* `SequentialAsString` 用于[MySQL](Entity-Framework-Core-MySQL.md)和[PostgreSQL](Entity-Framework-Core-PostgreSQL.md). -* `SequentialAsBinary` 用于[Oracle](Entity-Framework-Core-Oracle.md). - -在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项,如下: - -````csharp -Configure(options => -{ - options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; -}); -```` - -> EF Core[集成包](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS)已为相关的DBMS设置相应的值. 如果你正在使用这些集成包,在大多数情况下则无需设置此选项. diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md deleted file mode 100644 index 7117cc01b0..0000000000 --- a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md +++ /dev/null @@ -1,210 +0,0 @@ -# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 - -本文介绍了如何将AzureAD集成到ABP应用程序中,用 **Azure Active Directory** 凭据使用 OAuth 2.0 登录. - -添加Azure Active Directory到ABP框架非常简单,只需要正确的完成几个配置. - -为了覆盖更多范围,我们演示两种不同的集成AzureAD的**方法**. - -1. **AddAzureAD**: 该方法使用微软[AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),在网络上搜索如何将AzureAD集成到应用程序时,这个包是最流行的. - -2. **AddOpenIdConnect**: 该方法使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/). 它不仅可用于AzureAD,还可用于所有OpenId连接. - -> 这些方法之间的功能**没有区别**,AddAzureAD是具有预定义Cookie设置的OpenIdConnection([源](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122))的抽象方法. -> -> 但是默认配置的登录方案在与ABP应用程序集成方面存在关键差异,下面将对此进行说明. - -## 1. AddAzureAD - -这个方法使用 [Microsoft AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),它是最常用的集成AzureAD方法. - -如果选择这种方法,需要将 `Microsoft.AspNetCore.Authentication.AzureAD.UI` 软件包安装到 **.Web** 项目中. 由于AddAzureAD扩展使用[配置绑定](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration),你需要更改 **.Web** 项目中的appsettings.json文件. - -#### **更改 `appsettings.json`** - -你添加向 `appsettings.json` 添加新的配置节,在配置 `OpenIdConnectOptions` 时绑定配置: - -````json - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "TenantId": "", - "ClientId": "", - "Domain": "domain.onmicrosoft.com", - "CallbackPath": "/signin-azuread-oidc" - } -```` - -> 这里重要的配置是CallbackPath. 值必须与你的 Azure AD-> app registrations-> Authentication -> RedirectUri 之一相同. - -然后你需要配置 `OpenIdConnectOptions` 完成集成. - -#### 配置 OpenIdConnectOptions - -在你的 **.Web** 项目找到 **ApplicationWebModule** 使用以下代码修改 `ConfigureAuthentication` 方法: - -````csharp -private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); - context.Services.AddAuthentication() - .AddIdentityServerAuthentication(options => - { - options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = false; - options.ApiName = "Acme.BookStore"; - }) - .AddAzureAD(options => configuration.Bind("AzureAd", options)); - - context.Services.Configure(AzureADDefaults.OpenIdScheme, options => - { - options.Authority = options.Authority + "/v2.0/"; - options.ClientId = configuration["AzureAd:ClientId"]; - options.CallbackPath = configuration["AzureAd:CallbackPath"]; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.RequireHttpsMetadata = false; - - options.TokenValidationParameters.ValidateIssuer = false; - options.GetClaimsFromUserInfoEndpoint = true; - options.SaveTokens = true; - options.SignInScheme = IdentityConstants.ExternalScheme; - - options.Scope.Add("email"); - }); - } -```` - -> **不要忘记:** -> -> * 在 `AddAuthentication()` 之后添加 `.AddAzureAD(options => configuration.Bind("AzureAd", options))` . 它绑定了你的 AzureAD 配置并且容易忘记. -> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. 它会禁用默认的 Microsoft claim type 映射. -> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. 映射 [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) 很重要,因为默认SignIn Manager和行为使用这个claim type用于外部登录信息. -> * 添加 `options.SignInScheme = IdentityConstants.ExternalScheme` 因为 [默认登录方法为 `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). -> * 如果你使用的是 **v2.0** 端点,应添加 `options.Scope.Add("email")` 因为 v2.0 端点不会将 `email` 做为默认值返回. [账户模块](../Modules/Account.md) 使用 `email` claim 来 [注册外部账户](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). - -你已经完成了集成. - -## 2. 替代方法: AddOpenIdConnect - -如果你不想在应用程序安装一个额外的NuGet包,你可以使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/),它适用于所有的OpenId连接,包括AzureAD外部认证. - -你不必使用 `appsettings.json` 配置, 但将AzureAD信息放在 `appsettings.json` 是一个很好的做法. - -为了从 `appsettings.json` 获取AzureAD信息在 `OpenIdConnectOptions` 配置使用,只需要在你的 **.Web** 项目中的 `appsettings.json` 添加一个新的配置节: - -````json - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "TenantId": "", - "ClientId": "", - "Domain": "domain.onmicrosoft.com", - "CallbackPath": "/signin-azuread-oidc" - } -```` - -然后在你的 **.Web** 项目的 **ApplicationWebModule** 用以下代码修改 `ConfigureAuthentication` 方法: - -````csharp -private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); - - context.Services.AddAuthentication() - .AddIdentityServerAuthentication(options => - { - options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = false; - options.ApiName = "BookStore"; - }) - .AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => - { - options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; - options.ClientId = configuration["AzureAd:ClientId"]; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.CallbackPath = configuration["AzureAd:CallbackPath"]; - options.RequireHttpsMetadata = false; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - - options.Scope.Add("email"); - }); - } -```` - -集成结束. 请记住你可以连接任何其他外部认证供应商. - -## 本文的源代码 - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization)找到已完成的示例源码. - -# FAQ - -* Help! `GetExternalLoginInfoAsync` 返回 `null`! - - * 有两方面的原因; - - 1. 你在尝试验证错误的方案. 检查是否设置 **SignInScheme** 为 `IdentityConstants.ExternalScheme`: - - ````csharp - options.SignInScheme = IdentityConstants.ExternalScheme; - ```` - - 2. 你的 `ClaimTypes.NameIdentifier` 为 `null`. 检查是否添加 claim 映射: - - ````csharp - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); - ```` - -* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! - - * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 - - ````csharp - options.Scope.Add("email"); - ```` - - 到你的 openid 配置. - -* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! - - * 如果你在appsettings设置 **CallbackPath** 为: - - ````csharp - "AzureAd": { - ... - "CallbackPath": "/signin-azuread-oidc" - } - ```` - - 你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的域, 而不仅是 `/signin-azuread-oidc`. - -* Help! 我一直得到 ***AADSTS700051: The response_type 'token' is not enabled for the application.*** 错误! - - * 当你请求**token**(访问令牌)和**id_token**时没有在Azure门户应用程序启用访问令牌时会发生这个错误,只需勾选ID令牌顶部的**访问令牌**复选框即可同时请求令牌. - -* Help! 我一直得到 ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** 错误! - - * 当你与 **id_token** 一起请求 **code**时,你需要在Azure门户的**证书和机密**菜单下添加**证书和机密**. 然后你需要添加openid配置选项: - - ````csharp - options.ClientSecret = "Value of your secret on azure portal"; - ```` - -* 如何**调试/监视**在映射之前获得的声明? - - * 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如: - - ````csharp - options.Events.OnTokenValidated = (async context => - { - var claimsFromOidcProvider = context.Principal.Claims.ToList(); - await Task.CompletedTask; - }); - ```` - -## 另请参阅 - -* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). -* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md). \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md b/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md deleted file mode 100644 index bba8e80b75..0000000000 --- a/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md +++ /dev/null @@ -1,112 +0,0 @@ -# 如何为MVC / Razor页面应用程序自定义登录页面 - -当你使用[应用程序启动模板](../Startup-Templates/Application.md)创建了一个新的应用程序, 登录页面的源代码并不在你的解决方案中,所以你不能直接更改. 它来自[账户模块](../Modules/Account.md),使用[NuGet包](https://www.nuget.org/packages/Volo.Abp.Account.Web)引用. - -本文介绍了如何为自己的应用程序自定义登录页面. - -## 创建登录 PageModel - -创建一个新的类继承账户模块的[LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs). - -````csharp -public class CustomLoginModel : LoginModel -{ - public CustomLoginModel( - Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, - Microsoft.Extensions.Options.IOptions accountOptions) - : base(schemeProvider, accountOptions) - { - } -} -```` - -> 在这里命令约定很重要. 如果你的类名不是以 `LoginModel` 结束,你需要手动在[依赖注入](../Dependency-Injection.md)系统替换 `LoginModel`. - -然后你可以覆盖任何方法并添加用户界面需要的新方法和属性. - -## 重写登录页面UI - -在 **Pages** 目录下创建名为 **Account** 的文件夹,并在这个文件夹中创建 `Login.cshtml` , 它会自动覆盖账户模块的页面文件. - -自定义页面一个很好的开始是复制它的源代码. [点击这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到登录页面的源码. 在编写本文档时,源代码如下: - -````xml -@page -@using Volo.Abp.Account.Settings -@using Volo.Abp.Settings -@model Acme.BookStore.Web.Pages.Account.CustomLoginModel -@inject Volo.Abp.Settings.ISettingProvider SettingProvider -@if (Model.EnableLocalLogin) -{ -
    -
    -

    @L["Login"]

    - @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) - { - - @L["AreYouANewUser"] - @L["Register"] - - } -
    - - -
    - - - -
    -
    - - - -
    -
    - -
    - @L["Login"] -
    -
    - - -
    -} - -@if (Model.VisibleExternalProviders.Any()) -{ -
    -

    @L["UseAnotherServiceToLogIn"]

    -
    - - - @foreach (var provider in Model.VisibleExternalProviders) - { - - } -
    -
    -} - -@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) -{ -
    - @L["InvalidLoginRequest"] - @L["ThereAreNoLoginSchemesConfiguredForThisClient"] -
    -} -```` - -只需更改 `@model` 为 `Acme.BookStore.Web.Pages.Account.CustomLoginModel` 使用自定义的 `PageModel` 类. 你可以做任何应用程序需要的更改. - -## 本文的源代码 - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization)找到已完成的示例源码. - -## 另请参阅 - -* [ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南](../UI/AspNetCore/Customization-User-Interface.md). diff --git a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md deleted file mode 100644 index eea0364e6e..0000000000 --- a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md +++ /dev/null @@ -1,101 +0,0 @@ -# 如何为ABP应用程序定制SignIn Manager - -在使用[应用程序启动模板](../Startup-Templates/Application.md)创建新项目后,你可能想要扩展或更改SignIn Manager的默认行为,以满足你需要的身份验证和注册流程. ABP[账户模块](../Modules/Account.md)使用[身份管理模块](../Modules/Identity.md)做为SignIn Manager,而[身份管理模块](../Modules/Identity.md)使用默认的[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)([参阅此处]((https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17))). - -编写自定义SignIn Manager,你需要扩展[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)类并注入到DI容器. - -本文介绍了如何为你自己的应用程序自定义SignIn Manager. - -## 创建 CustomSignInManager - -创建一个类并继承自Microsoft Identity 包的 [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs). - -````csharp -public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager -{ - public CustomSignInManager( - Microsoft.AspNetCore.Identity.UserManager userManager, - Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, - Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory claimsFactory, - Microsoft.Extensions.Options.IOptions optionsAccessor, - Microsoft.Extensions.Logging.ILogger> logger, - Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, - Microsoft.AspNetCore.Identity.IUserConfirmation confirmation) - : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) - { - } -} -```` - -> 重点是使用**Volo.Abp.Identity.IdentityUser**做为泛型参数,而不是应用程序的AppUser. - -然后你可以覆盖SignIn Manager的任何方法并且为你的身份验证和注册流程添加需要的方法和属性. - -## 重写 GetExternalLoginInfoAsync 方法 - -在这个用例中我们重写第三方身份验证时使用的 `GetExternalLoginInfoAsync` 方法实现. - -一个好的开始是从复制[源码](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674)而不是从零开始. 在这个用例中我们对源码进行较少的修改,为了帮助理解概念它显式显示了方法和属性的命名空间. - -````csharp -public async override Task GetExternalLoginInfoAsync(string expectedXsrf = null) -{ - var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); - var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) - { - return null; - } - - if (expectedXsrf != null) - { - if (!items.ContainsKey(XsrfKey)) - { - return null; - } - var userId = items[XsrfKey] as string; - if (userId != expectedXsrf) - { - return null; - } - } - - var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - var provider = items[LoginProviderKey] as string; - if (providerKey == null || provider == null) - { - return null; - } - - var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName - ?? provider; - return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) - { - AuthenticationTokens = auth.Properties.GetTokens() - }; -} -```` - -要使你自定义的SignIn Manager类生效,你需要将其注册[依赖注入系统](../Dependency-Injection.md)中. - -## 注册到依赖注入 - -应该使用 [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs) 的 [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) 类的 **AddSignInManager** 扩展方法注册 `CustomSignInManager`. - -在你的 `.Web` 项目找到 `YourProjectNameWebModule` 的 `PreConfigureServices` 方法添加以下代码替换老的 `SignInManager`: - -````csharp -PreConfigure(identityBuilder => -{ - identityBuilder.AddSignInManager(); -}); -```` - -## 本文的源代码 - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization)找到已完成的示例源码. - -## 另请参阅 - -* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). -* [身份管理模块](../Modules/Identity.md). diff --git a/docs/zh-Hans/Index.md b/docs/zh-Hans/Index.md deleted file mode 100644 index b628418d76..0000000000 --- a/docs/zh-Hans/Index.md +++ /dev/null @@ -1,19 +0,0 @@ -# ABP 文档 - -ABP是一个**开源应用程序框架**,专注于基于ASP.NET Core的Web应用程序开发,但也支持开发其他类型的应用程序. - -浏览导航菜单以深入了解文档. - -## 入门 - -使用ABP开发新Web应用程序的最简单方法是使用[入门](Getting-Started.md)教程: - -然后你可以继续进行[Web应用程序开发教程](Tutorials/Part-1.md). - -## 源码 - -ABP托管在GitHub上, 参见[源代码](https://github.com/abpframework). - -## 贡献代码 - -ABP是一个社区驱动的开源项目.如果你想成为该项目的一部分,请参阅[贡献指南](Contribution/Index.md). diff --git a/docs/zh-Hans/JavaScript/Dynamic-JavaScript-API-Clients.md b/docs/zh-Hans/JavaScript/Dynamic-JavaScript-API-Clients.md deleted file mode 100644 index 539d3ac15a..0000000000 --- a/docs/zh-Hans/JavaScript/Dynamic-JavaScript-API-Clients.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic JavaScript API Clients - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/Local-Event-Bus.md b/docs/zh-Hans/Local-Event-Bus.md deleted file mode 100644 index ebdb3f20a6..0000000000 --- a/docs/zh-Hans/Local-Event-Bus.md +++ /dev/null @@ -1,224 +0,0 @@ -# 本地事件总线 - -本地事件总线允许服务发布和订阅**进程内事件**. 这意味着如果两个服务(发布者和订阅者)在同一个进程中运行,那么它是合适的. - -## 发布事件 - -以下介绍了两种发布本地事件的方法. - -### ILocalEventBus - -可以[注入](Dependency-Injection.md) `ILocalEventBus` 并且使用发布本地事件. - -**示例: 产品的存货数量发生变化时发布本地事件** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Local; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly ILocalEventBus _localEventBus; - - public MyService(ILocalEventBus localEventBus) - { - _localEventBus = localEventBus; - } - - public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) - { - //TODO: IMPLEMENT YOUR LOGIC... - - //PUBLISH THE EVENT - await _localEventBus.PublishAsync( - new StockCountChangedEvent - { - ProductId = productId, - NewCount = newCount - } - ); - } - } -} -```` - -`PublishAsync` 方法需要一个参数:事件对象,它负责保持与事件相关的数据,是一个简单的普通类: - -````csharp -using System; - -namespace AbpDemo -{ - public class StockCountChangedEvent - { - public Guid ProductId { get; set; } - - public int NewCount { get; set; } - } -} -```` - -即使你不需要传输任何数据也需要创建一个类(在这种情况下为空类). - -### 实体/聚合根类 - -[实体](Entities.md)不能通过依赖注入注入服务,但是在实体/聚合根类中发布本地事件是非常常见的. - -**示例: 在聚合根方法内发布本地事件** - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace AbpDemo -{ - public class Product : AggregateRoot - { - public string Name { get; set; } - - public int StockCount { get; private set; } - - private Product() { } - - public Product(Guid id, string name) - : base(id) - { - Name = name; - } - - public void ChangeStockCount(int newCount) - { - StockCount = newCount; - - //ADD an EVENT TO BE PUBLISHED - AddLocalEvent( - new StockCountChangedEvent - { - ProductId = Id, - NewCount = newCount - } - ); - } - } -} -```` - -`AggregateRoot` 类定义了 `AddLocalEvent` 来添加一个新的本地事件,事件在聚合根对象保存(创建,更新或删除)到数据库时发布. - -> 如果实体发布这样的事件,以可控的方式更改相关属性是一个好的实践,就像上面的示例一样 - `StockCount`只能由保证发布事件的 `ChangeStockCount` 方法来更改. - -#### IGeneratesDomainEvents 接口 - -实际上添加本地事件并不是 `AggregateRoot` 类独有的. 你可以为任何实体类实现 `IGeneratesDomainEvents`. 但是 `AggregateRoot` 默认实现了它简化你的工作. - -> 不建议为不是聚合根的实体实现此接口,因为它可能不适用于此类实体的某些数据库提供程序. 例如它适用于EF Core,但不适用于MongoDB. - -#### 它是如何实现的? - -调用 `AddLocalEvent` 不会立即发布事件. 当你将更改保存到数据库时发布该事件; - -* 对于 EF Core, 它在 `DbContext.SaveChanges` 中发布. -* 对于 MongoDB, 它在你调用仓储的 `InsertAsync`, `UpdateAsync` 或 `DeleteAsync` 方法时发由 (因为MongoDB没有更改跟踪系统). - -## 订阅事件 - -一个服务可以实现 `ILocalEventHandler` 来处理事件. - -**示例: 处理上面定义的`StockCountChangedEvent`** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus; - -namespace AbpDemo -{ - public class MyHandler - : ILocalEventHandler, - ITransientDependency - { - public async Task HandleEventAsync(StockCountChangedEvent eventData) - { - //TODO: your code that does somthing on the event - } - } -} -```` - -这就是全部,`MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEvent` 事件时调用 `HandleEventAsync`. - -* 事件可以由**0个或多个处理程序**订阅. -* 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `ILocalEventHandler` 接口. - -> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项. - -如果您执行**数据库操作**并在事件处理程序中使用[仓储](Repositories.md),那么您可能需要创建一个[工作单元](Unit-Of-Work.md),因为一些存储库方法需要在**活动的工作单元**中工作. 确保处理方法设置为 `virtual`,并为该方法添加一个 `[UnitOfWork]` attribute. 或者手动使用 `IUnitOfWorkManager` 创建一个工作单元范围. - -### 事务和异常行为 - -当一个事件发布,订阅的事件处理程序将立即执行.所以; - -* 如果处理程序**抛出一个异常**,它会影响发布该事件的代码. 这意味着它在 `PublishAsync` 调用上获得异常. 因此如果你想隐藏错误,在事件处理程序中**使用try-catch**. -*如果在一个[工作单元](Unit-Of-Work.md)范围内执行的事件发布的代码,该事件处理程序也由工作单元覆盖. 这意味着,如果你的UOW是事务和处理程序抛出一个异常,事务会回滚. - -## 预定义的事件 - -**发布实体创建,更新,删除事件**是常见的操作. ABP框架为所有的实体**自动**发布这些事件. 你只需要订阅相关的事件. - -**示例: 订阅用户创建事件** - -````csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities.Events; -using Volo.Abp.EventBus; - -namespace AbpDemo -{ - public class MyHandler - : ILocalEventHandler>, - ITransientDependency - { - public async Task HandleEventAsync( - EntityCreatedEventData eventData) - { - var userName = eventData.Entity.UserName; - var email = eventData.Entity.Email; - //... - } - } -} -```` - -这个类订阅 `EntityCreatedEventData`,它在用户创建后发布. 你可能需要向新用户发送一封"欢迎"电子邮件. - -有以下预定义的事件类型; - -* `EntityCreatedEventData` 当实体创建成功后发布. -* `EntityUpdatedEventData` 当实体更新成功后发布. -* `EntityDeletedEventData` 当实体删除成功后发布. -* `EntityChangedEventData` 当实体创建,更新,删除后发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件. - -### 它是如何实现的? - -在将更改保存到数据库时发布预构建事件; - -* 对于 EF Core, 他们在 `DbContext.SaveChanges` 发布. -* 对于 MongoDB, 在你调用仓储的 `InsertAsync`, `UpdateAsync` 或 `DeleteAsync` 方法发布(因为MongoDB没有更改追踪系统). - - -#### AbpEntityChangeOptions - -`AbpEntityChangeOptions`类中有一个`PublishEntityUpdatedEventWhenNavigationChanges`选项,默认值为`true`。 -如果将其设置为`false`,则当导航属性更改时,将不会发布`EntityUpdatedEventData`事件。 - -> 此选项仅用于EF Core - -## 另请参阅 - -* [分布式事件总线](Distributed-Event-Bus.md) \ No newline at end of file diff --git a/docs/zh-Hans/Localization.md b/docs/zh-Hans/Localization.md deleted file mode 100644 index 1292655e62..0000000000 --- a/docs/zh-Hans/Localization.md +++ /dev/null @@ -1,244 +0,0 @@ -## 本地化 - -ABP的本地化系统与`Microsoft.Extensions.Localization`无缝集成,并与[AspnetCore的本地化文档](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/localization?view=aspnetcore-2.1)兼容. 它添加了一些实用功能和增强功能, 使其更易于在实际开发中应用. - -### Volo.Abp.Localization Package - -> 启动模板默认已经安装了此nuget包, 所以在大多数情况下, 你不需要手动安装它. - -Volo.Abp.Localization是本地化系统的核心包. 使用程序包管理器控制台 (PMC) 将其安装到项目中: - -``` -Install-Package Volo.Abp.Localization -``` - -然后, 你可以将 **AbpLocalizationModule** 依赖项添加到模块: - -```c# -using Volo.Abp.Modularity; -using Volo.Abp.Localization; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpLocalizationModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -#### 创建本地化资源 - -本地化资源用于将相关的本地化字符串组合在一起,并将它们与应用程序的其他本地化字符串分开.通常一个模块会定义自己的本地化资源. 本地化资源就是一个普通的类. 例如: - -````C# -public class TestResource -{ -} -```` - -然后应该使用 `AbpLocalizationOptions` 添加如下所示: - -````C# -[DependsOn(typeof(AbpLocalizationModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - // "YourRootNameSpace" 是项目的根命名空间名字. 如果你的项目的根命名空间名字为空,则无需传递此参数. - options.FileSets.AddEmbedded("YourRootNameSpace"); - }); - - Configure(options => - { - options.Resources - .Add("en") - .AddVirtualJson("/Localization/Resources/Test"); - }); - } -} -```` - -在这个例子中; - -* 添加了一个新的本地化资源, 使用"en"(英语)作为默认的本地化. -* 用JSON文件存储本地化字符串. -* 使用[虚拟文件系统](Virtual-File-System.md) 将JSON文件嵌入到程序集中. - -JSON文件位于 "/Localization/Resources/Test" 项目文件夹下, 如下图所示: - -![localization-resource-json-files](images/localization-resource-json-files.png) - -本地化文件内容如下所示: - -````json -{ - "culture": "en", - "texts": { - "HelloWorld": "Hello World!" - } -} -```` - -* 每个本地化文件都需要定义 `culture` (文化) 代码 (例如 "en" 或 "en-US"). -* `texts` 部分只包含本地化字符串的键值集合 (键也可能有空格). - -### 默认资源 - -可以将 `AbpLocalizationOptions.DefaultResourceType` 设置为资源类型,在未指定本地化资源时使用: - -````csharp -Configure(options => -{ - options.DefaultResourceType = typeof(TestResource); -}); -```` - -> [启动模板]](Startup-Templates/Application.md) 设置 `DefaultResourceType` 为应用程序的本地化资源. - -请参阅下面的*客户端*部分获取用例 - -##### 简短的本地化资源名称 - -本地化资源也可以在客户端(JavaScript)使用. 因此, 为本地化资源设置一个简短的名称可以更方便的本地化文本. 例如: - -````C# -[LocalizationResourceName("Test")] -public class TestResource -{ -} -```` - -请参阅下面的获取本地化资源Test中客户端部分. - -##### 继承其他资源 - -资源可以从其他资源继承,这使得可以在不引用现有资源的情况下重用现有的本地化字符串. 例如: - -````C# -[InheritResource(typeof(AbpValidationResource))] -public class TestResource -{ -} -```` - -也可以通过 `AbpLocalizationOptions` 配置: - -````C# -services.Configure(options => -{ - options.Resources - .Add("en") //Define the resource by "en" default culture - .AddVirtualJson("/Localization/Resources/Test") //Add strings from virtual json files - .AddBaseTypes(typeof(AbpValidationResource)); //Inherit from an existing resource -}); -```` - -* 资源可以从多个资源继承. -* 如果新的本地化资源定义了相同的本地化字符串, 那么它会覆盖该字符串 - -##### 扩展现有资源 - -继承资源可以创建新的资源, 无需修改现有的资源. 但是在某些情况下, 你可能不想创建新资源,而是直接扩展现有资源. 例如: - -````C# -services.Configure(options => -{ - options.Resources - .Get() - .AddVirtualJson("/Localization/Resources/Test/Extensions"); -}); -```` - -* 如果扩展文件定义了相同的本地化字符串, 那么它会覆盖该字符串. - -#### 获取本地化文本 - -##### 服务器端 - -在服务端获取本地化文本的用法是非常标准的(它与AspNetCore提供的获取本地化资源方式无缝集成). - -###### 在类中简单的用法 - -````C# -public class MyService -{ - private readonly IStringLocalizer _localizer; - - public MyService(IStringLocalizer localizer) - { - _localizer = localizer; - } - - public void Foo() - { - var str = _localizer["HelloWorld"]; - } -} -```` - -##### 格式参数 - -格式参数可以在本地化Key参数后传递,如果你的消息是 `Hello {0}, welcome!`,可以将 `{0}` 传递给localizer,例如: `_localizer["HelloMessage", "John"]`. - -###### 在Razor视图/Page中简单的用法 - -````c# -@inject IHtmlLocalizer Localizer - -

    @Localizer["HelloWorld"]

    -```` - -有关在服务器端使用本地化的详细使用方法, 请参阅[AspNetCore的本地化文档](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/localization) - -##### 客户端 - -ABP提供了JavaScript服务, 可以在客户端使用相同的本地化文本. - -#### getResource - -`abp.localization.getResource` 函数用于获取本地化资源: - -````js -var testResource = abp.localization.getResource('Test'); -```` - -本地化字符串: - -````js -var str = testResource('HelloWorld'); -```` - -#### 本地化 - -`abp.localization.localize` 函数用于获取本地化文本,你可以传递本地化Key和资源名称: - -````js -var str = abp.localization.localize('HelloWorld', 'Test'); -```` - -`HelloWorld` 是本地化文本的Key, `Test` 是本地化资源的名称. - -如果未指定本地化资源名称,它使用 `AbpLocalizationOptions` 中定义的默认本地化资源(参见上面的*默认资源*部分). 例: - -````js -var str = abp.localization.localize('HelloWorld'); //uses the default resource -```` - -##### 格式参数 - -如果本地化字符串包含参数, 例如 `Hello {0}, welcome!`. 你可以将参数传递给本地化方法. 例: - -````js -var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John'); -var str2 = abp.localization.localize('HelloWorld', 'Test', 'John'); -```` - -上面的两个示例都会输出 `Hello John, welcome!`. - -## 另请参阅 - -* [Angular UI中的本地化](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/Logging.md b/docs/zh-Hans/Logging.md deleted file mode 100644 index 247609fb41..0000000000 --- a/docs/zh-Hans/Logging.md +++ /dev/null @@ -1,5 +0,0 @@ -# 日志 - -ABP框架没有实现任何日志基础设施. 它使用[ASP.NET Core日志系统](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging). - -> .NET Core 的日志系统是独立于ASP.NET Core的,它可以在任何类型的应用程序中使用. \ No newline at end of file diff --git a/docs/zh-Hans/MailKit.md b/docs/zh-Hans/MailKit.md deleted file mode 100644 index 0adb88c85f..0000000000 --- a/docs/zh-Hans/MailKit.md +++ /dev/null @@ -1,48 +0,0 @@ -# MailKit 集成 - -[MailKit](http://www.mimekit.net/) 是一个用于 .net 的跨平台、流行的开源邮件客户端库.ABP 框架提供了一个集成包来使用 MailKit 作为[邮件发送系统](Emailing.md)的收发组件. - -## 安装 - -建议使用 [ABP CLI](CLI.md) 安装包.在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令: - -````bash -abp add-package Volo.Abp.MailKit -```` - -如果执行失败,你首先需要安装 ABP CLI.有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.MailKit). - -## 发送电子邮件 - -### IEmailSender - -[注入](Dependency-Injection.md) 标准的 `IEmailSender` 到任何服务并使用 `SendAsync` 方法发送电子邮件.详见 [邮件发送文档](Emailing.md). - -> `IEmailSender` 是建议的发送邮件的方式,即使你使用MailKit,因为它使你的代码独立. - -### IMailKitSmtpEmailSender - -`BuildClientAsync()` 方法扩展了`IEmailSender`.此方法可用于获取可用于执行 MailKit 特定操作的`MailKit.Net.Smtp.SmtpClient`对象. - -## 配置 - -MailKit 集成包使用电子邮件发送系统相同配置选项.请参阅[电子邮件发送文档](Emailing.md) 进行配置. - -除了标准设置之外,这个包还定义了 `AbpMailKitOptions` 作为一个简单的 [选项](Options.md) 类.此类仅定义一个选项: - -* **SecureSocketOption**:用于设置“SecureSocketOptions” . Default:`null`(使用默认值) - -**示例: 使用 *SecureSocketOptions.SslOnConnect*** - -````csharp -Configure(options => -{ - options.SecureSocketOption = SecureSocketOptions.SslOnConnect; -}); -```` - -请参阅 [MailKit 文档](http://www.mimekit.net/) 了解更多信息. - -## 也可以看看 - -* [电子邮件发送系统](Emailing.md) diff --git a/docs/zh-Hans/Microservice-Architecture.md b/docs/zh-Hans/Microservice-Architecture.md deleted file mode 100644 index 856cfee034..0000000000 --- a/docs/zh-Hans/Microservice-Architecture.md +++ /dev/null @@ -1,30 +0,0 @@ -# 微服务架构 - -*"作为**面向服务架构**(SOA)的一个变体,微服务是一种将应用程序分解成**松散耦合服务**的新型架构风格. 通过**细粒度**的服务和**轻量级**的协议,微服务提供了更多的**模块化**,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够**开发,部署和扩展**各自的服务,实现开发的**并行化**.它还允许通过**连续重构**形成单个服务的架构. 基于微服务架构可以实现**持续交付和部署**."* - -— [维基百科](https://zh.wikipedia.org/wiki/Microservices) - -## 介绍 - -ABP框架的主要目标之一就是提供**便捷的基础设施来创建微服务解决方案**. 我们做了以下工作: - -* 提供[模块系统](Module-Development-Basics.md),允许将应用程序拆分为模块,其中每个模块可以拥有自己的数据库,实体,服务,API,UI组件/页面....等. -* 提供[架构模型](Best-Practices/Module-Architecture.md)来开发模块,与微服务开发和部署兼容. -* 提供[最佳实践指南](Best-Practices/Index.md)制定模块开发标准. -* 提供基础设施来实现微服务中的[领域驱动设计](Domain-Driven-Design.md). -* 提供从应用程序服务[自动生成REST风格的API](API/Auto-API-Controllers.md)的服务. -* 提供[自动创建C#API客户端](API/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. -* 提供[分布式事件总线](Event-Bus.md)用于服务通信. -* 提供更多其他服务,使日常开发更加简便. - -## 在新应用程序中使用微服务 - -开始一个新解决方案建议**始终从单体开始**, 保持模块化,在单体成为问题时将其拆分为微服务.这使初期进度会很快,特别是如果你的团队人数不多,并且不想处理微服务架构带来的各种挑战. - -然而开发一个良好的模块化应用程序不是那么简单,因为很难像微服务那样**保持模块之间的隔离** (参阅 [Stefan Tilkov的文章](https://martinfowler.com/articles/dont-start-monolith.html)). 微服务架构会自然的让你开发隔离的服务,但是在模块化的单体应用程序中,模块很容易彼此紧密耦合并设计出**弱模块边界**和API约定. - -ABP可以帮助你,它提供了与**与微服务兼容的严格模块架构** 在这个架构中你的模块被分割成多个层/项目,在自己的VS解决方案中进行开发,该解决方案完全独立于其它模块. 这种方式开发的模块是一种天然的微服务,但是它可以很容易的插入到单体应用程序中. 请参阅**微服务优先的模块设计**的[模块开发最佳实践指南](Best-Practices/Index.md). 所有[标准的ABP模块](https://github.com/abpframework/abp/tree/master/modules)都是基于本指南开发的. 因此你可以将这些模块嵌入到单体解决方案中使用它们,也可以单独部署通过远程API调用. 它们可以共享一个数据库,也可以通过简单配置使用自己的数据库. - -## 微服务解决方案示例 - -[微服务解决方案示例](Samples/Microservice-Demo.md)演示了基于ABP框架的完整的微服务的解决方案. \ No newline at end of file diff --git a/docs/zh-Hans/Module-Development-Basics.md b/docs/zh-Hans/Module-Development-Basics.md deleted file mode 100644 index 9cdc56ee9f..0000000000 --- a/docs/zh-Hans/Module-Development-Basics.md +++ /dev/null @@ -1,134 +0,0 @@ -## 模块化 - -### 介绍 - -ABP本身是一个包含许多nuget包的模块化框架.它还提供了一个完整的基础架构来开发你自己的具有实体, 服务, 数据库集成, API, UI组件等等功能的应用程序模块. - -### 模块类 - -每个模块都应该定义一个模块类.定义模块类的最简单方法是创建一个派生自``AbpModule``的类,如下所示: - -````C# -public class BlogModule : AbpModule -{ - -} - -```` - -#### 配置依赖注入和其他模块 - -##### ConfigureServices方法 - -``ConfigureServices``是将你的服务添加到依赖注入系统并配置其他模块的主要方法.例: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //... - } -} -```` - -你可以按照Microsoft的[文档](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection)中的说明逐个注册依赖项.但ABP有一个**依照约定的依赖注册系统**,可以自动注册程序集中的所有服务.有关依赖项注入系统的更多信息,请参阅[依赖项注入](Dependency-Injection.md)文档. - -你也可以通过这种方式配置其他服务和模块.例: - -````C# -public class BlogModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //为应用程序配置默认的连接字符串 - Configure(options => - { - options.ConnectionStrings.Default = "......"; - }); - } -} -```` -有关配置系统的更多信息,请参阅[配置](Configuration.md)文档. - -##### 配置服务前和后 - -``AbpModule``类还定义了``PreConfigureServices``和``PostConfigureServices``方法用来在``ConfigureServices``之前或之后覆盖和编写你的代码.请注意,在这些方法中编写的代码将在所有其他模块的``ConfigureServices``方法之前/之后执行. - -#### 应用程序初始化 - -一旦配置了所有模块的所有服务,应用程序就会通过初始化所有模块来启动.在此阶段,你可以从``IServiceProvider``中获取服务,因为这时它已准备就绪且可用. - -##### OnApplicationInitialization方法 - -你可以在启动应用程序时覆盖``OnApplicationInitialization``方法来执行代码.例: - -````C# -public class BlogModule : AbpModule -{ - //... - - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var myService = context.ServiceProvider.GetService(); - myService.DoSomething(); - } -} -```` - -``OnApplicationInitialization``通常由启动模块用于构建ASP.NET Core应用程序的中间件管道.例: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -public class AppModule : AbpModule -{ - //... - - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMvcWithDefaultRoute(); - } -} -```` - -如果模块需要,你还可以执行启动逻辑 - -##### 应用程序初始化前和后 - -``AbpModule``类还定义了``OnPreApplicationInitialization``和``OnPostApplicationInitialization``方法用来在``OnApplicationInitialization``之前或之后覆盖和编写你的代码.请注意,在这些方法中编写的代码将在所有其他模块的``OnApplicationInitialization``方法之前/之后执行. - -#### 应用程序关闭 - -最后,如果要在应用程序关闭时执行某些代码,你可以覆盖``OnApplicationShutdown``方法. - -### 模块依赖 - -在模块化应用程序中,一个模块依赖于另一个或几个模块并不罕见.如果一个Abp模块依赖于另一个模块,它必须声明``[DependsOn]``特性,如下所示: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] -public class BlogModule -{ - //... -} -```` - -你可以根据需要使用多个``DependsOn``特性或将多个模块类型传递给单个``DependsOn``特性. - -依赖模块可能依赖于另一个模块,但你只需要定义直接依赖项.ABP在启动时会调查应用程序的依赖关系,并以正确的顺序初始化/关闭模块. - -## 框架模块 vs 应用程序模块 - -**模块分为两种类型.** 这两种类型并没有任何结构上的区别,只是按功能和用途分类: - -- **框架模块**: 这些是**框架的核心模块** 如缓存, 邮件, 主题, 安全, 序列化, 验证, EF Core集成, MongoDB集成... 等. 它们没有应用/业务功能,它们提供了日常开发经常用到的通用基础设施,集成和抽象. -- **应用程序模块**: 这些模块实现了 **特定的应用/业务功能** 像博客, 文档管理, 身份管理, 租户管理... 等等. 它们通常有自己的实体,服务,API和UI组件. 请参阅 [预构建的应用程序模块](Modules/Index.md). diff --git a/docs/zh-Hans/Modules/Account.md b/docs/zh-Hans/Modules/Account.md deleted file mode 100644 index 336e939c37..0000000000 --- a/docs/zh-Hans/Modules/Account.md +++ /dev/null @@ -1,36 +0,0 @@ -# 账户模块 - -该模块提供必要的UI页面与组件使用户登录和注册到应用程序. - -> 文档正在更新 - -## 社交/外部登录 - -### 示例: Facebook 认证 - -按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. - -#### 添加NuGet包 - -添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.AuthServer`(对于分层启动)或 `.Host` 项目. - -#### 配置提供程序 - -在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: - -````csharp -context.Services.AddAuthentication() - .AddFacebook(facebook => - { - facebook.AppId = "..."; - facebook.AppSecret = "..."; - facebook.Scope.Add("email"); - facebook.Scope.Add("public_profile"); - }); -```` - -> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. - -### Angular UI - -从v3.1开始,Angular UI使用授权码流程(作为最佳实践)通过重定向到MVC UI登录页面来对用户进行身份验证. 因此,即使你使用的是Angular UI,社交/外部登录集成也与上面说明的相同.并且可以开箱即用. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Audit-Logging.md b/docs/zh-Hans/Modules/Audit-Logging.md deleted file mode 100644 index 0748b27160..0000000000 --- a/docs/zh-Hans/Modules/Audit-Logging.md +++ /dev/null @@ -1,7 +0,0 @@ -# 审计日志模块 - -审计日志模块实现了 `IAuditingStore` 将审计日志对象保存到数据库中. - -> [启动模板](../Startup-Templates/Index.md)已经安装并配置了审计日志模块,所以你不需要手动安装到你的应用程序. - -参阅[审计日志系统](../Audit-Logging.md)文档了解更多关于审计日志的内容. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Background-Jobs.md b/docs/zh-Hans/Modules/Background-Jobs.md deleted file mode 100644 index fe021f1a56..0000000000 --- a/docs/zh-Hans/Modules/Background-Jobs.md +++ /dev/null @@ -1,55 +0,0 @@ -# 后台作业模块 - -后台作业模块实现了 `IBackgroundJobStore` 接口,并且可以使用ABP框架的默认后台作业管理.如果你不想使用这个模块,那么你需要自己实现 `IBackgroundJobStore` 接口. - -> 本文档仅介绍后台作业模块,该模块将后台作业持久化到数据库.有关后台作业系统的更多信息,请参阅[后台作业](../Background-Jobs.md)文档. - -## 如何使用 - -当你使用ABP框架[创建一个新的解决方案](https://abp.io/get-started)时,这个模块是(作为NuGet/NPM包)预先安装的.你可以继续将其作为软件包使用并轻松获取更新,也可以将其源代码包含到解决方案中(请参阅 `get-source` [CLI](../CLI.md)命令)以开发自定义模块. - -### 源代码 - -此模块的源代码可在[此处](https://github.com/abpframework/abp/tree/dev/modules/background-jobs)访问.源代码是由[MIT](https://choosealicense.com/licenses/mit/)授权的,所以你可以自由使用和定制它. - -## 内部结构 - -### 领域层 - -#### 聚合 - -- `BackgroundJobRecord` (聚合根): 表示后台工作记录. - -#### 仓储 - -为该模块定义了以下自定义仓储: - -- `IBackgroundJobRepository` - -### 数据库提供程序 - -#### 通用 - -##### 表/集合的前缀与架构 - -默认情况下,所有表/集合都使用 `Abp` 前缀.如果需要更改表前缀或设置架构名称(如果数据库提供程序支持),请在 `BackgroundJobsDbProperties` 类上设置静态属性. - -##### 连接字符串 - -此模块使用 `AbpBackgroundJobs` 作为连接字符串名称.如果不使用此名称定义连接字符串,它将返回 `Default` 连接字符串.有关详细信息,请参阅[连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings)文档. - -#### Entity Framework Core - -##### 表 - -- **AbpBackgroundJobs** - -#### MongoDB - -##### 集合 - -- **AbpBackgroundJobs** - -## 另请参阅 - -* [后台作业系统](../Background-Jobs.md) diff --git a/docs/zh-Hans/Modules/Cms-Kit/Blogging.md b/docs/zh-Hans/Modules/Cms-Kit/Blogging.md deleted file mode 100644 index 1d2c3ecd9e..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Blogging.md +++ /dev/null @@ -1,3 +0,0 @@ -# CMS Kit: Blogging - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Comments.md b/docs/zh-Hans/Modules/Cms-Kit/Comments.md deleted file mode 100644 index faeb6116c9..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Comments.md +++ /dev/null @@ -1,3 +0,0 @@ -# CMS Kit: Comments - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Dynamic-Widget.md b/docs/zh-Hans/Modules/Cms-Kit/Dynamic-Widget.md deleted file mode 100644 index 14e861b204..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Dynamic-Widget.md +++ /dev/null @@ -1,47 +0,0 @@ -# 动态部件 - -CMS kit提供了组件系统在页面和博客文章和生成动态部件. 这是一个在 `Page` 端的示例投票部件截图 - -> 重要提示: 投票部件是ABP Commercial实现的唯一部件 -![cmskit-example-output-on-page](../../images/cmskit-example-output-on-page.png) - -> 你也可以对其他小部件执行相同的操作.这只是一个例子. -要添加部件,你应该去页面或博客创建或更新, 然后单击 `W` 按钮添加一个动态部件, 如下图所示. 不要忘了这是设计模态框,你需要在保存后查看你的页面. 此外 `预览` 选项卡可以轻松的查看部件配置的部件输出. - -![cmskit-add-widget-on-page](../../images/cmskit-add-widget-on-page.png) - -在这张图中, 选择投票后(在其他情况下,它根据你的配置自动改变, 这里是投票组件,它的参数名是 `editorWidgetName`),你看到下一个部件, 输入值或选择值或选择并单击 `添加`. 你将看到以下输出 - -> [Widget Type="Poll" Code="SelectedValue"] -如果编码有任何错误(错误的值或拼写错误)你可以手动修改输出. - -## 选项 - -使添加的部件工作,你必须在模块类中进行配置: - -```csharp -Configure(options => -{ - options.AddWidget("widgetKey", "widgetName", "editorWidgetName"); -}); -``` - -* `widgetKey` 用于最终用户更具有可读性的名称. - [Widget Type="**Poll**" Code="SelectedValue"] -* `widgetName` 用于代码中通过 `[widget]` Attribute使用的小部件名称 - -```csharp -[Widget] -public class WidgetNameViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View(); - } -} - -``` - -* `editorWidgetName` 用于编辑器组件端,在 `添加部件` 模态框中查看. - -另请参阅[部件](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Widgets). \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Index.md b/docs/zh-Hans/Modules/Cms-Kit/Index.md deleted file mode 100644 index 2409a98156..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Index.md +++ /dev/null @@ -1,80 +0,0 @@ -# 内容管理系统套件模块 - -此模块为您的应用程序提供内容管理系统 (Content Management System, CMS) 功能. 它提供 **核心构建块** 和完整工作的 **子系统**, 以创建启用 CMS 功能的您自己的网站, 或出于任何目的使用网站中的构建块. - -> **此模块目前仅适用于 MVC / Razor 页面 UI**. 虽然没有官方的 Blazor 软件包, 但它也可以在 Blazor 服务器 UI 中工作, 因为实际上 Blazor 服务器 UI 实际上是一个运行在 ASP.NET Core MVC / Razor 页面应用程序的混合型应用程序. - -目前提供以下功能: - -* 提供 [**页面**](Pages.md) 管理系统来管理具有动态 URL 的动态页面. -* 提供 [**博客**](Blogging.md) 系统来创建发表具有多种博客支持的博客文章. -* 提供 [**标签**](Tags.md) 系统来标记任何资源, 如博客文章. -* 提供 [**评论**](Comments.md) 系统来添加对任何资源的评论功能, 如博客文章或产品评价页面. -* 提供 [**反应**](Reactions.md) 系统来添加对任何资源的反应 (表情符号) 功能, 如博客文章或评论. -* 提供 [**评级**](Ratings.md) 系统来添加对任何资源的评级功能. -* 提供 [**菜单**](Menus.md) 系统来动态管理公共菜单. -* 提供 [**动态部件**](Dynamic-Widget.md) 系统在页面和博客文章中创建动态部件. - -点击功能以了解和学习如何去使用它. - -所有功能均可单独使用. 如果你禁用了一个功能, 则在 [全局功能](../../Global-Features.md) 系统的帮助下, 该功能会从你的应用程序甚至数据库表中完全消失. - -## 预备要求 -- 此模块依赖于 [Blob 存储](../../Blob-Storing.md) 模块来保存媒体内容. -> 确保 `BlobStoring` 模块已安装并至少正确地配置了一个提供程序. 请查阅 [文档](../../Blob-Storing.md) 了解更多信息. - -- CMS Kit 使用 [分布式缓存](../../Caching.md) 来提高响应速度. -> 强烈建议在分布式/集群部署中为实现数据一致性使用分布式缓存, 如 [Redis](../../Redis-Cache.md). - -## 如何安装 - -可以使用 [ABP CLI](../../CLI.md) 的 `add-module` 命令为解决方案安装模块. 您可以使用以下命令在命令行中安装 CMS Kit 模块: - -```bash -abp add-module Volo.CmsKit -``` - -> 默认情况下, Cms-Kit `GlobalFeature` 被禁用. 因此初始迁移将为空. 所以, 当你使用 EF Core 安装时,你可以添加 `--skip-db-migrations` 命令来跳过迁移. 启用 Cms-Kit 全局功能后, 请添加新的迁移. - -安装过程完成后, 在您的解决方案 `Domain.Shared` 项目中打开 `GlobalFeatureConfigurator` 类, 并将以下代码写入 `Configure` 方法中, 以启用 CMS Kit 模块的全部功能. - -```csharp -GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => -{ - cmsKit.EnableAll(); -}); -``` - -你可能更愿意逐个启用这些功能, 而不是启用全部功能. 以下示例仅启用了 [标签](Tags.md) 和 [评论](Comments.md) 功能: - -````csharp -GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => -{ - cmsKit.Tags.Enable(); - cmsKit.Comments.Enable(); -}); -```` - -> 如果你使用 EF Core, 不要忘记添加一个新的迁移并更新你的数据库. - -## 软件包 - -此模块遵循 [模块开发最佳实践指南](https://docs.abp.io/zh-Hans/abp/latest/Best-Practices/Index), 由多个 NuGet 和 NPM 软件包组成. 如果你想了解软件包及其之间的关系, 请参阅指南. - -CMS Kit 软件包专为各种使用场景而设计. 如果您查阅了 [CMS Kit 软件包](https://www.nuget.org/packages?q=Volo.CmsKit) 您将看到一些有 `Admin` 和 `Public` 后缀的软件包. 该模块有两个应用程序层, 原因是他们可能被用于不同类型的应用程序. 这些应用程序层仅使用一个领域层. - - - `Volo.CmsKit.Admin.*` 软件包包括管理员 (后台) 应用程序所必须的功能. - - `Volo.CmsKit.Public.*` 软件包包括被用于用户阅读博客文章和发表评论的公共网站上的功能. - - `Volo.CmsKit.*` (不带 Admin/Public 后缀) 软件包称为统一包. 统一包分别是添加 Admin 和 Public (相关层的) 软件包的快照. 如果您有一个用于管理和公共网站的单应用程序, 您可以使用这些软件包. - -## 内部结构 - -### 表/集合 前缀&架构 - -所有表/集合使用 `Cms` 作为默认前缀. 如果需要更改表的前缀或者设置一个架构名称 (如果你的数据库提供程序支持), 请在 `CmsKitDbProperties` 类中设置静态属性. - -### 连接字符串 - -此模块使用 `CmsKit` 作为连接字符串的名称. 如果您未使用此名称定义连接字符串, 它将回退为 `Default` 连接字符串. - -有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings) 文档. diff --git a/docs/zh-Hans/Modules/Cms-Kit/Menus.md b/docs/zh-Hans/Modules/Cms-Kit/Menus.md deleted file mode 100644 index fb214e358c..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Menus.md +++ /dev/null @@ -1,3 +0,0 @@ -# CMS Kit: Pages - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Pages.md b/docs/zh-Hans/Modules/Cms-Kit/Pages.md deleted file mode 100644 index fb214e358c..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Pages.md +++ /dev/null @@ -1,3 +0,0 @@ -# CMS Kit: Pages - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Ratings.md b/docs/zh-Hans/Modules/Cms-Kit/Ratings.md deleted file mode 100644 index 2c6cf44460..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Ratings.md +++ /dev/null @@ -1,3 +0,0 @@ -# Rating System - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Reactions.md b/docs/zh-Hans/Modules/Cms-Kit/Reactions.md deleted file mode 100644 index 9b714adf26..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Reactions.md +++ /dev/null @@ -1,3 +0,0 @@ -# Reaction System - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit/Tags.md b/docs/zh-Hans/Modules/Cms-Kit/Tags.md deleted file mode 100644 index 3977d06f5a..0000000000 --- a/docs/zh-Hans/Modules/Cms-Kit/Tags.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tag Management - -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md deleted file mode 100644 index 96a4937e53..0000000000 --- a/docs/zh-Hans/Modules/Docs.md +++ /dev/null @@ -1,626 +0,0 @@ -# 文档模块 - -## 什么是文档模块? - -文档模块是ABP框架的一个应用程序模块. 它简化了软件文档的制作. 这个模块是开源免费的. - -### 集成 - -目前文档模块提供提供了两种支持的存储,Github与文件系统. - -### 托管 - -文档模块是一个应用程序模块,不提供任何托管的解决方案,你可以在本地或云上托管文档. - -### 版本 - -当你使用GitHub存储文档时,文档模块支持多版本. 如果你的文档具有多个版本, UI上有一个组合框,用于切换版本. 如果你选择使用文件系统存储文档, 那么它不支持多版本. - -ABP框架的[文档](docs.abp.io)也是使用的此模块. - -> 文档模块遵循 [模块化架构最佳实践](../Best-Practices/Module-Architecture.md) 指南. - -## 安装 - -### 1- 下载 - -如果你没有现有的ABP项目, 这个步骤向你展示如何在[abp.io](https://abp.io)创建一个新项目并添加文档模块. 如果你本地已经有了一个ABP项目, 那么你可以跳过这一步. - -推荐使用ABP CLI创建新项目,使用以下命令行: - -`abp new Acme.MyProject` - -你也可以在浏览器中导航到 https://abp.io/get-started. 输入项目名称为 `Acme.MyProject`, 其它保持默认选项. - -请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. - -![创建新项目](../images/docs-module_download-new-abp-project.png) - -### 2- 运行这个空项目 - -下载项目后, 解压压缩文档并且打开 `Acme.MyProject.sln`. 你可以看到这个解决方案包含了 `Application`, `Application.Contrawcts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `EntityFrameworkCore.DbMigations`, `HttpApi`, `HttpApi.Client` 和 `Web` 项目. 右键选择 `Acme.MyProject.Web` 项目**设置为启动项目**. - -![创建新项目](../images/docs-module_solution-explorer.png) - -数据库连接字符串位于`Acme.MyProject.Web`项目的`appsettings.json`中. 如果你有不同的数据库配置, 可以修改这个连接字符串. - -```json -{ - "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" - } -} -``` - -运行 `Acme.MyProject.DbMigrator` 项目,它会负责应用迁移与初始化种子数据. 数据库`MyProject`将在数据库服务器中创建. - -现在一个空的ABP项目已经创建完成! 现在你可以运行项目并且查看网站. - -输入用户名 `admin` 密码 `1q2w3E*` 登录到网站. - -### 3- 安装模块 - -文档模块包托管在Nuget上面. 需要有四个包安装到你的应用程序中. 每个包必须安装到相关的项目. - -#### 3.1- 使用ABP CLI - -建议使用ABP CLI安装模块,在解决方案文件 (`.sln`) 目录打开 `CMD` 窗口,运行以下命令: - -`abp add-module Volo.Docs` - -#### 3.2- 手动安装 - -或者你也可以手动安装nuget包到每个项目: - -* 安装[Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Domain` 项目. - - `Install-Package Volo.Docs.Domain` - -* 安装[Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget包到 `Acme.MyProject.EntityFrameworkCore` 项目. - - `Install-Package Volo.Docs.EntityFrameworkCore` - -* 安装[Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget包到 `Acme.MyProject.Application` 项目. - - `Install-Package Volo.Docs.Application` - -* 安装[Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Web` 项目. - `Install-Package Volo.Docs.Web` - -##### 3.2.1- 添加模块依赖 - -一个ABP模块必须声明 `[DependsOn]` attribute 如果它依赖于另一个模块. 每个模块都必须在相关的项目的`[DependsOn]`Attribute 中添加. - -* 打开 `MyProjectDomainModule.cs`并且添加 `typeof(DocsDomainModule)` 如下所示; - - ```csharp - [DependsOn( - typeof(DocsDomainModule), - typeof(AbpIdentityDomainModule), - typeof(AbpAuditingModule), - typeof(BackgroundJobsDomainModule), - typeof(AbpAuditLoggingDomainModule) - )] - public class MyProjectDomainModule : AbpModule - { - //... - } - ``` - -* 打开 `MyProjectEntityFrameworkCoreModule.cs`并且添加 `typeof(DocsEntityFrameworkCoreModule)` 如下所示; - - ```csharp - [DependsOn( - typeof(DocsEntityFrameworkCoreModule), - typeof(MyProjectDomainModule), - typeof(AbpIdentityEntityFrameworkCoreModule), - typeof(AbpPermissionManagementEntityFrameworkCoreModule), - typeof(AbpSettingManagementEntityFrameworkCoreModule), - typeof(AbpEntityFrameworkCoreSqlServerModule), - typeof(BackgroundJobsEntityFrameworkCoreModule), - typeof(AbpAuditLoggingEntityFrameworkCoreModule) - )] - public class MyProjectEntityFrameworkCoreModule : AbpModule - { - //... - } - ``` - -* 打开 `MyProjectApplicationModule.cs`并且添加 `typeof(DocsApplicationModule)` 如下所示; - - ```csharp - [DependsOn( - typeof(DocsApplicationModule), - typeof(MyProjectDomainModule), - typeof(AbpIdentityApplicationModule))] - public class MyProjectApplicationModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.DefinitionProviders.Add(); - }); - - Configure(options => - { - options.AddProfile(); - }); - } - } - ``` - -* 打开 `MyProjectWebModule.cs`并且添加 `typeof(DocsWebModule)` 如下所示; - - ```csharp - [DependsOn( - typeof(DocsWebModule), - typeof(MyProjectApplicationModule), - typeof(MyProjectEntityFrameworkCoreModule), - typeof(AbpAutofacModule), - typeof(AbpIdentityWebModule), - typeof(AbpAccountWebModule), - typeof(AbpAspNetCoreMvcUiBasicThemeModule) - )] - public class MyProjectWebModule : AbpModule - { - //... - } - ``` - -##### 3.2.2- 添加NPM包 - -打开 `package.json` 添加 `@abp/docs` 如下所示: - - ```json - { - "version": "1.0.0", - "name": "my-app", - "private": true, - "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^5.0.0", - "@abp/docs": "^5.0.0" - } - } - ``` - - 然后在 `Acme.MyProject.Web` 项目目录打开命令行终端运行以下命令: - -````bash -abp install-libs -```` - -### 4- 数据库集成 - -#### 4.1- Entity Framework 集成 - -如果你选择了Entity Framework 做为数据库供应者,你需要配置文档模块. 做以下操作; - -* 打开 `MyProjectMigrationsDbContext.cs` 并且添加 `builder.ConfigureDocs()` 到 `OnModelCreating()` 方法中 - - ```csharp - public class MyProjectMigrationsDbContext : AbpDbContext - { - public MyProjectMigrationsDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - builder.ConfigureSettingManagement(); - builder.ConfigureBackgroundJobs(); - builder.ConfigureAuditLogging(); - builder.ConfigureIdentity(); - builder.ConfigureIdentityServer(); - builder.ConfigureFeatureManagement(); - builder.ConfigureTenantManagement(); - builder.ConfigureDocs(); //Add this line to configure the Docs Module - - /* Configure customizations for entities from the modules included */ - - builder.Entity(b => - { - b.ConfigureCustomUserProperties(); - }); - - /* Configure your own tables/entities inside the ConfigureQaDoc method */ - - builder.ConfigureMyProject(); - } - } - ``` - -* 打开 `Visual Studio` 的 `包管理控制台` 选择 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 做为默认项目. 然后编写以下命令为文档模块添加迁移. - - ```csharp - add-migration Added_Docs_Module - ``` - - 当命令执行成功后 , 你会看到`Acme.MyProject.EntityFrameworkCore.DbMigrations\Migrations` 目录下有名为 `20181221111621_Added_Docs_Module` 的迁移文件. - - 现在更新数据库. 在 `Visual Studio` 的 `包管理控制台` 中执行以下代码. 要确认已 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 项目设置为默认项目. - - ```csharp - update-database - ``` - - 最后你可以查看数据库中创建的新表,例如你可以看到 `DocsProjects` 表已经添加到数据库中. - -### 5- 链接文档模块 - -文档模块的默认路由是; - -``` -/Documents -``` - -添加文档模块的链接到你的应用程序菜单中; - -* 打开 `MyProjectMenuContributor.cs` 并且在 `ConfigureMainMenuAsync()` 方法方法中添加以下代码. - - ```csharp - context.Menu.Items.Add(new ApplicationMenuItem("MyProject.Docs", l["Menu:Docs"], "/Documents")); - ``` - - 最后 **MyProjectMenuContributor.cs** 有以下内容 - - ```csharp - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - var l = context.ServiceProvider.GetRequiredService>(); - - context.Menu.Items.Insert(0, new ApplicationMenuItem("MyProject.Home", l["Menu:Home"], "/")); - - context.Menu.Items.Add(new ApplicationMenuItem("MyProject.Docs", l["Menu:Docs"], "/Documents")); - } - ``` - -`Menu:Docs` 关键词是本地化的Key. 要本地化菜单文本, 打开`Acme.MyProject.Domain` 中的 `Localization\MyProject\zh-Hans.json`. 添加以下行. - -```json -"Menu:Docs": "文档" -``` - -最后 **zh-Hans.json** 有以下内容 - -```json -{ - "culture": "zh-Hans", - "texts": { - "Menu:Home": "首页", - "Welcome": "欢迎", - "LongWelcomeMessage": "欢迎来到该应用程序. 这是一个基于ABP框架的启动项目. 有关更多信息, 请访问 abp.io.", - "Menu:Docs": "文档" - } -} -``` - -现在菜单中已经添加了文档模块项. 运行Web应用程序并且在浏览器中打开 `http://localhost:YOUR_PORT_NUMBER/documents` URL. - -你会看到一个警告; - -``` -There are no projects yet! -``` - -这个警告是正常的,因为我们还没有添加任何项目. - -### 6- 添加文档项目 - -在数据库中打开 `DocsProjects`, 并且插入包含以下字段的新记录; - -* **Name**: 在Web页面上文档的显示名称. -* **ShortName**: 在文档URL中使用的友好的简短URL名称. -* **Format**: 文档的格式 ( Markdown: `md`, HTML: `html`) -* **DefaultDocumentName**: 文档的初始页面. -* **NavigationDocumentName**: 导航菜单(索引)的文档. -* **MinimumVersion**: 显示文档的最低版本. 低于此的版本不会列出. -* **DocumentStoreType**: 文档的来源 ( GitHub:`GitHub`,文件系统`FileSystem`). -* **ExtraProperties**: 序列化的`JSON`, 它存储所选 `DocumentStoreType` 的特殊配置. -* **MainWebsiteUrl**: 用户单击文档模块页面Logo时跳转的URL.你只需设置为`/`即可链接到网站根地址. -* **LatestVersionBranchName**: 这是GitHub的配置.它是检索文档的分支名称.你可以将其设置为`master`. - -#### "GitHub" 项目的示例记录 - -你可以使用 [ABP Framework](https://github.com/abpframework/abp/) GitHub文档来配置Github文档存储. - -- Name: `ABP framework (GitHub)` - -- ShortName: `abp` - -- Format: `md` - -- DefaultDocumentName: `Index` - -- NavigationDocumentName: `docs-nav.json` - -- MinimumVersion: `` (no minimum version) - -- DocumentStoreType: `GitHub` - -- ExtraProperties: - - ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***","GitHubUserAgent":""} - ``` - - 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - -- MainWebsiteUrl: `/` - -- LatestVersionBranchName: `dev` - -对于 `SQL` 数据库,你可以使用下面的 `T-SQL` 命令将指定的示例插入到 `DocsProjects` 表中: - -```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'dev', N'') -``` - -请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. - -现在你可以运行应用程序并导航到 `/Documents`. - -#### "FileSystem" 项目的示例记录 - -你可以使用 [ABP Framework](https://github.com/abpframework/abp/) GitHub文档来配置你的文件系统存储. - -- Name: `ABP framework (FileSystem)` - -- ShortName: `abp` - -- Format: `md` - -- DefaultDocumentName: `Index` - -- NavigationDocumentName: `docs-nav.json` - -- MinimumVersion: `` (no minimum version) - -- DocumentStoreType: `FileSystem` - -- ExtraProperties: - - ```json - {"Path":"C:\\Github\\abp\\docs"} - ``` - - 请注意 `Path` 必须使用本地docs目录替换. 你可以从https://github.com/abpframework/abp/tree/master/docs获取ABP Framework的文档并且复制到该目录 `C:\\Github\\abp\\docs` 使其正常工作. - -- MainWebsiteUrl: `/` - -- LatestVersionBranchName: `` - -对于 `SQL` 数据库,你可以使用下面的 `T-SQL` 命令将指定的示例插入到 `DocsProjects` 表中: - -```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL, N'') -``` - -添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. - -到目前为止, 我们已经从abp.io网站创建了一个新的应用程序,并为Docs模块做好准备. - -### 7- 添加一个新文档 - -在示例项目记录中, 你可以看到 `Format` 被指定为 `md` 指的是 [Mark Down](https://en.wikipedia.org/wiki/Markdown). 你可以打开下面的链接查看语法; - -https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet - -ABP文档模块可以把MarkDown渲染为HTML. - -现在让我们看一下Markdown格式的示例文档. - -~~~markdown -# This is a header - -Welcome to Docs Module. - -## This is a sub header - - [This is a link](https://abp.io) - -![This is an image](https://abp.io/assets/my-image.png) - -## This is a code block - -```csharp -public class Person -{ - public string Name { get; set; } - - public string Address { get; set; } -} -``` -~~~ - -你可以使用 ABP Framework 的文档做为示例: - -[https://github.com/abpframework/abp/blob/master/docs/zh-Hans/](https://github.com/abpframework/abp/blob/master/docs/zh-Hans/) - -#### 有条件的部分功能(使用Scriban) - -文档模块使用[Scriban]()有条件的显示或隐藏文档的某些部分. 使用该功能你需要为每一种语言创建一个JSON文件做为**参数文档**. 它包含所有键值以及它们的显示名称. - -例如 [en/docs-params.json](https://github.com/abpio/abp-commercial-docs/blob/master/en/docs-params.json): - -```json -{ - "parameters": [{ - "name": "UI", - "displayName": "UI", - "values": { - "MVC": "MVC / Razor Pages", - "NG": "Angular" - } - }, - { - "name": "DB", - "displayName": "Database", - "values": { - "EF": "Entity Framework Core", - "Mongo": "MongoDB" - } - }, - { - "name": "Tiered", - "displayName": "Tiered", - "values": { - "No": "Not Tiered", - "Yes": "Tiered" - } - }] -} -``` - -因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. - -例如 [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): - -``` -..... - -​````json -//[doc-params] -{ - "UI": ["MVC","NG"], - "DB": ["EF", "Mongo"], - "Tiered": ["Yes", "No"] -} -​```` - -........ -``` - -这个部分会在渲染时自动删除.前提是这些键值必须与**参数文档**中的键值匹配. - -![Interface](../images/docs-section-ui.png) - -现在你可以使用 **Scriban** 语法在文档中创建章节. - -示例 : - -```` -{{ if UI == "NG" }} - -* `-u` argument specifies the UI framework, `angular` in this case. - -{{ end }} - -{{ if DB == "Mongo" }} - -* `-d` argument specifies the database provider, `mongodb` in this case. - -{{ end }} - -{{ if Tiered == "Yes" }} - -* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. - -{{ end }} - -```` - -还可以在文本中使用变量,在其键中添加 **_Value** 后缀: - -```` -This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. -```` - -如果你想要得到的当前文档的语言或版本,可以使用预定义的 **Document_Language_Code** 和 **DOCUMENT_VERSION** 键(这对于创建重定向到另一个地区中另一个文档系统的链接很有用). - ------- - -**重要提示**: Scriban 的语法是 "{{" and "}}". 如果要在文档(如Angular文档)中使用转义,则必须使用转义块. 参阅 [Scriban文档]( ) 了解更多信息. - -### 8- 创建文档导航 - -导航文档是文档页面的主菜单. 它位于页面的左侧,是一个`JSON` 文件. 请查看以下示例导航文档以了解结构. - -```json -{ - "items":[ - { - "text":"Sample Menu Item - 1", - "items":[ - { - "text":"Sample Menu Item - 1.1", - "items":[ - { - "text":"Sample Menu Item - 1.1.1", - "path":"SampleMenuItem_1_1_1.md" - } - ] - }, - { - "text":"Sample Menu Item - 1.2", - "items":[ - { - "text":"Sample Menu Item - 1.2.1", - "path":"SampleMenuItem_1_2_1.md" - }, - { - "text":"Sample Menu Item - 1.2.2", - "path":"SampleMenuItem_1_2_2.md" - } - ] - } - ] - }, - { - "text":"Sample Menu Item - 2", - "items":[ - { - "text":"Sample Menu Item - 2.1", - "items":[ - { - "text":"Sample Menu Item - 2.1.1", - "path":"SampleMenuItem_2_1_1.md" - } - ] - } - ] - } - ] -} -``` - -上面的示例 `JSON` 文件将下面的导航菜单呈现为 `HTML` . - -![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) - -最后,为你的项目添加了一个新的Docs模块, 该模块由GitHub提供. - -## 全文搜索(Elastic Search) - -文档模块支持使用Elastic Search对内容进行全文搜索. 默认没有启用, 你可以配置`DocsElasticSearchOptions`启用它. - -``` -Configure(options => -{ - options.Enable = true; - options.IndexName = "your_index_name"; //default IndexName is abp_documents -}); -``` - -应用程序启动后如果`Index`不存在则会自动创建`Index`. - -`DefaultElasticClientProvider`负责创建`IElasticClient`, 默认情况下它会从`IConfiguration`中读取Elastic Search的`Url`. -如果你的 `IElasticClient` 需要其它配置请使用重写 `IElasticClientProvider` 服务并在[依赖注入](../Dependency-Injection.md)系统中替换它. -``` -{ - "ElasticSearch": { - "Url": "http://localhost:9200" - } -} -``` - -## 下一步 - -文档模块也可以做为独立的应用程序. 查看 [VoloDocs](../Apps/VoloDocs). \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Feature-Management.md b/docs/zh-Hans/Modules/Feature-Management.md deleted file mode 100644 index d408c17328..0000000000 --- a/docs/zh-Hans/Modules/Feature-Management.md +++ /dev/null @@ -1,105 +0,0 @@ -# 功能管理模块 - -功能管理模块实现了 [功能系统](../Features.md) 定义的 `IFeatureManagementStore` 接口. - -> 此文档仅介绍了将功能值持续保存到数据库的功能管理模块. 请参阅 [功能](../Features.md) 来了解功能系统. - -## 如何安装 - -当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中 (请参阅 `get-source` [CLI](../CLI.md) 命令) 以开发自定义模块. - -### 源代码 - -可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/feature-management) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. - -## 用户界面 - -### 功能管理对话框 - -功能管理模块提供了一个可重用的对话框来管理与对象相关的功能. 例如: [租户管理模块](Tenant-Management.md) 用它在租户管理页面管理租户的功能. - -![features-module-opening](../images/features-module-opening.png) - -当你点击租户的 *操作* -> *功能* 时, 将打开功能管理对话框. 下面是定义了两个功能的对话框示例截图: - -![features-modal](../images/features-modal.png) - -在对话框中, 你可以为租户启用, 禁用或设置功能值. - -## IFeatureManager - -`IFeatureManager` 是此模块提供的主要服务. 在多租户应用程序中, 它常用来读取和更改功能值. `IFeatureManager` 通常被 *租户管理对话框* 使用. 但如果需要设置功能值, 你也可以注入它. - -> 如果你只是想要读取或检查功能值, 请如 [功能文档](../Features.md) 中所述那样使用 `IFeatureChecker`. - -**示例: 为租户获取或设置功能值** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.FeatureManagement; - -namespace Demo -{ - public class MyService : ITransientDependency - { - private readonly IFeatureManager _featureManager; - - public MyService(IFeatureManager featureManager) - { - _featureManager = featureManager; - } - - public async Task SetFeatureDemoAsync(Guid tenantId, string value) - { - await _featureManager - .SetForTenantAsync(tenantId, "Feature1", value); - - var currentValue = await _featureManager - .GetOrNullForTenantAsync("Feature1", tenantId); - } - } -} -```` - -## 功能管理提供程序 - -功能管理模块是可扩展的, 就像 [功能系统](../Features.md) 一样. 你可以通过定义功能管理提供程序来扩展它. 有3个预编译的功能管理提供程序已按下面的顺序注册: - -* `DefaultValueFeatureManagementProvider`: 从功能定义的默认值中获取功能值. 它无法设置默认值, 因为默认值是在功能定义中硬编码的. -* `EditionFeatureManagementProvider`: 获取或设置版本的功能值. 版本是分配给租户的一个功能分组. 租户管理模块没有实现版本系统. 你可以自己实现它, 或者购买已实现它并且提供更多SaaS功能的 ABP 商业版 [SaaS 模块](https://commercial.abp.io/modules/Volo.Saas), 像订阅和支付. -* `TenantFeatureManagementProvider`: 获取或设置租户的功能. - -`IFeatureManager` 在获取和设置的方法中使用了这些提供程序. 通常, 在 `IFeatureManager` 服务中, 每个功能管理提供程序都定义了扩展方法 (像租户功能管理提供程序就定义了 `SetForTenantAsync`). - -如果想要自定义提供程序, 你需要实现 `IFeatureManagementProvider` 接口或从 `FeatureManagementProvider` 基类继承: - -````csharp -public class CustomFeatureProvider : FeatureManagementProvider -{ - public override string Name => "Custom"; - - public CustomFeatureProvider(IFeatureManagementStore store) - : base(store) - { - } -} -```` - -`FeatureManagementProvider` 基类 (使用 `IFeatureManagementStore`) 为你做了默认的实现. 你可以按需重写基础方法. 在这个 `Custom` 的示例中, 每个提供程序必须有一个唯一的名称 (请保持简短, 因为每个功能值记录都会被保存到数据库中). - -创建提供程序类后, 你应该用 `FeatureManagementOptions` [选项类](../Options.md) 注册它: - -````csharp -Configure(options => -{ - options.Providers.Add(); -}); -```` - -提供程序的顺序很重要. 提供程序按相反的顺序被执行. 这意味着在此示例中 `CustomFeatureProvider` 是第一个被执行的. 你可以在 `Providers` 列表的任意位置插入你的提供程序. - -## 另请参见 - -* [功能](../Features.md) diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md deleted file mode 100644 index d6a71f8b21..0000000000 --- a/docs/zh-Hans/Modules/Identity.md +++ /dev/null @@ -1,320 +0,0 @@ -# 身份管理模块 - -身份模块基于 [Microsoft Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 用于管理角色,用户及其权限. - -## 如何安装 - -当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装 (作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块. - -### 源代码 - -可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identity) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. - -## 用户界面 - -此模块提供了 [Blazor](../UI/Blazor/Overall.md), [Angular](../UI/Angular/Quick-Start.md) 和 [MVC / Razor Pages](../UI/AspNetCore/Overall.md) 的 UI 可选. - -### 菜单项 - -此模块在 *管理* 菜单下添加了一个 *身份管理* 菜单项: - -![身份模块菜单](../images/identity-module-menu.png) - -这个菜单项和相关页面已获得授权. 这意味着当前用户必须拥有相关权限才能使其可见. `admin` 角色 (和拥有此角色的用户, 如 `admin` 用户) 已经拥有这些权限. 如果你想要使其他角色/用户也启用权限, 请打开 *角色* 或 *用户* 页面的 *权限* 对话框, 并检查如下所示的权限: - -![身分模块权限](../images/identity-module-permissions.png) - -请参阅 [授权文档](../Authorization.md) 以了解权限系统. - -### 页面 - -本节介绍此模块提供的主要页面. - -#### 用户 - -此页用于查看用户列表. 您可以创建/编辑和删除用户, 将角色分配给用户. - -![身分模块用户](../images/identity-module-users.png) - -一个用户可以有零个或多个角色. 用户从其角色继承权限. 此外, 你可以给用户直接分配权限 (通过点击 *操作* 按钮, 然后选择 *权限*) . - -#### 角色 - -角色用于按分组给用户分配权限. - -![身份模块角色](../images/identity-module-roles.png) - -角色除了角色名称之外还有2个属性: - -* `Default`: 如果某个角色被标记为 "default", 那么当新用户 (使用 [账户模块](Account.md)) 注册到应用程序时, 其将被作为默认角色分配给新用户. -* `Public`: 用户的公共角色可以被应用程序中的其他用户看到. 这个功能在身份模块中没有用到, 但被提供作为你可能希望在你自己的应用程序中使用的功能. - -## 其他功能 - -本节包括此模块提供的没有 UI 页面的其他功能. - -### 组织单元 - -组织单元 (OU) 可被用于 **按层级对用户和实体进行分组**. - -#### 组织单元实体 - -组织单元由 **OrganizationUnit** 实体表示. 它的基本属性是: - -- **TenantId**: 组织单元的租户Id. 对于宿主可以是 null. -- **ParentId**: 父级组织单元的Id. 如果这是一个根级组织单元,它可以是 null. -- **Code**: 对于租户唯一的层级字符串编码. -- **DisplayName**: 组织单元的显示名称. - -#### 组织树 - -由于组织单元可以有父级, 因此租户的所有组织单元是一个 **树** 结构. 这个树有一些规则; - -- 可以有多个根级 (`ParentId` 是 `null` 的) . -- 一个组织单元的第一级子项数量有限制 (因为下面解释的固定的组织单元编码单位长度) . - -#### 组织单元编码 - -组织单元编码是通过 `OrganizationUnitManager` 服务自动生成和维护的. 它是一个字符串, 像这样: - -"**00001.00042.00005**" - -通常用这种编码可以轻易地 (递归) 查询出数据库中组织单元的所有子项. 这个编码有一些规则 (当你使用 `OrganizationUnitManager` 时自动被应用的) : - -- 对 [租户](../Multi-Tenancy.md) 是 **唯一的**. -- 同一组织单元的全部子项都具有 **以父级组织单元编码开头的** 编码. -- 如示例中所示, 它是固定长度的且是基于树中组织单元的级别的. -- 尽管组织单元编码是唯一的, 但如果你移动了相关的组织单元, 它也可以被更改. - -请注意, 你必须根据Id引用一个组织单元, 而不是编码, 因为编码后续是可以被更改的. - -#### 组织单元管理 - -`OrganizationUnitManager` 类可以被 [注入](../Dependency-Injection.md) 并用来管理组织单元. 常见用例如下: - -- 创建, 更新和删除组织单元. -- 在组织单元树中移动一个组织单元. -- 获取关于组织单元树及其项的信息. - -### 身份安全日志 - -安全日志系统可以记录账户的一些重要的操作或者改动 (例如 *登录* 和 *更改密码*) . 如果需要, 你也可以保存安全日志. - -你可以注入和使用 `IdentitySecurityLogManager` 或 `ISecurityLogManager` 来写入安全日志. 默认它会创建一个日志对象并填充常用的值, 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant` 等等. 当然你可以覆盖这些值. - -```cs -await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() -{ - Identity = "IdentityServer", - Action = "ChangePassword" -}); -``` - -通过配置 `AbpSecurityLogOptions` 来为日志提供应用程序的名称 (如果你有多个应用程序并且想要在日志中区分应用程序) 或者禁用安全日志功能. - -```cs -Configure(options => -{ - options.ApplicationName = "AbpSecurityTest"; -}); -``` -## 选项 - -`IdentityOptions` 是由 Microsoft [Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 提供的标准 [选项类](../Options.md) . 所以, 你可以在 [模块](../Module-Development-Basics.md) 类的 `ConfigureServices` 方法中设置这些选项. - -**例如: 设置所需的密码最小长度** - -````csharp -Configure(options => -{ - options.Password.RequiredLength = 5; -}); -```` - -ABP 允许你在运行时通过使用 [设置系统](../Settings.md) 更进一步地更改这些选项. 你可以 [注入](../Dependency-Injection.md) `ISettingManager` 并使用 `Set...` 方法中的一种来更改用户, 租户或全局所有用户的选项值. - -**例如: 更改当前租户所需的密码最小长度** - -````csharp -public class MyService : ITransientDependency -{ - private readonly ISettingManager _settingManager; - - public MyService(ISettingManager settingManager) - { - _settingManager = settingManager; - } - - public async Task ChangeMinPasswordLength(int minLength) - { - await _settingManager.SetForCurrentTenantAsync( - IdentitySettingNames.Password.RequiredLength, - minLength.ToString() - ); - } -} -```` - -`IdentitySettingNames` 类 (在 `Volo.Abp.Identity.Settings` 命名空间中) 为设置名称定义了一些常量. - -## 分布式事件 - -此模块定义了如下 ETOs (事件传输对象) 以允许你去订阅模块中实体的改动. - -* `UserEto` 在 `IdentityUser` 实体更改完成时发布. -* `IdentityRoleEto` 在 `IdentityRole` 实体更改完成时发布. -* `IdentityClaimTypeEto` 在 `IdentityClaimType` 实体更改完成时发布. -* `OrganizationUnitEto` 在 `OrganizationUnit` 实体更改完成时发布. - -**例如: 当一个新用户被创建时接收通知** - -````csharp -public class MyHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - UserEto user = eventData.Entity; - // TODO: ... - } -} -```` - -`UserEto` 和 `IdentityRoleEto` 默认自动发布事件. 你应该自己配置其他的 Eto. 请参阅 [分布式事件总线文档](../Distributed-Event-Bus.md) 了解预定义事件的详细信息. - -> 订阅分布式事件对于分布式场景 (如微服务架构) 特别有用. 如果你正在构建单体式应用程序, 或者在运行了身份模块的同一进程内监听事件, 那么订阅 [本地事件](../Local-Event-Bus.md) 更高效, 更简单. - -## 内部构件 - -本节包括模块的一些内部详细信息, 您不是那么需要, 但在一些情况下可能需要使用. - -### 领域层 - -#### 聚合 - -##### 用户 - -用户通常是一个登录并使用应用程序的人. - -* `IdentityUser` (聚合根): 表示系统中的一个用户. - * `IdentityUserRole` (集合): 分配给用户的角色. - * `IdentityUserClaim` (集合): 用户的自定义声明. - * `IdentityUserLogin` (集合): 用户的外部登录. - * `IdentityUserToken` (集合): 用户的令牌 (用于 Microsoft Identity 服务). - -##### 角色 - -角色通常是分配给用户的一组权限. - -* `IdentityRole` (聚合根): 表示系统中的角色. - * `IdentityRoleClaim` (集合): 角色的自定义声明. - -##### 声明类型 - -声明类型是可以被分配给系统中其他实体 (如角色和用户) 的自定义声明的定义. - -* `IdentityClaimType` (聚合根): 表示声明类型的定义. 它包括一些用于定义声明类型和验证规则的属性 (例如, 必须的, 正则表达式, 描述, 值类型) . - -##### 身份安全日志 - -`IdentitySecurityLog` 对象表示系统中与授权相关的操作 (如 *登录*) . - -* `IdentitySecurityLog` (聚合根): 表示系统中的安全日志. - -##### 组织单元 - -组织单元是一个有层级结构的实体. - -* ```OrganizationUnit``` (聚合根): 表示系统中的组织单元. - * ```Roles``` (集合): 组织单元的角色. - -#### 仓储 - -此模块定义了以下自定义仓储: - -* `IIdentityUserRepository` -* `IIdentityRoleRepository` -* `IIdentityClaimTypeRepository` -* ```IIdentitySecurityLogRepository``` -* ```IOrganizationUnitRepository``` - -#### 领域服务 - -##### 用户管理 - -`IdentityUserManager` 常用于管理用户, 他们的权限, 声明, 密码, 电子邮件等等. 它派生自 Microsoft Identity 的 `UserManager` 类, 其中 `T` 是 `IdentityUser`. - -##### 角色管理 - -`IdentityRoleManager` 常用于管理角色和他们的声明. 它派生自 Microsoft Identity 的 `RoleManager` 类, 其中 `T` 是 `IdentityRole`. - -##### 声明类型管理 - -`IdenityClaimTypeManager` 常用于对聚合根 `IdentityClaimType` 执行某些操作. - -##### 组织单元管理 - -```OrganizationUnitManager``` 常用于对聚合根 `OrganizationUnit` 执行某些操作. - -##### 安全日志管理 - -```IdentitySecurityLogManager``` 常用于保存安全日志. - -### 服务层 - -#### 应用服务 - -* `IdentityUserAppService` (实现 `IIdentityUserAppService`): 实现了用户管理 UI 的用例. -* `IdentityRoleAppService` (实现 `IIdentityRoleAppService`): 实现了角色管理 UI 的用例. -* `IdentityClaimTypeAppService` (实现 `IIdentityClaimTypeAppService`): 实现了声明类型管理 UI 的用例. -* `IdentitySettingsAppService` (实现 `IIdentitySettingsAppService`): 用于获取和更新身份模块的设置. -* `IdentityUserLookupAppService` (实现 `IIdentityUserLookupAppService`): 用于根据 `id` 或 `userName` 获取用户信息. 它旨在由ABP内部使用. -* `ProfileAppService` (实现 `IProfileAppService`): 用于更改用户的简介和密码. -* ```IdentitySecurityLogAppService``` (实现 ```IIdentitySecurityLogAppService```): 实现了安全日志 UI 的用例. -* ```OrganizationUnitAppService``` (实现 ```OrganizationUnitAppService```): 实现了组织单元管理 UI 的用例. - -### 数据库提供程序 - -此模块为数据库提供 [Entity Framework Core](../Entity-Framework-Core.md) 和 [MongoDB](../MongoDB.md) 两种选择. - -#### EF Core - -NuGet 包 [Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore) 实现了 EF Core 的集成. - -##### 数据库表 - -* **AbpRoles** - * AbpRoleClaims -* **AbpUsers** - * AbpUserClaims - * AbpUserLogins - * AbpUserRoles - * AbpUserTokens -* **AbpClaimTypes** -* **AbpOrganizationUnits** - * AbpOrganizationUnitRoles - * AbpUserOrganizationUnits -* **AbpSecurityLogs** - -#### MongoDB - -NuGet 包 [Volo.Abp.Identity.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.MongoDB) 实现了 MongoDB 的集成. - -##### 数据库集合 - -* **AbpRoles** -* **AbpUsers** -* **AbpClaimTypes** -* **AbpOrganizationUnits** -* **AbpSecurityLogs** - -#### 常用数据库属性 - -你可以设置 `AbpIdentityDbProperties` 中的以下属性来更改数据库选项: - -* `DbTablePrefix` (`Abp` 作为默认值) 是表/集合名称的前缀. -* `DbSchema` (`null` 作为默认值) 是数据库架构. -* `ConnectionStringName` (`AbpIdentity` 作为默认值) 是此模块的 [连接字符串](../Connection-Strings.md) 名称. - -它们是静态属性. 你需要在开始运行应用程序前设置它们 (通常在 `Program.cs` 中). diff --git a/docs/zh-Hans/Modules/IdentityServer.md b/docs/zh-Hans/Modules/IdentityServer.md deleted file mode 100644 index 1359c04e53..0000000000 --- a/docs/zh-Hans/Modules/IdentityServer.md +++ /dev/null @@ -1,173 +0,0 @@ -# 身份服务器模块 - -身份服务器模块提供了一个 [IdentityServer](https://github.com/IdentityServer/IdentityServer4) (IDS) 的完全集成, 该框架提供高级身份验证功能, 如单点登录和API访问控制.此模块将客户端,资源以及其他 IDS 相关的对象保存到数据库中. - -## 如何安装 - -当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包).你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块. - -### 源代码 - -可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identityserver) 访问源代码.源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. - -## 用户界面 - -此模块使用了领域逻辑和数据库集成, 但没有提供任何 UI.如果你需要动态添加客户端和资源, 管理 UI 是非常有用的.在这种情况下, 你可以自己构建管理 UI, 或者考虑购买为此模块提供了管理 UI 的 [ABP 商业版](https://commercial.abp.io/). - -## 与其他模块的关系 - -此模块基于 [身份模块](Identity.md) 并且[账户模块](Account.md) 有一个 [集成包](https://www.nuget.org/packages/Volo.Abp.Account.Web.IdentityServer). - -## 选项 - -### AbpIdentityServerBuilderOptions - -`AbpIdentityServerBuilderOptions` 在你的身份服务器 [模块](https://docs.abp.io/zh-Hans/abp/latest/Module-Development-Basics) 中的 `PreConfigureServices` 方法中配置.例如: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - //Set options here... - }); -} -```` - -`AbpIdentityServerBuilderOptions` 属性: - -* `UpdateJwtSecurityTokenHandlerDefaultInboundClaimTypeMap` (默认值:true):更新 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap` 使其与身份服务器声明兼容. -* `UpdateAbpClaimTypes` (默认值:true):更新 `AbpClaimTypes` 与身份服务器声明兼容. -* `IntegrateToAspNetIdentity` (默认值:true):集成到 ASP.NET Identity. -* `AddDeveloperSigningCredential` (默认值:true):设置为 false 禁止调用 IIdentityServerBuilder 中的 `AddDeveloperSigningCredential()`. - -`IIdentityServerBuilder` 可以在你的身份服务器 [模块](https://docs.abp.io/zh-Hans/abp/latest/Module-Development-Basics) 中的 `PreConfigureServices` 方法中配置.例如: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(builder => - { - builder.AddSigningCredential(...); - }); -} -```` - -## 内部结构 - -### 领域层 - -#### 聚合 - -##### API 资源 - -需要 API 资源来允许客户端请求访问令牌. - -* `ApiResource` (聚合根):表示系统中的 API 资源. - * `ApiSecret` (集合):API 资源的密钥. - * `ApiScope` (集合):API 资源的作用域. - * `ApiResourceClaim` (集合):API 资源的声明. - -##### 客户端 - -客户端表示可以从你的身份服务器请求令牌的应用程序. - -* `Client` (聚合根):表示一个身份服务器的客户端应用程序. - * `ClientScope` (集合):客户端的作用域. - * `ClientSecret` (集合):客户端的密钥. - * `ClientGrantType` (集合):客户端的授权类型. - * `ClientCorsOrigin` (集合):客户端的 CORS 源. - * `ClientRedirectUri` (集合):客户端的重定向 URIs. - * `ClientPostLogoutRedirectUri` (集合):客户端的登出重定向 URIs. - * `ClientIdPRestriction` (集合):客户端的提供程序约束. - * `ClientClaim` (集合):客户端的声明. - * `ClientProperty` (集合):客户端的自定义属性. - -##### 持续化授权 - -持续化授权存储了授权码,刷新令牌和用户准许. - -* `PersistedGrant` (聚合根):表示为身份服务器持续化授权. - -##### 身份资源 - -身份资源是用户的用户 ID ,名称或邮件地址等数据. - -* `IdentityResource` (聚合根):表示与身份服务器的身份资源. - * `IdentityClaim` (集合):身份资源的声明. - -#### 仓储 - -为此模块定义了以下自定义仓储: - -* `IApiResourceRepository` -* `IClientRepository` -* `IPersistentGrantRepository` -* `IIdentityResourceRepository` - -#### 领域服务 - -此模块不包含任何领域服务, 但重写了下面的服务; - -* `AbpProfileService` (当 `AbpIdentityServerBuilderOptions.IntegrateToAspNetIdentity` 为 true 时使用) -* `AbpClaimsService` -* `AbpCorsPolicyService` - -### 设置 - -此模块未定义任何设置. - -### 应用层 - -#### 应用服务 - -* `ApiResourceAppService` (实现 `IApiResourceAppService`):实现了 API 资源管理 UI 的用例. -* `IdentityServerClaimTypeAppService` (实现 `IIdentityServerClaimTypeAppService`):用于获取声明列表. -* `ApiResourceAppService` (实现 `IApiResourceAppService`):实现了 API 管理资源 UI 的用例. -* `IdentityResourceAppService` (实现 `IIdentityResourceAppService`):实现了身份资源管理 UI 的用例. - -### 数据库提供程序 - -#### 公共 - -##### 表/集合 前缀 & 架构 - -所有表/集合都使用 `IdentityServer` 作为默认前缀.如果你需要改变表的前缀或设置一个架构名称(如果你的数据库提供程序支持), 请设置 `AbpIdentityServerDbProperties` 类的静态属性. - -##### 连接字符串 - -此模块使用 `AbpIdentityServer` 作为连接字符串的名称.如果你没有用这个名称定义连接字符串, 它将回退到 `Default` 连接字符串. - -有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/zh-Hans/abp/latest/Connection-Strings) 文档. - -#### EF Core - -##### 表 - -* **IdentityServerApiResources** - * IdentityServerApiSecrets - * IdentityServerApiScopes - * IdentityServerApiScopeClaims - * IdentityServerApiClaims -* **IdentityServerClients** - * IdentityServerClientScopes - * IdentityServerClientSecrets - * IdentityServerClientGrantTypes - * IdentityServerClientCorsOrigins - * IdentityServerClientRedirectUris - * IdentityServerClientPostLogoutRedirectUris - * IdentityServerClientIdPRestrictions - * IdentityServerClientClaims - * IdentityServerClientProperties -* **IdentityServerPersistedGrants** -* **IdentityServerIdentityResources** - * IdentityServerIdentityClaims - -#### MongoDB - -##### 集合 - -* **IdentityServerApiResources** -* **IdentityServerClients** -* **IdentityServerPersistedGrants** -* **IdentityServerIdentityResources** diff --git a/docs/zh-Hans/Modules/Index.md b/docs/zh-Hans/Modules/Index.md deleted file mode 100644 index 1df7a04413..0000000000 --- a/docs/zh-Hans/Modules/Index.md +++ /dev/null @@ -1,33 +0,0 @@ -# 应用程序模块 - -ABP是一个 **模块化的应用程序框架** 由十多个 **NuGet & NPM packages** 组成. 它提供了一个完整的基础设施来构建你自己的应用程序模块,这些模块包含实体,服务,数据库集成,API,UI组件等. - -**有两种类型的模块.** 它们没有任何结构上的差异,只是按照功能和目地分类: - -* [**框架模块**](https://github.com/abpframework/abp/tree/master/framework/src): 这些是 **框架的核心模块**,像缓存,邮件,主题,安全性,序列化,验证,Ef Core集成,MongoDB集成...等等. 它们没有应用程序/业务功能,但通过提供通用基础架构,集成和抽象会使你的日常开发更加容易. -* [**应用程序模块**](https://github.com/abpframework/abp/tree/master/modules): 这些模块是实现特定的应用程序/业务功能,像 博客, 文档管理, 身份管理, 租户管理... 等等. 它是通常有自己的实体,服务,API和UI组件. - -## 开源的应用程序模块 - -有一些由ABP框架开发和维护的 **开源免费** 的应用程序模块: - -* [**Account**](Account.md): 提供账户管理UI,并允许用户登录/注册应用程序. -* [**Audit Logging**](Audit-Logging.md): 用于将审计日志持久化到数据库. -* [**Background Jobs**](Background-Jobs.md): 用于在使用默认后台作业管理器时保存后台作业. -* [**Blogging**](Blogging.md): 用于创建精美的博客. ABP的[博客](https://blog.abp.io/) 就使用了此模块. -* [**Docs**](Docs.md): 用于创建技术文档页面. ABP的[文档](https://abp.io/documents/) 就使用了此模块. -* [**Feature Management**](Feature-Management.md): 用于保存和管理功能. -* [**Identity**](Identity.md): 基于Microsoft Identity管理组织单元,角色,用户和他们的权限. -* [**OpenIddict**](OpenIddict.md): 集成了OpenIddict. -* [**Identity Server**](IdentityServer.md): 集成了IdentityServer4. -* [**Permission Management**](Permission-Management.md): 用于保存权限. -* [**Setting Management**](Setting-Management.md): 用于保存设置. -* [**Tenant Management**](Tenant-Management.md): 管理[多租户](../Multi-Tenancy.md)应用程序的租户. -* [**Users**](Users.md): 抽象用户, 因此其他模块可以依赖此模块而不是Identity模块. -* [**Virtual File Explorer**](Virtual-File-Explorer.md): 提供简单的UI查看[虚拟文件系统](../Virtual-File-System.md)中的文件. - -模块化文档正在编写中. 请参阅[这个仓库](https://github.com/abpframework/abp/tree/master/modules)获取所有模块的源代码. - -## 商业应用模块 - -[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/modules). diff --git a/docs/zh-Hans/Modules/Organization-Units.md b/docs/zh-Hans/Modules/Organization-Units.md deleted file mode 100644 index 8047b6a48d..0000000000 --- a/docs/zh-Hans/Modules/Organization-Units.md +++ /dev/null @@ -1,47 +0,0 @@ -# 组织单元管理 - -组织单元(OU)是"身份模块"的一部分,用于**对用户和实体进行分层分组**. - -### OrganizationUnit 实体 - -OU由 **OrganizationUnit** 实体表示. 实体有以下基本属性: - -- **TenantId**: OU租户的ID,为null代表是宿主OU. -- **ParentId**: OU的父亲Id,为null为根OU. -- **Code**: 租户唯一的分层字符串代码. -- **DisplayName**: OU的显示名称. - -OrganizationUnit实体的主键(Id)是 **Guid** 类型,派生自[**FullAuditedAggregateRoot**](../Entities.md)类. - -#### Organization 树 - -因为OU可以有父亲,租户所有的OU是一个**树**结构. 树有一些规则: - -- 可以有多个根(`ParentId` 为 `null`). -- OU的第一级子级数有限制(面说明的固定OU代码单位长度). - -#### OU Code - -OU代码由OrganizationUnit Manager自动生成和维护. 看起来像这样的字符串: - -"**00001.00042.00005**" - -此代码可用于轻松查询数据库中OU的所有子级(递归). 代码有一些规则: - -- 必须[租户](../Multi-Tenancy.md)**唯一**的. -- 同一OU的所有子代均以**父OU的代码开头**. -- 它是**固定长度**的,并且基于树中OU的级别,如示例中所示. -- 虽然OU代码是唯一的,但是如果移动OU,它是**可更改的**. -- 你必须通过Id而不是代码引用OU. - -### OrganizationUnit Manager - -可以注入 **OrganizationUnitManager** 管理OU. 常见的用例有: - -- 创建,更改或删除OU. -- 在OU树中移动OU. -- 获取有关OU树及其项的信息 - -#### 多租户 - -`OrganizationUnitManager` 设置为一次性为 **单个租户** 工作,默认是 **当前租户**. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Permission-Management.md b/docs/zh-Hans/Modules/Permission-Management.md deleted file mode 100644 index 3a53b6b07e..0000000000 --- a/docs/zh-Hans/Modules/Permission-Management.md +++ /dev/null @@ -1,109 +0,0 @@ -# 权限管理模块 - -此模块实现了 `IPermissionStore` 接口, 并在数据库中管理权限值. - -> 此文档仅介绍了将权限值持续保存到数据库的权限管理模块. 请参阅 [授权文档](../Authorization.md) 来了解授权和权限系统. - -## 如何安装 - -当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中 (请参阅 `get-source` [CLI](../CLI.md) 命令) 以开发自定义模块. - -### 源代码 - -可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/permission-management) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. - -## 用户界面 - -### 权限管理对话框 - -权限管理模块提供了一个可重用的对话框来管理与对象相关的权限. 例如: [身份模块](Identity.md) 用它去管理用户和角色的权限. 下图展示了身份模块的角色管理页面: - -![permissions-module-open-dialog](../images/permissions-module-open-dialog.png) - -当你点击角色的 *操作* -> *权限* 时, 将打开权限管理对话框. 下面是此对话框的一个示例截图: - -![permissions-module-dialog](../images/permissions-module-dialog.png) - -在对话框中, 你可以为所选角色授予权限. 左侧的选项卡表示主要权限组, 右侧包括了所选权限组中定义的权限. - -## IPermissionManager - -`IPermissionManager` 是此模块提供的主要服务. 它常用来读取和更改权限值. `IPermissionManager` 通常被 *权限管理对话框* 使用. 但如果需要设置权限值, 你也可以注入它. - -> 如果你只是想要读取或检查当前用户的权限值, 请如 [授权文档](../Authorization.md) 中所述那样使用 `IAuthorizationService` 或 `[Authorize]` 特性. - -**示例: 使用 `IPermissionManager` 服务为角色和用户授权** - -````csharp -public class MyService : ITransientDependency -{ - private readonly IPermissionManager _permissionManager; - - public MyService(IPermissionManager permissionManager) - { - _permissionManager = permissionManager; - } - - public async Task GrantRolePermissionDemoAsync( - string roleName, string permission) - { - await _permissionManager - .SetForRoleAsync(roleName, permission, true); - } - - public async Task GrantUserPermissionDemoAsync( - Guid userId, string roleName, string permission) - { - await _permissionManager - .SetForUserAsync(userId, permission, true); - } -} -```` - -## 权限管理提供程序 - -权限管理模块是可扩展的, 就像 [权限系统](../Authorization.md) 一样. 你可以通过定义权限管理提供程序来扩展它. - -[身份模块](Identity.md) 定义了如下权限管理提供程序. - -* `UserPermissionManagementProvider`: 管理用户基础权限. -* `RolePermissionManagementProvider`: 管理角色基础权限. - -`IPermissionManager` 使用这些提供程序获取或设置权限. 你可以通过实现 `IPermissionManagementProvider` 或从 `PermissionManagementProvider` 基类继承定义你自己的提供程序. - -**示例:** - -````csharp -public class CustomPermissionManagementProvider : PermissionManagementProvider -{ - public override string Name => "Custom"; - - public CustomPermissionManagementProvider( - IPermissionGrantRepository permissionGrantRepository, - IGuidGenerator guidGenerator, - ICurrentTenant currentTenant) - : base( - permissionGrantRepository, - guidGenerator, - currentTenant) - { - } -} -```` - -`PermissionManagementProvider` 基类 (使用 `IPermissionGrantRepository`) 为你做了默认的实现. 你可以按需重写基础方法. 在这个自定义的示例中, 每个提供程序必须有一个唯一的名称 (请保持简短, 因为每个权限值记录都会被保存到数据库中). - -创建提供程序类后, 你应该用 `PermissionManagementOptions` [选项类](../Options.md) 注册它: - -````csharp -Configure(options => -{ - options.ManagementProviders.Add(); -}); -```` - -提供程序的顺序很重要. 提供程序按相反的顺序被执行. 这意味着在此示例中 `CustomPermissionManagementProvider` 是第一个被执行的. 你可以在 `Providers` 列表的任意位置插入你的提供程序. - -## 另请参见 - -* [授权](../Authorization.md) \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Setting-Management.md b/docs/zh-Hans/Modules/Setting-Management.md deleted file mode 100644 index cf52d588a2..0000000000 --- a/docs/zh-Hans/Modules/Setting-Management.md +++ /dev/null @@ -1,284 +0,0 @@ -# 设置管理模块 - -设置管理模块实现了 `ISettingStore` (参阅 [设置系统](../Settings.md)) 将设置值存储在数据库中, 并提供 `ISettingManager` 管理 (更改) 数据库中设置值的功能. - -> [启动模板](../Startup-Templates/Index.md)默认安装并配置了设置管理模块. 大部分情况下你不需要手动的添加该到模块到应用程序中. - -## ISettingManager - -`ISettingManager` 用于获取和设定设置值. 示例: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.SettingManagement; - -namespace Demo -{ - public class MyService : ITransientDependency - { - private readonly ISettingManager _settingManager; - - //Inject ISettingManager service - public MyService(ISettingManager settingManager) - { - _settingManager = settingManager; - } - - public async Task FooAsync() - { - Guid user1Id = ...; - Guid tenant1Id = ...; - - //Get/set a setting value for the current user or the specified user - - string layoutType1 = - await _settingManager.GetOrNullForCurrentUserAsync("App.UI.LayoutType"); - string layoutType2 = - await _settingManager.GetOrNullForUserAsync("App.UI.LayoutType", user1Id); - - await _settingManager.SetForCurrentUserAsync("App.UI.LayoutType", "LeftMenu"); - await _settingManager.SetForUserAsync(user1Id, "App.UI.LayoutType", "LeftMenu"); - - //Get/set a setting value for the current tenant or the specified tenant - - string layoutType3 = - await _settingManager.GetOrNullForCurrentTenantAsync("App.UI.LayoutType"); - string layoutType4 = - await _settingManager.GetOrNullForTenantAsync("App.UI.LayoutType", tenant1Id); - - await _settingManager.SetForCurrentTenantAsync("App.UI.LayoutType", "LeftMenu"); - await _settingManager.SetForTenantAsync(tenant1Id, "App.UI.LayoutType", "LeftMenu"); - - //Get/set a global and default setting value - - string layoutType5 = - await _settingManager.GetOrNullGlobalAsync("App.UI.LayoutType"); - string layoutType6 = - await _settingManager.GetOrNullDefaultAsync("App.UI.LayoutType"); - - await _settingManager.SetGlobalAsync("App.UI.LayoutType", "TopMenu"); - } - } -} - -```` - -你可以从不同的设置值提供程序中(默认,全局,用户,租户...等)中获取或设定设置值. - -> 如果只需要读取设置值,建议使用 `ISettingProvider` 而不是`ISettingManager`,因为它实现了缓存并支持所有部署场景. 如果要创建设置管理UI,可以使用ISettingManager. - -### Setting Cache - -设置值缓存在 [分布式缓存](../Caching.md) 系统中. 建议始终使用 `ISettingManager` 更改设置值. - -## Setting Management Providers - -设置管理模块是可扩展的,像[设置系统](../Settings.md)一样. 你可以通过自定义设置管理提供程序进行扩展. 有5个预构建的设置管理程序程序按以下顺序注册: - -* `DefaultValueSettingManagementProvider`: 从设置定义的默认值中获取值,由于默认值是硬编码在设置定义上的,所以无法更改默认值. -* `ConfigurationSettingManagementProvider`:从 [IConfiguration 服务](../Configuration.md)中获取值. 由于无法在运行时更改配置值,所以无法更改配置值. -* `GlobalSettingManagementProvider`: 获取或设定设置的全局 (系统范围)值. -* `TenantSettingManagementProvider`: 获取或设定租户的设置值. -* `UserSettingManagementProvider`: 获取或设定用户的设置值. - -`ISettingManager` 在 `get/set` 方法中使用设置管理提供程序. 通常每个设置程序提供程序都在 `ISettingManagement` 服务上定义了模块方法 (比如用户设置管理程序提供定义了 `SetForUserAsync` 方法). - -## Setting Management UI. - -设置管理模块默认提供了邮件设置页面. - -![EmailSettingUi](../images/setting-management-email-ui.png) - -> 你可以点击发送测试邮件按钮发送一封测试邮件来检查你的邮件设置. - -设置UI是可扩展的; 你可以为你的应用程序设置添加设置标签到设置页面. - -### MVC UI - -#### 创建视图组件 - -在 `Components` 目录下创建 `MySettingGroup` 文件夹, 添加一个名为 `MySettingGroupViewComponent` 的视图组件: - -![MySettingGroupViewComponent](../images/my-setting-group-view-component.png) - -打开 `MySettingGroupViewComponent.cs` 替换为以下内容: - -```csharp -public class MySettingGroupViewComponent : AbpViewComponent -{ - public virtual IViewComponentResult Invoke() - { - return View("~/Components/MySettingGroup/Default.cshtml"); - } -} -``` - -> 你还可以使用 `InvokeAsync` 方法,在这个示例中我们使用 `Invoke` 方法. - -#### Default.cshtml - -在 `MySettingGroup` 目录下创建 `Default.cshtml` 文件. - -打开 `Default.cshtml` 替换为以下内容: - -```html -
    -

    My setting group page

    -
    -``` - -#### BookStoreSettingPageContributor - -在 `Settings` 目录下创建 `BookStoreSettingPageContributor.cs` 文件. - -![BookStoreSettingPageContributor](../images/my-setting-group-page-contributor.png) - -文件内容如下: - -```csharp -public class BookStoreSettingPageContributor : ISettingPageContributor -{ - public Task ConfigureAsync(SettingPageCreationContext context) - { - context.Groups.Add( - new SettingPageGroup( - "Volo.Abp.MySettingGroup", - "MySettingGroup", - typeof(MySettingGroupViewComponent), - order : 1 - ) - ); - - return Task.CompletedTask; - } - - public Task CheckPermissionsAsync(SettingPageCreationContext context) - { - // You can check the permissions here - return Task.FromResult(true); - } -} -``` - -打开 `BookStoreWebModule.cs` 文件添加以下代码: - -```csharp -Configure(options => -{ - options.Contributors.Add(new BookStoreSettingPageContributor()); -}); -``` - -#### 运行应用程序 - -导航到 `/SettingManagement` 路由查看更改: - -![Custom Settings Tab](../images/my-setting-group-ui.png) - -### Blazor UI - -#### 创建 Razor 组件 - -在 `Pages` 目录下创建 `MySettingGroup` 文件夹, 添加一个名为 `MySettingGroupComponent` 的Razor组件: - -![MySettingGroupComponent](../images/my-setting-group-component.png) - -打开 `MySettingGroupComponent.razor` 替换为以下内容: - -```csharp - -

    my setting group

    -
    -``` - -#### BookStoreSettingComponentContributor - -在 `Settings` 目录下创建 `BookStoreSettingComponentContributor.cs` 文件. - -![BookStoreSettingComponentContributor](../images/my-setting-group-component-contributor.png) - -文件内容如下: - -```csharp -public class BookStoreSettingComponentContributor : ISettingComponentContributor -{ - public Task ConfigureAsync(SettingComponentCreationContext context) - { - context.Groups.Add( - new SettingComponentGroup( - "Volo.Abp.MySettingGroup", - "MySettingGroup", - typeof(MySettingGroupComponent), - order : 1 - ) - ); - - return Task.CompletedTask; - } - - public Task CheckPermissionsAsync(SettingComponentCreationContext context) - { - // You can check the permissions here - return Task.FromResult(true); - } -} -``` - -打开 `BookStoreBlazorModule.cs` 文件添加以下代码: - -```csharp -Configure(options => -{ - options.Contributors.Add(new BookStoreSettingComponentContributor()); -}); -``` - -#### 运行应用程序 - -导航到 `/setting-management` 路由查看更改: - -![Custom Settings Tab](../images/my-setting-group-blazor.png) - -### Angular UI - -不同的模块提供它们的设置选项卡. 你可以通过3个步骤在项目中自定义设置页面. - -#### 创建组件 - -使用以下命令创建一个组件 - -```bash -yarn ng generate component my-settings -``` - -打开 `app.component.ts` 做以下修改: - -```js -import { Component } from '@angular/core'; -import { SettingTabsService } from '@abp/ng.setting-management/config'; // imported SettingTabsService -import { MySettingsComponent } from './my-settings/my-settings.component'; // imported MySettingsComponent - -@Component(/* component metadata */) -export class AppComponent { - constructor(private settingTabs: SettingTabsService) // injected MySettingsComponent - { - // added below - settingTabs.add([ - { - name: 'MySettings', - order: 1, - requiredPolicy: 'policy key here', - component: MySettingsComponent, - }, - ]); - } -} -``` - -#### 运行应用程序 - -导航到 `/setting-management` 路由你会看到以下变化: - -![Custom Settings Tab](../images/custom-settings.png) diff --git a/docs/zh-Hans/Modules/Tenant-Management.md b/docs/zh-Hans/Modules/Tenant-Management.md deleted file mode 100644 index 976221e85c..0000000000 --- a/docs/zh-Hans/Modules/Tenant-Management.md +++ /dev/null @@ -1,107 +0,0 @@ -# 租户管理模块 - -[多租户](../Multi-Tenancy.md) 是 ABP 框架的核心功能之一. 它提供了构建 SaaS (软件及服务) 解决方案的基础架构. ABP 的多租户系统通过提供 `ITenantStore` 接口来抽象租户存储的位置. 您需要做的就是实现该接口. - -**租户管理模块是 `ITenantStore` 接口的实现. 它将租户存储在数据库中. 它还提供了 UI 来管理租户及其 [功能](../Features.md).** - -> 请参阅 [多租户](../Multi-Tenancy.md) 文档,以了解 ABP 框架的多租户系统. 本文重点介绍租户管理模块. - -### 关于商业版 SaaS 模块 - -[SaaS 模块](https://commercial.abp.io/modules/Volo.Saas) 是此模块的替代实现, 具有更多的功能和可能性. 它作为 [ABP 商业版](https://commercial.abp.io/) 订阅的一部分分发. - -## 如何安装 - -当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中 (请参阅 `get-source` [CLI](../CLI.md)) 以开发自定义模块. - -### 源代码 - -可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/tenant-management) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. - -## 用户界面 - -此模块将 "管理 -> 租户管理 -> 租户" 菜单项添加到应用程序的主菜单, 这将打开如下页面: - -![租户管理页面](../images/module-tenant-management-page.png) - -在这个页面中, 你可以查看所有租户. 你可以按照如下所示创建一个新的租户: - -![租户管理新增租户](../images/module-tenant-management-new-tenant.png) - -在这个模态框中; - -* **租户名称**: 租户唯一的名称. 如果为租户使用二级域名, 这将是二级域名名称. -* **管理员电子邮件地址**: 租户管理员的电子邮件地址. -* **管理员密码**: 租户管理员的密码. - -当您点击租户附近的 *操作* 按钮时, 您将看见可以执行的操作: - -![租户管理操作](../images/module-tenant-management-actions.png) - -### 管理租户功能 - -功能操作会打开一个模态框, 来对相关租户进行启用/禁用/设置 [功能](../Features.md). 这有一个模态框的例子: - -![功能模态框](../images/features-modal.png) - -### 管理主机功能 - -如果您使用应用程序的功能也在主机端, *管理主机功能* 按钮则用于设置主机端的功能. - -## 内部结构 - -如果要 [自定义](../Customizing-Application-Modules-Guide.md) 此模块而不更改 [其源代码](https://github.com/abpframework/abp/tree/dev/modules/tenant-management), 则可以将此节作为参考. - -### 领域层 - -#### 聚合 - -* `Tenant` - -#### 仓储 - -* `ITenantRepository` - -#### 领域服务 - -* `TenantManager` - -### 应用层 - -#### 应用服务 - -* `TenantAppService` - -#### 权限 - -- `AbpTenantManagement.Tenants`: 租户管理. -- `AbpTenantManagement.Tenants.Create`: 创建一个新的租户. -- `AbpTenantManagement.Tenants.Update`: 编辑一个已存在的租户. -- `AbpTenantManagement.Tenants.Delete`: 删除一个已存在的租户. -- `AbpTenantManagement.Tenants.ManageFeatures`: 管理租户的功能. - -### EF Core 集成 - -* `TenantManagementDbContext` (实现了 `ITenantManagementDbContext`) - -**数据库表:** - -* `AbpTenants` -* `AbpTenantConnectionStrings` - -### MongoDB 集成 - -* `TenantManagementMongoDbContext` (实现了 `ITenantManagementMongoDbContext`) - -**数据库集合:** - -* `AbpTenants` (也包括连接字符串) - -## 注意事项 - -ABP 框架允许租户有专用的数据库, 以允许 *每个租户用一个数据库*. 此模块具有使该实现成为可能 (查阅其源代码) 的基础架构, 但是它没有实现应用程序层和 UI 功能以使其开箱即用. 您可以自己实现这些功能, 也可以考虑使用已完全实现这些功能并提供了更多业务功能的 [ABP 商业版 Saas 模块](https://docs.abp.io/en/commercial/latest/modules/saas). - -## 另请参见 - -* [多租户](../Multi-Tenancy.md) -* [ABP 商业版 SaaS 模块](https://docs.abp.io/en/commercial/latest/modules/saas) diff --git a/docs/zh-Hans/Modules/Virtual-File-Explorer.md b/docs/zh-Hans/Modules/Virtual-File-Explorer.md deleted file mode 100644 index e70306f13c..0000000000 --- a/docs/zh-Hans/Modules/Virtual-File-Explorer.md +++ /dev/null @@ -1,83 +0,0 @@ -# 虚拟文件浏览器模块 - -## 什么是虚拟文件浏览器模块 - -虚拟文件浏览器模块提供了一个简单的UI来查看[虚拟文件系统](../Virtual-File-System.md)所有的文件. - -> [启动模板](../Startup-Templates/Index.md)默认并没有安装这个模块,所以你需要手动安装到应用程序. - -### 安装 - -#### 1- 使用ABP CLI - -建议使用ABP CLI安装模块,在解决方案文件 (`.sln`) 目录打开 `CMD` 窗口,运行以下命令: - -`abp add-module Volo.VirtualFileExplorer` - -#### 2- 手动安装 - -或者你也可以手动安装nuget包到 `Acme.MyProject.Web` 项目: - -* 安装[Volo.Abp.VirtualFileExplorer.Web](https://www.nuget.org/packages/Volo.Abp.VirtualFileExplorer.Web/) nuget包到 `Acme.MyProject.Web` 项目. - - `Install-Package Volo.Abp.VirtualFileExplorer.Web` - -##### 2.1- 添加模块依赖 - -* 打开 `MyProjectWebModule.cs` 并且添加 `typeof(AbpVirtualFileExplorerWebModule)` 如下所示; - - ```csharp - [DependsOn( - typeof(AbpVirtualFileExplorerWebModule), - typeof(MyProjectApplicationModule), - typeof(MyProjectEntityFrameworkCoreModule), - typeof(AbpAutofacModule), - typeof(AbpIdentityWebModule), - typeof(AbpAccountWebModule), - typeof(AbpAspNetCoreMvcUiBasicThemeModule) - )] - public class MyProjectWebModule : AbpModule - { - //... - } - ``` - -##### 2.2- 添加NPM包 - - * 打开 `package.json` 添加 `@abp/virtual-file-explorer": "^2.9.0` 如下所示: - - ```json - { - "version": "1.0.0", - "name": "my-app", - "private": true, - "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^2.9.0", - "@abp/virtual-file-explorer": "^2.9.0" - } - } - ``` - - 然后在 `Acme.MyProject.Web` 项目目录打开命令行终端运行以下命令: - -````bash -abp install-libs -```` - -这就是全部,运行应用程序导航到 `/VirtualFileExplorer`. 你会看到虚拟文件浏览器页面: - -![Virtual-File-Explorer](../images/virtual-file-explorer.png) - -### 选项 - -你可以通过 `AbpVirtualFileExplorerOptions` 选项禁用虚拟文件浏览器模块: - -```csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.IsEnabled = false; - }); -} -``` \ No newline at end of file diff --git a/docs/zh-Hans/MongoDB.md b/docs/zh-Hans/MongoDB.md deleted file mode 100644 index 1602c61a38..0000000000 --- a/docs/zh-Hans/MongoDB.md +++ /dev/null @@ -1,382 +0,0 @@ -## MongoDB 集成 - -本文会介绍如何将MongoDB集成到基于ABP的应用程序中以及如何配置它 - -### 安装 - - 集成MongoDB需要用到`Volo.Abp.MongoDB`这个包.将它安装到你的项目中(如果是多层架构,安装到数据层和基础设施层): - -``` -Install-Package Volo.Abp.MongoDB -``` - -然后添加 `AbpMongoDbModule` 依赖到你的 [模块](Module-Development-Basics.md)中: - -```c# -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -### 创建一个Mongo Db Context - -ABP中引入了 **Mongo Db Context** 的概念(跟Entity Framework Core的DbContext很像)让使用和配置集合变得更简单.举个例子: - -```c# -public class MyDbContext : AbpMongoDbContext -{ - public IMongoCollection Questions => Collection(); - - public IMongoCollection Categories => Collection(); - - protected override void CreateModel(IMongoModelBuilder modelBuilder) - { - modelBuilder.Entity(b => - { - b.CollectionName = "Questions"; - }); - } -} -``` - -* 继承 `AbpMongoDbContext` 类 -* 为每一个mongo集合添加一个公共的 `IMongoCollection` 属性.ABP默认使用这些属性创建默认的仓储 -* 重写 `CreateModel` 方法,可以在方法中配置集合(如设置集合在数据库中的名字) - -### 为集合配置索引和 CreateCollectionOptions - -你可以在 `CreateModel` 方法中配置集合的索引和 `CreateCollectionOptions`: - -````csharp -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.Entity(b => - { - b.CreateCollectionOptions.Collation = new Collation(locale:"en_US", strength: CollationStrength.Secondary); - b.ConfigureIndexes(indexes => - { - indexes.CreateOne( - new CreateIndexModel( - Builders.IndexKeys.Ascending("MyProperty"), - new CreateIndexOptions { Unique = true } - ) - ); - } - ); - }); -} -```` - -在这个例子中,我们设置了集合的排序规则和一个唯一的索引. - -### 将 Db Context 注入到依赖注入中 - -在你的模块中使用 `AddAbpDbContext` 方法将Db Context注入到[依赖注入](Dependency-Injection.md)系统中. - -```c# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.MongoDB; -using Volo.Abp.Modularity; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMongoDbModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddMongoDbContext(); - - //... - } - } -} -``` - -#### 添加默认的仓储 - -在注入的时候使用 `AddDefaultRepositories()`, ABP就能自动为Db Context中的每一个实体创建[仓储](Repositories.md): - -````C# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); -}); -```` - -这样就会默认为每一个聚合根实体(继承自AggregateRoot的类)创建一个仓储.如果你也想为其他的实体创建仓储,将 `includeAllEntities` 设置为 `true`就可以了: - -```c# -services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(includeAllEntities: true); -}); -``` - -现在可以在你的服务中注入并使用`IRepository` 或 `IQueryableRepository`了.比如你有一个主键类型为`Guid`的`Book`实体: - -```csharp -public class Book : AggregateRoot -{ - public string Name { get; set; } - - public BookType Type { get; set; } -} -``` - -(`BookType`是个枚举)你想在[领域服务](Domain-Services.md)中创建一个`Book`实体: - -```csharp -public class BookManager : DomainService -{ - private readonly IRepository _bookRepository; - - public BookManager(IRepository bookRepository) //注入默认的仓储 - { - _bookRepository = bookRepository; - } - - public async Task CreateBook(string name, BookType type) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var book = new Book - { - Id = GuidGenerator.Create(), - Name = name, - Type = type - }; - - await _bookRepository.InsertAsync(book); - //使用仓储中的方法 - - return book; - } -} -``` - -这是一个使用`InsertAsync`方法将一个实体插入到数据库的例子. - -#### 添加自定义仓储 - -大多数情况下默认的泛型仓储已经足够用了(因为它们实现了`IQueryable`).然而,你可能需要创建自定义的仓库并添加自己的仓储方法. - -比如你想要通过books类型删除书籍.建议像下面这样为你的仓储定义一个接口: - -```csharp -public interface IBookRepository : IRepository -{ - Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken) - ); -} -``` - -通常你希望从`IRepository`中继承标准的仓储方法.其实,你不必那么做.仓储接口定义在领域层,在数据层/基础设施层实现.([启动模板](https://abp.io/Templates)中的`MongoDB`项目) - -实现`IBookRepository`接口的例子: - -```csharp -public class BookRepository : - MongoDbRepository, - IBookRepository -{ - public BookRepository(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task DeleteBooksByType( - BookType type, - CancellationToken cancellationToken = default(CancellationToken)) - { - await Collection.DeleteManyAsync( - Builders.Filter.Eq(b => b.Type, type), - cancellationToken - ); - } -} -``` - -现在,就能在需要的时候[注入](Dependency-Injection.md)`IBookRepository`并使用`DeleteBooksByType`方法了. - -##### 重写默认的泛型仓储 - -即使你创建了自定义仓储,你仍然可以注入默认的泛型仓储(本例中的`IRepository`).默认的仓储实现不会使用你创建的类. - -如果你想用自定义的仓储替换默认的仓储实现,在`AddMongoDbContext`中做: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - options.AddRepository(); //替换 IRepository -}); -``` - -当你想**重写基础仓储方法**时,这一点尤为重要.例如,你想要重写`DeleteAsync`方法,以便更有效的删除实体: - -```csharp -public async override Task DeleteAsync( - Guid id, - bool autoSave = false, - CancellationToken cancellationToken = default) -{ - //TODO: 自定义实现删除方法 -} -``` - -#### 访问MongoDB API - -大多数情况下,你想要将MongoDB API隐藏在仓储后面(这是仓储的主要目的).如果你想在仓储之上访问MongoDB API,你可以使用`GetDatabaseAsync()`, `GetAggregateAsync()` 或`GetCollectionAsync()`方法.例如: - -```csharp -public class BookService -{ - private readonly IRepository _bookRepository; - - public BookService(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task FooAsync() - { - IMongoDatabase database = await _bookRepository.GetDatabaseAsync(); - IMongoCollection books = await _bookRepository.GetCollectionAsync(); - IAggregateFluent bookAggregate = await _bookRepository.GetAggregateAsync(); - } -} -``` - -> 重要:如果你想访问MongoDB API,你需要在你的项目中引用`Volo.Abp.MongoDB`.这会破坏封装,但在这种情况下,这就是你想要的. - -#### 事务 - -MongoDB在4.0版本开始支持事务, ABP在3.2版本加入了对MongoDb事务的支持. 如果你升级到3.2版本,需要将[MongoDbSchemaMigrator](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.MongoDB/MongoDb/MongoDbMyProjectNameDbSchemaMigrator.cs)添加到你的 `.MongoDB` 项目中. - -[启动模板](Startup-Templates/Index.md)默认在 `.MongoDB` 项目中**禁用**了工作单元事务. 如果你的MongoDB服务器支持事务,你可以手动启用工作单元的事务: - -```csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled; -}); -``` - -#### 高级主题 - -##### 设置默认的仓储类 - -默认的泛型仓储默认被`MongoDbRepository`类实现.你可以创建自己的实现并在默认的仓储中使用. - -首先,像下面这样定义你的仓储类: - -```csharp -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} - -public class MyRepositoryBase - : MongoDbRepository - where TEntity : class, IEntity -{ - public MyRepositoryBase(IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -第一个是[复合主键的实体](Entities.md),第二个是只有一个主键的实体. - -如果需要重写方法建议继承`MongoDbRepository`类,否则,你需要手动实现所有的仓储方法. - -现在,你可以使用`SetDefaultRepositoryClasses`: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.SetDefaultRepositoryClasses( - typeof(MyRepositoryBase<,>), - typeof(MyRepositoryBase<>) - ); - //... -}); -``` - -##### 为默认的仓储设置基类或接口 - -如果你的MongoDbContext继承自另一个MongoDbContext或者实现了某个接口,你可以使用这个基类或者接口作为默认仓储的类型.如: -```csharp -public interface IBookStoreMongoDbContext : IAbpMongoDbContext -{ - Collection Books { get; } -} -``` - -`IBookStoreMongoDbContext`被`BookStoreMongoDbContext`类实现.然后你就可以在`AddDefaultRepositories`中使用: - -```csharp -context.Services.AddMongoDbContext(options => -{ - options.AddDefaultRepositories(); - //... -}); -``` - -现在,你自定义的`BookRepository`类也可以使用`IBookStoreMongoDbContext`接口: - -```csharp -public class BookRepository - : MongoDbRepository, - IBookRepository -{ - //... -} -``` - -为MongoDbContext使用接口的优点就是它可以被另一个实现替换. - -##### 替换其他的DbContexts - -一旦你正确定义并为MongoDbContext使用了接口,任何其他实现都可以使用以下方法替换它: - -**ReplaceDbContextAttribute** - -```csharp -[ReplaceDbContext(typeof(IBookStoreMongoDbContext))] -public class OtherMongoDbContext : AbpMongoDbContext, IBookStoreMongoDbContext -{ - //... -} -``` - -**ReplaceDbContext option** - -```csharp -context.Services.AddMongoDbContext(options => -{ - //... - options.ReplaceDbContext(); -}); -``` - -这个例子中,`OtherMongoDbContext`实现了`IBookStoreMongoDbContext`.这个特性允许你在发开的时候使用多个MongoDbContext(每个模块一个),但是运行的时候只能使有一个MongoDbContext(实现所有MongoDbContexts的所有接口) diff --git a/docs/zh-Hans/Moodule-Entity-Extensions.md b/docs/zh-Hans/Moodule-Entity-Extensions.md deleted file mode 100644 index 6e8c73c2a2..0000000000 --- a/docs/zh-Hans/Moodule-Entity-Extensions.md +++ /dev/null @@ -1,3 +0,0 @@ -# Module Entity Extensions - -参阅 https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions (文档会在近期完成). \ No newline at end of file diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md deleted file mode 100644 index 6db625e7d9..0000000000 --- a/docs/zh-Hans/Multi-Tenancy.md +++ /dev/null @@ -1,386 +0,0 @@ -## 多租户 - -ABP的多租户模块提供了创建多租户应用程序的基本功能. - -维基百科中是这样[定义](https://en.wikipedia.org/wiki/Multitenancy)多租户的: - -> 软件多租户技术指的是一种软件架构,这种架构可以使用软件的单实例运行并为多个租户提供服务.租户是通过软件实例的特定权限共享通用访问的一组用户.使用多租户架构,软件应用为每个租户提供实例的专用共享,包括实例的数据、配置、用户管理、租户的私有功能和非功能属性.多租户与多实例架构形成对比,将软件实例的行为根据不同的租户分割开来. - -### Volo.Abp.MultiTenancy - -Volo.Abp.MultiTenancy"multi-tenancy ready",使用包管理器控制台(PMC)将它安装到你的项目中: - -```` -Install-Package Volo.Abp.MultiTenancy -```` - -> 这个包默认安装在了快速启动模板中.所以,大多数情况下,你不需要手动安装它. - -然后你可以添加 **AbpMultiTenancyModule** 依赖到你的模块: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule - { - //... - } -} -```` - -> 随着"Multi-tenancy ready"的概念,我们打算开发我们的代码和多租户方法兼容.然后它可以被用于多租户和非多租户的程序中,这取决于最终程序的需求. - -### AbpMultiTenancyOptions: 处理不活跃或不存在的租户 - -`MultiTenancyMiddlewareErrorPageBuilder` 或 `AbpMultiTenancyOptions` 用于 处理不活跃或不存在的租户. - -默认情况下会响应错误页面, 你可以根据自己的需要更改它, 比如: 只输出错误日志并继续ASP NET Core的请求管道 - -```csharp -Configure(options => -{ - options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) => - { - // Handle the exception. - }; -}); -``` - -#### 定义实体 - -你可以在你的实体中实现 **IMultiTenant** 接口来实现多租户,例如: - -````C# -using System; -using Volo.Abp.Domain.Entities; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - public class Product : AggregateRoot, IMultiTenant - { - public Guid? TenantId { get; set; } //IMultiTenant 定义了 TenantId 属性 - - public string Name { get; set; } - - public float Price { get; set; } - } -} -```` - -实现IMultiTenant接口,需要在实体中定义一个 **TenantId** 的属性(查看更多有关[实体](Entities.md)的文档) - -#### 获取当前租户的Id - -你的代码中可能需要获取当前租户的Id(先不管它具体是怎么取得的).对于这种情况你可以[注入](Dependency-Injection.md)并使用 **ICurrentTenant** 接口.例如: - -````C# -using Volo.Abp.DependencyInjection; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - public class MyService : ITransientDependency - { - private readonly ICurrentTenant _currentTenant; - - public MyService(ICurrentTenant currentTenant) - { - _currentTenant = currentTenant; - } - - public void DoIt() - { - var tenantId = _currentTenant.Id; - //在你的代码中使用tenantId - } - } -} -```` - -#### 改变当前租户 - -TODO: ... - -#### 确定当前租户 - -多租户的应用程序运行的时候首先要做的就是确定当前租户. -Volo.Abp.MultiTenancy只提供了用于确定当前租户的抽象(称为租户解析器),但是并没有现成的实现. - -**Volo.Abp.AspNetCore.MultiTenancy**已经实现了从当前Web请求(从子域名,请求头,cookie,路由...等)中确定当前租户.本文后面会介绍Volo.Abp.AspNetCore.MultiTenancy. - -##### 自定义租户解析器 - -你可以像下面这样,在你模块的ConfigureServices方法中将自定义解析器并添加到 **AbpTenantResolveOptions**中: - -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.TenantResolvers.Add(new MyCustomTenantResolveContributor()); - }); - - //... - } - } -} -```` - -`MyCustomTenantResolveContributor`必须像下面这样实现**ITenantResolveContributor**接口: - -````C# -using System.Threading.Tasks; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - public class MyCustomTenantResolveContributor : ITenantResolveContributor - { - public override Task ResolveAsync(ITenantResolveContext context) - { - context.TenantIdOrName = ... //从其他地方获取租户id或租户名字... - } - } -} -```` - -如果能确定租户id或租户名字可以在租户解析器中设置 **TenantIdOrName**.如果不能确定,那就空着让下一个解析器来确定它. - -#### 租户存储 - -Volo.Abp.MultiTenancy中定义了 **ITenantStore** 从框架中抽象数据源.你可以实现ITenantStore,让它跟任何存储你租户的数据源(例如关系型数据库)一起工作. - - -##### 配置数据存储 - -有一个内置的(默认的)租户存储,叫ConfigurationTenantStore.它可以被用于存储租户,通过标准的[配置系统](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)(使用[Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration)).因此,你可以通过硬编码或者在appsettings.json文件中定义租户. - -###### 例子:硬编码定义租户 - -````C# -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Data; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.Tenants = new[] - { - new TenantConfiguration( - Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id - "tenant1" //Name - ), - new TenantConfiguration( - Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id - "tenant2" //Name - ) - { - //tenant2 有单独的数据库连接字符串 - ConnectionStrings = - { - {ConnectionStrings.DefaultConnectionStringName, "..."} - } - } - }; - }); - } - } -} -```` - -###### 例子:appsettings.json定义租户 - -首先从appsetting.json文件中创建你的配置. - -````C# -using System.IO; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = BuildConfiguration(); - - Configure(configuration); - } - - private static IConfigurationRoot BuildConfiguration() - { - return new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .Build(); - } - } -} -```` - -然后在appsettings.json中添加 "**Tenants**" 节点: - -````json -"Tenants": [ - { - "Id": "446a5211-3d72-4339-9adc-845151f8ada0", - "Name": "tenant1" - }, - { - "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", - "Name": "tenant2", - "ConnectionStrings": { - "Default": "...write tenant2's db connection string here..." - } - } - ] -```` - -##### Volo.Abp... Package (TODO) - -TODO: This package implements ITenantStore using a real database... - -#### 租户信息 - -ITenantStore跟 **TenantConfiguration**类一起工作,并且包含了几个租户属性: - -* **Id**:租户的唯一Id. -* **Name**: 租户的唯一名称. -* **ConnectionStrings**:如果这个租户有专门的数据库来存储数据.它可以提供数据库的字符串(它可以具有默认的连接字符串和每个模块的连接字符串). - - -多租户应用程序可能需要其他租户属性,但这些属性是框架与多个租户一起使用的最低要求. - -#### 代码中改变租户 - -TODO... - -### Volo.Abp.AspNetCore.MultiTenancy - -Volo.Abp.AspNetCore.MultiTenancy将多租户整合到了ASP.NET Core的程序中.在PMC中使用下面的代码将它安装到项目中. - -```` -Install-Package Volo.Abp.AspNetCore.MultiTenancy -```` - -然后添加 **AbpAspNetCoreMultiTenancyModule** 依赖到你的模块: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))] - public class MyModule : AbpModule - { - //... - } -} -```` - -#### 多租户中间件 - -Volo.Abp.AspNetCore.MultiTenancy包含了多租户中间件... - -````C# -app.UseMultiTenancy(); -```` - -TODO:... - -#### 从Web请求中确定当前租户 - -Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前Web请求(按优先级排序)中确定当前租户. - -* **CurrentUserTenantResolveContributor**: 如果当前用户已登录,从当前用户的声明中获取租户Id. **出于安全考虑,应该始终将其做为第一个Contributor**. -* **QueryStringTenantResolveContributor**: 尝试从query string参数中获取当前租户,默认参数名为"__tenant". -* **RouteTenantResolveContributor**:尝试从当前路由中获取(URL路径),默认是变量名是"__tenant".所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户. -* **HeaderTenantResolveContributor**: 尝试从HTTP header中获取当前租户,默认的header名称是"__tenant". -* **CookieTenantResolveContributor**: 尝试从当前cookie中获取当前租户.默认的Cookie名称是"__tenant". - -> 如果你使用nginx作为反向代理服务器,请注意如果`TenantKey`包含下划线或其他特殊字符可能存在问题, 请参考: -http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers -http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers - -可以使用AbpAspNetCoreMultiTenancyOptions修改默认的参数名"__tenant".例如: - -````C# -services.Configure(options => -{ - options.TenantKey = "MyTenantKey"; -}); -```` - -##### 域名租户解析器 - -实际项目中,大多数情况下你想通过子域名(如mytenant1.mydomain.com)或全域名(如mytenant.com)中确定当前租户.如果是这样,你可以配置AbpTenantResolveOptions添加一个域名租户解析器. - -###### 例子:添加子域名解析器 - -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AspNetCore.MultiTenancy; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后) - options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com")); - }); - - //... - } - } -} -```` - -{0}是用来确定当前租户唯一名称的占位符. - -你可以使用下面的方法,代替``options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));``: - -````C# -options.AddDomainTenantResolver("{0}.mydomain.com"); -```` - -###### 例子:添加全域名解析器 - -````C# -options.AddDomainTenantResolver("{0}.com"); -```` - diff --git a/docs/zh-Hans/Nightly-Builds.md b/docs/zh-Hans/Nightly-Builds.md deleted file mode 100644 index 14a5b8ad8c..0000000000 --- a/docs/zh-Hans/Nightly-Builds.md +++ /dev/null @@ -1,19 +0,0 @@ -# 每日构建 - -所有框架和模块包每晚都部署到MyGet. 因此你可以使用或测试最新的代码,而无需等待下一个版本. - -## 安装和卸载每晚预览包 - -可以通过在应用程序的根文件夹中运行以下命令安装最新版本的夜间预览软件包: - -```bash -abp switch-to-nightly -``` - -如果你正在使用ABP框架每晚预览包,你可以使用此命令切换回稳定版本: - -```bash -abp switch-to-stable -``` - -参阅 [ABP CLI 文档](./CLI.md) 了解更多信息. \ No newline at end of file diff --git a/docs/zh-Hans/Object-Extensions.md b/docs/zh-Hans/Object-Extensions.md deleted file mode 100644 index 22f4ae9322..0000000000 --- a/docs/zh-Hans/Object-Extensions.md +++ /dev/null @@ -1,405 +0,0 @@ -# 对象扩展 - -ABP框架提供了 **实体扩展系统** 允许你 **添加额外属性** 到已存在的对象 **无需修改相关类**. 它允许你扩展[应用程序依赖模块](Modules/Index.md)实现的功能,尤其是当你要扩展[模块定义的实体](Customizing-Application-Modules-Extending-Entities.md)和[DTO](Customizing-Application-Modules-Overriding-Services.md)时. - -> 你自己的对象通常不需要对象扩展系统,因为你可以轻松的添加常规属性到你的类中. - -## IHasExtraProperties 接口 - -这是一个使类可扩展的接口. 它定义了 `Dictionary` 属性: - -````csharp -Dictionary ExtraProperties { get; } -```` - -然后你可以使用此字典添加或获取其他属性. - -### 基类 - -默认以下基类实现了 `IHasExtraProperties` 接口: - -* 由 `AggregateRoot` 类实现 (参阅 [entities](Entities.md)). -* 由 `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... [DTO](Data-Transfer-Objects.md)基类实现. -* 由 `ExtensibleObject` 实现, 它是一个简单的基类,任何类型的对象都可以继承. - -如果你的类从这些类继承,那么你的类也是可扩展的,如果没有,你也可以随时手动继承. - -### 基本扩展方法 - -虽然可以直接使用类的 `ExtraProperties` 属性,但建议使用以下扩展方法使用额外属性. - -#### SetProperty - -用于设置额外属性值: - -````csharp -user.SetProperty("Title", "My Title"); -user.SetProperty("IsSuperUser", true); -```` - -`SetProperty` 返回相同的对象, 你可以使用链式编程: - -````csharp -user.SetProperty("Title", "My Title") - .SetProperty("IsSuperUser", true); -```` - -#### GetProperty - -用于读取额外属性的值: - -````csharp -var title = user.GetProperty("Title"); - -if (user.GetProperty("IsSuperUser")) -{ - //... -} -```` - -* `GetProperty` 是一个泛型方法,对象类型做为泛型参数. -* 如果未设置给定的属性,则返回默认值 (`int` 的默认值为 `0` , `bool` 的默认值是 `false` ... 等). - -##### 非基本属性类型 - -如果你的属性类型不是原始类型(int,bool,枚举,字符串等),你需要使用 `GetProperty` 的非泛型版本,它会返回 `object`. - -#### HasProperty - -用于检查对象之前是否设置了属性. - -#### RemoveProperty - -用于从对象中删除属性. 使用此方法代替为属性设置 `null` 值. - -### 一些最佳实践 - -为属性名称使用魔术字符串很危险,因为你很容易输入错误的属性名称-这并不安全; - -* 为你的额外属性名称定义一个常量. -* 使用扩展方法轻松设置你的属性. - -示例: - -````csharp -public static class IdentityUserExtensions -{ - private const string TitlePropertyName = "Title"; - - public static void SetTitle(this IdentityUser user, string title) - { - user.SetProperty(TitlePropertyName, title); - } - - public static string GetTitle(this IdentityUser user) - { - return user.GetProperty(TitlePropertyName); - } -} -```` - -然后, 你可以很容易地设置或获取 `Title` 属性: - -````csharp -user.SetTitle("My Title"); -var title = user.GetTitle(); -```` - -## Object Extension Manager - -你可以为可扩展对象(实现 `IHasExtraProperties`接口)设置任意属性, `ObjectExtensionManager` 用于显式定义可扩展类的其他属性. - -显式定义额外的属性有一些用例: - -* 允许控制如何在对象到对象的映射上处理额外的属性 (参阅下面的部分). -* 允许定义属性的元数据. 例如你可以在使用[EF Core](Entity-Framework-Core.md)时将额外的属性映射到数据库中的表字段. - -> `ObjectExtensionManager` 实现单例模式 (`ObjectExtensionManager.Instance`) ,你应该在应用程序启动之前定义对象扩展. [应用程序启动模板](Startup-Templates/Application.md) 有一些预定义的静态类,可以安全在内部定义对象扩展. - -### AddOrUpdate - -`AddOrUpdate` 是定义对象额外属性或更新对象额外属性的主要方法. - -示例: 为 `IdentityUser` 实体定义额外属性: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdate(options => - { - options.AddOrUpdateProperty("SocialSecurityNumber"); - options.AddOrUpdateProperty("IsSuperUser"); - } - ); -```` - -### AddOrUpdateProperty - -虽然可以如上所示使用 `AddOrUpdateProperty`, 但如果要定义单个额外的属性,也可以使用快捷的扩展方法: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty("SocialSecurityNumber"); -```` - -有时将单个额外属性定义为多种类型是可行的. 你可以使用以下代码,而不是一个一个地定义: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - new[] - { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), - typeof(IdentityUserUpdateDto) - }, - "SocialSecurityNumber" - ); -```` - -#### 属性配置 - -`AddOrUpdateProperty` 还可以为属性定义执行其他配置的操作. - -Example: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.CheckPairDefinitionOnMapping = false; - }); -```` - -> 参阅 "对象到对象映射" 部分了解 `CheckPairDefinitionOnMapping` 选项. - -`options` 有一个名为 `Configuration` 的字典,该字典存储对象扩展定义甚至可以扩展. EF Core使用它来将其他属性映射到数据库中的表字段. 请参阅[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md). - -#### 默认值 - -自动为新属性设置默认值,默认值是属性类型的自然默认值,例如: `string`: `null` , `bool`: `false` 或 `int`: `0`. - -有两种方法可以覆盖默认值: - -##### DefaultValue 选项 - -`DefaultValue` 选项可以设置任何值: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "MyIntProperty", - options => - { - options.DefaultValue = 42; - }); -```` - -##### DefaultValueFactory 选项 - -`DefaultValueFactory` 可以设置返回默认值的函数: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "MyDateTimeProperty", - options => - { - options.DefaultValueFactory = () => DateTime.Now; - }); -```` - -`options.DefaultValueFactory` 比 `options.DefaultValue` 优先级要高. - -> 提示: 只有在默认值可能发生变化时(如示例中的`DateTime.Now;`) 才使用 `DefaultValueFactory`,如果是一个常量请使用 `DefaultValue` 选项. - -#### CheckPairDefinitionOnMapping - -控制在映射两个可扩展对象时如何检查属性定义. 请参阅*对象到对象映射*部分,了解 `CheckPairDefinitionOnMapping` 选项. - -## Validation - -你可能要为你定义的额外属性添加一些 **验证规则**. `AddOrUpdateProperty` 方法选项允许进行验证的方法有两种: - -1. 你可以为属性添加 **数据注解 attributes**. -2. 你可以给定一个action(代码块)执行 **自定义验证**. - -当你在**自动验证**的方法(例如:控制器操作,页面处理程序方法,应用程序服务方法...)中使用对象时,验证会工作. 因此,每当扩展对象被验证时,所有额外的属性都会被验证. - -### 数据注解 Attributes - -所有标准的数据注解Attributes对于额外属性都是有效的. 例: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.Attributes.Add(new RequiredAttribute()); - options.Attributes.Add( - new StringLengthAttribute(32) { - MinimumLength = 6 - } - ); - }); -```` - -使用以上配置,如果没有提供有效的 `SocialSecurityNumber` 值, `IdentityUserCreateDto` 对象将是无效的. - -### 自定义验证 - -如果需要,可以添加一个自定义action验证额外属性. 例: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.Validators.Add(context => - { - var socialSecurityNumber = context.Value as string; - - if (socialSecurityNumber == null || - socialSecurityNumber.StartsWith("X")) - { - context.ValidationErrors.Add( - new ValidationResult( - "Invalid social security number: " + socialSecurityNumber, - new[] { "SocialSecurityNumber" } - ) - ); - } - }); - }); -```` - -`context.ServiceProvider` 可以解析服务. - -除了为单个属性添加自定义验证逻辑外,还可以添加在对象级执行的自定义验证逻辑. 例: - -````csharp -ObjectExtensionManager.Instance -.AddOrUpdate(objConfig => -{ - //Define two properties with their own validation rules - - objConfig.AddOrUpdateProperty("Password", propertyConfig => - { - propertyConfig.Attributes.Add(new RequiredAttribute()); - }); - - objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig => - { - propertyConfig.Attributes.Add(new RequiredAttribute()); - }); - - //Write a common validation logic works on multiple properties - - objConfig.Validators.Add(context => - { - if (context.ValidatingObject.GetProperty("Password") != - context.ValidatingObject.GetProperty("PasswordRepeat")) - { - context.ValidationErrors.Add( - new ValidationResult( - "Please repeat the same password!", - new[] { "Password", "PasswordRepeat" } - ) - ); - } - }); -}); -```` - -## 对象到对象映射 - -假设你已向可扩展的实体对象添加了额外的属性并使用了自动[对象到对象的映射](Object-To-Object-Mapping.md)将该实体映射到可扩展的DTO类. 在这种情况下你需要格外小心,因为额外属性可能包含**敏感数据**,这些数据对于客户端不可用. - -本节提供了一些**好的做法**,可以控制对象映射的额外属性. - -### MapExtraPropertiesTo - -`MapExtraPropertiesTo` 是ABP框架提供的扩展方法,用于以受控方式将额外的属性从一个对象复制到另一个对象. 示例: - -````csharp -identityUser.MapExtraPropertiesTo(identityUserDto); -```` - -`MapExtraPropertiesTo` 需要在**两侧**(本例中是`IdentityUser` 和 `IdentityUserDto`)**定义属性**. 以将值复制到目标对象. 否则即使源对象(在此示例中为 `identityUser` )中确实存在该值,它也不会复制. 有一些重载此限制的方法. - -#### MappingPropertyDefinitionChecks - -`MapExtraPropertiesTo` 获取一个附加参数来控制单个映射操作的定义检查: - -````csharp -identityUser.MapExtraPropertiesTo( - identityUserDto, - MappingPropertyDefinitionChecks.None -); -```` - -> 要小心,因为 `MappingPropertyDefinitionChecks.None` 会复制所有的额外属性而不进行任何检查. `MappingPropertyDefinitionChecks` 枚举还有其他成员. - -如果要完全禁用属性的定义检查,可以在定义额外的属性(或更新现有定义)时进行,如下所示: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.CheckPairDefinitionOnMapping = false; - }); -```` - -#### 忽略属性 - -你可能要在映射操作忽略某些属性: - -````csharp -identityUser.MapExtraPropertiesTo( - identityUserDto, - ignoredProperties: new[] {"MySensitiveProp"} -); -```` - -忽略的属性不会复制到目标对象. - -#### AutoMapper集成 - -如果你使用的是[AutoMapper](https://automapper.org/)库,ABP框架还提供了一种扩展方法来利用上面定义的 `MapExtraPropertiesTo` 方法. - -你可以在映射配置文件中使用 `MapExtraProperties()` 方法. - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .MapExtraProperties(); - } -} -```` - -它与 `MapExtraPropertiesTo()` 方法具有相同的参数. - -## Entity Framework Core 数据库映射 - -如果你使用的是EF Core,可以将额外的属性映射到数据库中的表字段. 例: - -````csharp -ObjectExtensionManager.Instance - .AddOrUpdateProperty( - "SocialSecurityNumber", - options => - { - options.MapEfCore(b => b.HasMaxLength(32)); - } - ); -```` - -参阅 [Entity Framework Core 集成文档](Entity-Framework-Core.md) 了解更多内容. \ No newline at end of file diff --git a/docs/zh-Hans/Object-To-Object-Mapping.md b/docs/zh-Hans/Object-To-Object-Mapping.md deleted file mode 100644 index 62c1d17add..0000000000 --- a/docs/zh-Hans/Object-To-Object-Mapping.md +++ /dev/null @@ -1,327 +0,0 @@ -# 对象到对象映射 - -将对象映射到另一个对象是常用并且繁琐重复的工作,大部分情况下两个类都具有相同或相似的属性. 例如下面的 [应用服务](Application-Services.md)方法: - -```csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task CreateUser(CreateUserInput input) - { - //Manually creating a User object from the CreateUserInput object - var user = new User - { - Name = input.Name, - Surname = input.Surname, - EmailAddress = input.EmailAddress, - Password = input.Password - }; - - await _userRepository.InsertAsync(user); - } -} -``` - -`CreateUserInput` 和 `User` 是一个简单的[DTO](Data-Transfer-Objects.md)和[实体](Entities.md)类. 上面的代码使用input对象创建了一个 `User` 实体. 上面的代码很简单,但在实际应用程序中 `User` 实体会拥有很多属性,手动创建实体乏味且容易出错. `User` 和 `CreateUserInput` 添加新属性时还需要再去修改代码. - -我们需要一个库自动处理类到类的映射. ABP提供了对象到对象映射的抽象并集成了[AutoMapper](http://automapper.org/)做为对象映射器. - -## IObjectMapper - -`IObjectMapper` 接口 (在 [Volo.Abp.ObjectMapping](https://www.nuget.org/packages/Volo.Abp.ObjectMapping)包中) 定义了一个简单的 `Map` 方法. 上面的手动映射示例可以用以下方式重写: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task CreateUser(CreateUserInput input) - { - //Automatically creating a new User object using the CreateUserInput object - var user = ObjectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -> 示例中的 `ObjectMapper` 属性在 `ApplicationService` 基类中属性注入. 在其他地方也可以直接注入 `IObjectMapper` 接口. - -Map方法有两个泛型参数: 第一个是源对象类型,第二个是目标对象类型. - -如果想要设置现有对象属性,可以使用 `Map` 的重载方法: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task UpdateUserAsync(Guid id, UpdateUserInput input) - { - var user = await _userRepository.GetAsync(id); - - //Automatically set properties of the user object using the UpdateUserInput - ObjectMapper.Map(input, user); - - await _userRepository.UpdateAsync(user); - } -} -```` - -必须先定义映射,然后才能映射对象. 请参阅AutoMapper集成部分了解如何定义映射. - -## AutoMapper 集成 - -[AutoMapper](http://automapper.org/) 是最流行的对象到对象映射库之一. [Volo.Abp.AutoMapper](https://www.nuget.org/packages/Volo.Abp.AutoMapper)程序包使用AutoMapper实现了 `IObjectMapper`. - -定义了以下部分的映射后就可以使 `IObjectMapper` 接口. - -### 定义映射 - -AutoMapper提供了多种定义类之间映射的方法. 有关详细信息请参阅[AutoMapper的文档](https://docs.automapper.org). - -其中定义一种映射的方法是创建一个[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) 类. 例如: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap(); - } -} -```` - -然后使用`AbpAutoMapperOptions`注册配置文件: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Add all mappings defined in the assembly of the MyModule class - options.AddMaps(); - }); - } -} -```` - -`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). - -### 配置验证 - -`AddMaps` 使用可选的 `bool` 参数控制[模块](Module-Development-Basics.md)的[配置验证](https://docs.automapper.org/en/stable/Configuration-validation.html): - -````csharp -options.AddMaps(validate: true); -```` - -如果此选项默认是 `false` , 但最佳实践建议启用. - -可以使用 `AddProfile` 而不是 `AddMaps` 来控制每个配置文件类的配置验证: - -````csharp -options.AddProfile(validate: true); -```` - -> 如果你有多个配置文件,并且只需要为其中几个启用验证,那么首先使用`AddMaps`而不进行验证,然后为你想要验证的每个配置文件使用`AddProfile`. - -### 映射对象扩展 - -[对象扩展系统](Object-Extensions.md) 允许为已存在的类定义额外属性. ABP 框架提供了一个映射定义扩展可以正确的映射两个对象的额外属性. - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .MapExtraProperties(); - } -} -```` - -如果两个类都是可扩展对象(实现了 `IHasExtraProperties` 接口),建议使用 `MapExtraProperties` 方法. 更多信息请参阅[对象扩展文档](Object-Extensions.md). - -### 其他有用的扩展方法 - -有一些扩展方法可以简化映射代码. - -#### 忽视审计属性 - -当你将一个对象映射到另一个对象时,通常会忽略审核属性. - -假设你需要将 `ProductDto` ([DTO](Data-Transfer-Objects.md))映射到Product[实体](Entities.md),该实体是从 `AuditedEntity` 类继承的(该类提供了 `CreationTime`, `CreatorId`, `IHasModificationTime` 等属性). - -从DTO映射时你可能想忽略这些基本属性,可以使用 `IgnoreAuditedObjectPropertie()` 方法忽略所有审计属性(而不是手动逐个忽略它们): - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .IgnoreAuditedObjectProperties(); - } -} -```` - -还有更多扩展方法, 如 `IgnoreFullAuditedObjectProperties()` 和 `IgnoreCreationAuditedObjectProperties()`,你可以根据实体类型使用. - -> 请参阅[实体文档](Entities.md)中的"*基类和接口的审计属性*"部分了解有关审计属性的更多信息。 - -#### 忽视其他属性 - -在AutoMapper中,通常可以编写这样的映射代码来忽略属性: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .ForMember(x => x.CreationTime, map => map.Ignore()); - } -} -```` - -我们发现它的长度是不必要的并且创建了 `Ignore()` 扩展方法: - -````csharp -public class MyProfile : Profile -{ - public MyProfile() - { - CreateMap() - .Ignore(x => x.CreationTime); - } -} -```` - -## 高级主题 - -### IObjectMapper 接口 - -假设你已经创建了一个**可重用的模块**,其中定义了AutoMapper配置文件,并在需要映射对象时使用 `IObjectMapper`. 根据[模块化](Module-Development-Basics.md)的性质,你的模块可以用于不同的应用程序. - -`IObjectMapper` 是一个抽象,可以由最终应用程序替换使用另一个映射库. 这里的问题是你的可重用模块设计为使用AutoMapper,因为它为其定义映射配置文件. 这种情况下即使最终应用程序使用另一个默认对象映射库,你也要保证模块始终使用AutoMapper. - -`IObjectMapper`将对象映射器上下文化,你可以为不同的 模块/上下文 使用不同的库. - -用法示例: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - private readonly IObjectMapper _objectMapper; - - public UserAppService( - IRepository userRepository, - IObjectMapper objectMapper) //Inject module specific mapper - { - _userRepository = userRepository; - _objectMapper = objectMapper; - } - - public async Task CreateUserAsync(CreateUserInput input) - { - //Use the module specific mapper - var user = _objectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -`UserAppService` 注入 `IObjectMapper`, 它是模块的特定对象映射器,用法与 `IObjectMapper` 完全相同. - -上面的示例代码未使用 `ApplicationService` 中定义的 `ObjectMapper` 属性,而是注入了 `IObjectMapper`. 但是 `ApplicationService` 定义了可以在类构造函数中设置的 `ObjectMapperContext` 属性, 因此仍然可以使用基类属性. 示例可以进行以下重写: - -````csharp -public class UserAppService : ApplicationService -{ - private readonly IRepository _userRepository; - - public UserAppService(IRepository userRepository) - { - _userRepository = userRepository; - //Set the object mapper context - ObjectMapperContext = typeof(MyModule); - } - - public async Task CreateUserAsync(CreateUserInput input) - { - var user = ObjectMapper.Map(input); - - await _userRepository.InsertAsync(user); - } -} -```` - -虽然使用上下文化的对象映射器与普通的对象映射器相同, 但是也应该在模块的 `ConfigureServices` 方法中注册上下文化的映射器: - -````csharp -[DependsOn(typeof(AbpAutoMapperModule))] -public class MyModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Use AutoMapper for MyModule - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddMaps(validate: true); - }); - } -} -```` - -`IObjectMapper`是可重用模块的一项基本功能,可在多个应用程序中使用,每个模块可以使用不同的库进行对象到对象的映射. 所有预构建的ABP模块都在使用它. 但是对于最终应用程序,你可以忽略此接口,始终使用默认的 `IObjectMapper` 接口. - -### IObjectMapper 接口 - -ABP允许自定义特定类的映射代码. 假设你要创建一个自定义类从 `User` 映射到 `UserDto`. 这种情况下,你可以创建一个实现 `IObjectMapper`的类 : - -````csharp -public class MyCustomUserMapper : IObjectMapper, ITransientDependency -{ - public UserDto Map(User source) - { - //TODO: Create a new UserDto - } - - public UserDto Map(User source, UserDto destination) - { - //TODO: Set properties of an existing UserDto - return destination; - } -} -```` - -ABP会自动发现注册 `MyCustomUserMapper`, 在你使用IObjectMapper将用户映射到UserDto时会自动使用自定义映射. - -一个类可以为不同的对象实现多个 `IObjectMapper`. - -> 这种方法功能强大, `MyCustomUserMapper`可以注入任何其他服务并在Map方法中使用. \ No newline at end of file diff --git a/docs/zh-Hans/Options.md b/docs/zh-Hans/Options.md deleted file mode 100644 index 5bb4b29a8b..0000000000 --- a/docs/zh-Hans/Options.md +++ /dev/null @@ -1,121 +0,0 @@ -# 选项 - -微软引入[选项模式](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options),它是用于配置框架服务使用的设置. 选项模式由[Microsoft.Extensions.Options](https://www.nuget.org/packages/Microsoft.Extensions.Options)NuGet包实现,除了ASP.NET Core应用,它还适用于任何类型的应用程序. - -ABP框架遵循选项模式,并定义了用于配置框架和模块的选项类(在相关功能文档中有详细的说明). - -由于[微软的文档](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options)详细解释了选项模式,本文中只会介绍ABP增加的一些功能. - -## 配置选项 - -通常配置选项在 `Startup` 类的 `ConfigureServices` 方法中. 但由于ABP框架提供了模块化基础设施,因此你可以在[模块](Module-Development-Basics.md)的`ConfigureServices` 方法配置选项. -例: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.IsEnabled = false; - }); -} -```` - -* `AbpAuditingOptions` 是一个简单的类,定义了一些属性,例如这里使用的 `IsEnabled`. -* `AbpModule` 基类定义 `Configure` 方法简化代码. 你可以直接使用 `Configure<...>`,而不是`context.Services.Configure <...>`. - -如果你正在开发一个可重用的模块,你可能需要定义一个允许开发人员配置模块的选项类. 这时定义一个如下所示的普通类: - -````csharp -public class MyOptions -{ - public int Value1 { get; set; } - public bool Value2 { get; set; } -} -```` - -然后开发人员可以像上面 `AbpAuditingOptions` 示例一样配置你的选项: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.Value1 = 42; - options.Value2 = true; - }); -} -```` - -* 在本示例中,使用了简化的 `Configure<...>`方法. - -### 获取选项值 - -在你需要获得一个选项值时,将 `IOptions` 服务[注入](Dependency-Injection.md)到你的类中,使用它的 `.Value` 属性得到值. -例: - -````csharp -public class MyService : ITransientDependency -{ - private readonly MyOptions _options; - - public MyService(IOptions options) - { - _options = options.Value; //Notice the options.Value usage! - } - - public void DoIt() - { - var v1 = _options.Value1; - var v2 = _options.Value2; - } -} -```` - -阅读[微软文档](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options)了解选择模式的所有细节. - -## 预配置 - -选项模式的限制之一是你只能解析(注入) `IOptions ` 并在依赖注入配置完成(即所有模块的`ConfigureServices`方法完成)后获取选项值. - -如果你正在开发一个模块,可能需要让开发者能够设置一些选项,并在依赖注入注册阶段使用这些选项. 你可能需要根据选项值配置其他服务或更改依赖注入的注册代码. - -对于此类情况,ABP为 `IServiceCollection` 引入了 `PreConfigure` 和 `ExecutePreConfiguredActions` 扩展方法. 该模式的工作原理如下所述. - -在你的模块中定义预先选项类. 例: - -````csharp -public class MyPreOptions -{ - public bool MyValue { get; set; } -} -```` - -然后任何依赖于模块的模块类都可以在其 `PreConfigureServices` 方法中使用 `PreConfigure` 方法. -例: - -````csharp -public override void PreConfigureServices(ServiceConfigurationContext context) -{ - PreConfigure(options => - { - options.MyValue = true; - }); -} -```` - -> 多个模块可以预配置选项,并根据它们的依赖顺序覆盖选项值. - -最后在你的模块 `ConfigureServices` 方法中执行 `ExecutePreConfiguredActions` 方法来获得配置的选项值. -例: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - var options = context.Services.ExecutePreConfiguredActions(); - if (options.MyValue) - { - //... - } -} -```` \ No newline at end of file diff --git a/docs/zh-Hans/PlugIn-Modules.md b/docs/zh-Hans/PlugIn-Modules.md deleted file mode 100644 index 48a507ac7d..0000000000 --- a/docs/zh-Hans/PlugIn-Modules.md +++ /dev/null @@ -1,233 +0,0 @@ -# 模块化插件 - -可以将[模块](Module-Development-Basics.md)加载为插件.这意味着你可能不需要在解决方案中引用模块的程序集,就可以像其它模块一样在启动应用时加载该模块. - -## 基本用法 - -`IServiceCollection.AddApplication()` 扩展方法可以获取配置插件源的选项. - -**示例: 从文件夹加载插件** - -````csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity.PlugIns; - -namespace MyPlugInDemo.Web -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddApplication(options => - { - options.PlugInSources.AddFolder(@"D:\Temp\MyPlugIns"); - }); - } - - public void Configure(IApplicationBuilder app) - { - app.InitializeApplication(); - } - } -} -```` - -* 这是典型的ASP.NET Core应用程序的`Startup`类. -* `PlugInSources.AddFolder`从指定的目录中加载程序集(通常为dll). - -就这样.ABP将在这个目录中发现这些模块,像其它常规一样配置和初始化它们. - -### 插件源 - -`options.PlugInSources`类实际上是`IPlugInSource`接口的一系列实现并且 `AddFolder`方法仅仅是以下表达式的便捷方法: - -````csharp -options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); -```` - -> `AddFolder()`方法仅在给定目录下查找程序集文件,而不在子目录中查找.你可以传递一个`SearchOption.AllDirectories`参数作为第二个参数,来递归地查找它的子目录. - -这里有两个内置插件源的示例: - -* `PlugInSources.AddFiles()`方法获取程序集(通常是dll)文件列表.这是使用`FilePlugInSource`类的快捷方式. -* `PlugInSources.AddTypes()`方法获取模块类类型的列表.如果实用化此方法,则需要自己加载模块的程序集,但是在需要时它提供了灵活性.这是使用`TypePlugInSource`类的快捷方式. - -如果需要,你可以创建自己的`IPlugInSource`的接口实现,并像其它方法一样添加到`options.PlugInSources`中. - -## 示例:创建一个简单的插件 - -在一个解决方案中创建一个简单的**类库项目** - -![简单插件库](images/simple-plugin-library.png) - -你可以在模块中添加需要使用的ABP框架包.至少,你应该为这个项目添加包`Volo.Abp.Core`: - -```` -Install-Package Volo.Abp.Core -```` - -每个[模块](Module-Development-Basics.md)必须声明为一个继承自`AbpModule`的类.这里是一个简单的模块类,用于解析一个服务并在应用启动时对其初始化: - -````csharp -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; -using Volo.Abp.Modularity; - -namespace MyPlugIn -{ - public class MyPlungInModule : AbpModule - { - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var myService = context.ServiceProvider - .GetRequiredService(); - - myService.Initialize(); - } - } -} -```` - -`MyService`可以是注册在[依赖注入](Dependency-Injection.md)系统中的任意类,如下所示: - -````csharp -using Microsoft.Extensions.Logging; -using Volo.Abp.DependencyInjection; - -namespace MyPlugIn -{ - public class MyService : ITransientDependency - { - private readonly ILogger _logger; - - public MyService(ILogger logger) - { - _logger = logger; - } - - public void Initialize() - { - _logger.LogInformation("MyService has been initialized"); - } - } -} -```` - -编译这个项目,打开build目录,找到`MyPlugIn.dll`: - -![简单dll插件](images/simple-plug-in-dll-file.png) - -将`MyPlugIn.dll`复制到到插件目录中(此实例为`D:\Temp\MyPlugIns`). - -如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动时,你可以看到“MyService has been initialized(MyService已经初始化)的日志. - -## 示例:创建一个Razor Pages插件 - -创建内部带视图的插件需要更多的注意. - -> 这个示例假设你已经使用应用程序启动模板和MVC / Razor Pages UI[创建了一个新的Web应用程序](https://abp.io/get-started). - -在解决方案中创建一个新的**类库**项目: - -![简单razor插件](images/simple-razor-plugin.png) - -编辑这个`.csproj`文件内容: - -````xml - - - - net5.0 - Library - true - - - - - - - -```` - -* 将`Sdk`修改为`Microsoft.NET.Sdk.Web`. -* 添加了`OutputType`和`IsPackable`属性. -* 添加了`Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared`NuGet包. - -> 不需要[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) 包.你可以引用更基础的程序包,例如[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc/). 但是,如果需要构建一个UI视图/组件,建议参考[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared)程序包,因为它是最高级的程序包,不依赖于特定[theme](UI/AspNetCore/Theming.md).如果依赖特定主题没有问题,则可以直接引用该主题的程序包,以便能够使用插件中特定于主题的功能. - -接下来在插件中创建模块类: - -````csharp -using System.IO; -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.Modularity; - -namespace MyMvcUIPlugIn -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] - public class MyMvcUIPlugInModule : AbpModule - { - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(mvcBuilder => - { - // 添加插件程序集 - mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); - - // 添加视图程序集 - var viewDllPath = Path.Combine(Path.GetDirectoryName(typeof(MyMvcUIPlugInModule).Assembly.Location), "MyMvcUIPlugIn.Views.dll"); - var viewAssembly = new CompiledRazorAssemblyPart(Assembly.LoadFrom(viewDllPath)); - mvcBuilder.PartManager.ApplicationParts.Add(viewAssembly); - }); - } - } -} -```` - -* 由于我们添加了相关的NuGet包,因此取决于`AbpAspNetCoreMvcUiThemeSharedModule`. -* 添加插件程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你插件中的控制器将无法正常工作. -* 添加插件的视图程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你在插件中的视图将不起作用. - -现在,你可以在`Pages`目录下添加一个razor页面,例如`MyPlugInPage.cshtml`: - -````html -@page -@model MyMvcUIPlugIn.Pages.MyPlugInPage -

    Welcome to my plug-in page

    -

    This page is located inside a plug-in module! :)

    -```` - -现在,你可以构建插件项目.它将产生以下输出: - -![simple-razor-plug-in-dll-file](images/simple-razor-plug-in-dll-file.png) - -将`MyMvcUIPlugIn.dll`和`MyMvcUIPlugIn.Views.dll`复制到到插件目录下(此示例中为`D:\Temp\MyPlugIns`). - -如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动的时候,你应该能够访问`/MyPlugInPage`URL: - -![simple-plugin-output](images/simple-plugin-output.png) - -## 讨论 - -在现实世界中,你的插件可能具有一些外部依赖性.另外,你的应用程序可能被设计为支持插件.所有这些都是你自己的系统要求.ABP做的仅仅是在应用程序启动时加载模块.你在这些模块中执行什么操作由你决定. - -但是,我们可以为一些常见情况提供一些建议. - -### 库依赖 - -对于包/dll依赖,你可以将相关的dll复制到插件目录下.ABP会自动将所有程序集加载到该目录下,并且你的插件将按预期工作. - -> 请参见[Microsoft文档](https://docs.microsoft.com/zh-cn/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies). - -### 数据库模式 - -如果你的模块使用关系型数据库和[Entity Framework Core](Entity-Framework-Core.md), 那么它需要在数据库中提供表.有多种不同的方法可确保在应用程序使用插件时创建表.一些例子; - -1. 插件可以检查数据库表是否存在,并在应用程序启动时创建表,或者如果插件已更新且需要进行某些架构更改时,则会迁移它们.你可以使用EF Core的迁移API来做到这一点. -2. 你可以改进`DbMigrator`应用程序,用于查找插件的迁移并执行它们. - -可能还有其它解决方案.例如,如果你的数据库管理员不允许你在应用程序代码中更改数据库模式,则可能需要手动将SQL文件发送给数据库管理员,以将其应用于数据库. diff --git a/docs/zh-Hans/Previews.md b/docs/zh-Hans/Previews.md deleted file mode 100644 index cc7c939bab..0000000000 --- a/docs/zh-Hans/Previews.md +++ /dev/null @@ -1,40 +0,0 @@ -# 预览版 - -预览版在发布ABP 框架的主要版本或功能版本之前约 4 周发布。它们是为开发人员发布的,以尝试提供反馈以获得更稳定的版本。 - -预览版的版本控制是类似这样的: - -* 3.1.0-rc.1 -* 4.0.0-rc.1 - -在稳定版本(如 3.1.0)之前,可能会发布多个预览版本(如 3.1.0-rc.2 和 3.1.0-rc.3)。 - -## 使用预览版 - -### 新解决方案 - -要创建用于测试预览版的项目,您可以选择[下载页面](https://abp.io/get-started)上的“预览”选项或带有 **--preview** 参数的[ABP CLI](CLI.md) **abp new** 命令: - -````bash -abp new Acme.BookStore --preview -```` - -此命令将使用最新的预览版 NuGet 包、NPM 包和解决方案模板创建一个新项目。每当发布稳定版本时,您都可以使用解决方案abp switch-to-stable根文件夹中的命令切换到解决方案的稳定版本。 - -### 现有解决方案 - -如果您已有解决方案并希望使用/测试最新的预览版,请在解决方案的根文件夹中使用以下 [ABP CLI](CLI.md) 命令. - -````bash -abp switch-to-preview -```` - -您可以稍后使用 `abp switch-to-stable ` 命令返回最新的稳定版. - -````bash -abp switch-to-stable -```` - -## 提供反馈 - -如果您发现错误或想要提供任何类型的反馈,您可以在[GitHub 存储库](https://github.com/abpframework/abp/issues/new)上打开一个问题。 diff --git a/docs/zh-Hans/Redis-Cache.md b/docs/zh-Hans/Redis-Cache.md deleted file mode 100644 index 1c6d3caa42..0000000000 --- a/docs/zh-Hans/Redis-Cache.md +++ /dev/null @@ -1,45 +0,0 @@ -# Redis 缓存 - -ABP Framework [缓存系统](Caching.md) 拓展了 [ASP.NET Core 分布式缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed). 因此, 标准 ASP.NET Core 分布式缓存支持的 **任何提供程序** 都可以在你的应用程序中使用,并且可以像 **微软文档** 那样进行配置. - -不过, ABP为 Redis Cache 还提供了一个集成包: [Volo.Abp.Caching.StackExchangeRedis](https://www.nuget.org/packages/Volo.Abp.Caching.StackExchangeRedis). -为何使用中这个包而不是标准的[Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis/)包有两个原因. - -1. 它实现 `SetManyAsync` 和 `GetManyAsync` 方法. 这些都不是 Microsoft 缓存库的标准方法,而是由ABP框架[缓存](Caching.md)系统添加的. 当你需要通过单个方法调用设置/获取多个缓存项时,它们**显著提高了性能**. -2. 它 **简化** 了 Redis 缓存 **配置** (将在下面解释). - -> Volo.Abp.Caching.StackExchangeRedis 已经使用了 Microsoft.Extensions.Caching.StackExchangeRedis 包,但对其进行了扩展和改进. -## 安装 - -> 如果使用Redis,则此软件包已安装在应用程序启动模板中. -在项目`.csproj`的文件夹中打开命令行窗口并输入命令: - -````bash -abp add-package Volo.Abp.Caching.StackExchangeRedis -```` - -## 配置 - -Volo.Abp.Caching.StackExchangeRedis 包自动从`IConfiguration`获取Redis[配置](Configuration.md). 因此,你可以在以下位置设置配置 `appsettings.json`: - -````js -"Redis": { - "IsEnabled": "true", - "Configuration": "127.0.0.1" -} -```` -设置`IsEnabled`为可选的,如果未设置将默认视为`true`. - - -或者,你可以在[模块](Module-Development-Basics.md)的ConfigureServices方法中配置标准的[RedisCacheOptions](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions)类: - -````csharp -Configure(options => -{ - //... -}); -```` - -## 另请参阅 - -* [缓存](Caching.md) diff --git a/docs/zh-Hans/Repositories.md b/docs/zh-Hans/Repositories.md deleted file mode 100644 index b650bd4810..0000000000 --- a/docs/zh-Hans/Repositories.md +++ /dev/null @@ -1,404 +0,0 @@ -# 仓储 - -"*在领域层和数据映射层之间进行中介,使用类似集合的接口来操作领域对象.*" (Martin Fowler). - -实际上,仓储用于领域对象在数据库(参阅[实体](Entities.md))中的操作, 通常每个 **聚合根** 或不同的实体创建对应的仓储. - -## 通用(泛型)仓储 - -ABP为每个聚合根或实体提供了 **默认的通用(泛型)仓储** . 你可以在服务中[注入](Dependency-Injection.md) `IRepository` 使用标准的**CRUD**操作. - -> 数据库提供程序层应正确配置为能够使用默认的通用存储库. 如果你已经使用启动模板创建了项目,则这些配置 **已经完成**了. 如果不是,请参考数据库提供程序文档([EF Core](Entity-Framework-Core.md) / [MongoDB](MongoDB.md))进行配置. - -**默认通用仓储用法示例:** - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Demo -{ - public class PersonAppService : ApplicationService - { - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task CreateAsync(CreatePersonDto input) - { - var person = new Person(input.Name); - - await _personRepository.InsertAsync(person); - } - - public async Task GetCountAsync(string filter) - { - return await _personRepository.CountAsync(p => p.Name.Contains(filter)); - } - } -} -```` - -在这个例子中; - -* `PersonAppService` 在它的构造函数中注入了 `IRepository` 。 -* `CreateAsync` 方法使用了 `InsertAsync` 创建并保存新的实体。 -* `GetCountAsync` 方法用来从数据库中获取符合指定条件的的人员的数量。 - -### 标准仓储方法 - -通用仓储提供了一些开箱即用的标准CRUD功能: - -* `GetAsync`: 根据指定的`Id`或断言(lambda表达式)返回实体。 - * 将在指定的实体不存在时,抛出异常 `EntityNotFoundException` - * 如果指定的条件存在多个实体时,抛出异常 `InvalidOperationException` -* `FindAsync`: 根据指定的`Id`或断言(lambda表达式)返回实体。 - * 如果指定的实体不存在时,返回 `null` 。 - * 如果指定的条件存在多个实体时,抛出异常 `InvalidOperationException` -* `InsertAsync`: 在数据库里插入一个新的实体。 -* `UpdateAsync`: 在数据库里更新一个已经存在的实体。 -* `DeleteAsync`: 从数据库里删除指定的实体。 - * 这个方法还有一个重载根据指定的断言(lambda表达式)来删除满足条件的多个实体。 -* `GetListAsync`: 返回数据库里的所有实体。 -* `GetPagedListAsync`: 返回一个指定长度的实体列表。 他拥有 `skipCount`, `maxResultCount` and `sorting` 参数. -* `GetCountAsync`: 获取数据库里所有实体的数量 - -这些方法还有还一些重载。 - -* 提供 `UpdateAsync` 和 `DeleteAsync` 方法根据实体对象或者id来更新或者删除实体。 -* 提供 `DeleteAsync` 方法用来删除符合指定条件的多个实体。 - -### 在实体中使用LINQ - -仓储提供了一个`GetQueryableAsync`方法来获取一个`IQueryable`对象。你可以通过这个对象来对实体执行LINQ查询以操作数据库。 - -**示例: 在仓储中使用LINQ表达式** - -````csharp -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Demo -{ - public class PersonAppService : ApplicationService - { - private readonly IRepository _personRepository; - - public PersonAppService(IRepository personRepository) - { - _personRepository = personRepository; - } - - public async Task> GetListAsync(string filter) - { - // 获取 IQueryable - IQueryable queryable = await _personRepository.GetQueryableAsync(); - - // 创建一个查询 - var query = from person in queryable - where person.Name == filter - orderby person.Name - select person; - - // 执行查询 - var people = query.ToList(); - - // 转DTO并返回给客户端 - return people.Select(p => new PersonDto {Name = p.Name}).ToList(); - } - } -} -```` - -你也可以使用LINQ扩展方法: - -````csharp -public async Task> GetListAsync(string filter) -{ - // 获取 IQueryable - IQueryable queryable = await _personRepository.GetQueryableAsync(); - - // 创建一个查询 - var people = queryable - .Where(p => p.Name.Contains(filter)) - .OrderBy(p => p.Name) - .ToList(); - - // 转DTO并返回给客户端 - return people.Select(p => new PersonDto {Name = p.Name}).ToList(); -} -```` - -你可以使用仓储返回的`IQueryable` 配合标准LINQ方法自由查询。 - -> 在这个例子中使用了 `ToList()` 方法, 但是**强烈建议使用异步方法**来执行数据库查询,比如在这个例子中,可以使用 `ToListAsync()` -> -> 查看 **IQueryable & 异步操作** 小节来学习如何做到这一点。 - -### 批量操作 - -下面这些方法可以用来对数据库执行批量操作; - -* `InsertManyAsync` -* `UpdateManyAsync` -* `DeleteManyAsync` - -这些方法可以操作多个实体,如果底层数据库提供程序支持,则可以进行批量操作。 - -> 使用`UpdateManyAsync`和`DeleteManyAsSync`方法时,乐观锁可能会失效。 - -### 软 / 硬 删除 - -如果一个实体是**软删除**实体(即实现了`ISoftDelete`接口),则仓储的`DeleteSync`方法不会删除该实体,而是在数据库中标记为“已删除”。数据过滤器系统确保不会从数据库中正常检索软删除的实体。 - -如果您的实体是软删除实体,如果您需要物理删除这个实体,您可以使用`HardDeleteAsync`方法强制删除。 - -> 阅读 [数据过滤](Data-Filtering.md) 文档以了解更多关于软删除。 - -### 确保实体存在 - -`EnsureExistsAsync`扩展方法通过实体id或实体查询表达式来确保实体存在,如果其不存在,它将抛出`EntityNotFoundException`异常。 - -## 其他通用仓储类型 - -`IRepository` 接口扩展了标准 `IQueryable` 你可以使用标准LINQ方法自由查询。这对于大多数应用程序都很好。但是,某些ORM提供程序或数据库系统可能不支持`IQueryable`接口。如果您想使用这样的提供者,就不能依赖`IQueryable`。 - -### 基础仓储 - -ABP提供了 `IBasicRepository` 和 `IBasicRepository` 接口来支持这样的场景. 你可以扩展这些接口(并可选择性地从`BasicRepositoryBase`派生)为你的实体创建自定义存储库. - -依赖于 `IBasicRepository` 而不是依赖 `IRepository` 有一个优点, 即使它们不支持 `IQueryable` 也可以使用所有的数据源。 - -但主要的供应商, 像 Entity Framework, NHibernate 或 MongoDb 已经支持了 `IQueryable`. - -因此, 使用 `IRepository` 是典型应用程序的 **建议方法**. 但是可重用的模块开发人员可能会考虑使用 `IBasicRepository` 来支持广泛的数据源. - - -### 只读仓储 - -对于想要使用只读仓储的开发者,我们提供了`IReadOnlyRepository` 与 `IReadOnlyBasicRepository`接口。 - -### 无主键的通用(泛型)仓储 - -如果你的实体没有id主键 (例如, 它可能具有复合主键) 那么你不能使用上面定义的 `IRepository`, 在这种情况下你可以仅使用实体(类型)注入 `IRepository`。 - -> `IRepository` 有一些缺失的方法, 通常与实体的 `Id` 属性一起使用. 由于实体在这种情况下没有 `Id` 属性, 因此这些方法不可用. 比如 `Get` 方法通过id获取具有指定id的实体. 不过, 你仍然可以使用`IQueryable`的功能通过标准LINQ方法查询实体。 - -### 自定义仓储 - -对于大多数情况, 默认通用仓储就足够了。但是, 你可能会需要为实体创建自定义仓储类。 - -#### 自定义仓储示例 - -ABP不会强制你实现任何接口或从存储库的任何基类继承。它可以只是一个简单的POCO类。 但是建议继承现有的仓储接口和类,获得开箱即用的标准方法使你的工作更轻松。 - -#### 自定义仓储接口 - -首先在领域层定义一个仓储接口: - -```c# -public interface IPersonRepository : IRepository -{ - Task FindByNameAsync(string name); -} -``` - -此接口扩展了 `IRepository` 以使用已有的通用仓储功能。 - -#### 自定义仓储实现 - -自定义存储库依赖于你使用的数据访问工具。 在此示例中, 我们将使用Entity Framework Core: - -````C# -public class PersonRepository : EfCoreRepository, IPersonRepository -{ - public PersonRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - public async Task FindByNameAsync(string name) - { - var dbContext = await GetDbContextAsync(); - return await dbContext.Set() - .Where(p => p.Name == name) - .FirstOrDefaultAsync(); - } -} -```` - -你可以直接使用数据库访问提供程序 (本例中是 `dbContext` ) 来执行操作. - -> 请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)了解如何自定义仓储. - -## IQueryable & 异步操作 - -`IRepository`提供`GetQueryableAsync()`来获取`IQueryable`,这意味着您可以**直接在其上使用LINQ扩展方法**,如上面的“*在实体中使用LINQ”部分所示。 - - -**示例: 使用 `Where(...)` 和 `ToList()` 扩展方法** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = queryable - .Where(p => p.Name.Contains(nameFilter)) - .ToList(); -```` - -`.ToList`, `Count()`... 是在 `System.Linq` 命名空间下定义的扩展方法. ([参阅所有方法](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)). - -你通常想要使用 `.ToListAsync()`, `.CountAsync()`.... 来编写**真正的异步代码**. - -但在你使用标准的[应用程序启动模板](Startup-Templates/Application.md)时会发现无法在应用层或领域层使用这些异步扩展方法,因为: - -* 这里异步方法**不是标准LINQ方法**,它们定义在[Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)Nuget包中. -* 标准模板应用层与领域层**不引用**EF Core 包以实现数据库提供程序独立. - -根据你的需求和开发模式,你可以根据以下选项使用异步方法. - -> 强烈建议使用异步方法! 在执行数据库查询时不要使用同步LINQ方法,以便能够开发可伸缩的应用程序. - -### 选项-1: 引用EF Core - -**最简单的方法**是在你想要使用异步方法的项目直接引用EF Core包. - -> 添加[Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore)NuGet包到你的项目间接引用EF Core包. 这可以确保你的应用程序其余部分兼容正确版本的EF Core. - -当你添加NuGet包后,你可以使用全功能的EF Core扩展方法. - -**示例: 直接使用 `ToListAsync()`** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = queryable - .Where(p => p.Name.Contains(nameFilter)) - .ToListAsync(); -```` - -当以下情况时,这个方法是推荐的: - -* 如果你正在开发一个应用程序并且**不打算在将来** 更新FE Core,或者如果以后真的需要更改,你也能**容忍**它。我们认为,如果您正在开发最终应用程序,这是合理的。 - -#### MongoDB - -如果使用的是MongoDB,则需要将[Volo.Abp.MongoDB] NuGet包添加到项目中. 但在这种情况下你也不能直接使用异步LINQ扩展(例如`ToListAsync`),因为MongoDB不提供 `IQueryable`的异步扩展方法,而是提供 `IMongoQueryable`. 你需要先将查询强制转换为 `IMongoQueryable` 才能使用异步扩展方法. - -**示例: 转换Cast `IQueryable` 为 `IMongoQueryable` 并且使用 `ToListAsync()`** - -````csharp -var queryable = await _personRepository.GetQueryableAsync(); -var people = ((IMongoQueryable) queryable - .Where(p => p.Name.Contains(nameFilter))) - .ToListAsync(); -```` - -### 选项-2: 使用IRepository异步扩展方法 - -ABP框架为仓储提供异步扩展方法,与异步LINQ扩展方法类似。 - -**示例: 在仓储中使用 `CountAsync` 和 `FirstOrDefaultAsync` 方法 ** - -````csharp -var countAll = await _personRepository - .CountAsync(); - -var count = await _personRepository - .CountAsync(x => x.Name.StartsWith("A")); - -var book1984 = await _bookRepository - .FirstOrDefaultAsync(x => x.Name == "John"); -```` - -支持这些标准的LINQ方法: *AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync*. - -这种方法仍有**局限性**。您需要直接在存储库对象上调用扩展方法。例如,以下用法**不受支持**: - -```csharp -var queryable = await _bookRepository.GetQueryableAsync(); -var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync(); -``` - -这是因为本例中的`CountAsync()`方法是在`IQueryable`接口上调用的,而不是在存储库对象上调用的。请参见此类情况的其他选项。 - -建议 **尽可能使用此方法**. - -### 选项-3: IAsyncQueryableExecuter - -`IAsyncQueryableExecuter` 是一个用于异步执行 `IQueryable` 对象的服务,**不依赖于实际的数据库提供程序**. - -**示例: 注入并使用 `IAsyncQueryableExecuter.ToListAsync()` 方法** - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Linq; - -namespace AbpDemo -{ - public class ProductAppService : ApplicationService, IProductAppService - { - private readonly IRepository _productRepository; - private readonly IAsyncQueryableExecuter _asyncExecuter; - - public ProductAppService( - IRepository productRepository, - IAsyncQueryableExecuter asyncExecuter) - { - _productRepository = productRepository; - _asyncExecuter = asyncExecuter; - } - - public async Task> GetListAsync(string name) - { - //Obtain the IQueryable - var queryable = await _productRepository.GetQueryableAsync(); - - //Create the query - var query = queryable - .Where(p => p.Name.Contains(name)) - .OrderBy(p => p.Name); - - //Run the query asynchronously - List products = await _asyncExecuter.ToListAsync(query); - - //... - } - } -} -```` - -> `ApplicationService` 和 `DomainService` 基类已经预属性注入了 `AsyncExecuter` 属性,所以你可直接使用. - -ABP框架使用实际数据库提供程序的API异步执行查询。虽然这不是执行查询的常见方式,但它是使用异步API而不依赖于数据库提供者的最佳方式。 - -当以下情况时,这个方法是推荐的: - -* 如果您想开发应用程序代码**而不依赖**数据库提供程序。 -* 如果你正在构建一个没有数据库提供程序集成包的**可重用库**,但是在某些情况下需要执行 `IQueryable`对象. - -例如,ABP框架在 `CrudAppService` 基类中(参阅[应用程序](Application-Services.md)文档)使用 `IAsyncQueryableExecuter`. - - -### 选项-4: 自定义仓储方法 - -你始终可以创建自定义仓储方法并使用特定数据库提供程序的API,比如这里的异步扩展方法. 有关自定义存储库的更多信息,请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)文档. - -当以下情况时,这个方法是推荐的: - -* 如果你想**完全隔离**你的领域和应用层和数据库提供程序. -* 如果你开发可**重用的[应用模块](Modules/Index.md)**,并且不想强制使用特定的数据库提供程序,这应该作为一种[最佳实践](Best-Practices/Index.md). diff --git a/docs/zh-Hans/Road-Map.md b/docs/zh-Hans/Road-Map.md deleted file mode 100644 index d8a277ac0e..0000000000 --- a/docs/zh-Hans/Road-Map.md +++ /dev/null @@ -1,30 +0,0 @@ -# ABP Framework 路线图 - -您可以随时在[GitHub仓库](https://github.com/abpframework/abp/milestones)上查看里程碑计划和优先的积压问题,获取详细的路线图. - -虽然我们将**继续添加其他令人兴奋的功能**,但我们将在`中期`中处理以下主要项目: - -* 为所有预构建模块实现 **Blazor UI**. -* **.NET 5.0**! Microsoft宣布.NET 5.0将在2020年11月发布,我们将为此更改做准备并在Microsoft发布它之后立即迁移到.NET 5.0. 我们希望顺利过渡.虽然已经可以为您的应用程序创建或使用gRPC端点,但我们计划为标准应用程序模块创建端点 - -除了中期目录,还有一些[积压](https://github.com/abpframework/abp/milestone/2)的功能, 这里积压中重要功能的列表: - -* [#2882](https://github.com/abpframework/abp/issues/2882) / 提供 **gRPC集成** 基础设施 (虽然[已经可以](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo)为你的应用程序创建和使用gRPC端点,但我们计划为所有[标准应用程序模块](https://docs.abp.io/en/abp/latest/Modules/Index)创建端点) -* [#236](https://github.com/abpframework/abp/issues/236) 基于权限系统的资源 -* [#1754](https://github.com/abpframework/abp/issues/1754) / 多语言实体 -* [#347](https://github.com/abpframework/abp/issues/347) / 支持MongoDB ACID事务 -* [#633](https://github.com/abpframework/abp/issues/633) / 实时通知系统 -* [#57](https://github.com/abpframework/abp/issues/57) / 内置CQRS基础设施 -* [#4222](https://github.com/abpframework/abp/issues/4222) / 分布式事件总线Kafka集成 -* [#336](https://github.com/abpframework/abp/issues/336) / 健康检查抽象 -* [#2532](https://github.com/abpframework/abp/issues/2532), [#2564](https://github.com/abpframework/abp/issues/2465) / EF Core 与 MongoDB API 集成CosmosDB -* [#1168](https://github.com/abpframework/abp/issues/1168) / Vue 启动模板 -* [#1638](https://github.com/abpframework/abp/issues/1638) React 启动模板 -* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook系统 -* [#162](https://github.com/abpframework/abp/issues/162) / 为多租户集成Azure ElasticDB -* [#2296](https://github.com/abpframework/abp/issues/2296) / 功能切换基础架构 - -积压的项目有可能发生变化. 我们将根据社区反馈和项目目标添加新项目与更改优先级. - -在Github相关issue为你感兴趣的功能投票(并写下你的想法) -. 你可以在[GitHub仓库](https://github.com/abpframework/abpork/abp/milestones)为你的功能请求创建issue,但在创建前请先搜索是否已存在类似的issues. \ No newline at end of file diff --git a/docs/zh-Hans/SMS-Sending.md b/docs/zh-Hans/SMS-Sending.md deleted file mode 100644 index 5c3c111656..0000000000 --- a/docs/zh-Hans/SMS-Sending.md +++ /dev/null @@ -1,111 +0,0 @@ -# 短信发送 - -ABP 框架提供一个用于发送短信的抽象, 有如下优点: - -- 在不改变应用程序代码的情况下, 你可以**非常容易地切换**短信发送提供者(提供商). -- 如果你想创建可重用的应用程序模块, 则不需要假设短信是如何发送的. - -## 安装 - -建议你使用[ABP CLI](CLI.md)来安装这个包. - -### 使用Abp CLI - -在项目所在目录(.csproj 文件所在目录)打开命令行工具, 输入如下命令: - -```bash -abp add-package Volo.Abp.Sms -``` - -### 手动安装 - -如果你想要手动安装; - -1. 添加 [Volo.Abp.Sms](https://www.nuget.org/packages/Volo.Abp.Sms) NuGet 包到你的项目中: - -``` -Install-Package Volo.Abp.Sms -``` - -2. 在你的模块的依赖列表中添加对模块`AbpSmsModule`的依赖: - -```csharp -[DependsOn( - //...其它依赖 - typeof(AbpSmsModule) //添加新模块的依赖 - )] -public class YourModule : AbpModule -{ -} -``` - -## 发送短信 - -[注入](Dependency-Injection.md) `ISmsSender`, 并使用`SendAsync`方法来发送短信. - -**例子:** - -```csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Sms; - -namespace MyProject -{ - public class MyService : ITransientDependency - { - private readonly ISmsSender _smsSender; - - public MyService(ISmsSender smsSender) - { - _smsSender = smsSender; - } - - public async Task DoItAsync() - { - await _smsSender.SendAsync( - "+012345678901", // 目标手机号 - "This is test sms..." // 消息内容 - ); - } - } -} -``` - -示例中给定的`SendAsync`方法是一个扩展方法, 用于发送带有基本参数的短信.此外, 你也可以传入一个有如下属性的`SmsMessage`对象: - -- `PhoneNumber` (`string`):目标手机号 -- `Text` (`string`):短信消息内容 -- `Properties` (`Dictionary`):用于传入自定义参数的键值对 - -## NullSmsSender - -`NullSmsSender`是`ISmsSender`的一个默认实现.它写入内容到[日志](Logging.md)中, 而不是真正地发送短信. - -开发时, 你不想真正地发送短信时, 这个类是非常有用的.**然而, 若你想真实发送短信, 你需要在你的应用程序代码中实现`ISmsSender`接口.** - -## 实现ISmsSender接口 - -通过创建一个实现`ISmsSender`接口的类, 你可以很容易创建你自己的短信发送实现, 如下所示: - -```csharp -using System.IO; -using System.Threading.Tasks; -using Volo.Abp.Sms; -using Volo.Abp.DependencyInjection; - -namespace AbpDemo -{ - public class MyCustomSmsSender : ISmsSender, ITransientDependency - { - public async Task SendAsync(SmsMessage smsMessage) - { - // 发送短信 - } - } -} -``` - -## More - -[ABP Commercial](https://commercial.abp.io/)提供Twilio的集成包, 用于使用[Twilio service](https://docs.abp.io/en/commercial/latest/modules/twilio-sms)来发送短信. diff --git a/docs/zh-Hans/Samples/Index.md b/docs/zh-Hans/Samples/Index.md deleted file mode 100644 index db2e680883..0000000000 --- a/docs/zh-Hans/Samples/Index.md +++ /dev/null @@ -1,69 +0,0 @@ -# 示例应用 - -这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. - -### 微服务示例 - -演示如何基于微服务体系结构构建系统的完整解决方案. - -* [示例的文档](Microservice-Demo.md) -* [源码](https://github.com/abpframework/abp-samples/tree/master/MicroserviceDemo) -* [微服务架构文档](../Microservice-Architecture.md) - -### Book Store - -一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: - -* **Book Store: Razor Pages UI & Entity Framework Core** - * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF) - * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) - -* **Book Store: Angular UI & MongoDB** - * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG&DB=Mongo) - * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -* **Book Store: Modular application (Razor Pages UI & EF Core)** - * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) - -如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. - -### 其他示例 - -* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. - * [源码](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo) - * [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) -* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. - * [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) - * [SignalR 集成文档](../SignalR-Integration.md) -* **分布式架构中的实时消息** (使用 SingalR & RabbitMQ) - * [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo) - * [文章](https://community.abp.io/articles/real-time-messaging-in-a-distributed-architecture-using-abp-framework-singalr-rabbitmq-daf47e17) -* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. - * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) - * [Widget 文档](../UI/AspNetCore/Widgets.md) -* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. - * [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) - * [分布式事件总线文档](../Distributed-Event-Bus.md) - * [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) -* **文本模板 Demo**: 文本模板系统的不同用例. - * [源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) - * [文本模板文档](../Text-Templating.md) -* **存储过程 Demo**: 演示如何以最佳实践使用存储过程,数据库视图和函数. - * [源码](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo) -* **无密码认证**: 演示如何添加自定义令牌提供者使用链接验证用户身份,而不是输入密码. - * [源码](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication) - * [文章](https://community.abp.io/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj) -* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. - * [源码](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization) - * 相关文章: - * [Azure Active Directory 认证](https://community.abp.io/articles/how-to-use-the-azure-active-directory-authentication-for-mvc-razor-page-applications-4603b9cf) - * [自定义登录页面](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd) - * [自定义 SignIn Manager](https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753) -* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. - * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) - * [文档](../Getting-Started-AspNetCore-Application.md) -* **GRPC Demo**: 演示如何将gRPC服务添加到基于ABP框架的Web应用程序以及如何从控制台应用程序使用它. - * [源码](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) -* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. - * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) - * [文档](../Getting-Started-Console-Application.md) \ No newline at end of file diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md deleted file mode 100644 index ca8b90a03f..0000000000 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ /dev/null @@ -1,1421 +0,0 @@ -# 微服务解决方案示例 - -*"作为**面向服务架构**(SOA)的一个变体,微服务是一种将应用程序分解成**松散耦合服务**的新型架构风格. 通过**细粒度**的服务和**轻量级**的协议,微服务提供了更多的**模块化**,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够**开发,部署和扩展**各自的服务,实现开发的**并行化**.它还允许通过**连续重构**形成单个服务的架构. 基于微服务架构可以实现**持续交付和部署**."* - -— [维基百科](https://zh.wikipedia.org/wiki/Microservices) - -## 介绍 - -ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服务解决方案](../Microservice-Architecture.md). - -此示例演示了一个简单而完整的微服务解决方案; - -* 拥有多个可独立可单独部署的**微服务**. -* 多个**Web应用程序**, 每一个都使用不同的API网关. -* 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库开发了多个**网关** / BFFs ([用于前端的后端](https://docs.microsoft.com/zh-cn/azure/architecture/patterns/backends-for-frontends)). -* 包含使用[IdentityServer](https://identityserver.io/)框架开发的 **身份认证服务**. 它也是一个带有UI的SSO(单点登陆)应用程序. -* 有**多个数据库**. 一些微服务有自己的数据库,也有一些服务/应用程序共享同一个数据库(以演示不同的用例). -* 有不同类型的数据库: **SQL Server** (与 **Entity Framework Core** ORM) 和 **MongoDB**. -* 有一个**控制台应用程序**使用身份验证展示使用服务最简单的方法. -* 使用[Redis](https://redis.io/)做**分布式缓存**. -* 使用[RabbitMQ](https://www.rabbitmq.com/)做服务间的**消息**传递. -* 使用 [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) 来**部署**&**运行**所有的服务和应用程序. -* 使用 [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) 来存储和可视化日志 (使用[Serilog](https://serilog.net/)写日志). - -下图展示了该系统: - -![microservice-sample-diagram](../images/microservice-sample-diagram-3.png) - -### 源码 - -你可以从[GitHub仓库](https://github.com/abpframework/abp-samples/tree/master/MicroserviceDemo)获取源码. - -## 运行解决方案 - -### 预先要求 - -为了能够从源代码运行解决方案, 应在你的计算机上安装并运行以下工具: - -* [SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 2015+ (可以是 [express edition](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express)) -* [Redis](https://redis.io/download) 5.0+ -* [RabbitMQ](https://www.rabbitmq.com/install-windows.html) 3.7.11+ -* [MongoDB](https://www.mongodb.com/download-center) 4.0+ -* [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ -* [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (可选,建议显示日志) - -### 运行基础设施 - -- 默认情况下运行Docker-compose可以轻松运行预先要求. 如果你没有安装,在windows环境下你可以在[这里](https://docs.docker.com/docker-for-windows/install/)下载并使用[Docker for Windows](https://docs.docker.com/docker-for-windows/). -* 在 `MicroserviceDemo` 目录下运行命令 `docker-compose -f docker-compose.infrastructure.yml -f docker-compose.infrastructure.override.yml up -d` 或使用powershell运行 `MicroserviceDemo/_run` 文件夹下的 `__Run_Infrastructure.ps1` 脚本. -* 如果你不想使用docker运行预先要求的服务在你的本地环境,你需要更改微服务示例解决方案项目中的 `appsettings.json` 文件. - -### 打开并构建Visual Studio解决方案 - -* 在Visual Studio 2017 (15.9.0+)中打开`samples\MicroserviceDemo\MicroserviceDemo.sln`. -* 在`samples\MicroserviceDemo`文件夹中的命令行运行`dotnet restore`命令. -* 在Visual Studio中构建解决方案. - -### 创建数据库 - -MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构.其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移. - -这个解决方案中有两个 SQL server 数据库. - -#### MsDemo_Identity 数据库 - -* 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. -* 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) -* 选择 `AuthServer.Host` 成为 **默认项目**. -* 执行 `Update-Database` 命令. - -![microservice-sample-update-database-authserver](../images/microservice-sample-update-database-authserver.png) - -#### MsDemo_ProductManagement - -* 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. -* 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) -* 选择 `ProductService.Host` 成为 **默认项目**. -* 执行 `Update-Database` 命令. - -![microservice-sample-update-database-products](../images/microservice-sample-update-database-products.png) - -### 运行项目 - -按以下顺序运行项目(右键单击每个项目设置为启动项目,按Ctrl+F5运行,无需调试): - -* AuthServer.Host -* IdentityService.Host -* TenantManagementService.Host -* BloggingService.Host -* ProductService.Host -* InternalGateway.Host -* BackendAdminAppGateway.Host -* PublicWebSiteGateway.Host -* BackendAdminApp.Host -* PublicWebSite.Host - -## 解决方案简介 - -Visual Studio解决方案由多个项目组成,每个项目在系统中具有不同的角色: - -![microservice-sample-solution](../images/microservice-sample-solution-2.png) - -### 应用程序(Applications) - -这些是具有用户界面以与用户交互并使用系统的实际应用程序. - -- **AuthServer.Host**: 托管IdentityServer4以向其他服务和应用程序提供身份验证服务. 它是一个单点登录服务器,包含登录页面. -- **BackendAdminApp.Host**: 这是一个后端管理应用程序,用于托管身份和产品管理模块的UI. -- **PublicWebSite.Host**: 作为包含简单产品列表页面和博客模块UI的公共网站. -- **ConsoleClientDemo**: 一个简单的控制台应用程序,用于演示C#应用程序中使用服务. - -### 网关/BFF(前端后端) - -网关用于为应用程序提供单一入口点.它还可以用于速率限制,负载平衡等. 使用[Ocelot](https://github.com/ThreeMammals/Ocelot)类库. - -* **BackendAdminAppGateway.Host**: 由BackendAdminApp.Host应用程序用作后端. -* **PublicWebSiteGateway.Host**: 由PublicWebSite.Host应用程序用作后端. -* **InternalGateway.Host**: 用于服务间通信(微服务之间的通信). - -### 微服务 - -微服务没有UI,但暴露了一些REST API. - -- **IdentityService.Host**: 托管用于管理用户和角色的ABP Identity模块. 它没有其他服务,仅托管Identity模块的API. -- **TenantManagementService.Host**: 托管用于管理角色的ABP租户管理模块. 它没有其他服务,仅托管租户管理模块的API. -- **BloggingService.Host**: 托管ABP博客模块,该模块用于管理博客和帖子(典型的博客应用程序). 它没有其他服务,仅托管Blogging模块的API. -- **ProductService.Host**: 托管用于管理产品的产品模块(位于解决方案内). 它还包含用于创建/更新产品管理数据库架构的EF Core迁移. - -### 模块 - -* **产品**: 使用[模块开发最佳实践](../Best-Practices/Index.md)开发的分层模块. 它可以嵌入到单个应用程序中,也可以通过单独部署API和UI作为微服务托管(如本演示解决方案中所述). - -### 数据库 - -此解决方案使用多个数据库: - -* **MsDemo_Identity**: 一个SQL数据库. 默认使用**SQL Server**,但可以是EF Core支持的任何DBMS. 由AuthServer,IdentityService和TenantManagementService共享. 审计日志,权限和设置也存储在此数据库中(虽然它们可以轻松拥有自己的数据库,共享相同的数据库以保持简单). -* **MsDemo_ProductManagement**: 一个SQL数据库. 同样默认使用 **SQL Server**,但可以是EF Core支持的任何DBMS. 由ProductService用作专用数据库. -* **MsDemo_Blogging**: **MongoDB**数据库. 由BloggingService使用. -* **Elasticsearch**: 用于在Serilog上写日志. - -## 应用 - -### 认证服务器 (AuthServer.Host) - -所有其他服务和应用程序都使用此项目进行身份验证和单点登录. 主要使用**IdentityServer4**来提供这些服务. 它使用了一些[预构建ABP模块](../Modules/Index) 如 *Identity*, *Audit Logging* 和 *Permission Management*. - -#### 数据库和EF Core配置 - -此应用程序使用SQL数据库(将其命名为**MsDemo_Identity**)并通过**Entity Framework Core迁移**维护其架构. - -它有一个名为**AuthServerDbContext**的DbContext,定义如下: - -````csharp -public class AuthServerDbContext : AbpDbContext -{ - public AuthServerDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureIdentity(); - modelBuilder.ConfigureIdentityServer(); - modelBuilder.ConfigureAuditLogging(); - modelBuilder.ConfigurePermissionManagement(); - modelBuilder.ConfigureSettingManagement(); - } -} -```` - -在**OnModelCreating**方法中, 你会看到 **ConfigureX()** 方法调用. 具有数据库模式的模块通常声明这样的扩展方法,以便为其自己的实体配置EF Core映射. 这是一种灵活的方法, 可以在其中安排数据库和模块; 可以为每个模块使用不同的数据库,或者将它们中的一些组合在一个共享数据库中. 在AuthServer项目中,我们决定在单个物理数据库中将单个EF Core DbContext中的多个模块模式组合在一起. 这些模块是Identity,IdentityServer,AuditLogging,PermissionManagement和SettingManagement模块. - -请注意,此DbContext仅用于数据库迁移. 所有模块都有自己的`DbContext`类,模块在运行时使用这些类. - -#### 用户界面 - -AuthServer有一个简单的主页,如果当前用户已登录,则显示当前用户信息: - -![microservice-sample-authserver-home](../images/microservice-sample-authserver-home.png) - -它还提供登录和注册页面: - -![microservice-sample-authserver-login](../images/microservice-sample-authserver-login.png) - -这些页面不包含在项目本身中. 相反,AuthServer项目使用带有IdentityServer扩展的预构建ABP[帐户模块](https://github.com/abpframework/abp/tree/master/modules/account). 这意味着它还可以充当具有必要UI和逻辑的OpenId Connect服务器. - -#### 依赖 - -* **RabbitMQ** 用于向其他服务发送消息. -* **Redis** 用于分布式/共享缓存. -* **Elasticsearch** 用于存储日志. - -### 后端管理应用程序 (BackendAdminApp.Host) - -这是一个Web应用程序,用于管理系统中的用户,角色,权限和产品. - -#### 认证 - -BackendAdminApp重定向到AuthServer进行身份验证. 用户输入正确的用户名和密码后,页面将再次重定向到后端应用程序. 身份验证配置在`BackendAdminAppHostModule`类中设置: - -````charp -context.Services.AddAuthentication(options => -{ - options.DefaultScheme = "Cookies"; - options.DefaultChallengeScheme = "oidc"; -}) -.AddCookie("Cookies", options => -{ - options.Cookie.Expiration = TimeSpan.FromDays(365); - options.ExpireTimeSpan = TimeSpan.FromDays(365); -}) -.AddOpenIdConnect("oidc", options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - options.RequireHttpsMetadata = false; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.Scope.Add("role"); - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("BackendAdminAppGateway"); - options.Scope.Add("IdentityService"); - options.Scope.Add("ProductService"); - options.ClaimActions.MapAbpClaimTypes(); -}); -```` - -* 它将"Cookies"身份验证添加为主要身份验证类型. -* "oidc"身份验证配置为使用AuthServer应用程序作为身份验证服务器. -* 它需要额外的身份范围(scopes) *role*, *email* 和 *phone*. -* 它需要API资源范围 *BackendAdminAppGateway*, *IdentityService* 和 *ProductService*,因为它将这些服务用作API. - -IdentityServer客户端设置存储在`appsettings.json`文件中: - -````json -"AuthServer": { - "Authority": "http://localhost:64999", - "ClientId": "backend-admin-app-client", - "ClientSecret": "1q2w3e*" -} -```` - -#### 用户界面 - -BackendAdminApp.Host项目本身没有单个UI元素/页面. 它仅用于提供身份和产品管理模块的UI页面. - -`BackendAdminAppHostModule`将依赖关系添加到`AbpIdentityWebModule`(*[Volo.Abp.Identity.Web](https://www.nuget.org/packages/Volo.Abp.Identity.Web)* 包)和`ProductManagementWebModule`(*ProductManagement.Web*项目)为此目的. - -用户管理页面的屏幕截图: - -![microservice-sample-backend-ui](../images/microservice-sample-backend-ui.png) - -来自权限的权限管理模式的屏幕截图: - -![microservice-sample-backend-ui-permissions](../images/microservice-sample-backend-ui-permissions.png) - -#### 使用微服务 - -后端管理应用程序通过后端管理网关对所有操作使用Identity和Product微服务(BackendAdminAppGateway.Host). - -##### 远程端点 - -`appsettings.json`文件包含`RemoteServices`部分,用于声明远程服务端点. 每个微服务通常都有不同的端点. 但是,此解决方案使用API网关模式为应用程序提供单个端点: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65115/" - } -} -```` - -`http://localhost:65115/` 是 *BackendAdminAppGateway.Host* 项目的URL. 它知道身份和产品服务的位置. - -##### HTTP Clients - -ABP应用程序模块通常提供C#客户端库以轻松地使用服务(API)(它们通常使用ABP框架的[Dynamic C# API客户端](../API/Dynamic-CSharp-API-Clients.md)). 这意味着如果你需要使用Identity Service API, 你可以引用其客户端软件包,并通过提供的接口轻松使用API. - -为此`BackendAdminAppHostModule`类声明了`AbpIdentityHttpApiClientModule`和`ProductManagementHttpApiClientModule`的依赖关系. - -一旦引用这些客户端软件包,就可以直接注入应用程序服务接口(例如`IIdentityUserAppService`)并使用其方法,如本地方法调用. 它实际上通过HTTP调用到相关服务端点的远程服务调用. - -##### 传递访问令牌(Access Token) - -由于微服务需要身份验证和授权,因此每个远程服务调用都应包含Authentication头. 该头是从当前用户的当前`HttpContext`中的`access_token`获得的. 当你使用`Volo.Abp.Http.Client.IdentityModel`包时,会自动执行此操作. `BackendAdminAppHostModule`声明对此包和相关的`AbpHttpClientIdentityModelModule`类的依赖. 它集成到上面解释的HTTP客户端. - -#### 依赖 - -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 公共网站 (PublicWebSite.Host) - -这是一个公共网站项目,具有Web博客和产品列表页面. - -#### 认证 - -公共网站可以在不登录的情况下显示博客文章和产品列表. 如果你登录,你还可以管理博客. 它重定向到AuthServer进行身份验证. 用户输入正确的用户名和密码后,页面将再次重定向到公共网站应用程序. 身份验证配置在`PublicWebSiteHostModule`类中设置: - -```charp -context.Services.AddAuthentication(options => -{ - options.DefaultScheme = "Cookies"; - options.DefaultChallengeScheme = "oidc"; -}) -.AddCookie("Cookies", options => -{ - options.Cookie.Expiration = TimeSpan.FromDays(365); - options.ExpireTimeSpan = TimeSpan.FromDays(365); -}) -.AddOpenIdConnect("oidc", options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - options.RequireHttpsMetadata = false; - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.Scope.Add("role"); - options.Scope.Add("email"); - options.Scope.Add("phone"); - options.Scope.Add("PublicWebSiteGateway"); - options.Scope.Add("ProductService"); - options.Scope.Add("BloggingService"); - options.ClaimActions.MapAbpClaimTypes(); -}); -``` - -- 它将"Cookies"身份验证添加为主要身份验证类型. -- "oidc"身份验证配置为使用AuthServer应用程序作为身份验证服务器. -- 它需要额外的身份范围 *role*, *email* and *phone*. -- 它需要API资源范围 *PublicWebSiteGateway*,*BloggingService*和*ProductService*,因为它将这些服务用作API. - -IdentityServer客户端设置存储在`appsettings.json`文件中: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ClientId": "public-website-client", - "ClientSecret": "1q2w3e*" -} -``` - -#### 用户界面 - -PublicWebSite.Host项目有一个列出产品的页面 (`Pages/Products.cshtml`). 它还使用博客模块中的UI. 为此`PublicWebSiteHostModule`加入了`BloggingWebModule`(*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* 包)的依赖项. - -产品页面的屏幕截图: - -![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) - -#### 使用微服务 - -公共网站应用程序使用Blogging和Product微服务通过公共网站网关进行所有操作(PublicWebSiteGateway.Host). - -##### 远程端点 - - -`appsettings.json`文件包含`RemoteServices`部分,用于声明远程服务端点. 每个微服务通常都有不同的端点. 但是,此解决方案使用API网关模式为应用程序提供单个端点: - -```json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:64897/" - } -} -``` - -`http://localhost:64897/` 是*PublicWebSiteGateway.Host*项目的URL. 它知道Blogging和产品服务的位置. - -##### HTTP Clients - -`PublicWebSiteHostModule`类声明`BloggingHttpApiClientModule`和`ProductManagementHttpApiClientModule`的依赖关系,以便能够为这些服务使用远程HTTP API. - -##### 传递访问令牌(Access Token) - -正如后端管理应用程序部分中所述, Public Web Site项目还使用`AbpHttpClientIdentityModelModule`将`access_token`传递给调用服务进行身份验证. - -#### Dependencies - -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 控制台客户端演示 - -最后,该解决方案包括一个非常简单的控制台应用程序,名为ConsoleClientDemo,它通过AuthServer进行身份验证来使用Identity和Product服务. 它使用内部网关(InternalGateway.Host)来执行HTTP API调用. - -#### 远程服务配置 - -`appsettings.json`文件中的`RemoteService`配置很简单: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65129/" - } -} -```` - -`http://localhost:65129/` 是内部网关的URL. 对服务的所有API调用都是通过此URL执行的. - -#### 身份验证(IdentityServer客户端)配置 - -`appsettings.json`还有一个IdentityServer身份验证配置: - -````json -"IdentityClients": { - "Default": { - "GrantType": "client_credentials", - "ClientId": "console-client-demo", - "ClientSecret": "1q2w3e*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService ProductService" - } -} -```` - -此示例使用`client_credentials` 授予类型,该类型需要`ClientId`和`ClientSecret`进行身份验证过程. 还有[其他授予类型](http://docs.identityserver.io/en/latest/topics/grant_types.html). 例如, 你可以使用以下配置切换到`password`(Resource Owner Password)授予类型: - -````json -"IdentityClients": { - "Default": { - "GrantType": "password", - "ClientId": "console-client-demo", - "ClientSecret": "1q2w3e*", - "UserName": "admin", - "UserPassword": "1q2w3E*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService ProductService" - } -} -```` - -除客户端凭据外,Resource Owner Password还需要`UserName` 和 `UserPassword`. 此授权类型对于代表用户调用远程服务很有用. - -`Scope` 声明API(和网关)以授予访问权限. 此应用程序使用内部网关. - -#### HTTP Client依赖 - -`ConsoleClientDemoModule`与`AbpIdentityHttpApiClientModule`和`ProductManagementHttpApiClientModule`有依赖关系,以便使用Identity和Product API. 它还具有“AbpHttpClientIdentityModelModule”依赖性,可通过IdentityServer进行身份验证. - -#### 使用服务 - -使用这些服务非常简单. 请参阅`ClientDemoService`类,它只是注入`IIdentityUserAppService`和`IProductAppService`并使用它们. 该类还显示了使用`HttpClient`对象的手动HTTP调用. 有关详细信息,请参阅`ClientDemoService`的源代码. - -## API网关/ BFF(前端后端) - -网关用于为应用程序提供 **单一入口点**. 通过这种方式,应用程序仅处理单个服务地址(API端点),而不是每个服务的不同地址. 网关还用于速率限制,安全性,身份验证,负载平衡和更多要求. - -"**后端前端**"(BFF)是一种常见的架构模式,可为每种不同的应用程序/客户端类型构建**专注而专业的**网关. 此解决方案使用此模式并具有多个网关. - -此解决方案使用[Ocelot](https://github.com/ThreeMammals/Ocelot)库来构建API网关. 它是ASP.NET核心广泛接受的API网关库. - -### 后端管理应用程序网关 (BackendAdminAppGateway.Host) - -这是"后端管理应用程序"的后端(服务器端API)(不要混淆命名;后端管理应用程序实际上是前端Web应用程序,但系统管理员而不是普通用户使用). - -#### 认证 - -此网关使用IdentityServer `Bearer`身份验证并配置如下: - -````csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -```` - -`AddIdentityServerAuthentication` 扩展方法来自 [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) 包, IdentityServer4项目的一部分 (参见 [identityserver文档](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName`是受保护的API,在这种情况下是`BackendAdminAppGateway`. 因此,此解决方案将网关定义为API资源. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关. `appsettings.json`中的身份验证相关的配置很简单: - -````json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "BackendAdminAppGateway" -} -```` - -#### Ocelot配置 - -Ocelot需要知道微服务的真实URL才能重定向HTTP请求. 此网关的配置如下所示: - -````json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/identity/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 63568 - } - ], - "UpstreamPathTemplate": "/api/identity/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:65115" -} -```` - -`ReRoutes`是一个URL映射数组. `GlobalConfiguration`部分中的`BaseUrl`是该网关的URL(Ocelot需要知道自己的URL). 参见 [ocelot文档](https://ocelot.readthedocs.io/en/latest/features/configuration.html) 更好地了解配置. - -Ocelot是一个终结ASP.NET核心中间件,应该写成管道中的最后一项: - -````csharp -app.UseOcelot().Wait(); -```` - -它根据上面的配置处理和重定向请求. - -#### ABP 配置端点 - -ABP提供了一些内置API,以从服务器获取一些配置和信息. 例子: - -* `/api/abp/application-configuration` 返回本地化文本,权限和设置值 (http://localhost:65115/api/abp/application-configuration). -* `/Abp/ServiceProxyScript` 返回动态javascript代理以从javascript客户端调用服务 (http://localhost:65115/Abp/ServiceProxyScript ). - -这些端点应由网关服务提供,而不是由微服务提供. 微服务只能知道与该微服务相关的权限. 但是,一旦正确配置,网关就可以将多个服务的权限值聚合为一个更适合客户端的列表. - -为此, ASP.NET Core管道配置为通过MVC而不是Ocelot处理某些特定路由. 为了实现这一点,MapWhen扩展方法使用如下: - -````csharp -app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") || - ctx.Request.Path.ToString().StartsWith("/Abp/"), - app2 => - { - app2.UseConfiguredEndpoints(); - }); - -app.UseOcelot().Wait(); -```` - -当请求路径以 `/api/abp/` 或 `/Abp/`.开头时,此配置使用标准MVC中间件. - -#### Swagger - -此网关配置为使用[swagger UI](https://swagger.io/tools/swagger-ui/), 这是一种用于发现和测试HTTP API的流行工具. 通常,Ocelot不支持在swagger上显示API,因为它无法了解每个微服务API的详细信息. 但是当你遵循ABP分层模块架构[最佳实践](../Best-Practices/Index.md)时,它是可能的. - -`BackendAdminAppGatewayHostModule`将依赖性添加到`AbpIdentityHttpApiModule`(*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* 包)和`ProductManagementHttpApiModule`(*ProductManagement.HttpApi* 项目)以包含它们的HTTP API控制器. 通过这种方式,swagger可以发现它们. 虽然它引用了API层,但它没有引用应用程序服务的实现,因为它们将在相关的微服务端点中运行,并由Ocelot根据请求URL重定向. - -无论如何,当你打开URL`http://localhost:65115/swagger/index.html`时, 你将看到所有已配置的微服务的API. - -#### 权限管理 - -后端管理应用程序提供权限管理UI(之前见过),并使用此网关获取/设置权限. 权限管理API托管在网关内,而不是单独的服务. 这是一个设计决策,但如果你愿意,它可以作为另一个微服务托管. - -#### Dependencies - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 公共网站网关 (PublicWebSiteGateway.Host) - -这是"公共网站"应用程序的后端(服务器端API网关). - -#### 认证 - -此网关使用IdentityServer `Bearer`身份验证并配置如下: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`AddIdentityServerAuthentication` 扩展方法来自 [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation)包, IdentityServer4项目的一部分 (参见 [identityserver文档](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName`是受保护的API,在这种情况下是`PublicWebSiteGateway`. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关. 与`appsettings.json`中的身份验证相关的配置很简单: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "PublicWebSiteGateway" -} -``` - -#### Ocelot配置 - -Ocelot需要知道微服务的真实URL才能重定向HTTP请求. 此网关的配置如下所示: - -```json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/blogging/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 62157 - } - ], - "UpstreamPathTemplate": "/api/blogging/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:64897" -} -``` - -参见 [ocelot文档](https://ocelot.readthedocs.io/en/latest/features/configuration.html) 更好地了解配置. - -#### 其它 - -请参阅"后端管理应用程序网关"部分中的"ABP配置端点"和"Swagger"主题,这些主题与此网关非常相似. - -#### 依赖 - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 内部网关 (InternalGateway.Host) - -该网关不是BFF. 它专为微服务间通信而设计,不会公开. - -#### 认证 - -此网关使用IdentityServer `Bearer`身份验证并配置如下: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`AddIdentityServerAuthentication` 扩展方法来自 [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation)包, IdentityServer4项目的一部分 (参见 [identityserver文档](http://docs.identityserver.io/en/latest/topics/apis.html)). - -`ApiName`是受保护的API,在这种情况下是`InternalGateway`. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关. 与`appsettings.json`中的身份验证相关的配置很简单: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "InternalGateway" -} -``` - -#### Ocelot 配置 - -Ocelot需要知道微服务的真实URL才能重定向HTTP请求. 此网关的配置如下所示: - -```json -"ReRoutes": [ - { - "DownstreamPathTemplate": "/api/identity/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 63568 - } - ], - "UpstreamPathTemplate": "/api/identity/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/productManagement/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 60244 - } - ], - "UpstreamPathTemplate": "/api/productManagement/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - }, - { - "DownstreamPathTemplate": "/api/blogging/{everything}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 62157 - } - ], - "UpstreamPathTemplate": "/api/blogging/{everything}", - "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] - } -], -"GlobalConfiguration": { - "BaseUrl": "http://localhost:65129" -} -``` - -`ReRoutes`配置涵盖了系统中的所有微服务. 参见 [ocelot文档](https://ocelot.readthedocs.io/en/latest/features/configuration.html) 更好地了解配置. - -#### 其它 - -请参阅"后端管理应用程序网关"部分中的"ABP配置端点"和"Swagger"主题,这些主题与此网关非常相似. - -#### 依赖 - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -## 微服务 - -微服务是独立的HTTP API,它们以分布式方式实现系统业务. - -* 它们由应用程序和其他微服务通过网关和HTTP API使用. -* 他们可以提升或注册系统中的事件. -* 它们可以通过异步消息传递相互通信. - -### Identity Service (IdentityService.Host) - -此服务提供用户和角色管理API. - -#### 数据库 - -与AuthServer应用程序共享相同的数据库(MsDemo_Identity). - -#### Identity模块 - -该服务实际上只托管ABP身份包/模块. 不包含任何API本身. 为了托管它,添加以下依赖项: - -* `AbpIdentityHttpApiModule` (*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* 包) 提供身份API. -* `AbpIdentityApplicationModule` (*[Volo.Abp.Identity.Application](https://www.nuget.org/packages/Volo.Abp.Identity.Application)* 包)承载模块的应用程序和域层的实现. -* `AbpIdentityEntityFrameworkCoreModule` (*[Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* 包) 使用EF Core作为数据库API. - -请参阅[module architecture best practice guide](../Best-Practices/Module-Architecture)以更好地理解分层. - -#### 认证 - -这个微服务使用IdentityServer`Bearer`身份验证,并配置如下: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName`是受保护的API,在这种情况下是`IdentityService`. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关. 与`appsettings.json`中的身份验证相关的配置很简单: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "IdentityService" -} -``` - -#### Swagger - -Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://localhost:63568/`, 你将被重定向到swagger页面以查看和测试API. - -#### 依赖 - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 博客服务 (BloggingService.Host) - -此服务托管博客API. - -#### 数据库 - -它有一个专门的MongoDB数据库(MsDemo_Blogging)来存储博客和帖子. 它还使用MsDemo_Identity SQL数据库来审核日志,权限和设置. 因此,`appsettings.json`文件中有两个连接字符串: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", - "Blogging": "mongodb://localhost/MsDemo_Blogging" -} -```` - -#### 博客模块 - -该服务实际上只托管ABP Blogging包/模块. 不包含任何API本身. 为了托管它,添加以下依赖项: - -- `BloggingHttpApiModule` (*[Volo.Blogging.HttpApi](https://www.nuget.org/packages/Volo.Blogging.HttpApi)* 包) 提供Blogging API. -- `BloggingApplicationModule` (*[Volo.Blogging.Application](https://www.nuget.org/packages/Volo.Blogging.Application)* 包) 承载模块的应用程序和域层的实现. -- `BloggingMongoDbModule` (*[Volo.Blogging.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* 包) 使用MongoDB作为数据库. - -请参阅[module architecture best practice guide](../Best-Practices/Module-Architecture)以更好地理解分层. - -#### 认证 - -这个微服务使用IdentityServer `Bearer`身份验证,并配置如下: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName`是受保护的API,在这种情况下是`BloggingService`. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关.与`appsettings.json`中的身份验证相关的配置很简单: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "BloggingService" -} -``` - -#### IdentityServer Client - -此微服务还通过内部网关使用Identity微服务API, 因为在某些情况下它需要查询用户详细信息(username, email, phone, name 和 surname). 因此,它也是IdentityServer的客户端,并在`appsettings.json`文件中定义了一个部分: - -````json -"IdentityClients": { - "Default": { - "GrantType": "client_credentials", - "ClientId": "blogging-service-client", - "ClientSecret": "1q2w3e*", - "Authority": "http://localhost:64999", - "Scope": "InternalGateway IdentityService" - } -} -```` - -由于它使用内部网关, 因此它还应配置网关的远程端点: - -````json -"RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:65129/", - "UseCurrentAccessToken": "false" - } -} -```` - -当你将`UseCurrentAccessToken`设置为`false`时,ABP会忽略当前`HttpContext`中的当前`access_token`,并使用上面定义的凭据对AuthServer进行身份验证. - -为什么不在当前请求中使用当前用户的令牌? 因为,用户可能没有Identity模块所需的权限,因此它不能直接将当前身份验证令牌传递给Identity服务. 此外,某些博客服务API是匿名的(不需要经过身份验证的用户),因此在某些情况下,HTTP请求中没有"当前用户". 出于这些原因,应将Blogging服务定义为具有自己的凭据和权限的Identity服务的客户端. - -如果检查`MsDemo_Identity`数据库中的`AbpPermissionGrants`表,则可以看到`blogging-service-client`的相关权限. - -![microservice-sample-blogservice-permission-in-database](../images/microservice-sample-blogservice-permission-in-database.png) - -#### Swagger - -Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://localhost:62157/`, 你将被重定向到swagger页面以查看和测试API. - -#### 依赖 - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -### 产品服务 (ProductService.Host) - -此服务托管产品管理API. - -#### 数据库和EF核心迁移 - -它有一个单独的SQL数据库,名为**MsDemo_ProductManagement**,用于产品管理模块. 它使用EF Core作为数据库提供程序,并具有名为`ProductServiceMigrationDbContext`的DbContext: - -````csharp -public class ProductServiceMigrationDbContext : AbpDbContext -{ - public ProductServiceMigrationDbContext( - DbContextOptions options - ) : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureProductManagement(); - } -} -```` - -实际模型配置在`modelBuilder.ConfigureProductManagement()`扩展方法内完成. 此项目使用EF Core迁移维护数据库模式. - -请注意,此DbContext仅用于数据库迁移. Product Management模块有自己的`DbContext`类,它在运行时使用(参见ProductManagement.EntityFrameworkCore项目中的`ProductManagementDbContext`类). - -`appsettings.json`文件中有两个连接字符串: - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", - "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True" -} -```` - -`Default`连接字符串指向MsDemo_Identity数据库,该数据库用于审计日志记录,权限和设置存储. 产品模块使用`ProductManagement`连接字符串. - -#### 产品模块 - -该服务实际上只托管产品管理模块. 不包含任何API本身. 为了托管它,添加以下依赖项: - -- `ProductManagementHttpApiModule` 提供产品管理API. -- `ProductManagementApplicationModule` 承载模块的应用程序和域层的实现. -- `ProductManagementEntityFrameworkCoreModule` 使用EF Core作为数据库API. - -请参阅[module architecture best practice guide](../Best-Practices/Module-Architecture)以更好地理解分层. 有关此模块的更多信息,请参阅下面的"产品管理"模块部分 - -#### 认证 - -这个微服务使用IdentityServer `Bearer`身份验证,并配置如下: - -```csharp -context.Services.AddAuthentication("Bearer") -.AddIdentityServerAuthentication(options => -{ - options.Authority = configuration["AuthServer:Authority"]; - options.ApiName = configuration["AuthServer:ApiName"]; - options.RequireHttpsMetadata = false; - options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; - options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; - options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; - options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; - options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; - options.InboundJwtClaimTypeMap["phone_number_verified"] = - AbpClaimTypes.PhoneNumberVerified; - options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; -}); -``` - -`ApiName`是受保护的API,在这种情况下是`ProductService`. 其余配置与声明映射(计划在下一个ABP版本中自动化)相关. 与`appsettings.json`中的身份验证相关的配置很简单: - -```json -"AuthServer": { - "Authority": "http://localhost:64999", - "ApiName": "ProductService" -} -``` - -#### Swagger - -Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://localhost:60244/`, 你将被重定向到swagger页面以查看和测试API. - -#### 依赖 - -- **RabbitMQ** 用于向其他服务发送消息. -- **Redis** 用于分布式/共享缓存. -- **Elasticsearch** 用于存储日志. - -## 模块 - -ABP提供强大的基础架构,通过提供服务和架构,使模块化应用程序开发更容易(参见[模块开发最佳实践指南](../Best-Practices/Index.md)). - -此解决方案演示了如何在分布式体系结构中使用[预构建的应用程序模块](../Modules/Index.md). 该解决方案还包括一个简单的"产品管理"模块,用于显示分层模块示例的实现. - -### 产品管理 - -产品管理是一个由多个层和包/项目组成的模块: - -![microservice-sample-product-module-in-solution](../images/microservice-sample-product-module-in-solution.png) - -* `ProductManagement.Domain.Shared` 包含所有层之间共享的常量和类型. -* `ProductManagement.Domain` 包含域逻辑并定义实体,域服务,域事件,业务/域异常. -* `ProductManagement.Application.Contracts` 包含应用程序服务接口和DTO. -* `ProductManagement.Application` 包含应用程序服务的实现. -* `ProductManagement.EntityFrameworkCore` 包含DbContext和其他与EF Core相关的类和配置. -* `ProductManagement.HttpApi` 包含API控制器. -* `ProductManagement.HttpApi.Client` 包含C#代理以远程直接使用HTTP API. 使用ABP的[Dynamic C#API客户端](../API/Dynamic-CSharp-API-Clients.md)功能. - -* `ProductManagement.Web` 包含UI元素(页面,脚本,样式..等). - -通过此分层的帮助,可以在单个应用程序中使用相同的模块作为包引用,或者用作在另一个服务器中运行的服务. 可以分离UI(Web)和API层,因此它们可以在不同的服务器中运行. - -在此解决方案中,Web层在后端管理应用程序中运行,而API层由产品微服务托管. - -本教程将重点介绍该模块的一些重要方面. 但是,建议查看源代码以便更好地理解. - -#### Domain Layer - -`Product`是这个模块的主要[聚合根](../Entities.md): - -````csharp -public class Product : AuditedAggregateRoot -{ - /// - /// A unique value for this product. - /// ProductManager ensures the uniqueness of it. - /// It can not be changed after creation of the product. - /// - [NotNull] - public string Code { get; private set; } - - [NotNull] - public string Name { get; private set; } - - public float Price { get; private set; } - - public int StockCount { get; private set; } - - //... -} -```` - -它的所有属性都有私有的set方法,可以防止属性从类中直接更改. 产品类通过自己的构造函数和方法确保其自身的完整性和有效性. - -它有两个构造函数: - -````csharp -private Product() -{ - //Default constructor is needed for ORMs. -} - -internal Product( - Guid id, - [NotNull] string code, - [NotNull] string name, - float price = 0.0f, - int stockCount = 0) -{ - Check.NotNullOrWhiteSpace(code, nameof(code)); - - if (code.Length >= ProductConsts.MaxCodeLength) - { - throw new ArgumentException( - $"Product code can not be longer than {ProductConsts.MaxCodeLength}" - ); - } - - Id = id; - Code = code; - SetName(Check.NotNullOrWhiteSpace(name, nameof(name))); - SetPrice(price); - SetStockCountInternal(stockCount, triggerEvent: false); -} - -```` - -默认(**无参数**)构造函数是私有的,不在应用程序代码中使用. 这是必需的,因为大多数ORM在从数据库获取时需要在反序列化实体时使用无参数构造函数. - -第二个构造函数是**internal**,这意味着它只能在域层内使用. 这强制在创建新的`Product`时使用`ProductManager`. 因为`ProductManager`应该在新产品创建上实现业务规则. 此构造函数仅需要最少的必需参数来创建具有一些可选参数的新产品. 它会检查一些简单的业务规则,以确保将实体创建为有效产品. - -该类的其余部分具有操纵实体属性的方法. 例: - -````csharp -public Product SetPrice(float price) -{ - if (price < 0.0f) - { - throw new ArgumentException($"{nameof(price)} can not be less than 0.0!"); - } - - Price = price; - return this; -} - -```` - -`SetPrice`方法用于以安全的方式更改产品的价格(通过检查验证规则). - -`SetStockCount`是另一种用于更改产品库存数量的方法: - -````csharp -public Product SetStockCount(int stockCount) -{ - return SetStockCountInternal(stockCount); -} - -private Product SetStockCountInternal(int stockCount, bool triggerEvent = true) -{ - if (StockCount < 0) - { - throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!"); - } - - if (StockCount == stockCount) - { - return this; - } - - if (triggerEvent) - { - AddDistributedEvent( - new ProductStockCountChangedEto( - Id, StockCount, stockCount - ) - ); - } - - StockCount = stockCount; - return this; -} - -```` - -此方法还触发**分布式事件**,其中带有`ProductStockCountChangedEto`参数(Eto是传统的后缀代表**E**vent **T**ransfer **O**bject,但不是必需的)通知产品库存数量的听众已发生变化. 任何订户都可以接收此事件并根据该知识执行操作. - -RabbitMQ为此解决方案分发事件. 但是ABP通过提供必要的抽象来独立于消息代理[Event Bus](../Event-Bus.md)文档). - -如前所述,该模块强制始终使用`ProductManager`来创建新的`Product`. `ProductManager`是一个简单的域服务,定义如下: - -````csharp -public class ProductManager : DomainService -{ - private readonly IRepository _productRepository; - - public ProductManager(IRepository productRepository) - { - _productRepository = productRepository; - } - - public async Task CreateAsync( - [NotNull] string code, - [NotNull] string name, - float price = 0.0f, - int stockCount = 0) - { - var existingProduct = - await _productRepository.FirstOrDefaultAsync(p => p.Code == code); - - if (existingProduct != null) - { - throw new ProductCodeAlreadyExistsException(code); - } - - return await _productRepository.InsertAsync( - new Product( - GuidGenerator.Create(), - code, - name, - price, - stockCount - ) - ); - } -} -```` - -* 它检查之前是否使用过给定的代码. 否则抛出`ProductCodeAlreadyExistsException`. -* 使用`GuidGenerator`(`IGuidGenerator`)服务来创建一个新的`Guid`. -* 它将实体插入存储库. - -因此,通过这种设计,产品代码的唯一性得到保证. - -`ProductCodeAlreadyExistsException`是一个域/业务异常,定义如下: - -````csharp -public class ProductCodeAlreadyExistsException : BusinessException -{ - public ProductCodeAlreadyExistsException(string productCode) - : base("PM:000001", $"A product with code {productCode} has already exists!") - { - - } -} -```` - -`PM:000001`是发送给客户端的异常类型的代码,因此他们可以理解错误类型. 在这种情况下没有实现,但也可以本地化业务异常. 请参阅[异常处理文档](../Exception-Handling.md). - -#### 应用层 - -该模块的应用层有两个服务: - -* `ProductAppService`主要由后端管理应用程序用于管理(创建,更新,删除.)产品. 它需要许可才能执行任何操作. -* 公共网站使用`PublicProductAppService`来向访问者显示产品列表. 它不需要任何权限,因为大多数访问者都没有登录到应用程序. - -请注意; 每个应用程序分离应用程序层可能是更好的原则, 而不是将两个应用程序服务放入同一个项目中. 但是我们在这个解决方案中简化了它们. - -例如,`ProductAppService`具有以下更新产品的方法: - -````csharp -[Authorize(ProductManagementPermissions.Products.Update)] -public async Task UpdateAsync(Guid id, UpdateProductDto input) -{ - var product = await _productRepository.GetAsync(id); - - product.SetName(input.Name); - product.SetPrice(input.Price); - product.SetStockCount(input.StockCount); - - return ObjectMapper.Map(product); -} -```` - -* 它定义了所需的权限(*ProductManagementPermissions.Products.Update*是一个值为`ProductManagement.Update`的常量)来执行此操作. -* 获取产品的ID,DTO包含要更新的值. -* 从存储库中获取相关的产品实体. -* 使用`Product`类的相关方法(如`SetName`)来更改属性,因为它们是私有set方法,更改值的唯一方法是使用实体方法. -* 通过使用[ObjectMapper](../Object-To-Object-Mapping.md)向客户端返回更新的`ProductDto`(客户端可能由于某种原因需要它). - -实施可能会根据要求而有所不同. 此实现遵循此处提供的[最佳实践](../Best-Practices/Application-Services.md). - -#### 其他层 - -请参阅源代码中的其他层. - -## 基础设施 - -### 消息和RabbitMQ - -异步消息传递是分布式系统中的关键概念. 它可以以松散耦合的方式与容错进行通信. 在发送消息时,它不要求双方都在线. 因此,它是微服务架构中广泛使用的通信模式. - -#### 分布式事件总线 - -分布式事件(事件总线)是一种消息传递方式,其中服务引发/触发事件,而其他服务注册/侦听这些事件,以便在发生重要事件时得到通知. ABP通过提供约定,服务和集成使分布式事件更易于使用. - -你已经看到`Product`类使用以下代码行发布事件: - -````csharp -AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount)); -```` - -`ProductStockCountChangedEto` 定义如下: - -````csharp -[Serializable] -public class ProductStockCountChangedEto : EtoBase -{ - public Guid Id { get; } - - public int OldCount { get; set; } - - public int CurrentCount { get; set; } - - private ProductStockCountChangedEto() - { - //Default constructor is needed for deserialization. - } - - public ProductStockCountChangedEto(Guid id, int oldCount, int currentCount) - { - Id = id; - OldCount = oldCount; - CurrentCount = currentCount; - } -} -```` - -该对象存储有关该事件的必要信息. 通过使用通用的`ProductStockCountChangedEto`参数实现`IDistributedEventHandler`接口,另一个服务可以轻松注册到此事件: - -````csharp -public class MyHandler : IDistributedEventHandler -{ - public async Task HandleEventAsync(ProductStockCountChangedEto eventData) - { - var productId = eventData.Id; - //... - } -} -```` - -当你使用[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)包时,所有集成和通信都由ABP框架完成.如果需要从实体发布事件,只需注入`IDistributedEventBus`并使用`PublishAsync`方法. - -有关分布式事件系统的更多信息, 请参见[Event Bus](../Event-Bus.md)文档. - -#### RabbitMQ配置 - -在此解决方案中, [RabbitMQ](https://www.rabbitmq.com/)用于消息传递和分布式事件. - -[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)包需要集成到RabbitMQ以用于分布式事件系统. 然后你需要为模块的`AbpEventBusRabbitMqModule`添加依赖项. 例如`ProductServiceHostModule`声明了这种依赖. - -默认情况下,`abpEventBusRabbitMqModule`从`appsettings.json`获取配置. 例如产品服务具有以下配置: - -````json -"RabbitMQ": { - "Connections": { - "Default": { - "HostName": "localhost" - } - }, - "EventBus": { - "ClientName": "MsDemo_ProductService", - "ExchangeName": "MsDemo" - } -} -```` - -### 缓存和Redis - -分布式系统显然需要分布式和共享缓存,而不是每个服务的隔离内存缓存. - -[Redis](https://redis.io/)在此解决方案中用作分布式缓存. 该解决方案使用Microsoft的标准[Microsoft.Extensions.Caching.Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Redis) 包进行集成. 使用和配置此程序包时,所有应用程序和服务都使用Redis缓存. 有关详细信息请参阅[Microsoft的文档](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). - -该解决方案还使用[Microsoft.AspNetCore.DataProtection.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.StackExchangeRedis)包在Redis缓存上共享应用程序和服务之间的数据保护密钥. - -### Logging,Serilog,Elasticsearch和Kibana - -该解决方案使用[Serilog](https://serilog.net/)作为日志库. 它是一个广泛使用的库,有许多数据源集成,包括[Elasticsearch](https://www.elastic.co/products/elasticsearch). - -使用类似于下面给出的代码块在`Program.cs`文件中完成日志配置: - -````csharp -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.WithProperty("Application", "ProductService") - .Enrich.FromLogContext() - .WriteTo.File("Logs/logs.txt") - .WriteTo.Elasticsearch( - new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"])) - { - AutoRegisterTemplate = true, - AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6, - IndexFormat = "msdemo-log-{0:yyyy.MM}" - }) - .CreateLogger(); -```` - -这会配置多个日志输出目标:File和Elasticsearch.对于此示例,`Application`属性设置为`ProductService`. 这是一种区分单个数据库中多个服务的日志的方法. 然后, 你可以通过`Application`名称查询日志. - -从`appsettings.json`配置文件中读取Elasticsearch URL: - -````json -"ElasticSearch": { - "Url": "http://localhost:9200" -} -```` - -如果你使用Kibana, 它是一个与Elasticsearch完美集成的可视化工具,可以看到有关你的日志的一些UI: - -![microservice-sample-kibana-2](../images/microservice-sample-kibana-2.png) - -*Figure - 一个仪表板,显示服务/应用程序的日志和错误计数.* - -![microservice-sample-kibana-1](../images/microservice-sample-kibana-1.png) - -*Figure - 日志条目列表* - -Kibana URL默认为`http://localhost:5601/`. - -### 审计日志 - -ABP提供自动审计日志记录,详细保存每个请求(当前用户,浏览器/客户端,执行了哪些操作,哪些实体更改,甚至实体的哪些属性已更新). 有关详细信息,请参阅[审计日志文档](../Audit-Logging.md). - -所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,你可以从单个点查询所有应用程序的所有审核日志. - -审核日志记录具有`CorrelationId`属性,可用于跟踪请求. 当服务在单个Web请求中调用另一个服务时,它们都会使用相同的`CorrelationId`保存审核日志. 请参阅数据库中的`AbpAuditLogs`表. - -### 多租户 - -该解决方案已配置提供[多租户](../Multi-Tenancy.md)系统,其中每个租户可以拥有其隔离的用户,角色,权限和其他数据. diff --git a/docs/zh-Hans/Settings.md b/docs/zh-Hans/Settings.md deleted file mode 100644 index 73d3115b19..0000000000 --- a/docs/zh-Hans/Settings.md +++ /dev/null @@ -1,231 +0,0 @@ -# Settings - -[配置系统](Configuration.md) 是在启动时配置应用程序很好的方式. 除了配置之外, ABP提供了另外一种设置和获取应用程序设置的方式. - -设置存储在动态数据源(通常是数据库)中的键值对. 设置系统预构建了用户,租户,全局和默认设置方法并且可以进行扩展. - -## 定义设置 - -使用设置之前需要定义它. ABP是 [模块化](Module-Development-Basics.md)的, 不同的模块可以拥有不同的设置. 模块中派生 `SettingDefinitionProvider` 类定义模块内的配置. 示例如下: - -````csharp -public class EmailSettingProvider : SettingDefinitionProvider -{ - public override void Define(ISettingDefinitionContext context) - { - context.Add( - new SettingDefinition("Smtp.Host", "127.0.0.1"), - new SettingDefinition("Smtp.Port", "25"), - new SettingDefinition("Smtp.UserName"), - new SettingDefinition("Smtp.Password", isEncrypted: true), - new SettingDefinition("Smtp.EnableSsl", "false") - ); - } -} -```` - -ABP会自动发现并注册设置的定义. - -### SettingDefinition - -`SettingDefinition` 类具有以下属性: - -* **Name**: 应用程序中设置的唯一名称. 是**具有约束的唯一属性**, 在应用程序获取/设置此设置的值 (设置名称定义为常量而不是`magic`字符串是个好主意). -* **DefaultValue**: 设置的默认值. -* **DisplayName**: 本地化的字符串,用于在UI上显示名称. -* **Description**: 本地化的字符串,用于在UI上显示描述. -* **IsVisibleToClients**: 布尔值,表示此设置是否在客户端可用. 默认为false,避免意外暴漏内部关键设置. -* **IsInherited**: 布尔值,此设置值是否从其他提供程序继承. 如果没有为请求的提供程序设置设定值,那么默认值是true并回退到下一个提供程序 (参阅设置值提供程序部分了解更多). -* **IsEncrypted**: 布尔值,表示是否在保存值是加密,读取时解密. 在数据库中存储加密的值. -* **Providers**: 限制可用于特定的设置值提供程序(参阅设置值提供程序部分了解更多). -* **Properties**: 设置此值的自定义属性 名称/值 集合,可以在之后的应用程序代码中使用. - -### 更改依赖模块的设置定义 - -在某些情况下,你可能希望更改应用程序/模块所依赖的其他模块中定义的设置的某些属性. 设置定义提供程序可以查询和更新设置定义. - -下面的示例中获取了由 [Volo.Abp.Emailing](Emailing.md) 包定义的设置并将其更改: - -````csharp -public class MySettingDefinitionProvider : SettingDefinitionProvider -{ - public override void Define(ISettingDefinitionContext context) - { - var smtpHost = context.GetOrNull("Abp.Mailing.Smtp.Host"); - if (smtpHost != null) - { - smtpHost.DefaultValue = "mail.mydomain.com"; - smtpHost.DisplayName = - new LocalizableString( - typeof(MyLocalizationResource), - "SmtpServer_DisplayName" - ); - } - } -} -```` - -> 使用常量作为设置名称是一种好习惯,ABP的包就是这样做的. `Abp.Mailing.Smtp.Host`设置名称是在`EmailSettingNames`类(在Volo.Abp.Emailing命名空间中)定义的常量. - -## 读取设置值 - -### ISettingProvider - -`ISettingProvider` 用于获取指定设置的值或所有设置的值. 示例用法: - -````csharp -public class MyService -{ - private readonly ISettingProvider _settingProvider; - - //Inject ISettingProvider in the constructor - public MyService(ISettingProvider settingProvider) - { - _settingProvider = settingProvider; - } - - public async Task FooAsync() - { - //Get a value as string. - string userName = await _settingProvider.GetOrNullAsync("Smtp.UserName"); - - //Get a bool value and fallback to the default value (false) if not set. - bool enableSsl = await _settingProvider.GetAsync("Smtp.EnableSsl"); - - //Get a bool value and fallback to the provided default value (true) if not set. - bool enableSsl = await _settingProvider.GetAsync( - "Smtp.EnableSsl", defaultValue: true); - - //Get a bool value with the IsTrueAsync shortcut extension method - bool enableSsl = await _settingProvider.IsTrueAsync("Smtp.EnableSsl"); - - //Get an int value or the default value (0) if not set - int port = (await _settingProvider.GetAsync("Smtp.Port")); - - //Get an int value or null if not provided - int? port = (await _settingProvider.GetOrNullAsync("Smtp.Port"))?.To(); - } -} -```` - -> `ISettingProvider` 是非常常用的服务,一些基类中(如`IApplicationService`)已经将其属性注入. 这种情况下可以直接使用`SettingProvider`. - -### 在客户端读取设置值 - -如果允许在客户端显示某个设置,可以使用 JavaScript 代码读取设置值. 示例: - -````js -//Gets a value as string. -var language = abp.setting.get('Abp.Localization.DefaultLanguage'); - -//Gets an integer value. -var requiredLength = abp.setting.getInt('Abp.Identity.Password.RequiredLength'); - -//Gets a boolean value. -var requireDigit = abp.setting.getBoolean('Abp.Identity.Password.RequireDigit'); -```` - -使用 `abp.setting.values` 可以读取所有设置值的字典. - -## 设置值提供程序 - -设置系统是可扩展的, 你可以定义设置值提供程序扩展它,根据任何条件从任何来源获取设置值. - -`ISettingProvider` 使用设置值提供程序来获取设置值. 如果值提供程序无法获取设置值,则会回退到下一个值提供程序. - -有五个预构建设置值提供程序按以下顺序注册: - -* `DefaultValueSettingValueProvider`: 从设置定义的默认值中获取值(参见上面的SettingDefinition部分). -* `ConfigurationSettingValueProvider`: 从[IConfiguration服务](Configuration.md)中获取值. -* `GlobalSettingValueProvider`: 获取设置的全局(系统范围)值. -* `TenantSettingValueProvider`: 获取当前租户的设置值(参阅 [多租户](Multi-Tenancy.md)文档). -* `UserSettingValueProvider`: 获取当前用户的设置值(参阅 [当前用户](CurrentUser.md) 文档). - -> 设置回退系统从底部 (用户) 到 顶部(默认) 方向起作用. - -全局,租户和用户设置值提供程序使用 `ISettingStore` 从数据源读取值(参见下面的小节). - -### 在应用程序配置中设置值 - -上一节提到 `ConfigurationSettingValueProvider` 从 `IConfiguration` 服务中读取设置, 该服务默认从 `appsettings.json` 中读取值. 所以在 `appsettings.json` 文件中配置设置值是最简单的方式. - -例如你可以像以下方式一样配置 [IEmailSender](Emailing.md) 设置: - -````json -{ - "Settings": { - "Abp.Mailing.DefaultFromAddress": "noreply@mydomain.com", - "Abp.Mailing.DefaultFromDisplayName": "My Application", - "Abp.Mailing.Smtp.Host": "mail.mydomain.com", - "Abp.Mailing.Smtp.Port": "547", - "Abp.Mailing.Smtp.UserName": "myusername", - "Abp.Mailing.Smtp.Password": "mySecretPassW00rd", - "Abp.Mailing.Smtp.EnableSsl": "True" - } -} -```` - -设置值应该在 `Settings` 部分配置,如本例所示. - -> `IConfiguration`是.NET Core的服务,它不仅可以从 `appsettings.json` 中读取值,还可以从环境,用户机密...等中读取值. 有关更多信息请参阅[微软文档](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/). - -### 自定义设置值提供程序 - -扩展设置系统的方式是定义一个派生自 `SettingValueProvider` 的类. 示例: - -````csharp -public class CustomSettingValueProvider : SettingValueProvider -{ - public override string Name => "Custom"; - - public CustomSettingValueProvider(ISettingStore settingStore) - : base(settingStore) - { - } - - public override Task GetOrNullAsync(SettingDefinition setting) - { - /* Return the setting value or null - Use the SettingStore or another data source */ - } -} -```` - -> 或者你直接可以实现 `ISettingValueProvider` 接口. 这时需要记得将其注册到 [依赖注入](Dependency-Injection.md). - -每一个提供程序都应该具有唯一的名称 (这里的名称是 "Custom" ). 内置提供程序使用给定的名称: - -* `DefaultValueSettingValueProvider`: "**D**". -* `ConfigurationSettingValueProvider`: "**C**". -* `GlobalSettingValueProvider`: "**G**". -* `TenantSettingValueProvider`: "**T**". -* `UserSettingValueProvider`: "**U**". - -最好使用一个字母的名称来减少数据库中的数据大小(提供者名称在每行中重复). - -定义自定义设置值提供程序后,需要将其显式注册到 `AbpSettingOptions`: - -````csharp -Configure(options => -{ - options.ValueProviders.Add(); -}); -```` - -本示例将其添加到最后一项,因此它将成为`ISettingProvider`使用的第一个值提供程序. 你也可以将其添加到`options.ValueProviders`列表的另一个位置. - -### ISettingStore - -尽管设置值提供程序可以自由使用任何来源来获取设置值,但 `ISettingStore` 服务是设置值的默认来源. 全局,租户和用户设置值提供者都使用它. - -## ISettingEncryptionService - -`ISettingEncryptionService` 用于在设置定义的 `isencryption` 属性设置为 `true` 时加密/解密设置值. - -你可以在依赖注入系统中替换此服务,自定义实现加密/解密过程. 默认实现 `StringEncryptionService` 使用AES算法(参见字符串[加密文档](String-Encryption.md)学习更多). - -## 设置管理模块 - -设置系统核心是相当独立的,不做任何关于如何管理(更改)设置值的假设. 默认的`ISettingStore`实现也是`NullSettingStore`,它为所有设置值返回null. - -设置管理模块通过管理数据库中的设置值来完成逻辑(实现`ISettingStore`).有关更多信息参阅[设置管理模块](Modules/Setting-Management.md)学习更多. diff --git a/docs/zh-Hans/SignalR-Integration.md b/docs/zh-Hans/SignalR-Integration.md deleted file mode 100644 index 7adc75daa4..0000000000 --- a/docs/zh-Hans/SignalR-Integration.md +++ /dev/null @@ -1,238 +0,0 @@ -# SignalR 集成 - -> 你可以按照[标准的微软教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signal)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序,但ABP提供了简化集成的SignalR集成包. - -## 安装 - -### 服务器端 - -建议使用[ABP CLI](CLI.md)安装包. - -#### 使用 ABP CLI - -在项目的文件夹(.csproj文件)中打开命令行窗口,然后输入以下命令: - -```bash -abp add-package Volo.Abp.AspNetCore.SignalR -``` - -> 你通常需要将此软件包添加到应用程序的Web或API层,具体取决于你的架构. - -#### 手动安装 - -如果你想手动安装: - -1. 添加[Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)NuGet包到你的项目: - - ``` - Install-Package Volo.Abp.AspNetCore.SignalR - ``` - - 或者使用VisualStudio提供的UI安装 - -2. 添加 `AbpAspNetCoreSignalRModule` 到你的模块的依赖列表. - -```csharp -[DependsOn( - //...other dependencies - typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -``` - -> 你不需要 `services.AddSignalR()` 和 `app.UseEndpoints(...)`,它们在 `AbpAspNetCoreSignalRModule` 中已经添加了. - -### 客户端 - -客户端安装取决于你的UI框架/客户端类型. - -#### ASP.NET Core MVC / Razor Pages UI - -在你的Web项目的根文件夹中运行以下命令: - -````bash -yarn add @abp/signalr -```` - -> 需要 [yarn](https://yarnpkg.com/) 环境. - -它会添加 `@abp/signalr` 到你的项目中的 `package.json` 依赖项: - -````json -{ - ... - "dependencies": { - ... - "@abp/signalr": "~2.7.0" - } -} -```` - -在你的Web项目的根文件夹中运行 `abp install-libs`: - -````bash -abp install-libs -```` - -它会将SignalR JavaScript文件拷贝到你的项目: - -![signal-js-file](images/signal-js-file.png) - -最后将以下代码添加到页面/视图中, 添加包含 `signalr.js` 文件: - -````xml -@section scripts { - -} -```` - -它需要将 `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` 添加到你的页面/视图. - -> 你可以用标准方式添加 `signalr.js` 文件. 但是使用 `SignalRBrowserScriptContributor` 具有其他好处. 有关详细信息,请参见[客户端程序包管理](UI/AspNetCore/Client-Side-Package-Management.md)和[捆绑和压缩文档](UI/AspNetCore/Bundling-Minification.md). - -这就是全部了,你可以在你的页面使用[SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client). - -#### 其他的UI框架/客户端 - -其他类型的客户端请参考[微软文档](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction). - -## ABP框架集成 - -本节介绍了使用ABP框架集成包的其他好处. - -### Hub 路由与Mapping - -ABP自动将所有集线器注册到[依赖注入](Dependency-Injection.md)(做为transient)并映射集线器端点. 因此你不需要使用 `app.UseEndpoints(...)` 即可映射你的集线器.集线器路由(URL)通常是根据你的集线器名称确定. - -示例: - -````csharp -public class MessagingHub : Hub -{ - //... -} -```` - -`MessagingHub` 集线器的路由为 `/signalr-hubs/messaging`: - -* 添加了标准 `/signalr-hubs/` 前缀. -* 使用**驼峰命名**集线器名称,不包含 `Hub` 后缀. - -如果你想指定路由,你可以使用 `HubRoute` attribute: - -````csharp -[HubRoute("/my-messaging-hub")] -public class MessagingHub : Hub -{ - //... -} -```` - -### AbpHub 基类 - -你可以从 `AbpHub` 或 `AbpHub` 继承标准的 `Hub` 和 `Hub` 类,它们具有实用的基本属性,如 `CurrentUser`. - -示例: - -````csharp -public class MessagingHub : AbpHub -{ - public async Task SendMessage(string targetUserName, string message) - { - var currentUserName = CurrentUser.UserName; //Access to the current user info - var txt = L["MyText"]; //Localization - } -} -```` - -> 虽然可以将相同的属性注入到集线器构造函数中,但是这种方式简化了集线器类. - -### 手动注册/Mapping - -ABP会自动将所有集线器注册到[依赖注入](Dependency-Injection.md)作为**transient service**. 如果想要禁用集线器类**自动添加依赖注入**,只需要使用 `DisableConventionalRegistration` attribute. 如果愿意,你仍然可以在模块的 `ConfigureServices` 方法中注册集线器类: - -````csharp -context.Services.AddTransient(); -```` - -当**你或ABP**将类注册到依赖注入时,如前几节所述,它会自动映射到端点路由配置. 如果要手动映射集线器类,你可以使用 `DisableAutoHubMap` attribute. - -对于手动映射,你有两个选择: - -1. 使用 `AbpSignalROptions` 添加map配置(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中),ABP会为集线器执行端点映射: - -````csharp -Configure(options => -{ - options.Hubs.Add( - new HubConfig( - typeof(MessagingHub), //Hub type - "/my-messaging/route", //Hub route (URL) - hubOptions => - { - //Additional options - hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - } - ) - ); -}); -```` - -这是提供其他SignalR选项的好方式. - -如果你不想禁用自动集线器map,但仍想执行其他SignalR配置,可以使用 `options.Hubs.AddOrUpdate(...)` 方法: - -````csharp -Configure(options => -{ - options.Hubs.AddOrUpdate( - typeof(MessagingHub), //Hub type - config => //Additional configuration - { - config.RoutePattern = "/my-messaging-hub"; //override the default route - config.ConfigureActions.Add(hubOptions => - { - //Additional options - hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - }); - } - ); -}); -```` - -你可以通过这种方式修改在依赖模块(没有源代码访问权限)中定义的集线器类的选项. - -2. 在[模块](Module-Development-Basics.md)的 `OnApplicationInitialization` 方法中更改 `app.UseConfiguredEndpoints`(添加了lambda方法作为参数). - -````csharp -app.UseConfiguredEndpoints(endpoints => -{ - endpoints.MapHub("/my-messaging-hub", options => - { - options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); - }); -}); -```` - -### UserIdProvider - -ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 服务提供当前用户ID(请参阅[当前用户服务](CurrentUser.md)),它将集成到应用程序的身份验证系统中,实现类是 `AbpSignalRUserIdProvider` (如果你想更改/覆盖它). - -## 示例应用程序 - -参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. - -![signalr-demo-chat](images/signalr-demo-chat.png) - -## 备注 - -ABP框架不会更改SignalR. 就像在其他ASP.NET Core应用程序中一样,它也可以在基于ABP框架的应用程序中工作. - -参考[微软文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/scale)托管和扩展您的应用程序,集成[Azure](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/publish-to-azure-web-app)或[Redis底版](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/redis-backplane)...等. - -## 另请参阅 - -* [微软SignalR文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction) -* [使用ABP,SignalR和RabbitMQ在分布式体系结构中的实时消息传递](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) \ No newline at end of file diff --git a/docs/zh-Hans/Specifications.md b/docs/zh-Hans/Specifications.md deleted file mode 100644 index 40a473dbc0..0000000000 --- a/docs/zh-Hans/Specifications.md +++ /dev/null @@ -1,257 +0,0 @@ -## 规约 - -规约模式用于为实体和其他业务对象定义 **命名、可复用、可组合和可测试的过滤器** . - -> 规约是领域层的一部分. - -## 安装 - -> 这个包 **已经安装** 在启动模板中.所以,大多数时候你不需要手动去安装. - -添加 [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) 包到你的项目. 如果当前文件夹是你的项目的根目录(`.csproj`)时,你可以在命令行终端中使用 [ABP CLI](CLI.md) *add package* 命令: - -````bash -abp add-package Volo.Abp.Specifications -```` - -## 定义规约 - -假设你定义了如下的顾客实体: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace MyProject -{ - public class Customer : AggregateRoot - { - public string Name { get; set; } - - public byte Age { get; set; } - - public long Balance { get; set; } - - public string Location { get; set; } - } -} -```` - -你可以创建一个由 `Specification` 派生的新规约类. - -**例如:规定选择一个18岁以上的顾客** - -````csharp -using System; -using System.Linq.Expressions; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class Age18PlusCustomerSpecification : Specification - { - public override Expression> ToExpression() - { - return c => c.Age >= 18; - } - } -} -```` - -你只需通过定义一个lambda[表达式](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions)来定义规约. - -> 你也可以直接实现`ISpecification`接口,但是基类`Specification`做了大量简化. - -## 使用规约 - -这里有两种常见的规约用例. - -### IsSatisfiedBy - -`IsSatisfiedBy` 方法可以用于检查单个对象是否满足规约. - -**例如:如果顾客不满足年龄规定,则抛出异常** - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class CustomerService : ITransientDependency - { - public async Task BuyAlcohol(Customer customer) - { - if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) - { - throw new Exception( - "这位顾客不满足年龄规定!" - ); - } - - //TODO... - } - } -} -```` - -### ToExpression & Repositories - -`ToExpression()` 方法可用于将规约转化为表达式.通过这种方式,你可以使用规约在**数据库查询时过滤实体**. - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; - -namespace MyProject -{ - public class CustomerManager : DomainService, ITransientDependency - { - private readonly IRepository _customerRepository; - - public CustomerManager(IRepository customerRepository) - { - _customerRepository = customerRepository; - } - - public async Task> GetCustomersCanBuyAlcohol() - { - var queryable = await _customerRepository.GetQueryableAsync(); - var query = queryable.Where( - new Age18PlusCustomerSpecification().ToExpression() - ); - - return await AsyncExecuter.ToListAsync(query); - } - } -} -```` - -> 规约被正确地转换为SQL/数据库查询语句,并且在DBMS端高效执行.虽然它与规约无关,但如果你想了解有关 `AsyncExecuter` 的更多信息,请参阅[仓储](Repositories.md)文档. - -实际上,没有必要使用 `ToExpression()` 方法,因为规约会自动转换为表达式.这也会起作用: - -````csharp -var queryable = await _customerRepository.GetQueryableAsync(); -var query = queryable.Where( - new Age18PlusCustomerSpecification() -); -```` - -## 编写规约 - -规约有一个强大的功能是,它们可以与`And`、`Or`、`Not`以及`AndNot`扩展方法组合使用. - -假设你有另一个规约,定义如下: - -```csharp -using System; -using System.Linq.Expressions; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class PremiumCustomerSpecification : Specification - { - public override Expression> ToExpression() - { - return (customer) => (customer.Balance >= 100000); - } - } -} -``` - -你可以将 `PremiumCustomerSpecification` 和 `Age18PlusCustomerSpecification` 结合起来,查询优质成人顾客的数量,如下所示: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Services; -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class CustomerManager : DomainService, ITransientDependency - { - private readonly IRepository _customerRepository; - - public CustomerManager(IRepository customerRepository) - { - _customerRepository = customerRepository; - } - - public async Task GetAdultPremiumCustomerCountAsync() - { - return await _customerRepository.CountAsync( - new Age18PlusCustomerSpecification() - .And(new PremiumCustomerSpecification()).ToExpression() - ); - } - } -} -```` - -如果你想让这个组合成为一个可复用的规约,你可以创建这样一个组合的规约类,它派生自`AndSpecification`: - -````csharp -using Volo.Abp.Specifications; - -namespace MyProject -{ - public class AdultPremiumCustomerSpecification : AndSpecification - { - public AdultPremiumCustomerSpecification() - : base(new Age18PlusCustomerSpecification(), - new PremiumCustomerSpecification()) - { - } - } -} -```` - -现在,你就可以向下面一样重新编写 `GetAdultPremiumCustomerCountAsync` 方法: - -````csharp -public async Task GetAdultPremiumCustomerCountAsync() -{ - return await _customerRepository.CountAsync( - new AdultPremiumCustomerSpecification() - ); -} -```` - -> 你可以从这些例子中看到规约的强大之处.如果你之后想要更改 `PremiumCustomerSpecification` ,比如将余额从 `100.000` 修改为 `200.000` ,所有查询语句和合并的规约都将受到本次更改的影响.这是减少代码重复的好方法! - -## 讨论 - -虽然规约模式通常与C#的lambda表达式相比较,算是一种更老的方式.一些开发人员可能认为不再需要它,我们可以直接将表达式传入到仓储或领域服务中,如下所示: - -````csharp -var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18); -```` - -自从ABP的[仓储](Repositories.md)支持表达式,这是一个完全有效的用法.你不必在应用程序中定义或使用任何规约,可以直接使用表达式. - -所以,规约的意义是什么?为什么或者应该在什么时候考虑去使用它? - -### 何时使用? - -使用规约的一些好处: - -- **可复用**:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改“优质顾客”的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式. -- **可组合**:可以组合多个规约来创建新规约.这是另一种可复用性. -- **命名**:`PremiumCustomerSpecification` 更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约. -- **可测试**:规约是一个单独(且易于)测试的对象. - -### 什么时侯不要使用? - -- **没有业务含义的表达式**:不要对与业务无关的表达式和操作使用规约. -- **报表**:如果只是创建报表,不要创建规约,而是直接使用 `IQueryable` 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表.DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要. diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md deleted file mode 100644 index b7dfaa4fd5..0000000000 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ /dev/null @@ -1,447 +0,0 @@ -# 应用程序启动模板 - -## 介绍 - -应用程序启动模板是基于[领域驱动设计](../Domain-Driven-Design.md)(DDD)分层的应用程序结构. - -在这篇文档中详细介绍了**解决方案结构**和项目,如果你想快速入门,请遵循以下指南: - -* 参阅[ASP.NET Core MVC 模板入门](../Getting-Started-With-Startup-Templates.md)创建此模板的新解决方案并运行它. -* 参阅[ASP.NET Core MVC 教程](../Tutorials/AspNetCore-Mvc/Part-I.md)学习使用此模板开发应用程序. - -* [入门文档](../Getting-Started-AspNetCore-MVC-Template.md) 介绍了如何在几分钟内创建一个新的应用程序. -* [应用程序开发教程](../Tutorials/AspNetCore-Mvc/Part-I.md) 学习使用此模板开发应用程序. - -## 如何开始 - -你可以使用[ABP CLI](../CLI.md)创建基于此启动模板的新项目,或者你也可以在[入门](https://abp.io/get-started)页面创建并下载项目. 在这里我们使用CLI创建新项目. - -如果未安装ABP CLI,第一步是安装ABP CLI - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -然后使用 `abp new` 命令在空文件夹中创建新解决方案: - -````bash -abp new Acme.BookStore -t app -```` - -* `Acme.BookStore` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. -* 示例中指定了启动模板 (`-t` 或 `--template` 选项). 示指定模板时,默认模板是`app` . - -### 指定UI框架 - -模板提供了多个UI框架 - -* `mvc`: ASP.NET Core MVC Razor页面 (默认) -* `angular`: Angular UI - -使用 `-u` 或 `--ui` 选择指定UI框架: - -````bash -abp new Acme.BookStore -u angular -```` - -### 指定数据库提供程序 - -`MVC`模板支持以下数据库提供程序: - -- `ef`: Entity Framework Core (默认) -- `mongodb`: MongoDB - -使用 `-d` (或 `--database-provider`) 选项指定数据库提供程序: - -````bash -abp new Acme.BookStore -d mongodb -```` - -### 指定移动应用程序框架 - -该模板支持以下移动应用程序框架: - -- `react-native`: React Native - -使用 `-m` (or `--mobile`) 选项来指定移动应用程序框架: - -````bash -abp new Acme.BookStore -m react-native -```` - -如果未指定, 则不会创建任何移动应用程序. - -## 解决方案结构 - -根据命令的选项,会创建略有不同的解决方案结构. - -### 默认结构 - -如果未指定附加选项,你会得到如下所示的解决方案: - -![bookstore-visual-studio-solution-v3](../images/bookstore-visual-studio-solution-v3.png) - -项目组织在`src`和`test`文件夹中. `src`文件夹包含实际应用程序,该应用程序基于前面提到的[DDD](../Domain-Driven-Design.md)原则进行分层. 下图展示了解决方案的层和项目的依赖关系: - -![layered-project-dependencies](../images/layered-project-dependencies.png) - -下面介绍解决方案中的项目及依赖关系. - -#### .Domain.Shared 项目 - -项目包含常量,枚举和其他对象,这些对象实际上是领域层的一部分,但是解决方案中所有的层/项目中都会使用到. - -例如 `BookType` 枚举和 `BookConsts` 类 (可能是 `Book` 实体用到的常数字段,像`MaxNameLength`)都适合放在这个项目中. - -* 该项目不依赖解决方案中的其他项目. 其他项目直接或间接依赖该项目 - -#### .Domain 项目 - -解决方案的领域层. 它主要包含 [实体, 集合根](../Entities.md), [领域服务](../Domain-Services.md), [值类型](../Value-Types.md), [仓储接口](../Repositories.md) 和解决方案的其他领域对象. - -例如 `Book` 实体和 `IBookRepository` 接口都适合放在这个项目中. - -* 它依赖 `.Domain.Shared` 项目,因为项目中会用到它的一些常量,枚举和定义其他对象. - -#### .Application.Contracts 项目 - -项目主要包含 [应用服务](../Application-Services.md) **interfaces** 和应用层的 [数据传输对象](../Data-Transfer-Objects.md) (DTO). 它用于分离应用层的接口和实现. 这种方式可以将接口项目做为约定包共享给客户端. - -例如 `IBookAppService` 接口和 `BookCreationDto` 类都适合放在这个项目中. - -* 它依赖 `.Domain.Shared` 因为它可能会在应用接口和DTO中使用常量,枚举和其他的共享对象. - -#### .Application 项目 - -项目包含 `.Application.Contracts` 项目的 [应用服务](../Application-Services.md) 接口**实现**. - -例如 `BookAppService` 类适合放在这个项目中. - -* 它依赖 `.Application.Contracts` 项目, 因为它需要实现接口与使用DTO. -* 它依赖 `.Domain` 项目,因为它需要使用领域对象(实体,仓储接口等)执行应用程序逻辑. - -#### .EntityFrameworkCore 项目 - -这是集成EF Core的项目. 它定义了 `DbContext` 并实现 `.Domain` 项目中定义的仓储接口. - -* 它依赖 `.Domain` 项目,因为它需要引用实体和仓储接口. - -> 只有在你使用了EF Core做为数据库提供程序时,此项目才会可用. 如果选择的是其他数据库提供程序那么项目的名称会改变 - -#### .EntityFrameworkCore.DbMigrations 项目 - -包含解决方案的EF Core数据库迁移. 它有独立的 `DbContext` 来专门管理迁移. - -ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 `DbContext` 类. 这时用于迁移的 `DbContext` 就会发挥作用. 它将所有的 `DbContext` 配置统一到单个模型中以维护单个数据库的模式. 对于更高级的场景,可以程序可以拥有多个数据库(每个数据库有一个或多个模块表)和多个迁移`DbContext`(每个都维护不同的数据库模式) - -需要注意,迁移 `DbContext` 仅用于数据库迁移,而不在*运行时*使用. - -* 它依赖 `.EntityFrameworkCore` 项目,因为它重用了应用程序的 `DbContext` 配置 . - -> 只有在你使用了EF Core做为数据库提供程序时,此项目才会可用. -> 参阅[Entity Framework Core迁移指南](../Entity-Framework-Core-Migrations.md)了解这个项目的详细信息. - -#### .DbMigrator 项目 - -这是一个控制台应用程序,它简化了在开发和生产环境执行数据库迁移的操作.当你使用它时; - -* 必要时创建数据库(没有数据库时). -* 应用未迁移的数据库迁移. -* 初始化种子数据(当你需要时). - -> 这个项目有自己的 `appsettings.json` 文件. 所以如果要更改数据库连接字符串,请记得也要更改此文件. - -初始化种子数据很重要,ABP具有模块化的种子数据基础设施. 种子数据的更多信息,请参阅[文档](../Data-Seeding.md). - -虽然创建数据库和应用迁移似乎只对关系数据库有用,但即使你选择NoSQL数据库提供程序(如MongoDB),也会生成此项目. 这时,它会为应用程序提供必要的初始数据. - -* 它依赖 `.EntityFrameworkCore.DbMigrations` 项目 (针对EF Core),因为它需要访问迁移文件. -* 它依赖 `.Application.Contracts` 项目,因为它需要访问权限定义在初始化种子数据时为管理员用户赋予所有权限. - -#### .HttpApi 项目 - -用于定义API控制器. - -大多数情况下,你不需要手动定义API控制器,因为ABP的[动态API](../API/Auto-API-Controllers.md)功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方. - -* 它依赖 `.Application.Contracts` 项目,因为它需要注入应用服务接口. - -#### .HttpApi.Client 项目 - -定义C#客户端代理使用解决方案的HTTP API项目. 可以将上编辑共享给第三方客户端,使其轻松的在DotNet应用程序中使用你的HTTP API(其他类型的应用程序可以手动或使用其平台的工具来使用你的API). - -ABP有[动态 C# API 客户端](../API/Dynamic-CSharp-API-Clients.md)功能,所以大多数情况下你不需要手动的创建C#客户端代理. - -`.HttpApi.Client.ConsoleTestApp` 项目是一个用于演示客户端代理用法的控制台应用程序. - -* 它依赖 `.Application.Contracts` 项目,因为它需要使用应用服务接口和DTO. - -> 如果你不需要为API创建动态C#客户端代理,可以删除此项目和依赖项 - -#### .Web 项目 - -包含应用程序的用户界面(UI).如果使用ASP.NET Core MVC UI, 它包括Razor页面,javascript文件,样式文件,图片等... - -包含应用程序主要的 `appsettings.json` 配置文件,用于配置数据库连接字符串和应用程序的其他配置 - -* 依赖 `.HttpApi` 项目,因为UI层需要使用解决方案的API和应用服务接口. - -> 如果查看 `.Web.csproj` 源码, 你会看到对 `.Application` 和 `.EntityFrameworkCore.DbMigrations` 项目的引用. -> -> 在编写UI层时实际上不需要这些引用. 因为UI层通常不依赖于EF Core或应用层的实现. 这个启动模板已经为分层部署做好了准备,API层托管在不同与UI层的服务器中. -> -> 但是如果你不选择 `--tiered` 选项, .Web项目会有这些引用,以便能够将Web,Api和应用层托管在单个应用程序站点. -> -> 你可以在表示层中使用领域实体和仓储,但是根据DDD的理论,这被认为是一种不好的做法. - -#### Test 项目 - -解决方案有多个测试项目,每一层都会有一个: - -* `.Domain.Tests` 用于测试领域层. -* `.Application.Tests` 用于测试应用层. -* `.EntityFrameworkCore.Tests` 用于测试EF Core配置与自定义仓储. -* `.Web.Tests` 用于测试UI(适用于ASP.NET Core MVC UI). -* `.TestBase` 所有测试项目的基础(共享)项目. - -此外, `.HttpApi.Client.ConsoleTestApp` 是一个控制台应用程序(不是自动化测试项目),它用于演示.Net应用程序中HTTP API的用法. - -测试项目是用于做集成测试的: - -* 它完全集成到ABP框架和应用程序的所有服务. -* 如果数据库提供程序是EF Core,测试项目会使用SQLite内存数据库,如果是MongoDB,它使用[EphemeralMongo](https://github.com/asimmon/ephemeral-mongo)库. -* 授权被禁用,任何的应用服务都可以在测试中轻松调用. - -你依然可以编写单元测试,只不过它很难写(因为你需要准备mock/fake对象),但它的运行速度更快(因为只测试单个类并跳过所有初始化过程). - -#### 如何运行? - -设置`.Web`为启动项目. 默认用户名 `admin`, 密码 `1q2w3E*`. - -更多信息请参阅[ASP.NET Core MVC 模板入门](../Getting-Started-AspNetCore-MVC-Template.md). - -### 分层结构 - -如果你选择了ASP.NET Core UI并指定了 `--tiered` 选项,CLI会创建分层解决方案. 分层结构的目的是**将Web应用程序和HTTP API部署到不同的服务器**: - -![tiered-solution-servers](../images/tiered-solution-servers.png) - -* 浏览器渲染HTML,执行CSS和JavaScript来运行UI. -* Web服务器托管静态文件(CSS,JavaScript,图片...等)和动态组件(如Razor页面),它通过HTTP请求到API服务器执行应用程序的业务逻辑. -* API服务器托管HTTP API,使用应用程序的应用层和领域层执行业务逻辑. -* 最后数据库服务器托管数据库. - -与之前默认结构的三层部署比较,分层解决方案允许四层部署. - -> 除非你真的需要四层部署,一般建议采用默认结构,它更易于开发,部署和维护. - -解决方案结构如下所示: - -![bookstore-visual-studio-solution-tiered](../images/bookstore-visual-studio-solution-tiered.png) - -与默认结构不同,我们得到了两个新项目: `.AuthServer` 和 `.HttpApi.Host`. - -#### .AuthServer 项目 - -用于其他项目的身份验证服务器. `.Web`项目使用OpenId AuthServer. 然后使用访问令牌调用HTTP API服务器. HTTP API服务器使用bearer token从访问令牌获取声明授权当前用户. - -![tiered-solution-applications](../images/tiered-solution-applications.png) - -ABP使用开源的[IdentityServer4](https://identityserver.io/) 和 [OpenIddict](https://github.com/openiddict/) 框架做应用程序间的身份验证. 有关IdentityServer4,OpenIddict和OpenId Connect协议的详细信息请参阅[IdentityServer4文档](http://docs.identityserver.io) [OpenIddict文档](https://documentation.openiddict.com/). - -它有自己的`appsettings.json`文件(数据库连接字符串等其他配置). - -#### .HttpApi.Host 项目 - -该项目是一个承载解决方案API的应用程序. 它有自己的`appsettings.json`文件(数据库连接字符串等其他配置). - -#### .Web 项目 - -与默认结构一样,包含应用程序的用户界面(UI). 包括Razor页面,javascript文件,样式文件,图片等... - -项目包含`appsetting.json`文件,但没有连接字符串配置, 它不需要连接到数据库. 文件中主要包含远程API服务器端点和身份验证服务器 - -#### 前置条件 - -* [Redis](https://redis.io/): 应用程序使用Redis做分布式缓存,你需要安装并运行Redis. - -#### 如何运行? - -你应该按照以下顺序运行应用: - -* 首先运行`.AuthServer`,因为其他应用程序依赖它做身份验证. -* 然后运行`.HttpApi.Host`,因为`.Web`应用程序需要访问HTTI API. -* 最后运行`.Web`并登录到应用程序(用户名: `admin` 密码: `1q2w3E*`). - -### Angular UI - -如果你选择Angular做为UI框架(使用 `-u angular` 选择), 解决方案会被分成两个文件夹: - -* `angular` 是客户端部分,文件夹中包含了Angular UI 解决方案. -* `aspnet-core` 是服务端部分,文件夹中包含了ASP.NET Core解决方案. - -服务端部分与上面的描述的解决方案非常相似. `.HttpApi.Host` 项目提供API接口, Angular应用程序使用它提供的接口. - -`angular/src/environments` 文件夹下的文件含有应用程序的基础配置. - -#### AppModule(应用程序模块) - -`AppModule` 是应用程序的根模块. 一些ABP模块和一些基本模块导入到 `AppModule` 中. - -ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP 模块的初始需求. - -#### AppRoutingModule(应用程序路由模块) - -在 `AppRoutingModule` 中有可延迟加载的 ABP 模块作为路由. - -> 不应更改ABP模块的路径. - -你应该在 `data` 对象中添加 `routes` 属性, 以便在菜单中添加一个链接来重定向到自定义页面. - -```js -{ - path: 'dashboard', - loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), - canActivate: [authGuard, permissionGuard], - data: { - routes: { - name: 'ProjectName::Menu:Dashboard', - order: 2, - iconClass: 'fa fa-dashboard', - requiredPolicy: 'ProjectName.Dashboard.Host' - } as ABP.Route - } -} -``` -在上面的例子中; -* 如果用户没有登录, authGuard 会阻塞访问并重定向到登录页面. -* permissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页. -* `routes` 的 `name` 属性是菜单链接标签. 可以定义本地化 key. -* `routes` 对象的 `iconClass` 属性是菜单链接图标类. -* `routes` 对象的 `requiredPolicy` 属性是访问页面所需的策略 key. - -在上述 `routes` 定义之后, 如果用户被授权, 仪表盘链接将出现在菜单上. - -#### Shared Module(共享模块) - -所有模块可能需要的模块已导入到 `SharedModule`. 你应该将 `SharedModule` 导入所有模块. - -参见 [Sharing Modules(共享模块)](https://angular.io/guide/sharing-ngmodules) 文档. - -#### Environments(环境) - -`src/environments` 文件夹下的文件包含应用程序的基本配置. - -#### Home Module - -Home模块是一个可延迟加载的模块, 它加载应用程序的根地址. - -#### Styles(样式) - -在 `angular.json` 中向 `styles` 数组添加所需的样式文件. `AppComponent` 在主包加载后通过 `LazyLoadService` 加载一些样式文件, 以缩短第一次绘制的时间. - -#### Testing(测试) - -你应该在与要测试的文件相同的文件夹中创建测试. - -参见[测试文档](https://angular.io/guide/testing/). - -#### Depended Packages(依赖包) - -* [NG Bootstrap](https://ng-bootstrap.github.io/) 被用作UI组件库. -* [NGXS](https://www.ngxs.io/) 被用作状态管理库. -* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) 用于支持OAuth 2和OpenId Connect (OIDC). -* [Chart.js](https://www.chartjs.org/) 用于创建小部件. -* [ngx-validate](https://github.com/ng-turkey/ngx-validate) 用于对交互表单进行动态验证. - -### React Native - -如果使用 `-m react-native` 选项解决方案将[React Native](https://reactnative.dev/)应用程序作为默认值包含在 `react-native` 文件夹中. - -服务器端类似于上面描述的解决方案. `*.HttpApi.Host` 的项目提供 API, 所以 React 本机应用程序使用它. - -React 本机应用程序是用 [Expo](https://expo.io/)生成的. Expo 是一套基于 React Native 构建的工具, 帮助你快速启动一个应用程序, 尽管它有很多功能. - -React Native 应用文件夹结构, 如下图所示: - -![react-native-folder-structure](../images/react-native-folder-structure.png) - -* `App.js` 是应用程序的引导组件. -* `Environment.js` f文件有应用程序的基本配置. 在这个文件中定义了 `prod` and `dev` 配置. -* [Contexts](https://reactjs.org/docs/context.html) 是在 `src/contexts` 文件夹中创建的. -* [Higher order components](https://reactjs.org/docs/higher-order-components.html) 是在 `src/hocs` 文件夹中创建的. -* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) 是在 `src/hooks` 中创建的. -* [Axios interceptors](https://github.com/axios/axios#interceptors) 是在 `src/interceptors` 文件夹中创建. -* 工具函数从 `src/utils` 文件夹导出. - -#### Components(组件) - -可以在所有屏幕上使用的组件是在 `src/components` 文件夹中创建的. 所有组件都是作为一个能够使用 [hooks](https://reactjs.org/docs/hooks-intro.html) 的函数创建的. - -#### Screens(屏幕) - -![react-native-navigation-structure](../images/react-native-navigation-structure.png) - -Screens 是通过在 `src/screens` 文件夹中创建将名称分开的文件夹来创建的. 某些 screens 的某些部分可以拆分为组件. - -每个 screen 都在 `src/navigators` 文件夹中的导航器中使用. - -#### Navigation(导航) - -[React Navigation](https://reactnavigation.org/) 被用作导航库. 导航器是在 `src/navigators` 中创建的. 一个 [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) 导航器和几个 [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) 导航器在此文件夹中创建. 查看 [上图](#screens) 中的导航结构. - -#### State Management(状态管理) - -[Redux](https://redux.js.org/) 被用作状态管理库. [Redux Toolkit](https://redux-toolkit.js.org/) 库被用作高效Redux开发的工具集. - -在 `src/store` 文件夹中创建 Actions, reducers, sagas, selectors. 存储文件夹如下: - -![react-native-store-folder](../images/react-native-store-folder.png) - -* [**Store**](https://redux.js.org/basics/store) 在 `src/store/index.js` 文件中定义. -* [**Actions**](https://redux.js.org/basics/actions/) 是将数据从应用程序发送到存储的有效信息负载. -* [**Reducers**](https://redux.js.org/basics/reducers) 指定应用程序的状态如何更改以响应发送到存储的操作. -* [**Redux-Saga**](https://redux-saga.js.org/) 是一个库, 旨在使应用程序的副作用(即异步的事情, 如数据获取和不纯的事情, 如访问浏览器缓存)更容易管理. Sagas 是在 `src/store/sagas` 文件夹中创建的. -* [**Reselect**](https://github.com/reduxjs/reselect) 库用于创建缓存的选择器. 选择器是在 `src/store/selectors` 文件夹中创建的. - -#### APIs - -[Axios](https://github.com/axios/axios) 用作HTTP客户端库. Axios 实例从 `src/api/API.js` 导出 . 使用相同的配置进行HTTP调用. `src/api` 文件夹中还有为 API 调用创建的 API 文件. - -#### Theming(主题) - -[Native Base](https://nativebase.io/) 被用作UI组件库. 本地基本组件可以很容易地进行自定义.参见[Native Base customize](https://docs.nativebase.io/Customize.html#Customize) 文档.我们沿着同样的路走. - -* Native Base 主题变量在 `src/theme/variables` 文件夹中. -* Native Base 组件样式在 `src/theme/components` 文件夹中.这些文件是用 Native Base's `ejectTheme` 脚本生成的. -* 组件样式用 `src/theme/overrides` 文件夹下的文件覆盖. - -#### Testing(单元测试) - -将创建单元测试. - -参见[测试概述](https://reactjs.org/docs/testing.html)文档. - -#### Depended Libraries(依赖库) - -* [Native Base](https://nativebase.io/) 用作UI组件库. -* [React Navigation](https://reactnavigation.org/) 用作导航库. -* [Axios](https://github.com/axios/axios) 用作HTTP客户端库. -* [Redux](https://redux.js.org/) 用作状态管理库. -* [Redux Toolkit](https://redux-toolkit.js.org/) 库被用作高效Redux开发的工具集. -* [Redux-Saga](https://redux-saga.js.org/) 用于管理异步进程. -* [Redux Persist](https://github.com/rt2zz/redux-persist) 被用作状态持久化. -* [Reselect](https://github.com/reduxjs/reselect) 用于创建缓存的选择器. -* [i18n-js](https://github.com/fnando/i18n-js) 作为国际化库使用. -* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) 库可以轻松加载字体. -* [Formik](https://github.com/jaredpalmer/formik) 用于构建表单. -* [Yup](https://github.com/jquense/yup) 用于表单验证. - -## 社交/外部登录 - -如果你想要为你的应用程序配置社交/外部登录,请参阅[社交/外部登录](../Authentication/Social-External-Logins.md)文档. - -## 下一步是什么? - -* [入门文档](../Getting-Started.md)介绍了如何在几分钟内创建新应用程序. -* [Web应用程序开发教程](../Tutorials/Part-1.md)逐步介绍了应用程序开发. diff --git a/docs/zh-Hans/Startup-Templates/Console.md b/docs/zh-Hans/Startup-Templates/Console.md deleted file mode 100644 index c7299cd938..0000000000 --- a/docs/zh-Hans/Startup-Templates/Console.md +++ /dev/null @@ -1,27 +0,0 @@ -# 控制台应用程序启动模板 - -此模板用于创建一个最小的依赖关系的ABP控制台应用程序项目. - -## 如何开始? - -首先,如果你没有安装[ABP CLI](../CLI.md),请先安装它: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -在一个空文件夹使用 `abp new` 命令创建新解决方案: - -````bash -abp new Acme.MyConsoleApp -t console -```` - -`Acme.MyConsoleApp` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. - -## 解决方案结构 - -使用以上命令创建解决方案后,你会得到如下所示的解决方案: - -![basic-console-application-solution](../images/basic-console-application-solution.png) - -* `HelloWorldService` 是一个实现了 `ITransientDependency` 接口的示例服务. 它会自动注册到[依赖注入](../Dependency-Injection.md)系统. \ No newline at end of file diff --git a/docs/zh-Hans/Startup-Templates/Index.md b/docs/zh-Hans/Startup-Templates/Index.md deleted file mode 100644 index cdd5b8c332..0000000000 --- a/docs/zh-Hans/Startup-Templates/Index.md +++ /dev/null @@ -1,11 +0,0 @@ -# 启动模板 - -虽然你可以从一个空项目开始并手动添加所需的包,但启动模板可以非常轻松,舒适地使用ABP框架启动新的解决方案. - -单击下面列表中的名称以查看相关启动模板的文档: - -* [**app**](Application.md): 应用程序模板. -* [**module**](Module.md): 模块/服务模板. -* [**console**](Console.md): 控制台模板. -* [**WPF**](WPF.md): WPF模板. -* [**MAUI**](MAUI.md): MAUI模板. diff --git a/docs/zh-Hans/Startup-Templates/MAUI.md b/docs/zh-Hans/Startup-Templates/MAUI.md deleted file mode 100644 index d268da0942..0000000000 --- a/docs/zh-Hans/Startup-Templates/MAUI.md +++ /dev/null @@ -1,27 +0,0 @@ -# MAUI应用程序启动模板 - -此模板用于创建一个最小的依赖关系的ABP MAUI应用程序项目. - -## 如何开始? - -首先,如果你没有安装[ABP CLI](../CLI.md),请先安装它: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -在一个空文件夹使用 `abp new` 命令创建新解决方案: - -````bash -abp new Acme.MyMauiApp -t maui -```` - -`Acme.MyMauiApp` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. - -## 解决方案结构 - -使用以上命令创建解决方案后,你会得到如下所示的解决方案: - -![basic-maui-application-solution](../images/basic-maui-application-solution.png) - -* `HelloWorldService` 是一个实现了 `ITransientDependency` 接口的示例服务. 它会自动注册到[依赖注入](../Dependency-Injection.md)系统. \ No newline at end of file diff --git a/docs/zh-Hans/Startup-Templates/Module.md b/docs/zh-Hans/Startup-Templates/Module.md deleted file mode 100644 index d0c0be624f..0000000000 --- a/docs/zh-Hans/Startup-Templates/Module.md +++ /dev/null @@ -1,162 +0,0 @@ -# MVC模块启动模板 - -可用此模板开发基于[模块开发最佳实践和约定](../Best-Practices/Index.md)的可**复用 [应用程序模块](../Modules/Index.md)** . 它同样适用于开发**微服务**. - -## 如何开始? - -你可以使用[ABP CLI](../CLI.md)创建基于此启动模板的新项目,或者你也可以在[入门](https://abp.io/get-started)页面创建并下载项目. 在这里我们使用CLI创建新项目. - -如果未安装ABP CLI,第一步是安装ABP CLI - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -然后使用 `abp new` 命令在空文件夹中创建新解决方案: - -````bash -abp new Acme.IssueManagement -t module -```` - -* `Acme.IssueManagement` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. - -### 无用户界面 - -默认情况模板附带一个MVC UI,你可以使用`--no-ui`选项指定不包含UI层. - -````bash -abp new Acme.IssueManagement -t module --no-ui -```` - -## 解决方案结构 - -根据命令的选项,会创建略有不同的解决方案结构. 如果未指定选项,你会得到如下所示的解决方案: - -![issuemanagement-module-solution](../images/issuemanagement-module-solution.png) - -项目组织在`src`和`test`和`host`文件夹中: - -* `src` 文件夹包含基于[DDD](../Domain-Driven-Design.md)原则分层的实际模块. -* `test` 文件夹包含单元和集成测试. -* `host` 文件夹包含具有不同配置的应用程序,用于演示在应用程序中如何托管模块. 这些不是模块的一部分,但开发时会很有用. - -下图展示了模块的层和项目的依赖关系: - -![layered-project-dependencies-module](../images/layered-project-dependencies-module.png) - -下面介绍解决方案中的项目及依赖关系: - -### .Domain.Shared 项目 - -项目包含常量,枚举和其他对象,这些对象实际上是领域层的一部分,但是解决方案中所有的层/项目中都会使用到. - -例如 `IssueType` 枚举和 `IssueConsts` 类 (可能是 `Issue` 实体用到的常数字段,像`MaxTitleLength`)都适合放在这个项目中. - -* 该项目不依赖解决方案中的其他项目. 其他项目直接或间接依赖该项目. - -### .Domain 项目 - -解决方案的领域层. 它主要包含 [实体, 集合根](../Entities.md), [领域服务](../Domain-Services.md), 值类型, [仓储接口](../Repositories.md) 和解决方案的其他领域对象. - -例如 `Issue` 实体, `IssueManager` 领域服务和 `IIssueRepository` 接口都适合放在这个项目中. - -* 它依赖 `.Domain.Shared` 项目,因为项目中会用到它的一些常量,枚举和定义其他对象. - -### .Application.Contracts 项目 - -项目主要包含 [应用服务](../Application-Services.md) **interfaces** 和应用层的 [数据传输对象](../Data-Transfer-Objects.md) (DTO). 它用于分离应用层的接口和实现. 这种方式可以将接口项目做为约定包共享给客户端. - -例如 `IIssueAppService` 接口和 `IssueCreationDto` 类都适合放在这个项目中. - -* 它依赖 `.Domain.Shared` 因为它可能会在应用接口和DTO中使用常量,枚举和其他的共享对象. - -### .Application 项目 - -项目包含 `.Application.Contracts` 项目的 [应用服务](../Application-Services.md) 接口**实现**. - -例如 `IssueAppService` 类适合放在这个项目中. - -* 它依赖 `.Application.Contracts` 项目, 因为它需要实现接口与使用DTO. -* 它依赖 `.Domain` 项目,因为它需要使用领域对象(实体,仓储接口等)执行应用程序逻辑. - -### .EntityFrameworkCore 项目 - -这是集成EF Core的项目. 它定义了 `DbContext` 并实现 `.Domain` 项目中定义的仓储接口. - -* 它依赖 `.Domain` 项目,因为它需要引用实体和仓储接口. - -> 如果不需要使用EF Core,可以删除此项目. - -### .MongoDB 项目 - -这是集成MongoDB的项目. - -* 它依赖 `.Domain` 项目,因为它需要引用实体和仓储接口. - -> 如果不需要使用MongoDB,可以删除此项目. - -#### Test 项目 - -解决方案有多个测试项目,每一层都会有一个: - -* `.Domain.Tests` 用于测试领域层. -* `.Application.Tests` 用于测试应用层. -* `.EntityFrameworkCore.Tests` 用于测试EF Core配置与自定义仓储. -* `.MongoDB.Tests` 用于测试MongoDB配置与自定义仓储. -* `.TestBase` 所有测试项目的基础(共享)项目. - -此外, `.HttpApi.Client.ConsoleTestApp` 是一个控制台应用程序(不是自动化测试项目),它用于演示DotNet应用程序中HTTP API的用法. - -测试项目是用于做集成测试的: - -* 它完全集成到ABP框架和应用程序的所有服务. -* 如果数据库提供程序是EF Core,测试项目会使用SQLite内存数据库,如果是MongoDB,它使用[EphemeralMongo](https://github.com/asimmon/ephemeral-mongo)库. -* 授权被禁用,任何的应用服务都可以在测试中轻松调用. - -你依然可以编写单元测试,只不过它很难写(因为你需要准备mock/fake对象),但它的运行速度更快(因为只测试单个类并跳过所有初始化过程). - -> 领域层和应用层测试使用EF Core. 如果你删除了EF Core集成项目或者要使用MongoDB测试这些层,需要手动的修改项目引用和依赖. - -### Host 项目 - -解决方案中有一些宿主应用程序可以在开发时运行模块. 在开发时会很有用. 除了正在开发的模块外,宿主应用程序包括其他模块: - -宿主应用程序支持两种类型的场景. - -#### 单体 (统一) 应用场景 - -如果模块含有UI, 那么 `.Web.Unified` 应用程序于在一个站点上托管UI和API. 它有自己的 `appsettings.json` 配置文件(包括数据库连接字符串)和EF Core数据库迁移. - -`.Web.Unified` 应用程序有一个名为`YourProjectName_Unified`的数据库(本例中名为 *IssueManagement_Unified*). - -> 如果你选择了 `--no-ui` 选项, 这个项目不会出现在你的解决方案中. - -##### 如何运行? - -将`host/YourProjectName.Web.Unified`设置成启动项, 使用包管理控制台运行 `Update-Database` 命令迁移数据库,然后运行应用程序. 默认用户名: `admin` 密码: `1q2w3E*`. - -#### 分离部署&数据库 场景 - -在这个场景中,有三个应用程序; - -* `.AuthServer` 应用程序是其他应用程序使用的身份验证服务器,它有自己的 `appsettings.json` 包含数据库连接字符串和其他配置. -* `.HttpApi.Host` 托管模块的HTTP API. 它有自己的 `appsettings.json` 包含数据库连接字符串和其他配置. -* `.Web.Host` 托管模块的UI. 它包含 `appsettings.json` 文件, 但是其中没有数据库连接字符串, 因为它不需要连接到数据库, 它主要调用远程API服务器和身份认证服务器. - -下图展示了应用程序的关系: - -![tiered-solution-applications](../images/tiered-solution-applications.png) - -`.Web.Host` 项目使用OpenId Connect身份认证从`.AuthServer`获取当前用户的身份和访问令牌. 然后使用访问令牌调用 `.HttpApi.Host`. HTTP API 服务器使用bearer token验证访问令牌获取当前用户声明并授权用户. - -##### 前置条件 - -* [Redis](https://redis.io/): 应用程序使用Redis做分布式缓存,你需要安装并运行Redis. - -##### 如何运行? - -你需要按照以下顺序运行应用程序: - -- 首先, 运行 `.AuthServer`,因为其他应用程序依赖它做身份认证. -- 然后运行 `.HttpApi.Host`,因为`.Web.Host`应用程序使用API接口. -- 最后运行 `.Web.Host` 使用用户名: `admin` 密码: `1q2w3E*` 登录到应用程序. diff --git a/docs/zh-Hans/Startup-Templates/WPF.md b/docs/zh-Hans/Startup-Templates/WPF.md deleted file mode 100644 index 31a76eaa60..0000000000 --- a/docs/zh-Hans/Startup-Templates/WPF.md +++ /dev/null @@ -1,27 +0,0 @@ -# WPF应用程序启动模板 - -此模板用于创建一个最小的依赖关系的ABP WPF应用程序项目. - -## 如何开始? - -首先,如果你没有安装[ABP CLI](../CLI.md),请先安装它: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -在一个空文件夹使用 `abp new` 命令创建新解决方案: - -````bash -abp new Acme.MyWpfApp -t wpf -```` - -`Acme.MyWpfApp` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. - -## 解决方案结构 - -使用以上命令创建解决方案后,你会得到如下所示的解决方案: - -![basic-wpf-application-solution](../images/basic-wpf-application-solution.png) - -* `HelloWorldService` 是一个实现了 `ITransientDependency` 接口的示例服务. 它会自动注册到[依赖注入](../Dependency-Injection.md)系统. \ No newline at end of file diff --git a/docs/zh-Hans/Testing.md b/docs/zh-Hans/Testing.md deleted file mode 100644 index f2c54ea6a4..0000000000 --- a/docs/zh-Hans/Testing.md +++ /dev/null @@ -1,768 +0,0 @@ -# 自动化测试 - -## 介绍 - -ABP框架的设计考虑了可测试性. 有一些不同级别的自动化测试: - -* **单元测试**: 通常只测试一个类(或者一起测试几个类). 这些测试会很快. 然而, 你通常需要处理对服务依赖项的模拟. -* **集成测试**: 你通常会测试一个服务, 但这一次你不会模拟基本的基础设施和服务, 以查看它们是否正确地协同工作. -* **用户界面测试**: 测试应用程序的UI, 就像用户与应用程序交互一样. - -### 单元测试 vs 集成测试 - -与单元测试相比, 集成测试有一些显著的**优势**: - -* **编写更加简单** 因为你不需要模拟和处理依赖关系. -* 你的测试代码运行于所有真正的服务和基础设施(包括数据库映射和查询), 因此它更接近于**真正的应用程序测试**. - -同时它们有一些缺点: - -* 与单元测试相比, 它们**更慢**, 因为所有的基础设施都准备好了测试用例. -* 服务中的一个bug可能会导致多个测试用例失败, 因此在某些情况下, 可能会**更难找到真正的问题**. - -我们建议混合使用: 在必要的地方编写单元测试或集成测试, 并且有效的编写和维护它. - -## 应用程序启动模板 - -测试基础设施提供[应用程序启动模板](Startup-Templates/Application.md) , 并已经正确安装和配置. - -### 测试项目 - -请参见Visual Studio中的以下解决方案: - -![solution-test-projects](images/solution-test-projects.png) - -按层级系统分为多个测试项目: - -* `Domain.Tests` 用于测试领域层对象 (例如[领域服务](Domain-Services.md) 和 [实体](Entities.md)). -* `Application.Tests` 用于测试应用层对象 (例如[应用服务](Application-Services.md)). -* `EntityFrameworkCore.Tests` 用于测试你的自定义仓储实现或EF Core映射(如果你使用其他[数据访问](Data-Access.md))的话, 该项目将有所不同). -* `Web.Tests` 用于测试UI层(如页面、控制器和视图组件). 该项目仅适用于MVC / Razor页面应用程序. -* `TestBase` 包含一些由其他项目共享/使用的类. - -> `HttpApi.Client.ConsoleTestApp` 不是自动化测试的应用程序. 它是一个示例的控制台应用程序, 展示了如何从.NET控制台应用程序中调用HTTP API. - -以下的部分将介绍这些项目中包含的基类和其他基础设施. - -### 测试基础设施 - -解决方案中已经安装了以下库: - -* [xUnit](https://xunit.net/) 作为测试框架. -* [NSubstitute](https://nsubstitute.github.io/) 用于模拟. -* [Shouldly](https://github.com/shouldly/shouldly) 用于断言. - -虽然你可以用自己喜欢的工具替换它们, 但本文档和示例将基于这些工具. - -## 测试资源管理器 - -你可以在Visual Studio中使用测试资源管理器查看和运行测试. 其他IDE, 请参阅它们自己的文档. - -### 打开测试资源管理器 - -打开*测试*菜单下的*测试资源管理器*(如果尚未打开): - -![vs-test-explorer](images/vs-test-explorer.png) - -### 运行测试 - -然后, 你可以单击在视图中运行所有测试或运行按钮来运行测试. 初始启动模板为你提供了一些测试用例: - -![vs-startup-template-tests](images/vs-startup-template-tests.png) - -### 并行运行测试 - -支持并行运行测试. **强烈建议**并行运行所有测试, 这比逐个运行测试要快得多. - -要启用它, 请单击设置(齿轮)按钮附近的插入符号图标, 然后选择*并行运行测试*. - -![vs-run-tests-in-parallel](images/vs-run-tests-in-parallel.png) - -## 单元测试 - -对于单元测试, 不需要太多的配置. 通常会实例化你的类, 并对要测试的对象提供一些预先配置的模拟对象. - -### 没有依赖项的类 - -要测试的类没有依赖项是最简单的情况, 你可以直接实例化类, 调用其方法并做出断言. - -#### 示例: 测试实体 - -假设你有一个 `Issue` [实体](Entities.md), 如下所示: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace MyProject.Issues -{ - public class Issue : AggregateRoot - { - public string Title { get; set; } - public string Description { get; set; } - public bool IsLocked { get; set; } - public bool IsClosed { get; private set; } - public DateTime? CloseDate { get; private set; } - - public void Close() - { - IsClosed = true; - CloseDate = DateTime.UtcNow; - } - - public void Open() - { - if (!IsClosed) - { - return; - } - - if (IsLocked) - { - throw new IssueStateException("You can not open a locked issue!"); - } - - IsClosed = true; - CloseDate = null; - } - } -} - -```` - -请注意, `IsClosed`和`CloseDate`属性具有私有setter, 可以使用`Open()`和`Close()`方法强制执行某些业务逻辑: - -* 无论何时关闭issue, `CloseDate`都应设置为[当前时间](Timing.md). -* 如果issue被锁定, 则无法重新打开. 如果它被重新打开, `CloseDate`应该设置为`null`. - -由于`Issue`实体是领域层的一部分, 所以我们应该在`Domain.Tests`项目中测试它. 在`Domain.Tests`项目中创建一个`Issue_Tests`类: - -````csharp -using Shouldly; -using Xunit; - -namespace MyProject.Issues -{ - public class Issue_Tests - { - [Fact] - public void Should_Set_The_CloseDate_Whenever_Close_An_Issue() - { - // Arrange - - var issue = new Issue(); - issue.CloseDate.ShouldBeNull(); // null at the beginning - - // Act - - issue.Close(); - - // Assert - - issue.IsClosed.ShouldBeTrue(); - issue.CloseDate.ShouldNotBeNull(); - } - } -} -```` - -这个测试遵循AAA(Arrange-Act-Assert)模式: - -* **Arrange** 部分创建一个`Issue`实体, 并确保`CloseDate`在初始值为`null`. -* **Act** 部分执行我们想要测试的方法. -* **Assert** 部分检查`Issue`属性是否与我们预期的相同. - -`[Fact]`属性由[xUnit](https://xunit.net/)并将方法标记为测试方法. `Should...`扩展方法由[Shouldly](https://github.com/shouldly/shouldly)提供. 你可以直接使用xUnit中的`Assert`类, 使用Shouldly让它更舒适、更直观. - -当你执行测试时, 你将看到它成功通过: - -![issue-first-test](images/issue-first-test.png) - -让我们再添加两种测试方法: - -````csharp -[Fact] -public void Should_Allow_To_ReOpen_An_Issue() -{ - // Arrange - - var issue = new Issue(); - issue.Close(); - - // Act - - issue.Open(); - - // Assert - - issue.IsClosed.ShouldBeFalse(); - issue.CloseDate.ShouldBeNull(); -} - -[Fact] -public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() -{ - // Arrange - - var issue = new Issue(); - issue.Close(); - issue.IsLocked = true; - - // Act & Assert - - Assert.Throws(() => - { - issue.Open(); - }); -} -```` - -`Assert.Throws` 检查执行的代码是否匹配引发的异常. - -> 有关这些库的更多信息, 请参阅xUnit & Shoudly的文档. - -### 具有依赖项的类 - -如果你的服务中有依赖项, 并且你想对该服务进行单元测试, 那么你需要模拟这些依赖项. - -#### 示例: 测试领域服务 - -假设你有一个`IssueManager` [领域服务](Domain-Services.md), 定义如下: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp; -using Volo.Abp.Domain.Services; - -namespace MyProject.Issues -{ - public class IssueManager : DomainService - { - public const int MaxAllowedOpenIssueCountForAUser = 3; - - private readonly IIssueRepository _issueRepository; - - public IssueManager(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task AssignToUserAsync(Issue issue, Guid userId) - { - var issueCount = await _issueRepository.GetIssueCountOfUserAsync(userId); - - if (issueCount >= MaxAllowedOpenIssueCountForAUser) - { - throw new BusinessException( - code: "IM:00392", - message: $"You can not assign more" + - $"than {MaxAllowedOpenIssueCountForAUser} issues to a user!" - ); - } - - issue.AssignedUserId = userId; - } - } -} -```` - -`IssueManager`依赖于`IssueRepository`服务, 在本例中将模拟该服务. - -**业务逻辑**: 示例`AssignToUserAsync`不允许向用户分配超过3个issue (`MaxAllowedOpenIssueCountForAUser`常量). 在这种情况下, 如果要分配issue, 首先需要取消现有issue的分配. - -下面的测试用例给出一个有效的赋值: - -````csharp -using System; -using System.Threading.Tasks; -using NSubstitute; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueManager_Tests - { - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Arrange - - var userId = Guid.NewGuid(); - - var fakeRepo = Substitute.For(); - fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1); - - var issueManager = new IssueManager(fakeRepo); - - var issue = new Issue(); - - // Act - - await issueManager.AssignToUserAsync(issue, userId); - - //Assert - - issue.AssignedUserId.ShouldBe(userId); - await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); - } - } -} -```` - -* `Substitute.For` 创建一个模拟(假)对象, 该对象被传递到`IssueManager`构造函数中. -* `fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1)` 确保仓储中的`GetIssueContofuseRasync`方法返回`1`. -* `issueManager.AssignToUserAsync` 不会引发任何异常, 因为仓储统计当前分配的issue数量并且返回`1`. -* `issue.AssignedUserId.ShouldBe(userId);` 行检查`AssignedUserId`的值是否正确. -* `await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);` 检查 `IssueManager` 实际只调用了 `GetIssueCountOfUserAsync` 方法一次. - -让我们添加第二个测试, 看看它是否能阻止将issue分配给超过分配数量的用户: - -````csharp -[Fact] -public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() -{ - // Arrange - - var userId = Guid.NewGuid(); - - var fakeRepo = Substitute.For(); - fakeRepo - .GetIssueCountOfUserAsync(userId) - .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); - - var issueManager = new IssueManager(fakeRepo); - - // Act & Assert - - var issue = new Issue(); - - await Assert.ThrowsAsync(async () => - { - await issueManager.AssignToUserAsync(issue, userId); - }); - - issue.AssignedUserId.ShouldBeNull(); - await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); -} -```` - -> 有关模拟的更多信息, 请参阅[NSubstitute](https://nsubstitute.github.io/)文档. - -模拟单个依赖项相对容易. 但是, 当依赖关系增长时, 设置测试对象和模拟所有依赖关系变得越来越困难. 请参阅不需要模拟依赖项的*Integration Tests*部分. - -### 提示: 共享测试类构造函数 - -[xUnit](https://xunit.net/) 为每个测试方法创建一个**新测试类实例**(本例中为`IssueManager_Tests`). 因此, 你可以将一些*Arrange*代码移动到构造函数中, 以减少代码重复. 构造函数将针对每个测试用例执行, 并且不会相互影响, 即使它们是并行工作. - -**示例: 重构`IssueManager_Tests`以减少代码重复** - -````csharp -using System; -using System.Threading.Tasks; -using NSubstitute; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueManager_Tests - { - private readonly Guid _userId; - private readonly IIssueRepository _fakeRepo; - private readonly IssueManager _issueManager; - private readonly Issue _issue; - - public IssueManager_Tests() - { - _userId = Guid.NewGuid(); - _fakeRepo = Substitute.For(); - _issueManager = new IssueManager(_fakeRepo); - _issue = new Issue(); - } - - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Arrange - _fakeRepo.GetIssueCountOfUserAsync(_userId).Returns(1); - - // Act - await _issueManager.AssignToUserAsync(_issue, _userId); - - //Assert - _issue.AssignedUserId.ShouldBe(_userId); - await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); - } - - [Fact] - public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() - { - // Arrange - _fakeRepo - .GetIssueCountOfUserAsync(_userId) - .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await _issueManager.AssignToUserAsync(_issue, _userId); - }); - - _issue.AssignedUserId.ShouldBeNull(); - await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); - } - } -} -```` - -> 保持测试代码整洁, 以创建可维护的测试组件. - -## 集成测试 - -> 你还可以按照[Web应用程序开发教程](Tutorials/Part-1.md)学习开发全栈应用程序, 包括集成测试. - -### 集成测试基础 - -ABP为编写集成测试提供了完整的基础设施. 所有ABP基础设施和服务都将在你的测试中执行. 应用程序启动模板附带了为你预先配置的必要基础设施; - -#### 数据库 - -启动模板使用EF Core配置**内存中的SQLite**数据库(对于MongoDB, 它使用[EphemeralMongo](https://github.com/asimmon/ephemeral-mongo)). 因此, 所有配置和查询都是针对真实数据库执行的, 你甚至可以测试数据库事务. - -使用内存中的SQLite数据库有两个主要优点: - -* 它比外部DBMS更快. -* 它会为每个测试用例创建一个**新的数据库**, 这样测试就不会相互影响. - -> **提示**: 不要将EF Core的内存数据库用于高级集成测试. 它不是一个真正的DBMS, 在细节上有很多不同. 例如, 它不支持事务和回滚场景, 因此无法真正测试失败的场景. 另一方面, 内存中的SQLite是一个真正的DBMS, 支持SQL数据库的基本功能. - -### 种子数据 - -针对空数据库编写测试是不现实的. 在大多数情况下, 需要在数据库中保存一些初始数据. 例如, 如果你编写了一个查询、更新和删除产品的测试类, 那么在执行测试用例之前, 在数据库中有一些产品数据会很有帮助. - -ABP的[种子数据](Data-Seeding.md)系统是一种强大的初始化数据的方法. 应用程序启动模板在`.TestBase`项目中有一个*YourProject*TestDataSeedContributor类. 你可以在其中添加, 以获得可用于每个测试方法的初始数据. - -**示例: 创建一些Issue作为种子数据** - -````csharp -using System.Threading.Tasks; -using MyProject.Issues; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; - -namespace MyProject -{ - public class MyProjectTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IIssueRepository _issueRepository; - - public MyProjectTestDataSeedContributor(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue one", - Description = "Test issue one description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue two", - Description = "Test issue two description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue three", - Description = "Test issue three description", - AssignedUserId = TestData.User1Id - }); - - await _issueRepository.InsertAsync( - new Issue - { - Title = "Test issue four", - Description = "Test issue four description", - AssignedUserId = TestData.User2Id - }); - } - } -} -```` - -还创建了一个静态类来存储用户的 `Id`: - -````csharp -using System; - -namespace MyProject -{ - public static class TestData - { - public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B"); - public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999"); - } -} -```` - -通过这种方式, 我们可以使用这些已知Issue和用户的`Id`来运行测试. - -### 示例: 测试领域服务 - -`AbpIntegratedTest`类 (定义在[Volo.Abp.TestBase](https://www.nuget.org/packages/Volo.Abp.TestBase)) 用于编写集成到ABP框架的测试. `T`是用于设置和初始化应用程序的根模块的类型. - -应用程序启动模板在每个测试项目中都有基类, 因此你可以从这些基类派生, 以使其更简单. - -`IssueManager`测试将被重写成集成测试 - -````csharp -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueManager_Integration_Tests : MyProjectDomainTestBase - { - private readonly IssueManager _issueManager; - private readonly Issue _issue; - - public IssueManager_Integration_Tests() - { - _issueManager = GetRequiredService(); - _issue = new Issue - { - Title = "Test title", - Description = "Test description" - }; - } - - [Fact] - public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() - { - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await _issueManager.AssignToUserAsync(_issue, TestData.User1Id); - }); - - _issue.AssignedUserId.ShouldBeNull(); - } - - [Fact] - public async Task Should_Assign_An_Issue_To_A_User() - { - // Act - await _issueManager.AssignToUserAsync(_issue, TestData.User2Id); - - //Assert - _issue.AssignedUserId.ShouldBe(TestData.User2Id); - } - } -} -```` - -* 第一个测试方法将issue分配给User1, 其中User1已经分配了种子数据代码中的3个issue. 因此, 它抛出了一个`BusinessException`. -* 第二种测试方法将issue分配给User2, User2只分配了一个issue. 因此, 该方法成功了. - -这个类通常位于`.Domain.Tests`项目中, 因为它测试位于`.Domain`项目中的类. 它派生自`MyProjectDomainTestBase`, 并已经为正确运行测试进行了配置. - -编写这样一个集成测试类非常简单. 另一个好处是, 在以后向`IssueManager`类添加另一个依赖项时, 不需要更改测试类. - -### 示例: 测试应用服务 - -测试[应用服务](Application-Services.md)并没有太大的不同. 假设你已经创建了一个`IssueAppService`, 定义如下: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace MyProject.Issues -{ - public class IssueAppService : ApplicationService, IIssueAppService - { - private readonly IIssueRepository _issueRepository; - - public IssueAppService(IIssueRepository issueRepository) - { - _issueRepository = issueRepository; - } - - public async Task> GetListAsync() - { - var issues = await _issueRepository.GetListAsync(); - - return ObjectMapper.Map, List>(issues); - } - } -} -```` - -*(假设你还定义了`IIssueAppService`和`IssueDto`, 并在`Issue`和`IssueDto`之间创建了[对象映射](Object-To-Object-Mapping.md))* - -现在, 你可以在`.Application.Tests`项目中编写一个测试类: - -````csharp -using System.Threading.Tasks; -using Shouldly; -using Xunit; - -namespace MyProject.Issues -{ - public class IssueAppService_Tests : MyProjectApplicationTestBase - { - private readonly IIssueAppService _issueAppService; - - public IssueAppService_Tests() - { - _issueAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_All_Issues() - { - //Act - var issueDtos = await _issueAppService.GetListAsync(); - - //Assert - issueDtos.Count.ShouldBeGreaterThan(0); - } - } -} -```` - -就这么简单. 此测试方法测试的所有内容, 包括应用服务、EF Core映射、对象到对象映射和仓储实现. 通过这种方式, 你可以完全测试解决方案的应用层和领域层. - -### 处理集成测试中的工作单元 - -ABP的[工作单元](Unit-Of-Work.md)系统控制应用程序中的数据库连接和事务管理. 它可以在你编写应用程序代码时无缝工作, 因此你可能没有意识到它. - -在ABP框架中, 所有数据库操作都必须在一个工作单元作用域内执行. 当你测试[应用服务](Application-Services.md)方法时, 工作单元的作用域将是应用服务方法的作用域. 如果你正在测试[仓储](Repositories.md)方法, 那么工作单元作用域将是你的仓储方法的作用域. - -在某些情况下, 你可能需要手动控制工作单元作用域. 可以考虑下面的测试方法: - -````csharp -public class IssueRepository_Tests : MyProjectDomainTestBase -{ - private readonly IRepository _issueRepository; - - public IssueRepository_Tests() - { - _issueRepository = GetRequiredService>(); - } - - public async Task Should_Query_By_Title() - { - IQueryable queryable = await _issueRepository.GetQueryableAsync(); - var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - } -} -```` - -我们正在使用`_issueRepository.GetQueryableAsync`获取`IQueryable` 对象. 然后, 我们使用`FirstOrDefaultAsync`方法按标题查询issue. 此时执行数据库查询, 你将会得到一个异常, 表明没有起作用的工作单元. - -要使该测试正常工作, 你应该手动启动工作单元作用域, 如下所示: - -````csharp -public class IssueRepository_Tests : MyProjectDomainTestBase -{ - private readonly IRepository _issueRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public IssueRepository_Tests() - { - _issueRepository = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - public async Task Should_Query_By_Title() - { - using (var uow = _unitOfWorkManager.Begin()) - { - IQueryable queryable = await _issueRepository.GetQueryableAsync(); - var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - await uow.CompleteAsync(); - } - } -} -```` - -我们已经使用了`IUnitOfWorkManager`服务来创建一个工作单元作用域, 然后在该作用域内调用了`FirstOrDefaultAsync`方法, 所以不再有问题了. - -> 请注意, 我们测试了`FirstOrDefaultAsync`来演示工作单元的问题. 作为一个好的标准, 编写自己的代码. - -### 使用DbContext - -在某些情况下, 你可能希望使用Entity Framework的`DbContext`对象来执行测试方法中的数据库操作. 在这种情况下, 可以使用`IDbContextProvider`服务在工作单元内获取`DbContext`实例. - -下面的示例展示了如何在测试方法中创建`DbContext`对象: - -````csharp -public class MyDbContext_Tests : MyProjectDomainTestBase -{ - private readonly IDbContextProvider _dbContextProvider; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public IssueRepository_Tests() - { - _dbContextProvider = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - public async Task Should_Query_By_Title() - { - using (var uow = _unitOfWorkManager.Begin()) - { - var dbContext = await _dbContextProvider.GetDbContextAsync(); - var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "My issue title"); - issue.ShouldNotBeNull(); - await uow.CompleteAsync(); - } - } -} -```` - -就像我们在*集成测试中处理工作单元*一节中所做的那样, 我们应该在起作用的工作单元内执行`DbContext`操作. - -对于[MongoDB](MongoDB.md), 你可以使用`IMongoDbContextProvider`服务获取`DbContext`对象, 并在测试方法中直接使用MongoDB APIs. - -## 用户界面测试 - -一般来说, 有两种类型的UI测试: - -### 非可视化测试 - -此类测试完全取决于UI框架的选择: - -* 对于MVC / Razor页面UI, 通常向服务器发出请求, 获取HTML, 并测试返回的结果中是否存在一些预期的DOM元素. -* Angular有自己的基础设施和实践来测试组件、视图和服务. - -请参阅以下文档以了解非可视化UI测试: - -* [Testing in ASP.NET Core MVC / Razor Pages](UI/AspNetCore/Testing.md) -* [Testing in Angular](UI/Angular/Testing.md) -* [Testing in Blazor](UI/Blazor/Testing.md) - -### 可视化测试 - -与真实用户一样, 可视化测试用于与应用程序UI交互. 它全面测试应用程序, 包括页面和组件的外观. - -可视化UI测试超出了ABP框架的范围. 行业中有很多工具(比如[Selenium](https://www.selenium.dev/))可以用来测试应用程序的UI. diff --git a/docs/zh-Hans/Text-Templating-Razor.md b/docs/zh-Hans/Text-Templating-Razor.md deleted file mode 100644 index 7394133587..0000000000 --- a/docs/zh-Hans/Text-Templating-Razor.md +++ /dev/null @@ -1,570 +0,0 @@ -# Razor 集成 - -Razor模板是标准的C#类, 所以你可以使用任何C#的功能, 例如`依赖注入`, 使用`LINQ`, 自定义方法甚至使用`仓储` - -## 安装 - -建议使用[ABP CLI](CLI.md)安装此包. - -### 使用ABP CLI - -在项目文件夹(.csproj 文件)中打开命令行窗口并输入以下命令: - -````bash -abp add-package Volo.Abp.TextTemplating.Razor -```` - -### 手动安装 - -如果你想要手动安装: - -1. 添加 [Volo.Abp.TextTemplating.Razor](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Razor) NuGet 包到你的项目: - -```` -Install-Package Volo.Abp.TextTemplating.Razor -```` - -2.添加 `AbpTextTemplatingRazorModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpTextTemplatingRazorModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## 添加 MetadataReference到CSharpCompilerOptions - -你需要将添加`MetadataReference`模板中使用的类型添加到 `CSharpCompilerOptions` 的 `References`. - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - Configure(options => - { - options.References.Add(MetadataReference.CreateFromFile(typeof(YourModule).Assembly.Location)); - }); -} -``` - -## 添加MetadataReference到模板 - -你可以添加一些`MetadataReference`到模板 - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - services.Configure(options => - { - //Hello is template name. - options.TemplateReferences.Add("Hello", new List() - { - Assembly.Load("Microsoft.Extensions.Logging.Abstractions"), - Assembly.Load("Microsoft.Extensions.Logging") - } - .Select(x => MetadataReference.CreateFromFile(x.Location)) - .ToList()); - }); -} -``` - -## 定义模板 - -在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: - -````csharp -public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider -{ - public override void Define(ITemplateDefinitionContext context) - { - context.Add( - new TemplateDefinition("Hello") //template name: "Hello" - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/Hello/Hello.cshtml", //template content path - isInlineLocalized: true - ) - ); - } -} -```` - -* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. -* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). -* `/Demos/Hello/Hello.cshtml` 是模板文件的路径. -* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. -* `WithRenderEngine` 方法为模板设置渲染引擎. - -### 模板基类 - -每个 `cshtml` 模板页面都需要继承`RazorTemplatePageBase` 或 `RazorTemplatePageBase`. 基类提供了一些使用实用的属性可以在模板中使用. 例如: `Localizer`, `ServiceProvider`. - -### 模板内容 - -`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.cshtml` 文件,并在属性窗口中将其标记为"**嵌入式资源**": - -![hello-template-razor](images/hello-template-razor.png) - -示例 `Hello.cshtml` 内容如下所示: - -```csharp -namespace HelloModelNamespace -{ - public class HelloModel - { - public string Name { get; set; } - } -} -``` - -[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded("TextTemplateDemo"); -}); -```` - -* `TextTemplateDemoModule`是模块类. -* `TextTemplateDemo` 是你的项目的根命名空间. - -## 渲染模板 - -`ITemplateRenderer` 服务用于渲染模板内容. - -### 示例: 渲染一个简单的模板 - -````csharp -public class HelloDemo : ITransientDependency -{ - private readonly ITemplateRenderer _templateRenderer; - - public HelloDemo(ITemplateRenderer templateRenderer) - { - _templateRenderer = templateRenderer; - } - - public async Task RunAsync() - { - var result = await _templateRenderer.RenderAsync( - "Hello", //the template name - new HelloModel - { - Name = "John" - } - ); - - Console.WriteLine(result); - } -} -```` - -* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. -* `RenderAsync` 有两个基本参数: - * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). - * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). - -示例会返回以下结果: - -````csharp -Hello John :) -```` - -## 本地化 - -可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. - -### 内联本地化 - -内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. - -#### 示例: 重置密码链接 - -假设你需要向用户发送电子邮件重置密码. 模板内容: - -```csharp -namespace ResetMyPasswordModelNamespace -{ - public class ResetMyPasswordModel - { - public string Link { get; set; } - - public string Name { get; set; } - } -} -``` - -```csharp -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -@Localizer["ResetMyPassword", Model.Name] -``` - -`Localizer` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: - -````json -"ResetMyPasswordTitle": "Reset my password", -"ResetMyPassword": "Hi {0}, Click here to reset your password" -```` - -你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: - -````csharp -context.Add( - new TemplateDefinition( - "PasswordReset", //Template name - typeof(DemoResource) //LOCALIZATION RESOURCE - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/PasswordReset/PasswordReset.cshtml", //template content path - isInlineLocalized: true - ) -); -```` - -当你这样渲染模板时: - -````csharp -var result = await _templateRenderer.RenderAsync( - "PasswordReset", //the template name - new PasswordResetModel - { - Name = "john", - Link = "https://abp.io/example-link?userId=123&token=ABC" - } -); -```` - -你可以看到以下本地化结果: - -````csharp -Hi john, Click here to reset your password -```` - -> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. - -### 多个内容本地化 - -你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. - -#### 示例: 欢迎电子邮件模板 - -假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. - -首先创建一个文件夹,将模板放在里面,像 `en.cshtml`, `tr.cshtml` 每一个你支持的文化: - -![multiple-file-template-razor](images/multiple-file-template-razor.png) - -然后在模板定义提供程序类中添加模板定义: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en" - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", //template content folder - isInlineLocalized: false - ) -); -```` - -* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. -* 指定 **模板文件夹** 而不是单个模板文件. -* 设置 `isInlineLocalized` 为 `false`. - -就这些,你可以渲染当前文化的模板: - -````csharp -var result = await _templateRenderer.RenderAsync("WelcomeEmail"); -```` - -> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. - -### 指定文化 - -`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. - -````csharp -var result = await _templateRenderer.RenderAsync( - "WelcomeEmail", - cultureName: "en" -); -```` - -## 布局模板 - -布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. - -### 示例: 邮件HTML布局模板 - -例如,你想为所有电子邮件模板创建一个布局. - -首先像之前一样创建一个模板文件: - -```csharp -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase - - - - - - - @Body - - -``` - -* 布局模板必须具有 **Body** 部分作为渲染的子内容的占位符. - -在模板定义提供程序中注册模板: - -````csharp -context.Add( - new TemplateDefinition( - "EmailLayout", - isLayout: true //SET isLayout! - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/EmailLayout/EmailLayout.cshtml", - isInlineLocalized: true - ) -); -```` - -现在你可以将此模板用作任何其他模板的布局: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en", - layout: "EmailLayout" //Set the LAYOUT - ) - .WithRazorEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", - isInlineLocalized: false - ) -); -```` - -## 全局上下文 - -ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. - -示例模板内容: - -````csharp -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -A global object value: @GlobalContext["myGlobalObject"] -```` - -模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: - -````csharp -var result = await _templateRenderer.RenderAsync( - "GlobalContextUsage", - globalContext: new Dictionary - { - {"myGlobalObject", "TEST VALUE"} - } -); -```` - -渲染的结果将是: - -```` -A global object value: TEST VALUE -```` - -## 替换存在的模板 - -通过替换应用程序中使用的模块定义的模板. 这样你可以根据自己的需求自定义模板,而无需更改模块代码. - -### 选项-1: 使用虚拟文件系统 - -[虚拟文件系统](Virtual-File-System.md)允许你通过将相同文件放入项目中的相同路径来覆盖任何文件. - -#### 示例: 替换标准电子邮件布局模板 - -ABP框架提供了一个[邮件发送系统](Emailing.md), 它在内部使用文本模板来渲染邮件内容. 它在 `/Volo/Abp/Emailing/Templates/Layout.cshtml` 路径定义了一个标准邮件布局模板. 模板的唯一名称是 `Abp.StandardEmailTemplates.Layout` 并且这个字符中`Volo.Abp.Emailing.Templates.StandardEmailTemplates`静态类上定义为常量. - -执行以下步骤将替换模板替换成你自定义的; - -**1)** 在你的项目中相同的路径添加一个新文件 (`/Volo/Abp/Emailing/Templates/Layout.cshtml`): - -![replace-email-layout-razor](images/replace-email-layout-razor.png) - -**2)** 准备你的邮件布局模板: - -````html -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase - - - - - - -

    This my header

    - - @Body - -
    - This is my footer... -
    - - -```` - -此示例只是向模板添加页眉和页脚并呈现它们之间的内容(请参阅上面的布局模板部分). - - -**3)** 在`.csproj`文件配置嵌入式资源e - -* 添加 [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet 包到你的项目. -* 在 `.csproj` 中添加 `true` 到 `...` 部分. -* 添加以下代码到你的 `.csproj` 文件: - -````xml - - - - -```` - -这将模板文件做为"嵌入式资源". - -**4)** 配置虚拟文件系统 - -在[模块](Module-Development-Basics.md) `ConfigureServices` 方法配置 `AbpVirtualFileSystemOptions` 将嵌入的文件添加到虚拟文件系统中: - -```csharp -Configure(options => -{ - options.FileSets.AddEmbedded(); -}); -``` - -`BookStoreDomainModule` 应该是你的模块名称 - -> 确保你的模块(支持或间接)[依赖](Module-Development-Basics.md) `AbpEmailingModule`. 因为VFS可以基于依赖顺序覆盖文件. - -现在渲染邮件布局模板时会使用你的模板. - -### 选项-2: 使用模板定义提供者 - -你可以创建一个模板定义提供者类来获取邮件布局模板来更改它的虚拟文件路径. - -**示例: 使用 `/MyTemplates/EmailLayout.cshtml` 文件而不是标准模板** - -```csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace MyProject -{ - public class MyTemplateDefinitionProvider - : TemplateDefinitionProvider, ITransientDependency - { - public override void Define(ITemplateDefinitionContext context) - { - var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); - - emailLayoutTemplate - .WithVirtualFilePath( - "/MyTemplates/EmailLayout.cshtml", - isInlineLocalized: true - ); - } - } -} -``` - -你依然应该添加 `/MyTemplates/EmailLayout.cshtml` 到虚拟文件系统. 这种方法允许你在任何文件夹中找到模板,而不是在依赖模块定义的文件夹中. - -除了模板内容之外你还可以操作模板定义属性, 例如`DisplayName`, `Layout`或`LocalizationSource`. - -## 高级功能 - -本节介绍文本模板系统的一些内部知识和高级用法. - -### 模板内容Provider - -`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. - -> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. - -示例: - -````csharp -public class TemplateContentDemo : ITransientDependency -{ - private readonly ITemplateContentProvider _templateContentProvider; - - public TemplateContentDemo(ITemplateContentProvider templateContentProvider) - { - _templateContentProvider = templateContentProvider; - } - - public async Task RunAsync() - { - var result = await _templateContentProvider - .GetContentOrNullAsync("Hello"); - - Console.WriteLine(result); - } -} -```` - -结果是原始模板内容: - -```` -@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase -Hello @Model.Name -```` - -* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. -* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). - -### 模板内容贡献者 - -`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. - -你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. - -示例: - -````csharp -public class MyTemplateContentProvider - : ITemplateContentContributor, ITransientDependency -{ - public async Task GetOrNullAsync(TemplateContentContributorContext context) - { - var templateName = context.TemplateDefinition.Name; - - //TODO: Try to find content from another source - return null; - } -} -```` - -如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. - -### Template Definition Manager - -`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). - -## 另请参阅 - -* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). -* [本地化系统](Localization.md). -* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Text-Templating-Scriban.md b/docs/zh-Hans/Text-Templating-Scriban.md deleted file mode 100644 index 97cd0ab631..0000000000 --- a/docs/zh-Hans/Text-Templating-Scriban.md +++ /dev/null @@ -1,527 +0,0 @@ -# Scriban 集成 - -## 安装 - -建议使用[ABP CLI](CLI.md)安装此包. - -### 使用ABP CLI - -在项目文件夹(.csproj 文件)中打开命令行窗口并输入以下命令: - -````bash -abp add-package Volo.Abp.TextTemplating.Scriban -```` - -### 手动安装 - -如果你想要手动安装: - -1. 添加 [Volo.Abp.TextTemplating.Scriban](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Scriban) NuGet 包到你的项目: - -```` -Install-Package Volo.Abp.TextTemplating.Scriban -```` - -2.添加 `AbpTextTemplatingScribanModule` 到你的模块的依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpTextTemplatingScribanModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## 定义模板 - -在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: - -````csharp -public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider -{ - public override void Define(ITemplateDefinitionContext context) - { - context.Add( - new TemplateDefinition("Hello") //template name: "Hello" - .WithVirtualFilePath( - "/Demos/Hello/Hello.tpl", //template content path - isInlineLocalized: true - ) - .WithScribanEngine() - ); - } -} -```` - -* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. -* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). -* `/Demos/Hello/Hello.tpl` 是模板文件的路径. -* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. -* `WithScribanEngine` 方法为模板设置渲染引擎. - -### 模板内容 - -`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.tpl` 文件,并在属性窗口中将其标记为"**嵌入式资源**": - -![hello-template](images/hello-template.png) - -示例 `Hello.tpl` 内容如下所示: - -```` -Hello {%{{{model.name}}}%} :) -```` - -[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded("TextTemplateDemo"); -}); -```` - -* `TextTemplateDemoModule`是模块类. -* `TextTemplateDemo` 是你的项目的根命名空间. - -## 渲染模板 - -`ITemplateRenderer` 服务用于渲染模板内容. - -### 示例: 渲染一个简单的模板 - -````csharp -public class HelloDemo : ITransientDependency -{ - private readonly ITemplateRenderer _templateRenderer; - - public HelloDemo(ITemplateRenderer templateRenderer) - { - _templateRenderer = templateRenderer; - } - - public async Task RunAsync() - { - var result = await _templateRenderer.RenderAsync( - "Hello", //the template name - new HelloModel - { - Name = "John" - } - ); - - Console.WriteLine(result); - } -} -```` - -* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. -* `RenderAsync` 有两个基本参数: - * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). - * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). - -示例会返回以下结果: - -````csharp -Hello John :) -```` - -### 匿名模型 - -虽然建议为模板创建模型类,但在简单情况下使用匿名对象也是可行的: - -````csharp -var result = await _templateRenderer.RenderAsync( - "Hello", - new - { - Name = "John" - } -); -```` - -示例中我们并没有创建模型类,但是创建了一个匿名对象模型. - -### PascalCase 与 snake_case - -PascalCase 属性名(如 `UserName`) 在模板中使用蛇形命名(如 `user_name`). - -## 本地化 - -可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. - -### 内联本地化 - -内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. - -#### 示例: 重置密码链接 - -假设你需要向用户发送电子邮件重置密码. 模板内容: - -```` -{%{{{L "ResetMyPassword" model.name}}}%} -```` - -`L` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: - -````json -"ResetMyPasswordTitle": "Reset my password", -"ResetMyPassword": "Hi {0}, Click here to reset your password" -```` - -你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: - -````csharp -context.Add( - new TemplateDefinition( - "PasswordReset", //Template name - typeof(DemoResource) //LOCALIZATION RESOURCE - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/PasswordReset/PasswordReset.tpl", //template content path - isInlineLocalized: true - ) -); -```` - -当你这样渲染模板时: - -````csharp -var result = await _templateRenderer.RenderAsync( - "PasswordReset", //the template name - new PasswordResetModel - { - Name = "john", - Link = "https://abp.io/example-link?userId=123&token=ABC" - } -); -```` - -你可以看到以下本地化结果: - -````csharp -Hi john, Click here to reset your password -```` - -> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. - -### 多个内容本地化 - -你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. - -#### 示例: 欢迎电子邮件模板 - -假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. - -首先创建一个文件夹,将模板放在里面,像 `en.tpl`, `tr.tpl` 每一个你支持的文化: - -![multiple-file-template](images/multiple-file-template.png) - -然后在模板定义提供程序类中添加模板定义: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en" - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", //template content folder - isInlineLocalized: false - ) -); -```` - -* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. -* 指定 **模板文件夹** 而不是单个模板文件. -* 设置 `isInlineLocalized` 为 `false`. - -就这些,你可以渲染当前文化的模板: - -````csharp -var result = await _templateRenderer.RenderAsync("WelcomeEmail"); -```` - -> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. - -### 指定文化 - -`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. - -````csharp -var result = await _templateRenderer.RenderAsync( - "WelcomeEmail", - cultureName: "en" -); -```` - -## 布局模板 - -布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. - -### 示例: 邮件HTML布局模板 - -例如,你想为所有电子邮件模板创建一个布局. - -首先像之前一样创建一个模板文件: - -````xml - - - - - - - {%{{{content}}}%} - - -```` - -* 布局模板必须具有 **{%{{{content}}}%}** 部分作为渲染的子内容的占位符. - -在模板定义提供程序中注册模板: - -````csharp -context.Add( - new TemplateDefinition( - "EmailLayout", - isLayout: true //SET isLayout! - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/EmailLayout/EmailLayout.tpl", - isInlineLocalized: true - ) -); -```` - -现在你可以将此模板用作任何其他模板的布局: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en", - layout: "EmailLayout" //Set the LAYOUT - ) - .WithScribanEngine() - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", - isInlineLocalized: false - ) -); -```` - -## 全局上下文 - -ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. - -示例模板内容: - -```` -A global object value: {%{{{myGlobalObject}}}%} -```` - -模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: - -````csharp -var result = await _templateRenderer.RenderAsync( - "GlobalContextUsage", - globalContext: new Dictionary - { - {"myGlobalObject", "TEST VALUE"} - } -); -```` - -渲染的结果将是: - -```` -A global object value: TEST VALUE -```` - -## 替换存在的模板 - -通过替换应用程序中使用的模块定义的模板. 这样你可以根据自己的需求自定义模板,而无需更改模块代码. - -### 选项-1: 使用虚拟文件系统 - -[虚拟文件系统](Virtual-File-System.md)允许你通过将相同文件放入项目中的相同路径来覆盖任何文件. - -#### 示例: 替换标准电子邮件布局模板 - -ABP框架提供了一个[邮件发送系统](Emailing.md), 它在内部使用文本模板来渲染邮件内容. 它在 `/Volo/Abp/Emailing/Templates/Layout.tp` 路径定义了一个标准邮件布局模板. 模板的唯一名称是 `Abp.StandardEmailTemplates.Layout` 并且这个字符中`Volo.Abp.Emailing.Templates.StandardEmailTemplates`静态类上定义为常量. - -执行以下步骤将替换模板替换成你自定义的; - -**1)** 在你的项目中相同的路径添加一个新文件 (`/Volo/Abp/Emailing/Templates/Layout.tpl`): - -![replace-email-layout](images/replace-email-layout.png) - -**2)** 准备你的邮件布局模板: - -````html - - - - - - -

    This my header

    - - {%{{{content}}}%} - -
    - This is my footer... -
    - - -```` - -此示例只是向模板添加页眉和页脚并呈现它们之间的内容(请参阅上面的布局模板部分). - - -**3)** 在`.csproj`文件配置嵌入式资源e - -* 添加 [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet 包到你的项目. -* 在 `.csproj` 中添加 `true` 到 `...` 部分. -* 添加以下代码到你的 `.csproj` 文件: - -````xml - - - - -```` - -这将模板文件做为"嵌入式资源". - -**4)** 配置虚拟文件系统 - -在[模块](Module-Development-Basics.md) `ConfigureServices` 方法配置 `AbpVirtualFileSystemOptions` 将嵌入的文件添加到虚拟文件系统中: - -```csharp -Configure(options => -{ - options.FileSets.AddEmbedded(); -}); -``` - -`BookStoreDomainModule` 应该是你的模块名称 - -> 确保你的模块(支持或间接)[依赖](Module-Development-Basics.md) `AbpEmailingModule`. 因为VFS可以基于依赖顺序覆盖文件. - -现在渲染邮件布局模板时会使用你的模板. - -### 选项-2: 使用模板定义提供者 - -你可以创建一个模板定义提供者类来获取邮件布局模板来更改它的虚拟文件路径. - -**示例: 使用 `/MyTemplates/EmailLayout.tpl` 文件而不是标准模板** - -```csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Emailing.Templates; -using Volo.Abp.TextTemplating; - -namespace MyProject -{ - public class MyTemplateDefinitionProvider - : TemplateDefinitionProvider, ITransientDependency - { - public override void Define(ITemplateDefinitionContext context) - { - var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); - - emailLayoutTemplate - .WithVirtualFilePath( - "/MyTemplates/EmailLayout.tpl", - isInlineLocalized: true - ); - } - } -} -``` - -你依然应该添加 `/MyTemplates/EmailLayout.tpl` 到虚拟文件系统. 这种方法允许你在任何文件夹中找到模板,而不是在依赖模块定义的文件夹中. - -除了模板内容之外你还可以操作模板定义属性, 例如`DisplayName`, `Layout`或`LocalizationSource`. - -## 高级功能 - -本节介绍文本模板系统的一些内部知识和高级用法. - -### 模板内容Provider - -`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. - -> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. - -示例: - -````csharp -public class TemplateContentDemo : ITransientDependency -{ - private readonly ITemplateContentProvider _templateContentProvider; - - public TemplateContentDemo(ITemplateContentProvider templateContentProvider) - { - _templateContentProvider = templateContentProvider; - } - - public async Task RunAsync() - { - var result = await _templateContentProvider - .GetContentOrNullAsync("Hello"); - - Console.WriteLine(result); - } -} -```` - -结果是原始模板内容: - -```` -Hello {%{{{model.name}}}%} :) -```` - -* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. -* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). - -### 模板内容贡献者 - -`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. - -你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. - -示例: - -````csharp -public class MyTemplateContentProvider - : ITemplateContentContributor, ITransientDependency -{ - public async Task GetOrNullAsync(TemplateContentContributorContext context) - { - var templateName = context.TemplateDefinition.Name; - - //TODO: Try to find content from another source - return null; - } -} - -```` - -如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. - -### Template Definition Manager - -`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). - -## 另请参阅 - -* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). -* [本地化系统](Localization.md). -* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Text-Templating.md b/docs/zh-Hans/Text-Templating.md deleted file mode 100644 index 4a21c89c1f..0000000000 --- a/docs/zh-Hans/Text-Templating.md +++ /dev/null @@ -1,37 +0,0 @@ -# 文本模板 - -## 介绍 - -ABP框架提供了一个简单有效的文本模板系统,文本模板用于动态渲染基于模板和模型(数据对象)内容: - -Template + Model =renderer=> Rendered Content - -它非常类似于 ASP.NET Core Razor View (或 Page): - -*RAZOR VIEW (或 PAGE) + MODEL ==render==> HTML CONTENT* - -你可以将渲染的输出用于任何目的,例如发送电子邮件或准备一些报告. - -模板渲染引擎非常强大: - -* 它支持**条件逻辑**, **循环**等等. -* 模板内容**可以本地化**. -* 你可以为其他渲染模板定义**布局模板**。 -* 对于高级场景,你可以传递任何对象到模板上下文. - -ABP框架提供了两个模板引擎: - -* **[Razor](Text-Templating-Razor.md)** -* **[Scriban](Text-Templating-Scriban.md)** - -你可以在同一个应用应用程序中使用不同的模板引擎, 或者创建一个新的自定义模板引擎. - -## 源码 - -查看开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). - -## 另请参阅 - -* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). -* [本地化系统](Localization.md). -* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Timing.md b/docs/zh-Hans/Timing.md deleted file mode 100644 index 4cede317cf..0000000000 --- a/docs/zh-Hans/Timing.md +++ /dev/null @@ -1,113 +0,0 @@ -# 时钟 - -使用时间和[时区](https://en.wikipedia.org/wiki/Time_zone)总是很棘手,尤其是当你需要构建供**不同时区**的用户使用的**全局系统**时. - -ABP提供了一个基本的基础结构,使其变得容易并在可能的情况下自动进行处理. 本文档涵盖了与时间和时区相关的ABP框架服务和系统. - -> 如果你正在创建在单个时区区域运行的本地应用程序,则可能不需要这些系统. 但也建议使用本文中介绍的 `IClock` 服务. - -## IClock - -`DateTime.Now` 返回带有**服务器本地日期和时间**的 `DateTime` 对象. `DateTime` 对象**不存储时区信息**. 因此你无法知道此对象中存储的**绝对日期和时间**. 你只能做一些**假设**,例如假设它是在UTC+05时区创建的. 当你此值保存到数据库中并稍后读取,或发送到**不同时区**的客户端时,事情就变得特别复杂. - -解决此问题的一种方法是始终使用 `DateTime.UtcNow` 并将所有 `DateTime` 对象假定为UTC时间. 在这种情况下你可以在需要时将其转换为目标客户端的时区. - -`IClock` 在获取当前时间的同时提供了一种抽象,你可以在应用程序中的单个点上控制日期时间的类型(UTC或本地时间). - -**示例: 获取当前时间** - -````csharp -using Volo.Abp.DependencyInjection; -using Volo.Abp.Timing; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IClock _clock; - - public MyService(IClock clock) - { - _clock = clock; - } - - public void Foo() - { - //Get the current time! - var now = _clock.Now; - } - } -} -```` - -* 当你需要获取当前时间时注入 `IClock` 服务. 常用的服务基类(如ApplicationService)已经注入并且做为基类属性提供,所以你可以直接使用 `Clock`. -* 使用 `Now` 属性获取当前时间. - -> 在大多数情况下 `IClock` 是你需要在应用程序中了解和使用的唯一服务. - -### Clock 选项 - -`AbpClockOptions` 是用于设置时钟种类的[选项](Options.md)类. - -**示例: 使用 UTC Clock** - -````csharp -Configure(options => -{ - options.Kind = DateTimeKind.Utc; -}); -```` - -在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法添加以上内容. - -> 默认 `Kind` 是 `Unspecified`,实际上使时钟不存在. 如果要利用Clock系统,要么使用 `Utc` 或 `Local`. - -### DateTime 标准化 - -`IClock` 的其他重要功能是规范化 `DateTime` 对象. - -**示例用法 :** - -````csharp -DateTime dateTime = ...; //Get from somewhere -var normalizedDateTime = Clock.Normalize(dateTime) -```` - -`Normalize` 方法的工作原理如下: - -* 如果当前时钟为UTC,并且给定的 `DateTime` 为本地时间,将给定的 `DateTime` 转换为UTC(通过使用 `DateTime.ToUniversalTime()` 方法). -* 如果当前时钟是本地的,并且给定的 `DateTime` 是UTC,将给定的 `DateTime` 转换为本地时间(通过使用 `DateTime.ToLocalTime()` 方法). -* 如果未指定给定的 `DateTime` 的 `Kind`,将给定的 `DateTime` 的 `Kind`(使用 `DateTime.SpecifyKind(...)` 方法)设置为当前时钟的 `Kind`. - -当获取的 `DateTime` 不是由 `IClock` 创建且可能与当前Clock类型不兼容的时候,ABP框架会使用 `Normalize` 方法. 例如; - -* ASP.NET Core MVC模型绑定中的 `DateTime` 类型绑定. -* 通过[Entity Framework Core](Entity-Framework-Core.md)将数据保存到数据库或从数据库读取数据. -* 在[JSON反序列化](Json.md)上使用 `DateTime` 对象. - -#### DisableDateTimeNormalization Attribute - -`DisableDateTimeNormalization` attribute可用于禁用所需类或属性的规范化操作. - -### 其他 IClock 属性 - -除了 `Now`, `IClock` 服务还具有以下属性: - -* `Kind`: 返回当前使用的时钟类型(`DateTimeKind.Utc`, `DateTimeKind.Local` 或 `DateTimeKind.Unspecified`)的 `DateTimeKind`. -* `SupportsMultipleTimezone`: 如果当前时间是UTC,则返回 `true`. - -## 时区 - -本节介绍与管理时区有关的ABP框架基础结构 - -### 时区设置 - -ABP框架定义了一个名为 `Abp.Timing.TimeZone` 的**设置**,可用于为应用程序的用户,[租户](Multi-Tenancy.md)或全局设置和获取时区. 默认值为 `UTC`. - -参阅[设置系统]了解更多关于设置系统. - -### ITimezoneProvider - -`ITimezoneProvider` 是一个服务,可将[Windows时区ID](https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values)值简单转换为[Iana时区名称](https://www.iana.org/time-zones)值,反之亦然. 它还提供了获取这些时区列表与获取具有给定名称的 `TimeZoneInfo` 的方法. - -它已使用[TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter)库实现. diff --git a/docs/zh-Hans/Tutorials/Angular/Part-I.md b/docs/zh-Hans/Tutorials/Angular/Part-I.md deleted file mode 100644 index e981259db8..0000000000 --- a/docs/zh-Hans/Tutorials/Angular/Part-I.md +++ /dev/null @@ -1,8 +0,0 @@ -# 教程 - -## 应用程序开发 - -* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md deleted file mode 100644 index e981259db8..0000000000 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ /dev/null @@ -1,8 +0,0 @@ -# 教程 - -## 应用程序开发 - -* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md deleted file mode 100644 index e981259db8..0000000000 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md +++ /dev/null @@ -1,8 +0,0 @@ -# 教程 - -## 应用程序开发 - -* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md deleted file mode 100644 index e981259db8..0000000000 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ /dev/null @@ -1,8 +0,0 @@ -# 教程 - -## 应用程序开发 - -* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) -* [Angular UI](../Part-1?UI=NG) - - \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Index.md b/docs/zh-Hans/Tutorials/Index.md deleted file mode 100644 index f7eea987e4..0000000000 --- a/docs/zh-Hans/Tutorials/Index.md +++ /dev/null @@ -1,6 +0,0 @@ -# 教程 - -## 应用开发 - -* [使用ASP.NET Core MVC/ Razor Pages UI](AspNetCore-Mvc/Part-I.md) -* [使用Angular UI](Angular/Part-I.md) diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md deleted file mode 100644 index 0d9b61c3c0..0000000000 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ /dev/null @@ -1,497 +0,0 @@ -# Web应用程序开发教程 - 第一章: 创建服务端 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为数据库提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- **Part 1: 创建服务端 (本章)** -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-3.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户界面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -{{if UI == "MVC" && DB == "EF"}} - -### 视频教程 - -本章也被录制为视频教程 **发布在YouTube**. - -{{end}} -## 创建解决方案 - -在开始开发之前,请按照[入门教程](../Getting-Started.md)创建名为 `Acme.BookStore` 的新解决方案. - -## 创建Book实体 - -启动模板中的**领域层**分为两个项目: - - - `Acme.BookStore.Domain`包含你的[实体](../Entities.md), [领域服务](../Domain-Services.md)和其他核心域对象. - - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. - -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体. - -该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹(命名空间),并在其中添加名为 `Book` 的类,如下所示: - -````csharp -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore.Books -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -```` - -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](../Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](../Entities.md)). -* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些基础[审计](../Audit-Logging.md)属性(例如`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性. -* `Guid`是`Book`实体的**主键类型**. - -> 为了保持简单,本教程将实体属性保留为 **public get/set** . 如果你想了解关于DDD最佳实践,请参阅[实体文档](../Entities.md). - -### BookType枚举 - -`Book`实体使用了`BookType`枚举. 在`Acme.BookStore.Domain.Shared`项目中创建`Books`文件夹(命名空间),并在其中添加`BookType`: - -````csharp -namespace Acme.BookStore.Books -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -```` - -最终的文件夹/文件结构应该如下所示: - -![bookstore-book-and-booktype](images/bookstore-book-and-booktype.png) - -### 将Book实体添加到DbContext中 - -{{if DB == "EF"}} - -EF Core需要你将实体和 `DbContext` 建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: - -````csharp -public class BookStoreDbContext : AbpDbContext -{ - public DbSet Books { get; set; } - //... -} -```` - -{{end}} - -{{if DB == "Mongo"}} - -添加 `IMongoCollection Book` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. - -```csharp -public class BookStoreMongoDbContext : AbpMongoDbContext -{ - public IMongoCollection Books => Collection(); - //... -} -``` - -{{end}} - -{{if DB == "EF"}} - -### 将Book实体映射到数据库表 - - -打开`BookStoreDbContext`类的`OnModelCreating`方法,为`Book`实体添加映射代码: - -````csharp -using Acme.BookStore.Books; -... - -namespace Acme.BookStore.EntityFrameworkCore -{ - public class BookStoreDbContext : - AbpDbContext, - IIdentityDbContext, - ITenantManagementDbContext - { - ... - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - ... - - /* Configure your own tables/entities inside here */ - - builder.Entity(b => - { - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", - BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - }); - } - } -} -```` - -* `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 使用它不是强制的,但建议在统一的地方控制表前缀. -* `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应对所有的实体使用它. - -### 添加数据迁移 - -本示例使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/).因为我们修改了数据库映射配置,我们必须创建一个新的迁移并且应用到数据库. - -在 `Acme.BookStore.EntityFrameworkCore` 目录打开命令行终端输入以下命令: - -```bash -dotnet ef migrations add Created_Book_Entity -``` - -它会添加新迁移类到项目中: - -![bookstore-efcore-migration](./images/bookstore-efcore-migration.png) - -> 如果你使用Visual Studio, 你也许想要在*包管理控制台(PMC)*中使用 `Add-Migration Created_Book_Entity -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 确保 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 是 PMC 的*默认项目*. - -{{end}} - -#### 添加种子数据 - -> >在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的[数据种子系统](../Data-Seeding.md). 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。 - -在 `*.Domain` 项目下创建 `IDataSeedContributor` 的派生类,并且拷贝以下代码: - -```csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } - } - } -} -``` - -* 如果数据库中当前没有图书,则此代码使用 `IRepository`(默认[repository](../Repositories.md))将两本书插入数据库. - -### 更新数据库 - -运行 `Acme.BookStore.DbMigrator` 应用程序来更新数据库: - -![bookstore-dbmigrator-on-solution](images/bookstore-dbmigrator-on-solution.png) - -`.DbMigrator` 是一个控制台使用程序,可以在**开发**和**生产**环境**迁移数据库架构**和**初始化种子数据**. - -## 创建应用程序 - -应用程序层由两个分离的项目组成: - -* `Acme.BookStore.Application.Contracts` 包含你的[DTO](../Data-Transfer-Objects.md)和[应用服务](../Application-Services.md)接口. -* `Acme.BookStore.Application` 包含你的应用服务实现. - -在本部分中,你将创建一个应用程序服务,使用ABP Framework的 `CrudAppService` 基类来获取,创建,更新和删除书籍. - -### BookDto - -`CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建 `Books` 文件夹(命名空间), 并在其中添加名为 `BookDto` 的DTO类: - -````C# -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -```` - -* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.参阅[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects). -* 为了在用户界面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. -* `BookDto`继承自 `AuditedEntityDto`.与上面定义的 `Book` 实体一样具有一些审计属性. - -在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -> 参阅 [对象到对象映射](../Object-To-Object-Mapping.md) 文档了解详情. - -### CreateUpdateBookDto - -在`Acme.BookStore.Application.Contracts`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `CreateUpdateBookDto` 的DTO类: -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Books -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } - } -} -```` - -* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. -* 它定义了数据注释特性(如`[Required]`)来定义属性的验证规则. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). - -就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射,最终映射配置类如下: - - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } - } -} -```` - -### IBookAppService - -下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目创建 `Books` 文件夹(命名空间),并在其中添加名为`IBookAppService`的接口: - -````csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Books -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book - { - - } -} -```` - -* 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践. -* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 从这个接口扩展不是必需的,你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成). -* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO(例如使用不同的DTO进行创建和更新). - -### BookAppService - -是时候实现`IBookAppService`接口了.在`Acme.BookStore.Application`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `BookAppService` 的类: - -````csharp -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books -{ - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -```` - -* `BookAppService`继承了`CrudAppService<...>`.它实现了 `ICrudAppService` 定义的CRUD方法. -* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) -* `BookAppService`使用[`IObjectMapper`](../Object-To-Object-Mapping.md)将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 我们之前定义了映射, 因此它将按预期工作. - -### 自动生成API Controllers - -在典型的ASP.NET Core应用程序中,你创建**API Controller**以将应用程序服务公开为**HTTP API**端点. 这将允许浏览器或第三方客户端通过HTTP调用它们. - -ABP可以[**自动**](../API/Auto-API-Controllers.md)按照约定将你的应用程序服务配置为MVC API控制器. - -### Swagger UI - -启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. -使用`CTRL+F5`运行应用程序 ({{if UI=="MVC"}}`Acme.BookStore.Web`{{else}}`Acme.BookStore.HttpApi.Host`{{end}})并使用浏览器访问`https://localhost:/swagger/` on your browser. 使用你自己的端口号替换 ``. - -你会看到一些内置的服务端点和`Book`服务,它们都是REST风格的端点: - -![bookstore-swagger](./images/bookstore-swagger.png) - -Swagger有一个很好的UI来测试API. - -你可以尝试执行`[GET] /api/app/book` API来获取书籍列表, 服务端会返回以下JSON结果: - -````json -{ - "totalCount": 2, - "items": [ - { - "name": "The Hitchhiker's Guide to the Galaxy", - "type": 7, - "publishDate": "1995-09-27T00:00:00", - "price": 42, - "lastModificationTime": null, - "lastModifierId": null, - "creationTime": "2020-07-03T21:04:18.4607218", - "creatorId": null, - "id": "86100bb6-cbc1-25be-6643-39f62806969c" - }, - { - "name": "1984", - "type": 3, - "publishDate": "1949-06-08T00:00:00", - "price": 19.84, - "lastModificationTime": null, - "lastModifierId": null, - "creationTime": "2020-07-03T21:04:18.3174016", - "creatorId": null, - "id": "41055277-cce8-37d7-bb37-39f62806960b" - } - ] -} -```` - -这很酷,因为我们没有编写任何代码来创建API控制器,但是现在我们有了一个可以正常使用的REST API! - -## 下一章 - -参阅教程的[下一章](part-2.md). diff --git a/docs/zh-Hans/Tutorials/Part-10.md b/docs/zh-Hans/Tutorials/Part-10.md deleted file mode 100644 index 1ea5f4673f..0000000000 --- a/docs/zh-Hans/Tutorials/Part-10.md +++ /dev/null @@ -1,1210 +0,0 @@ -# Web应用程序开发教程 - 第十章: 图书到作者的关系 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Text}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- **Part 10: 图书到作者的关系 (本章)** - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -## 简介 - -我们已经为图书管理应用程序创建了 `图书` 和 `作者` 功能. 然而, 这些实体间还没有关联. - -在本章, 我们会在 `作者` 和 `图书` 实体间建立 **1 对 N** 的关系. - -## 在图书实体中加入关系 - -打开 `Acme.BookStore.Domain` 项目中的 `Books/Book.cs`, 在 `Book` 实体中加入下列属性: - -````csharp -public Guid AuthorId { get; set; } -```` - -{{if DB=="EF"}} - -> 在本章中, 我们选择不在 `Book` 类中加入 `Author` 实体的 **导航属性** (例如 `public Author Author { get; set; }`). 这是为了遵循 DDD 最佳实践 (规则: 仅通过id引用其它聚合对象). 但是, 你自己可以添加这样的导航属性, 并为EF Core配置它. 这样, 你在获取图书和它们的作者时就不需要写join查询了(如同下面我们做的一样), 这会使代码更简洁一些. - -{{end}} - -## 数据库 & 数据迁移 - -为 `Book` 实体新增一个不为空的 `AuthorId` 属性. 但是, 数据库中**已存在的图书怎么办**? 它们没有 `AuthorId`s, 当我们尝试运行应用程序时会出问题. - -这是一个 **典型的迁移问题**, 解决方案依赖于你的具体情况; - -* 如果你还没有发布应用程序到生产环境, 你可以直接删除数据库中的图书数据, 甚至你可以删除开发环境中的整个数据库. -* 你可以在数据迁移或生成种子阶段使用代码更新已有数据. -* 你可以手工处理这些数据. - -我们倾向于 **删除数据库** {{if DB=="EF"}}(你可以在 *Package Manager 控制台*中运行 `Drop-Database`){{end}}, 因为这只是个示例项目, 数据丢失并不要紧. 因为这个主题不是关于ABP框架的, 我们不会深入所有的场景. - -{{if DB=="EF"}} - -### 更新 EF Core 映射 - -定位到 `Acme.BookStore.EntityFrameworkCore` 项目的 `EntityFrameworkCore` 文件夹下的 `BookStoreDbContext` 类的 `OnModelCreating` 方法, 修改 `builder.Entity` 部分如下: - -````csharp -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - - // ADD THE MAPPING FOR THE RELATION - b.HasOne().WithMany().HasForeignKey(x => x.AuthorId).IsRequired(); -}); -```` - -### 新增 EF Core 迁移 - -启动解决方案被配置为使用 [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). 因为我们修改了数据库映射配置, 我们需要新建一个迁移并应用于数据库. - -在 `Acme.BookStore.EntityFrameworkCore` 项目的文件目录打开命令行终端, 输入命令: - -````bash -dotnet ef migrations add Added_AuthorId_To_Book -```` - -这会创建一个新的迁移类, 在它的 `Up` 方法中使用下列方法: - -````csharp -migrationBuilder.AddColumn( - name: "AuthorId", - table: "AppBooks", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - -migrationBuilder.CreateIndex( - name: "IX_AppBooks_AuthorId", - table: "AppBooks", - column: "AuthorId"); - -migrationBuilder.AddForeignKey( - name: "FK_AppBooks_AppAuthors_AuthorId", - table: "AppBooks", - column: "AuthorId", - principalTable: "AppAuthors", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); -```` - -* `AppBooks` 表增加一个 `AuthorId` 字段 . -* 根据 `AuthorId` 字段新建一个索引. -* 声明到 `AppAuthors` 表的外键. - -> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* . - -{{end}} - -## 修改数据种子 - -因为 `AuthorId` 是 `Book` 实体的不可为空属性, 当前的数据种子代码不能工作. 打开 `Acme.BookStore.Domain` 项目中的 `BookStoreDataSeederContributor`, 修改成以下代码: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - var orwell = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - var douglas = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = orwell.Id, // SET THE AUTHOR - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = douglas.Id, // SET THE AUTHOR - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } - } -} -```` - -唯一的区别是设置 `Book` 实体的 `AuthorId` 属性. - -> 执行 `DbMigrator` 前删除已有图书或数据库. 参阅上面的 *数据库 & 数据迁移* 小节获取详细信息. - -{{if DB=="EF"}} - -你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. - -{{else if DB=="Mongo"}} - -你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. - -{{end}} - -## 应用层 - -我们将修改 `BookAppService`, 支持作者关系. - -### 数据传输对象 - -让我们从DTOs开始. - -#### BookDto - -打开 `Acme.BookStore.Application.Contracts` 项目的 `Books` 文件夹下的 `BookDto` 类, 添加如下属性: - -```csharp -public Guid AuthorId { get; set; } -public string AuthorName { get; set; } -``` - -最终的 `BookDto` 类应该如下: - -```csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books -{ - public class BookDto : AuditedEntityDto - { - public Guid AuthorId { get; set; } - - public string AuthorName { get; set; } - - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -#### CreateUpdateBookDto - -打开 `Acme.BookStore.Application.Contracts` 项目的 `Books` 文件夹下的 `CreateUpdateBookDto` 类, 添加 `AuthorId` 属性: - -````csharp -public Guid AuthorId { get; set; } -```` - -#### AuthorLookupDto - -在 `Acme.BookStore.Application.Contracts` 项目的 `Books` 文件夹下新建一个类 `AuthorLookupDto`: - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Books -{ - public class AuthorLookupDto : EntityDto - { - public string Name { get; set; } - } -} -```` - -它会被一个将要添加到 `IBookAppService` 的新方法使用. - -### IBookAppService - -打开 `Acme.BookStore.Application.Contracts` 项目的 `Books` 文件夹下的 `IBookAppService` 接口, 添加一个名为 `GetAuthorLookupAsync` 的新方法: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Books -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book - { - // ADD the NEW METHOD - Task> GetAuthorLookupAsync(); - } -} -```` - -这个新方法将被UI用来获取作者列表, 填充一个下拉框. 使用这个下拉框选择图书作者. - -### BookAppService - -打开 `Acme.BookStore.Application` 项目的 `Books` 文件夹下的 `BookAppService` 类, 更新为以下代码: - -{{if DB=="EF"}} - -```csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Entities; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books -{ - [Authorize(BookStorePermissions.Books.Default)] - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService - { - private readonly IAuthorRepository _authorRepository; - - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } - - public override async Task GetAsync(Guid id) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - where book.Id == id - select new { book, author }; - - //Execute the query and get the book with author - var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); - if (queryResult == null) - { - throw new EntityNotFoundException(typeof(Book), id); - } - - var bookDto = ObjectMapper.Map(queryResult.book); - bookDto.AuthorName = queryResult.author.Name; - return bookDto; - } - - public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - select new {book, author}; - - //Paging - query = query - .OrderBy(NormalizeSorting(input.Sorting)) - .Skip(input.SkipCount) - .Take(input.MaxResultCount); - - //Execute the query and get a list - var queryResult = await AsyncExecuter.ToListAsync(query); - - //Convert the query result to a list of BookDto objects - var bookDtos = queryResult.Select(x => - { - var bookDto = ObjectMapper.Map(x.book); - bookDto.AuthorName = x.author.Name; - return bookDto; - }).ToList(); - - //Get the total count with another query - var totalCount = await Repository.GetCountAsync(); - - return new PagedResultDto( - totalCount, - bookDtos - ); - } - - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } - - private static string NormalizeSorting(string sorting) - { - if (sorting.IsNullOrEmpty()) - { - return $"book.{nameof(Book.Name)}"; - } - - if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) - { - return sorting.Replace( - "authorName", - "author.Name", - StringComparison.OrdinalIgnoreCase - ); - } - - return $"book.{sorting}"; - } - } -} -``` - -我们做了以下修改: - -* 给所有新建/覆写的方法增加 `[Authorize(BookStorePermissions.Books.Default)]` 进行授权(当授权特性应用于类时, 它对这个类的所有方法有效). -* 注入 `IAuthorRepository`, 从作者中查询. -* 覆写基类 `CrudAppService` 的 `GetAsync` 方法. 这个方法根据给定的 `id` 返回单一 `BookDto` 对象. - * 使用一个简单的LINQ表达式关联图书和作者, 根据给定的图书id查询, 查询结果同时包含图书和作者. - * 使用 `AsyncExecuter.FirstOrDefaultAsync(...)` 执行查询并得到一个结果. 这是一种无需依赖database provider API, 使用异步LINQ扩展的方法. 参阅 [repository文档](../Repositories.md)以理解我们为什么使用它. - * 如果请求的图书在数据库中不存在, 抛出一个 `EntityNotFoundException`, 这会导致一个 `HTTP 404` (not found) 状态码. - * 最后, 使用 `ObjectMapper`创建一个 `BookDto` 对象, 然后手工给 `AuthorName` 赋值. -* 覆写 `CrudAppService` 基类的 `GetListAsync` 方法, 返回图书列表. 逻辑与前一个方法类似, 所以很容易理解. -* 新建一个方法: `GetAuthorLookupAsync`. 这个方法只是简单地获取所有作者. UI使用这个方法填充一个下拉框, 当编辑图书时用来选择作者. - -{{else if DB=="Mongo"}} - -```csharp -using System; -using System.Collections.Generic; -using System.Linq.Dynamic.Core; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books -{ - [Authorize(BookStorePermissions.Books.Default)] - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService - { - private readonly IAuthorRepository _authorRepository; - - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; - } - - public async override Task GetAsync(Guid id) - { - var book = await Repository.GetAsync(id); - var bookDto = ObjectMapper.Map(book); - - var author = await _authorRepository.GetAsync(book.AuthorId); - bookDto.AuthorName = author.Name; - - return bookDto; - } - - public async override Task> - GetListAsync(PagedAndSortedResultRequestDto input) - { - //Set a default sorting, if not provided - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Book.Name); - } - - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Get the books - var books = await AsyncExecuter.ToListAsync( - queryable - .OrderBy(input.Sorting) - .Skip(input.SkipCount) - .Take(input.MaxResultCount) - ); - - //Convert to DTOs - var bookDtos = ObjectMapper.Map, List>(books); - - //Get a lookup dictionary for the related authors - var authorDictionary = await GetAuthorDictionaryAsync(books); - - //Set AuthorName for the DTOs - bookDtos.ForEach(bookDto => bookDto.AuthorName = - authorDictionary[bookDto.AuthorId].Name); - - //Get the total count with another query (required for the paging) - var totalCount = await Repository.GetCountAsync(); - - return new PagedResultDto( - totalCount, - bookDtos - ); - } - - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } - - private async Task> - GetAuthorDictionaryAsync(List books) - { - var authorIds = books - .Select(b => b.AuthorId) - .Distinct() - .ToArray(); - - var queryable = await _authorRepository.GetQueryableAsync(); - - var authors = await AsyncExecuter.ToListAsync( - queryable.Where(a => authorIds.Contains(a.Id)) - ); - - return authors.ToDictionary(x => x.Id, x => x); - } - } -} -``` - -我们做了以下修改: - -* 给所有新建/覆写的方法增加 `[Authorize(BookStorePermissions.Books.Default)]` 进行授权(当授权特性应用于类时, 它对这个类的所有方法有效). -* 注入 `IAuthorRepository`, 从作者中查询. -* 覆写基类 `CrudAppService` 的 `GetAsync` 方法. 这个方法根据给定的 `id` 返回单一 `BookDto` 对象. -* 覆写 `CrudAppService` 基类的 `GetListAsync` 方法, 返回图书列表. 这里将从数据库中查询作者和在应用层设置作者名进行了分离. 你也可以创建一个自定义repository方法, 执行一个join查询, 或者利用MongoDB AP在一个查询中获取图书的作者, 这种做法性能会更好. -* 新建一个方法: `GetAuthorLookupAsync`. 这个方法只是简单地获取所有作者. UI使用这个方法填充一个下拉框, 当编辑图书时用来选择作者. - -{{end}} - -### 对象到对象映射映射 - -引入 `AuthorLookupDto` 类, 在 `GetAuthorLookupAsync` 方法中使用对象映射. 所以, 我们需要在 `Acme.BookStore.Application` 项目的 `BookStoreApplicationAutoMapperProfile.cs` 文件中加入一个新的映射定义. - -````csharp -CreateMap(); -```` - -## 单元测试 - -因为修改了 `AuthorAppService`, 一些单元测试失败了. 打开 `Acme.BookStore.Application.Tests` 项目的 `Books` 目录中的 `BookAppService_Tests`, 修改成以下代码: - -```csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Modularity; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books; - -public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IBookAppService _bookAppService; - private readonly IAuthorAppService _authorAppService; - - protected BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - _authorAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984" && - b.AuthorName == "George Orwell"); - } - - [Fact] - public async Task Should_Create_A_Valid_Book() - { - var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); - var firstAuthor = authors.Items.First(); - - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - AuthorId = firstAuthor.Id, - Name = "New test book 42", - Price = 10, - PublishDate = System.DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); - } -} -``` - -* 修改 `Should_Get_List_Of_Books` 中的断言条件, 从 `b => b.Name == "1984"` 修改为 `b => b.Name == "1984" && b.AuthorName == "George Orwell"`, 检查用户名是否被填充. -* 修改 `Should_Create_A_Valid_Book` 方法, 当新建图书时, 设置 `AuthorId`, 因为它现在是不可为空的了. - -## 用户页面 - -{{if UI=="MVC"}} - -### 图书列表 - -图书列表页面的修改很小. 打开 `Acme.BookStore.Web` 项目上的 `Pages/Books/Index.js`, 在 `name` and `type` 列之间加入如下列定义: - -````js -... -{ - title: l('Name'), - data: "name" -}, - -// ADDED the NEW AUTHOR NAME COLUMN -{ - title: l('Author'), - data: "authorName" -}, - -{ - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType:' + data); - } -}, -... -```` - -运行应用程序, 你会在表格中看到 *Author* 列: - -![bookstore-added-author-to-book-list](images/bookstore-added-author-to-book-list.png) - -### 新建模态窗口 - -打开 `Acme.BookStore.Web` 项目中的 `Pages/Books/CreateModal.cshtml.cs`, 修改文件内容为: - -```csharp -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateBookViewModel Book { get; set; } - - public List Authors { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel( - IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - Book = new CreateBookViewModel(); - - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync( - ObjectMapper.Map(Book) - ); - return NoContent(); - } - - public class CreateBookViewModel - { - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } - - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } - } - } -} -``` - -* 将 `Book` 属性的类型从 `CreateUpdateBookDto` 修改为这个文件中新定义的 `CreateBookViewModel` 类. 这个修改的主要动机是根据UI需求自定义模型类. 我们不希望在 `CreateUpdateBookDto` 类中使用UI相关的 `[SelectItems(nameof(Authors))]` 和 `[DisplayName("Author")]` 特性. -* 新增 `Authors` 属性, 在 `OnGetAsync` 方法中使用前面定义的 `IBookAppService.GetAuthorLookupAsync` 方法填充它. -* 修改 `OnPostAsync` 方法, 映射 `CreateBookViewModel` 对象到 `CreateUpdateBookDto` 对象, 因为 `IBookAppService.CreateAsync` 需要一个这种类型的参数. - -### 编辑模态窗口 - -打开 `Acme.BookStore.Web` 项目中的 `Pages/Books/EditModal.cshtml.cs`, 修改文件内容为: - -```csharp -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [BindProperty] - public EditBookViewModel Book { get; set; } - - public List Authors { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync(Guid id) - { - var bookDto = await _bookAppService.GetAsync(id); - Book = ObjectMapper.Map(bookDto); - - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync( - Book.Id, - ObjectMapper.Map(Book) - ); - - return NoContent(); - } - - public class EditBookViewModel - { - [HiddenInput] - public Guid Id { get; set; } - - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } - - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; - - [Required] - public float Price { get; set; } - } - } -} -``` - -* 将 `Book` 属性的类型从 `CreateUpdateBookDto` 修改为这个文件中新定义的 `EditBookViewModel` 类, 和我们前面所做的创建模型的修改一样. -* 移动新类 `EditBookViewModel` 的 `Id` 属性. -* 新增 `Authors` 属性, 在 `OnGetAsync` 方法中使用前面定义的 `IBookAppService.GetAuthorLookupAsync` 方法填充它. -* 修改 `OnPostAsync` 方法, 映射 `EditBookViewModel` 对象到 `CreateUpdateBookDto` 对象, 因为 `IBookAppService.UpdateAsync` 需要一个这种类型的参数. - -这些修改需要对 `EditModal.cshtml` 进行一些小修改. 移除 `` 标签, 因为我们不再需要它了 (因为它被移动到 `EditBookViewModel` 中了). `EditModal.cshtml` 的最终内容应为: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - -```` - -### 对象到对象映射配置 - -以下修改需要定义一些对象到对象映射. 打开 `Acme.BookStore.Web` 项目中的 `BookStoreWebAutoMapperProfile.cs`, 在构造函数中添加下列映射定义: - -```csharp -CreateMap(); -CreateMap(); -CreateMap(); -``` - -你可以运行应用程序, 尝试新建或更新一本书. 你将在新建/更新表单上看到一个下拉框, 使用它指定图书的作者: - -![bookstore-added-authors-to-modals](images/bookstore-added-authors-to-modals.png) - -{{else if UI=="NG"}} - -### 生成服务代理 - -因为修改了 HTTP APIs, 你需要更新 Angular 客户端 [服务代理](../UI/Angular/Service-Proxies.md). 运行 `generate-proxy` 命令前, 应用程序必须启动运行. - -在 `angular` 文件夹中运行下面的命令 (你可能需要停止angular应用程序): - -```bash -abp generate-proxy -t ng -``` -这个命令将会更新 `/src/app/proxy/` 文件夹中的服务代理文件. - -### 图书列表 - -图书列表页面的修改很小. 打开 `/src/app/book/book.component.html`, 在 `Name` 和 `Type` 列之间加入以下列定义: - -````html - -```` - -运行应用程序, 你会在表格中看到 *作者* 列: - -![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular.png) - -### 新建/编辑 表单 - -下一步是添加作者选择下拉框到新建/编辑表单. 最终的页面如下图: - -![bookstore-angular-author-selection](images/bookstore-angular-author-selection.png) - -添加作者下拉框, 作为表单中的第一个元素. - -打开 `/src/app/book/book.component.ts`, 修改文件内容为: - -````js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions, AuthorLookupDto } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; - - selectedBook = {} as BookDto; - - authors$: Observable; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder, - private confirmation: ConfirmationService - ) { - this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items)); - } - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.selectedBook = {} as BookDto; - this.buildForm(); - this.isModalOpen = true; - } - - editBook(id: string) { - this.bookService.get(id).subscribe((book) => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - authorId: [this.selectedBook.authorId || null, Validators.required], - name: [this.selectedBook.name || null, Validators.required], - type: [this.selectedBook.type || null, Validators.required], - publishDate: [ - this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - Validators.required, - ], - price: [this.selectedBook.price || null, Validators.required], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - const request = this.selectedBook.id - ? this.bookService.update(this.selectedBook.id, this.form.value) - : this.bookService.create(this.form.value); - - request.subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } - - delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.bookService.delete(id).subscribe(() => this.list.get()); - } - }); - } -} -```` - -* 为 `AuthorLookupDto`, `Observable` 和 `map` 添加引用. -* 在 `selectedBook` 后添加 `authors$: Observable;` 成员. -* 在构造函数中添加 `this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));`. -* 在 `buildForm()` 函数中添加 `authorId: [this.selectedBook.authorId || null, Validators.required]`. - -打开 `/src/app/book/book.component.html`, 在图书名称表单组前加入下列表单组: - -````html -
    - * - -
    -```` - -这就是全部了. 运行应用程序, 尝试新建或编辑一个作者. - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -### The Book List - -在图书列表上显示 *作者姓名* 非常容易. 打开 `Acme.BookStore.Blazor` 项目中的 the `/Pages/Books.razor` 文件, 在 `名称` (图书名) 列后加入下列 `DataGridColumn` 定义: - -````xml - -```` - -运行应用程序, 你将在表格上看到 *作者* 列: - -![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors.png) - -### 新建图书模态窗口 - -在 `Books.razor` 文件的 `@code` 小节加入以下成员变量: - -````csharp -IReadOnlyList authorList = Array.Empty(); -```` - -覆写 `OnInitializedAsync` 方法, 加入以下代码: - -````csharp -protected override async Task OnInitializedAsync() -{ - await base.OnInitializedAsync(); - authorList = (await AppService.GetAuthorLookupAsync()).Items; -} -```` - -* 调用 `base.OnInitializedAsync()` 是有必要的, 因为 `AbpCrudPageBase` 有一些初始化代码要执行. - -最终的 `@code` 代码如下: - -````csharp -@code -{ - //ADDED A NEW FIELD - IReadOnlyList authorList = Array.Empty(); - - public Books() // Constructor - { - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } - - //GET AUTHORS ON INITIALIZATION - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - authorList = (await AppService.GetAuthorLookupAsync()).Items; - } -} -```` - -最后, 在 *新建* 模态窗口的 `ModalBody` 加入以下 `Field` 定义, 在 `Name` field之前, 做为第一项: - -````xml - - @L["Author"] - - -```` - -这需要在 `en.json` 文件中加入一个新的本地化键: - -````js -"PickAnAuthor": "Pick an author" -```` - -你可以运行应用程序, 当新建图书时, 可以看见 *作者选择*: - -![book-create-modal-with-author](images/book-create-modal-with-author.png) - -### 编辑图书模态窗口 - -在 *编辑* 模态窗口的 `ModalBody` 加入以下 `Field` 定义, 在 `Name` field之前, 做为第一项: - -````xml - - @L["Author"] - - -```` - -这就是全部了. 我们重用了定义在 *新建* 模态窗口中的 `authorList`. - -{{end}} diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md deleted file mode 100644 index dc8d804d5e..0000000000 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ /dev/null @@ -1,680 +0,0 @@ -# Web应用程序开发教程 - 第二章: 图书列表页面 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- **Part 2: 图书列表页面 (本章)** -- [Part 3: 创建,更新和删除图书](Part-3.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -{{if UI == "MVC" && DB == "EF"}} - -### 视频教程 - -本章也被录制为视频教程 **发布在YouTube**. - -{{end}} - -{{if UI == "MVC"}} - -## 动态JavaScript代理 - -在 **JavaScript** 端通过AJAX调用HTTP API端点是常见的做法. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法. - -ABP**动态**为所有API端点创建 **[JavaScript代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)**. 所以你可以像调用**Javascript本地方法**一样使用任何**端点**. - -### 在开发者控制台中进行测试 - -你可以在自己喜欢的浏览器的**开发者控制台**轻松的测试JavaScript代理. 运行应用程序,打开浏览器的**开发者人员工具**(*快捷键通常是F12*),切换到**控制台**选项卡,输入以下代码然后按回车: - -````js -acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); }); -```` - -* `acme.bookStore.books` 是 `BookAppService` 的命令空间转换成[小驼峰](https://en.wikipedia.org/wiki/Camel_case)形式. -* `book` 是 `BookAppService` 的约定名称(删除`AppService`后缀并且转换为小驼峰). -* `getList` 是 `CrudAppService` 基类定义的 `GetListAsync` 方法的约定名称(删除`Async`后缀并且转换为小驼峰). -* `{}` 参数将空对象发送到 `GetListAsync` 方法,该方法通常需要一个类型为 `PagedAndSortedResultRequestDto` 的对象,该对象用于将分页和排序选项发送到服务器(所有属性都是可选的,具有默认值. 因此你可以发送一个空对象). -* `getList` 函数返回一个 `promise`. 你可以传递一个回调到 `then`(或`done`)函数来获取从服务器返回的结果. - -运行该代码会产生以下输出: - -![bookstore-javascript-proxy-console](images/bookstore-javascript-proxy-console.png) - -你可以看到服务端返回的 **图书列表**. 你也可以在开发者人员工具的 **网络** 选项卡查看客户端到服务端的通信: - -![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png) - -让我们使用 `create` 函数**创建一本书**: - -````js -acme.bookStore.books.book.create({ - name: 'Foundation', - type: 7, - publishDate: '1951-05-24', - price: 21.5 - }).then(function (result) { - console.log('successfully created the book with id: ' + result.id); - }); -```` - -> 如果你下载了本教程的源代码并按照示例中的步骤操作,你需要传递`authorId`参数给创建方法以**创建一本新书**. - -您应该在控制台中看到类似以下的消息: - -````text -successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 -```` - -检查数据库中的 `Books` 表你会看到新的一行. 你可以自己尝试使用 `get`, `update` 和 `delete` 函数. - -在接下来的章节,我们将利用这些动态代理函数与服务器通信. - -{{end}} - -## 本地化 - -开始的UI开发之前,我们首先要准备本地化的文本(这是你通常在开发应用程序时需要做的). - -本地化文本位于 `Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹下: - -![bookstore-localization-files](images/bookstore-localization-files-v2.png) - -打开 `en.json` (*英文翻译*)文件并更改内容,如下所示: - -````json -{ - "Culture": "en", - "Texts": { - "Menu:Home": "Home", - "Welcome": "Welcome", - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", - "Menu:BookStore": "Book Store", - "Menu:Books": "Books", - "Actions": "Actions", - "Close": "Close", - "Delete": "Delete", - "Edit": "Edit", - "PublishDate": "Publish date", - "NewBook": "New book", - "Name": "Name", - "Type": "Type", - "Price": "Price", - "CreationTime": "Creation time", - "AreYouSure": "Are you sure?", - "AreYouSureToDelete": "Are you sure you want to delete this item?", - "Enum:BookType.Undefined": "Undefined", - "Enum:BookType.Adventure": "Adventure", - "Enum:BookType.Biography": "Biography", - "Enum:BookType.Dystopia": "Dystopia", - "Enum:BookType.Fantastic": "Fantastic", - "Enum:BookType.Horror": "Horror", - "Enum:BookType.Science": "Science", - "Enum:BookType.ScienceFiction": "Science fiction", - "Enum:BookType.Poetry": "Poetry" - } -} -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -* 本地化关键字名称是任意的. 你可以设置任何名称. 对于特定的文本类型,我们更喜欢遵循一些约定: - * 为按钮项添加 `Menu:` 前缀. - * 使用 `Enum::` 或 `.` 或 `` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化. - -如果未在本地化文件中定义文本,则文本将**回退**到本地化键(ASP.NET Core的标准行为). - -> ABP本地化系统建立在[ASP.NET Core标准本地化](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization)系统之上,并以多种方式进行了扩展. 有关详细信息请参见[本地化文档](../Localization.md). - -{{if UI == "MVC"}} - -## 创建图书页面 - -是时候创建可见的和可用的东西了! 我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start),而不是经典的MVC. - -在 `Acme.BookStore.Web` 项目的 `Pages` 文件夹下创建一个名为新的 `Books` 的文件夹. 然后在文件夹右键选择 **添加 > Razor Page** 菜单. 输入名称 `Index`: - -![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) - -打开 `Index.cshtml` 并把内容修改成下面这样: - -````html -@page -@using Acme.BookStore.Web.Pages.Books -@model IndexModel - -

    Books

    -```` - -`Index.cshtml.cs` 内容应该是: - -```csharp -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class IndexModel : PageModel - { - public void OnGet() - { - - } - } -} -``` - -### 将图书页面添加到主菜单 - -打开 `Menus` 文件夹中的 `BookStoreMenuContributor` 类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ) - ) -); -```` - -运行项目,使用用户名 `admin` 和密码 `1q2w3E*` 登录到应用程序. 看到新菜单项已添加到顶部栏: - -![bookstore-menu-items](./images/bookstore-new-menu-item.png) - -点击BookStore下的Books子菜单项就会跳转到空的图书页面. - -### 图书列表 - -我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示图书列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验. - -> Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件. - -#### Index.cshtml - -将 `Pages/Book/Index.cshtml` 改成下面的样子: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@section scripts -{ - -} - - -

    @L["Books"]

    -
    - - - -
    -```` - -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../UI/AspNetCore/Bundling-Minification.md)获取更多信息. -* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.5/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../UI/AspNetCore/Tag-Helpers/Index.md). - -#### Index.js - -在 `Pages/Books/` 文件夹中创建 `index.js`文件 - -![bookstore-index-js-file](images/bookstore-index-js-file-v3.png) - -`index.js` 的内容如下: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType:' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } - } - ] - }) - ); -}); -```` - -* `abp.localization.getResource` 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值. -* `abp.libs.datatables.normalizeConfiguration`是一个辅助方法.不是必须的, 但是它通过为缺省的选项提供约定的值来简化[Datatables](https://datatables.net/)配置. -* `abp.libs.datatables.createAjax`是另一个辅助方法,用来适配ABP的动态JavaScript API代理和[Datatable](https://datatables.net/)期望的参数格式. -* `acme.bookStore.books.book.getList` 是动态JavaScript代理函数(上面已经介绍过了) -* [luxon](https://moment.github.io/luxon/) 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作. - -> 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. - -## 运行最终应用程序 - -你可以运行应用程序!该部分的最终用户界面如下所示: - -![Book list](images/bookstore-book-list-3.png) - -这是一个可以正常工作的,服务端分页,排序和本地化的图书列表. - -{{else if UI == "NG"}} - -## 安装NPM包 - ->注意: 本教程基于ABP Framework v3.1.0+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md). - -在 `angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包: - -```bash -yarn -``` - -## 创建图书页面 - -是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: - -- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. -- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库. - -运行以下命令在angular应用程序根目录创建一个名为 `BookModule` 的新模块: - -```bash -yarn ng generate module book --module app --routing --route books -``` - -该命令应该产生以下的输出: - -````bash -> yarn ng generate module book --module app --routing --route books - -yarn run v1.19.1 -$ ng generate module book --module app --routing --route books -CREATE src/app/book/book-routing.module.ts (336 bytes) -CREATE src/app/book/book.module.ts (335 bytes) -CREATE src/app/book/book.component.html (19 bytes) -CREATE src/app/book/book.component.spec.ts (614 bytes) -CREATE src/app/book/book.component.ts (268 bytes) -CREATE src/app/book/book.component.scss (0 bytes) -UPDATE src/app/app-routing.module.ts (1289 bytes) -Done in 3.88s. -```` - -### BookModule - -打开 `/src/app/book/book.module.ts` 并使用以下内容替换: - -````js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { BookRoutingModule } from './book-routing.module'; -import { BookComponent } from './book.component'; - -@NgModule({ - declarations: [BookComponent], - imports: [ - BookRoutingModule, - SharedModule - ] -}) -export class BookModule { } - -```` - -* 添加了 `SharedModule`. `SharedModule` 导出了一些创建用户界面所需的通用模块. -* `SharedModule` 已经导出了 `CommonModule`,所以我们删除了 `CommonModule`. - -### 路由 - -生成的代码将新的路由定义放在 `src/app/app-routing.module.ts` 文件中,如下所示: - -````js -const routes: Routes = [ - // other route definitions... - { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) }, -]; -```` - -现在打开 `src/app/route.provider.ts` 替换 `configureRoutes` 函数为以下代码: - -```js -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', - order: 1, - layout: eLayoutType.application, - }, - { - path: '/book-store', - name: '::Menu:BookStore', - iconClass: 'fas fa-book', - order: 2, - layout: eLayoutType.application, - }, - { - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - }, - ]); - }; -} -``` - -`RoutesService` 是ABP框架提供的用于配置主菜单和路由的服务. - -* `path` 路由的URL. -* `name` 菜单项的名称(参阅[本地化文档](../UI/Angular/Localization.md)了解更多). -* `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标). -* `order` 菜单项的排序. -* `layout` BooksModule路由的布局. (有三个预定义的布局类型: `eLayoutType.application`, `eLayoutType.account` 或 `eLayoutType.empty`). - -更多信息请参阅[RoutesService 文档](../UI/Angular/Modifying-the-Menu.md#via-routesservice). - -### 生成服务代理 - -[ABP CLI](../CLI.md) 提供 `generate-proxy` 命令为HTTP APIs生成客户端代理.有了这些代理,在客户端使用HTTP APIs变得更加方便. 运行 `generate-proxy` 命令前, 你的 host 必须正在运行. - -> **警告**: 使用IIS Express时有一个问题; 它不允许从另一个进程连接应用程序. 如果你使用Visual Studio, 在运行按钮的下拉框中选择`Acme.BookStore.HttpApi.Host`,不要选择IIS Express, 如下图: - -![vs-run-without-iisexpress](images/vs-run-without-iisexpress.png) - -启动host应用程序后,在 `angular` 文件夹下运行以下命令: - -```bash -abp generate-proxy -t ng -``` - -这个命令将在`/src/app/proxy/books`文件夹下产生以下文件: - -![Generated files](images/generated-proxies-3.png) - -### BookComponent - -打开 `/src/app/book/book.component.ts` 用以下内容替换它: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto } from '@proxy/books'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } -} -``` - -* 我们引入并注入了生成的 `BookService`. -* 我们使用 [ListService](../UI/Angular/List-Service.md),它是一个工具服务,提供了易用的分页,排序和搜索. - -打开 `/src/app/book/book.component.html` 用以下内容替换它: - -```html -
    -
    -
    -
    -
    - {%{{{ '::Menu:Books' | abpLocalization }}}%} -
    -
    -
    -
    -
    -
    - - - - - {%{{{ '::Enum:BookType:' + row.type | abpLocalization }}}%} - - - - - {%{{{ row.publishDate | date }}}%} - - - - - {%{{{ row.price | currency }}}%} - - - -
    -
    -``` - -现在你可以在浏览器看到最终结果: - -![图书列表最终结果](images/bookstore-book-list.png) - -{{else if UI == "Blazor" || UI == "BlazorServer"}} - -## 创建图书页面 - -是时候创建可见和可用的东西了! 右击`Acme.BookStore.Blazor`项目下的`Pages`文件夹,新建一个名为`Books.razor`的**razor组件**. - -![blazor-add-books-component](images/blazor-add-books-component.png) - -用以下内容替换这个组件的内容: - -````html -@page "/books" - -

    Books

    - -@code { - -} -```` - -### 将图书页面添加到主菜单 - -打开`Blazor`项目中的`BookStoreMenuContributor`类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - ) - ) -); -```` - -运行项目,使用用户名 `admin` 和密码 `1q2w3E*` 登录到应用程序. 看到新菜单项已添加到顶部栏: - -![blazor-menu-bookstore](images/blazor-menu-bookstore.png) - -点击BookStore下的Books子菜单项就会跳转到空的图书页面. - -### 图书列表 - -我们将使用[Blazorise library](https://blazorise.com/)作为UI组件.它是一个强大的库,支持主要的HTML/CSS框架,包括Bootstrap. - -ABP提供了一个通用的基类,`AbpCrudPageBase<...>`,用来创建CRUD风格的页面.这个基类兼容用来构建`IBookAppService`的`ICrudAppService`.所以我们从`AbpCrudPageBase`继承,获得标准CRUD的默认实现. - -打开`Books.razor` 并把内容修改成下面这样: - -````xml -@page "/books" -@using Volo.Abp.Application.Dtos -@using Acme.BookStore.Books -@using Acme.BookStore.Localization -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L -@inherits AbpCrudPageBase - - - -

    @L["Books"]

    -
    - - - - - - - @L[$"Enum:BookType.{Enum.GetName(context.Type)}"] - - - - - @context.PublishDate.ToShortDateString() - - - - - - - @context.CreationTime.ToLongDateString() - - - - - -
    -```` - -> 如果你可以编译并运行成功,但看到一些语法错误.你可以忽略这些错误,因为Visual Studio处理Blazor还有一些bug. - -* `AbpCrudPageBase`实现了所有的CRUD细节,我们从它继承. -* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync`定义在基类中. -* 注入`IStringLocalizer` (作为`L`对象),用于本地化. - -虽然上面的代码非常容易理解,你仍然可以查看Blazorise [Card](https://blazorise.com/docs/components/card/)和[DataGrid](https://blazorise.com/docs/extensions/datagrid/)文档以更好地理解它们. - -#### 关于AbpCrudPageBase - -对于图书页面,我们将持续从`AbpCrudPageBase`获得益处. 你可以只注入`IBookAppService`并自己执行所有的服务端调用(感谢ABP的[动态C# HTTP API客户端代理](../API/Dynamic-CSharp-API-Clients.md)系统). - -## 运行最终应用程序 - -你可以运行应用程序!该部分的最终用户界面如下所示: - -![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png) - -这是一个可以正常工作的,服务端分页,排序和本地化的图书列表. - -{{end # UI }} - -## 下一章 - -查看本教程的[下一章](Part-3.md). diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md deleted file mode 100644 index 198e06801f..0000000000 --- a/docs/zh-Hans/Tutorials/Part-3.md +++ /dev/null @@ -1,1600 +0,0 @@ -# Web应用程序开发教程 - 第三章: 创建,更新和删除图书 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- **Part 3: 创建,更新和删除图书 (本章)** -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -{{if UI == "MVC" && DB == "EF"}} - -### 视频教程 - -本章也被录制为视频教程 **发布在YouTube**. - -{{end}} - -{{if UI == "MVC"}} - -## 创建新书籍 - -通过本节, 你将会了解如何创建一个 modal form 实现新增书籍的功能. model dialog将如下图所示: - -![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) - -### 创建 modal form - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: - -![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) - -#### CreateModal.cshtml.cs - -打开 `CreateModal.cshtml.cs` 代码文件(`CreateModalModel` 类),替换成以下代码: - -````C# -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public void OnGet() - { - Book = new CreateUpdateBookDto(); - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 间接继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. -* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. -* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. -* 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中赋值一些默认值或执行一些代码,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值. - -#### CreateModal.cshtml - -打开 `CreateModal.cshtml` 文件并粘贴如下代码: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model CreateModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - -```` - -* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateUpdateBookDto` 类自动构建了表单. -* `abp-model` 指定了 `Book` 属性为模型对象. -* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). - -> 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态窗口时,我们不希望包括所有布局. - -### 添加 "New book" 按钮 - -打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : - -````html - - - - @L["Books"] - - - - - - -```` - -`Index.cshtml` 的内容最终如下所示: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@section scripts -{ - -} - - - - - - @L["Books"] - - - - - - - - - - -```` - -如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: - -![bookstore-new-book-button](images/bookstore-new-book-button-2.png) - -打开 `Pages/Book/Index.js` 在 `datatable` 配置代码后面添加如下代码: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` 是一个在客户端管理modal的辅助类.它内部使用了Twitter Bootstrap的标准modal组件,但通过简化的API抽象了许多细节. -* `createModal.onResult(...)` 用于在创建书籍后刷新数据表格. -* `createModal.open();` 用于打开modal创建新书籍. - -`Index.js` 的内容最终如下所示: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType:' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } - } - ] - }) - ); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. - -## 更新书籍 - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: - -![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) - -### EditModal.cshtml.cs - -打开 `EditModal.cshtml.cs` 文件(`EditModalModel`类) 并替换成以下代码: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Books; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` - -* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串参数中获取Id的值. -* 在 `OnGetAsync` 方法中, 我们从 `BookAppService` 获得 `BookDto` ,并将它映射成DTO对象 `CreateUpdateBookDto`. -* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. - -### BookDto 到 CreateUpdateBookDto 对象映射 - -为了执行 `BookDto` 到 `CreateUpdateBookDto` 对象映射,请打开 `Acme.BookStore.Web` 项目中的 `BookStoreWebAutoMapperProfile.cs` 并更改它,如下所示: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* 我们添加了 `CreateMap();` 作为映射定义. - -> 请注意,我们在Web层中进行映射定义是一种最佳实践,因为仅在该层中需要它. - -### EditModal.cshtml - -将 `EditModal.cshtml` 页面内容替换成如下代码: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} - - - - - - - - - - -```` - -这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: - -* 它包含`id`属性的`abp-input`, 用于存储被编辑书籍的 `id` (它是隐藏的Input) -* 此页面指定的post地址是`Books/EditModal`. - -### 为表格添加 "操作(Actions)" 下拉菜单 - -我们将为表格每行添加下拉按钮 ("Actions"): - -打开 `Pages/Books/Index.js` 页面,并按下方所示修改表格部分的代码: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType:' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* 增加了一个新的 `ModalManager` 名为 `editModal` 打开编辑模态框. -* 在 `columnDefs` 部分的开头添加了一个新列,用于"*Actions*"下拉按钮. -* "*Edit*" 动作简单地调用 `editModal.open()` 打开编辑模态框. -* `editModal.onResult(...)` 当你关闭编程模态框时进行回调刷新数据表格. - -你可以运行应用程序,并通过选择一本书的编辑操作编辑任何一本书. - -最终的UI看起来如下: - -![bookstore-books-table-actions](./images/bookstore-edit-button-2.png) - -## 删除书籍 - -打开 `Pages/book/index.js` 文件,在 `rowAction` `items` 下新增一项: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.books.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` 执行 `action` 前向用户进行确认. -* `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求删除一个book. -* `abp.notify.info` 执行删除操作后显示一个通知信息. - -由于我们使用了两个新的本地化文本(`BookDeletionConfirmationMessage`和`SuccesslyDeleted`),因此你需要将它们添加到本地化文件(`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下的`en.json`): - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?", -"SuccessfullyDeleted": "Successfully deleted!" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -`Index.js` 的内容最终如下所示: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l( - 'BookDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - acme.bookStore.books.book - .delete(data.record.id) - .then(function() { - abp.notify.info( - l('SuccessfullyDeleted') - ); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('Type'), - data: "type", - render: function (data) { - return l('Enum:BookType:' + data); - } - }, - { - title: l('PublishDate'), - data: "publishDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - }, - { - title: l('Price'), - data: "price" - }, - { - title: l('CreationTime'), data: "creationTime", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(luxon.DateTime.DATETIME_SHORT); - } - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -你可以运行程序并尝试删除一本书. - -{{end}} - -{{if UI == "NG"}} - -## 创建新书籍 - -下面的章节中,你将学习到如何创建一个新的模态窗口新增书籍. - -### BookComponent - -打开 `/src/app/book/book.component.ts` 使用以下内容替换: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto } from '@proxy/books'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - isModalOpen = false; // add this line - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - // add new method - createBook() { - this.isModalOpen = true; - } -} -``` - -* 我们定义了一个名为 `isModalOpen` 的属性和 `createBook` 方法. - - -打开 `/src/app/book/book.component.html` 做以下更改: - -```html -
    -
    -
    -
    -
    {%{{{ '::Menu:Books' | abpLocalization }}}%}
    -
    -
    - -
    - -
    - -
    -
    -
    -
    - -
    -
    - - - - -

    {%{{{ '::NewBook' | abpLocalization }}}%}

    -
    - - - - - - -
    -``` - -* 添加了 `New book` 按钮到卡片头部. -* 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些额外的好处. - -你可以打开浏览器,点击**New book**按钮看到模态框. - -![Empty modal for new book](images/bookstore-empty-new-book-modal.png) - -### 添加响应式表单 - -[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. - -打开 `/src/app/book/book.component.ts` 使用以下内容替换: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // add bookTypeOptions -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; // add this line - - // add bookTypes as a list of BookType enum members - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder // inject FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.buildForm(); // add this line - this.isModalOpen = true; - } - - // add buildForm method - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } - - // add save method - save() { - if (this.form.invalid) { - return; - } - - this.bookService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* 从` @angular/forms `导入了 `FormGroup, FormBuilder and Validators`. -* 添加了 `form: FormGroup` 变量. -* 添加了 `bookTypes` 属性作为 `BookType` 枚举成员列表. 将在表单选项中使用. -* 我们注入了 `FormBuilder` 到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 提供了简便的方法生成表单控件. 它减少了构建复杂表单所需的样板文件的数量. -* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. -* 添加了`save` 方法. - -打开 `/src/app/book/book.component.html`,使用以下内容替换 ` `: - -```html - -
    -
    - * - -
    - -
    - * - -
    - -
    - * - -
    - -
    - * - -
    -
    -
    -``` - -同时使用下面的代码部分替换 ` `: - -````html - - - - - - -```` - -### Datepicker - -我们在这个组件中使用了[NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview). 因此需要添加与此组件相关的依赖项. - -打开 `/src/app/book/book.module.ts` 使用以下内容替换: - -```js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { BookRoutingModule } from './book-routing.module'; -import { BookComponent } from './book.component'; -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; // add this line - -@NgModule({ - declarations: [BookComponent], - imports: [ - BookRoutingModule, - SharedModule, - NgbDatepickerModule, // add this line - ] -}) -export class BookModule { } -``` - -* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. - -打开 `/src/app/book/book.component.ts` 使用以下内容替换: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -// added this line -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ - ListService, - { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter } // add this line - ], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - form: FormGroup; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.buildForm(); - this.isModalOpen = true; - } - - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - this.bookService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* 导入了 `NgbDateNativeAdapter` 和 `NgbDateAdapter`. -* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview). - -现在你可以打开浏览器看到以下变化: - -![Save button to the modal](images/bookstore-new-book-form-v2.png) - -## 更新书籍 - -打开 `/src/app/book/book.component.ts` 使用以下内容替换: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-book', - templateUrl: './book.component.html', - styleUrls: ['./book.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - selectedBook = {} as BookDto; // declare selectedBook - - form: FormGroup; - - bookTypes = bookTypeOptions; - - isModalOpen = false; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getList(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.selectedBook = {} as BookDto; // reset the selected book - this.buildForm(); - this.isModalOpen = true; - } - - // Add editBook method - editBook(id: string) { - this.bookService.get(id).subscribe((book) => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: [this.selectedBook.type || null, Validators.required], - publishDate: [ - this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - Validators.required, - ], - price: [this.selectedBook.price || null, Validators.required], - }); - } - - // change the save method - save() { - if (this.form.invalid) { - return; - } - - const request = this.selectedBook.id - ? this.bookService.update(this.selectedBook.id, this.form.value) - : this.bookService.create(this.form.value); - - request.subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. -* 我们添加了 `editBook` 方法, 根据给定书籍 `Id` 设置 `selectedBook` 对象. -* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. -* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. -* 我们修改了 `save` 方法,同时处理新建和更新操作. - -### 添加 "Actions" 下拉框到表格 - -打开 `/src/app/book/book.component.html` 在 `ngx-datatable` 第一列添加 `ngx-datatable-column` 定义: - -```html - - -
    - -
    - -
    -
    -
    -
    -``` - -在表格的第一列添加了一个 "Actions" 下拉菜单,如下图所示: - -![Action buttons](images/bookstore-actions-buttons.png) - -同时如下所示更改 `ng-template #abpHeader` 部分: - -```html - -

    {%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}

    -
    -``` - -模板将在标题中显示 **Edit** 文本用于编辑记录操作, **New Book** 用于添加新记录操作. - -## 删除书籍 - -打开 `/src/app/book/book.component.ts` 注入 `ConfirmationService`. - -所示替换构造函数: - -```js -// ... - -// add new imports -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; - -//change the constructor -constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder, - private confirmation: ConfirmationService // inject the ConfirmationService -) {} - -// Add a delete method -delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.bookService.delete(id).subscribe(() => this.list.get()); - } - }); -} -``` - -* 我们引入了 `ConfirmationService`. -* 我们注入了 `ConfirmationService` 到构造函数. -* 添加了 `delete` 方法. - -> 参阅[确认弹层文档](../UI/Angular/Confirmation-Service)了解该服务的更多信息. - -### 添加删除按钮: - - -打开 `/src/app/book/book.component.html` 修改 `ngbDropdownMenu` 添加删除按钮: - -```html -
    - - -
    -``` - -最终操作下拉框UI看起来如下: - -![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) - -点击 `delete` 操作调用 `delete` 方法,然后显示一个确认弹层如下图所示. - -![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -## 创建新书籍 - -通过本节, 你将会了解如何创建一个模态窗口实现新增书籍的功能. 因为我们已经从 `AbpCrudPageBase` 继承, 所以只需要开发视图部分. - -### 添加 "New Button" 按钮 - -打开 `Books.razor` 替换 `` 部分为以下代码: - -````xml - - - -

    @L["Books"]

    -
    - - - -
    -
    -```` - -如下图所示,卡片头 **右侧** 添加了 **New book** 按钮: - -![blazor-add-book-button](images/blazor-add-book-button.png) - -现在, 我们可以添加点击按钮后打开的模态窗口了. - -### 书籍创建模态窗口 - -打开 `Books.razor`, 添加以下代码到页面底部: - -````xml - - - -
    - - @L["NewBook"] - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
    -
    -
    -```` - -这段代码需要一个服务; 在文件顶部, `@inherits...` 行前, 注入 `AbpBlazorMessageLocalizerHelper`: - -````csharp -@inject AbpBlazorMessageLocalizerHelper LH -```` - -* 表单实现了验证功能, `AbpBlazorMessageLocalizerHelper` 用于本地化验证消息. -* `CreateModal` 对象, `CloseCreateModalAsync` 和 `CreateEntityAsync` 方法定义在基类中. 参阅 [Blazorise文档](https://blazorise.com/docs/) 以深入理解 `Modal` 和其它组件. - -这就是全部了. 运行应用程序, 尝试添加一本新书. - -![blazor-new-book-modal](images/blazor-new-book-modal.png) - -## 更新书籍 - -编辑书籍与新建书籍很类似. - -### 操作下拉菜单 - -打开 `Books.razor` , 在 `DataGridColumns` 中添加以下 `DataGridEntityActionsColumn` 作为第一项: - -````xml - - - - - - - -```` - -* `OpenEditModalAsync` 定义在基类中, 它接收实体(书籍)参数, 编辑这个实体. - -`DataGridEntityActionsColumn` 组件用于显示 `DataGrid` 每一行中的"操作" 下拉菜单. 如果其中只有唯一的操作, `DataGridEntityActionsColumn` 显示 **唯一按钮**, 而不是下拉菜单. - -![blazor-edit-book-action](images/blazor-edit-book-action-2.png) - -### 编辑模态窗口 - -我们现在可以定义一个模态窗口编辑书籍. 加入下面的代码到 `Books.razor` 页面的底部: - -````xml - - - -
    - - @EditingEntity.Name - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
    -
    -
    -```` - -### AutoMapper 配置 - -基类 `AbpCrudPageBase` 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 系统将 `BookDto` 对象转化为`CreateUpdateBookDto` 对象. 因此, 我们需要定义映射. - -打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreBlazorAutoMapperProfile `, 替换成以下内容: - -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Blazor -{ - public class BookStoreBlazorAutoMapperProfile : Profile - { - public BookStoreBlazorAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* `CreateMap();` 行用于定义映射. - -### 测试编辑模态窗口 - -你可以运行程序并尝试编辑一本书. - -![blazor-edit-book-modal](images/blazor-edit-book-modal.png) - -> 提示: 尝试保留 *Name* 字段为空并提交表单, 将显示验证错误消息. - -## 删除书籍 - -打开 `Books.razor` 页面, 在 `EntityActions` 中的"编辑" 操作下面加入以下的 `EntityAction`: - -````xml - -```` - -* `DeleteEntityAsync` 定义在基类中. 通过向服务器发起请求删除实体. -* `ConfirmationMessage` 执行操作前显示确认消息的回调函数. -* `GetDeleteConfirmationMessage` 定义在基类中. 你可以覆写这个方法 (或传递其它值给 `ConfirmationMessage` 参数) 以定制本地化消息. - -因为"操作" 按钮现在有了两个操作, 变成了下拉菜单: - -![blazor-edit-book-action](images/blazor-delete-book-action.png) - -运行程序并尝试删除一本书. - -## 完整的 CRUD UI 代码 - -下面是完整的创建图书管理CRUD页面的代码, 这些代码在上面是分成两部分开发的: - -````xml -@page "/books" -@using Volo.Abp.Application.Dtos -@using Acme.BookStore.Books -@using Acme.BookStore.Localization -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Components.Web -@inject IStringLocalizer L -@inject AbpBlazorMessageLocalizerHelper LH -@inherits AbpCrudPageBase - - - - - -

    @L["Books"]

    -
    - - - -
    -
    - - - - - - - - - - - - - - - @L[$"Enum:BookType.{Enum.GetName(context.Type)}"] - - - - - @context.PublishDate.ToShortDateString() - - - - - - - @context.CreationTime.ToLongDateString() - - - - - -
    - - - - -
    - - @L["NewBook"] - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
    -
    -
    - - - - -
    - - @EditingEntity.Name - - - - - - - @L["Name"] - - - - - - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - - -
    -
    -
    -```` - -{{end}} - -## 下一章 - -查看本教程的[下一章](Part-4.md). diff --git a/docs/zh-Hans/Tutorials/Part-4.md b/docs/zh-Hans/Tutorials/Part-4.md deleted file mode 100644 index 9d6349684e..0000000000 --- a/docs/zh-Hans/Tutorials/Part-4.md +++ /dev/null @@ -1,293 +0,0 @@ -# Web应用程序开发教程 - 第四章: 集成测试 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-3.md) -- **Part 4: 集成测试 (本章)** -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -{{if UI == "MVC" && DB == "EF"}} - -### 视频教程 - -本章也被录制为视频教程 **发布在YouTube**. - -{{end}} - -## 解决方案中的测试项目 - -这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目: - -![bookstore-test-projects-v2](./images/bookstore-test-projects-mvc.png) - -> 根据你选择的UI和数据库, 测试项目略微有所不同. 例如, 如果选择MongoDB, 那么 `Acme.BookStore.EntityFrameworkCore.Tests` 会变为 `Acme.BookStore.MongoDB.Tests`. - -每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: - -* [xunit](https://xunit.github.io/) 作为主测试框架. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. -* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. - -{{if DB=="EF"}} - -> 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用[数据种子系统](../Data-Seeding.md)初始化种子数据,为每个测试准备一个新的数据库. - -{{else if DB=="Mongo"}} - -> **[EphemeralMongo](https://github.com/asimmon/ephemeral-mongo)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用[数据种子系统](../Data-Seeding.md)初始化种子数据,为每个测试准备一个新的数据库. - -{{end}} - -## 添加测试数据 - -如果你已经按照[第一部分](Part-1.md)中的描述创建了数据种子贡献者,则相同的数据也在测试中可用. 因此你可以跳过此部分. 如果你尚未创建种子贡献者,可以使用 `BookStoreTestDataSeedContributor` 来为要在以下测试中使用的相同数据提供种子. - -## 测试 BookAppService - -在 `Acme.BookStore.Application.Tests` 项目的 `Books` 命名空间(文件夹)中创建一个名叫 `BookAppService_Tests` 的测试类: - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Modularity; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books; -{ {{if DB=="Mongo"}} -[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} -public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IBookAppService _bookAppService; - - protected BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } -} -```` -{{if DB == "EF"}} -添加 `BookAppService_Tests` 的实现类,命名为 `EfCoreBookAppService_Tests` ,并放置在 `EntityFrameworkCore\Applications\Books` 命名空间 (文件夹)下,在 `Acme.BookStore.EntityFrameworkCore.Tests` 项目中: - -````csharp -using Acme.BookStore.Books; -using Xunit; - -namespace Acme.BookStore.EntityFrameworkCore.Applications.Books; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class EfCoreBookAppService_Tests : BookAppService_Tests -{ - -} -```` -{{end}} - -{{if DB == "Mongo"}} -添加 `BookAppService_Tests` 的实现类,命名为`MongoDBBookAppService_Tests` ,并放置在 `MongoDb\Applications\Books` 命名空间 (文件夹)下,在 `Acme.BookStore.MongoDB.Tests` 项目中: - -````csharp -using Acme.BookStore.MongoDB; -using Acme.BookStore.Books; -using Xunit; - -namespace Acme.BookStore.MongoDb.Applications.Books; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class MongoDBBookAppService_Tests : BookAppService_Tests -{ - -} -```` -{{end}} - -* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. -* 我们可以安全地检查 "1984" 这本书的名称,因为我们知道这本书可以在数据库中找到,我们已将其添加到种子数据中. - -新增测试方法,用以测试创建一个**合法**book实体的场景: - -````csharp -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -新增测试方法,用以测试创建一个非法book实体失败的场景: - -````csharp -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. - -最终的测试类如下所示: - -````csharp -using System; -using System.Linq; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Validation; -using Xunit; - -namespace Acme.BookStore.Books -{ {{if DB=="Mongo"}} - [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} - public abstract class BookAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } - - [Fact] - public async Task Should_Create_A_Valid_Book() - { - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); - } - } -} -```` - -打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行所有**测试: - -![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) - -恭喜你, **绿色图标**表示测试已成功通过! - -## 下一章 - -查看本教程的[下一章](Part-5.md). diff --git a/docs/zh-Hans/Tutorials/Part-5.md b/docs/zh-Hans/Tutorials/Part-5.md deleted file mode 100644 index ec308f3ee7..0000000000 --- a/docs/zh-Hans/Tutorials/Part-5.md +++ /dev/null @@ -1,635 +0,0 @@ -# Web应用程序开发教程 - 第五章: 授权 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- **Part 5: 授权 (本章)** -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" 或 "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/zh-cn/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -{{if UI == "MVC" && DB == "EF"}} - -### 视频教程 - -本章也被录制为视频教程 **发布在YouTube**. - -{{end}} - -## 权限 - -ABP框架提供了一个基于ASP.NET Core[授权基础架构](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction)的[授权系统](../Authorization.md). 基于标准授权基础架构的一个主要功能是添加了 **权限系统**, 这个系统允许定义权限并且根据角色, 用户或客户端启用/禁用权限. - -### 权限名称 - -权限必须有唯一的名称 (一个 `字符串`). 最好的方法是把它定义为一个 `常量`, 这样我们就可以重用这个权限名称了. - -打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissions` 类 (位于 `Permissions` 文件夹) 并替换为以下代码: - -````csharp -namespace Acme.BookStore.Permissions -{ - public static class BookStorePermissions - { - public const string GroupName = "BookStore"; - - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } - } -} -```` - -权限名称具有层次结构. 例如, "创建图书" 权限被定义为 `BookStore.Books.Create`. ABP不强制必须如此, 但这是一种有益的做法. - -### 权限定义 - -在使用权限前必须定义它们. - -打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissionDefinitionProvider` 类 (位于 `Permissions` 文件夹) 并替换为以下代码: - -````csharp -using Acme.BookStore.Localization; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Localization; - -namespace Acme.BookStore.Permissions -{ - public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider - { - public override void Define(IPermissionDefinitionContext context) - { - var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); - - var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); - booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); - booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); - booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } - } -} -```` - -这个类定义了一个 **权限组** (在UI上分组权限, 下文会看到) 和 权限组中的**4个权限**. 而且, **创建**, **编辑** 和 **删除** 是 `BookStorePermissions.Books.Default` 权限的子权限. **仅当父权限被选择**时, 子权限才能被选择. - -最后, 编辑本地化文件 (`Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹中的 `en.json`) 定义上面使用的本地化键: - -````json -"Permission:BookStore": "Book Store", -"Permission:Books": "Book Management", -"Permission:Books.Create": "Creating new books", -"Permission:Books.Edit": "Editing the books", -"Permission:Books.Delete": "Deleting the books" -```` - -> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定. 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -### 权限管理界面 - -完成权限定义后, 可以在**权限管理模态窗口**看到它们. - -在*管理 -> Identity -> 角色* 页面, 选择admin角色的 *权限* 操作, 打开权限管理模态窗口: - -![bookstore-permissions-ui](images/bookstore-permissions-ui.png) - -授予你希望的权限并保存. - -> **提示**: 如果运行 `Acme.BookStore.DbMigrator` 应用程序, 新权限会被自动授予admin. - -## 授权 - -现在, 你可以使用权限授权图书管理. - -### 应用层 和 HTTP API - -打开 the `BookAppService` 类, 设置策略名称为上面定义的权限名称. - -````csharp -using System; -using Acme.BookStore.Permissions; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Books -{ - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } - } -} -```` - -加入代码到构造器. 基类中的 `CrudAppService` 自动在CRUD操作中使用这些权限. 这不仅实现了 **应用服务** 的安全性, 也实现了 **HTTP API** 安全性, 因为如前解释的, HTTP API 自动使用这些服务. (参阅 [自动 API controllers](../API/Auto-API-Controllers.md)). - -> 在稍后开发作者管理功能时, 你将会看到声明式授权, 使用 `[Authorize(...)]` 特性. - -{{if UI == "MVC"}} - -### Razor 页面 - -虽然安全的 HTTP API和应用服务阻止未授权用户使用服务, 但他们依然可以导航到图书管理页面. 虽然当页面发起第一个访问服务器的AJAX请求时会收到授权异常, 但为了更好的用户体验和安全性, 我们应该对页面进行授权. - -打开 `BookStoreWebModule` 在 `ConfigureServices` 方法中加入以下代码: - -````csharp -Configure(options => -{ - options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default); - options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create); - options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit); -}); -```` - -现在未授权用户会被重定向至**登录页面**. - -#### 隐藏新建图书按钮 - -图书管理页面有一个 *新建图书* 按钮, 当用户没有 *图书新建* 权限时就不可见的. - -![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png) - -打开 `Pages/Books/Index.cshtml` 文件, 替换内容为以下代码: - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Permissions -@using Acme.BookStore.Web.Pages.Books -@using Microsoft.AspNetCore.Authorization -@using Microsoft.Extensions.Localization -@model IndexModel -@inject IStringLocalizer L -@inject IAuthorizationService AuthorizationService -@section scripts -{ - -} - - - - - - @L["Books"] - - - @if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create)) - { - - } - - - - - - - -```` - -* 加入 `@inject IAuthorizationService AuthorizationService` 以访问授权服务. -* 使用 `@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))` 检查图书创建权限, 条件显示 *新建图书* 按钮. - -### JavaScript端 - -图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑* 和 *删除* 操作: - -![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png) - -如果用户没有权限, 应该隐藏相关的操作. 表格行中的操作有一个 `visible` 属性, 可以设置为 `false` 隐藏操作项. - -打开 `Acme.BookStore.Web` 项目中的 `Pages/Books/Index.js`, 为 `编辑` 操作加入 `visible` 属性: - -````js -{ - text: l('Edit'), - visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION - action: function (data) { - editModal.open({ id: data.record.id }); - } -} -```` - -对 `Delete` 操作进行同样的操作: - -````js -visible: abp.auth.isGranted('BookStore.Books.Delete') -```` - -* `abp.auth.isGranted(...)` 检查前面定义的权限. -* `visible` 也可以是一个返回 `bool` 值的函数. 这个函数可以稍后根据某些条件计算. - -### 菜单项 - -即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项. - -打开 `BookStoreMenuContributor` 类, 找到下面的代码: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - ) - ) -); -```` - -替换为以下代码: - -````csharp -var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" -); - -context.Menu.AddItem(bookStoreMenu); - -//CHECK the PERMISSION -if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - )); -} -```` - -你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字, 并重新组织返回值. 最终的 `BookStoreMenuContributor` 类应该如下: - -````csharp -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Acme.BookStore.Localization; -using Acme.BookStore.MultiTenancy; -using Acme.BookStore.Permissions; -using Volo.Abp.TenantManagement.Web.Navigation; -using Volo.Abp.UI.Navigation; - -namespace Acme.BookStore.Web.Menus -{ - public class BookStoreMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if (context.Menu.Name == StandardMenus.Main) - { - await ConfigureMainMenuAsync(context); - } - } - - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - if (!MultiTenancyConsts.IsEnabled) - { - var administration = context.Menu.GetAdministration(); - administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName); - } - - var l = context.GetLocalizer(); - - context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/")); - - var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ); - - context.Menu.AddItem(bookStoreMenu); - - //CHECK the PERMISSION - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - )); - } - } - } -} -```` - -{{else if UI == "NG"}} - -### Angular Guard 配置 - -UI的第一步是防止未认证用户看见"图书"菜单项并进入图书管理页面. - -打开 `/src/app/book/book-routing.module.ts` 替换为以下代码: - -````js -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import { authGuard, permissionGuard } from '@abp/ng.core'; -import { BookComponent } from './book.component'; - -const routes: Routes = [ - { path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class BookRoutingModule {} -```` - -* 从 `@abp/ng.core` 引入 `authGuard` 和 `permissionGuard`. -* 在路由定义中添加 `canActivate: [authGuard, permissionGuard]`. - -打开 `/src/app/route.provider.ts`, 在 `/books` 路由中添加 `requiredPolicy: 'BookStore.Books'`. `/books` 路由应该如以下配置: - -````js -{ - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Books', -} -```` - -### 隐藏新建图书按钮 - -当用户没有 *图书新建* 权限时, 图书管理页面上的 *新建图书* 按钮应该不可见. - -![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png) - -打开 `/src/app/book/book.component.html` 文件, 替换创建按钮的HTML内容如下: - -````html - - -```` - -* 加入 `*abpPermission="'BookStore.Books.Create'"`, 当用户没有权限时隐藏按钮. - -### 隐藏编辑和删除操作 - -图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑* 和 *删除* 操作: - -![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png) - -如果用户没有权限, 应该隐藏相关的操作. - -打开 `/src/app/book/book.component.html` 文件, 替换编辑和删除按钮的内容如下: - -````html - - - - - -```` - -* 加入 `*abpPermission="'BookStore.Books.Edit'"`, 当用户没有编辑权限时隐藏按钮. -* 加入 `*abpPermission="'BookStore.Books.Delete'"`, 当用户没有删除权限时隐藏按钮. - -{{else if UI == "Blazor"}} - -### Razor验证组件 - -打开 `Acme.BookStore.Blazor` 项目中的 `/Pages/Books.razor` 文件, 在` @page` 指令和命名空间引入(`@using` 行)后添加 `Authorize` 特性, 如下所示: - -````html -@page "/books" -@attribute [Authorize(BookStorePermissions.Books.Default)] -@using Acme.BookStore.Permissions -@using Microsoft.AspNetCore.Authorization -... -```` - -添加这个特性阻止未登录用户或未授权用户访问这个页面. 用户重试后, 会被重定向到登录页面. - -### 显示/隐藏操作 - -图书管理页面上的每一种图书都有 *新建* 按钮和 *编辑*, *删除* 操作. 如果用户没有相关权限, 这些按钮/操作应该被隐藏. - -基类 `AbpCrudPageBase` 已经具有这些操作需要的功能. - -#### 设置策略 (权限) 名称 - -加入以下代码到 `Books.razor` 文件结尾: - -````csharp -@code -{ - public Books() // Constructor - { - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } -} -```` - -基类 `AbpCrudPageBase` 自动检查相关操作的权限. 如果需要手动检查, 它也定义了相应的属性. - -* `HasCreatePermission`: True, 如果用户具有新建实体的权限. -* `HasUpdatePermission`: True, 如果用户具有编辑/更新实体的权限. -* `HasDeletePermission`: True, 如果用户具有删除实体的权限. - -> **Blazor 提示**: 当添加少量代码到 `@code` 是没有问题的. 当添加的代码变长时, 建议使用代码后置方法以便于维护. 我们将在作者部分使用这个方法. - -#### 隐藏新建图书按钮 - -检查 *新建图书* 按钮权限: - -````xml -@if (HasCreatePermission) -{ - -} -```` - -#### 隐藏编辑/删除操作 - -`EntityAction` 组件定义了 `Visible` 属性 (参数) 以条件显示操作. - -更新 `EntityActions` 部分: - -````xml - - - - -```` - -#### 关于权限缓存 - -你可以运行和测试权限. 从admin角色中移除一个图书相关权限, 观察到相关按钮/操作从UI上消失. - -在客户端, **ABP框架缓存当前用户的权限** . 所以, 当你修改了你的权限, 你需要手工 **刷新页面**. 如果不刷新并试图使用被禁的操作, 你会从服务器收到一个HTTP 403 (forbidden) 响应. - -> 修改角色或用户的权限在服务端立即生效. 所以, 缓存系统不会导致安全问题. - -### 菜单项 - -即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项. - -打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreMenuContributor` 类, 找到以下代码: - -````csharp -context.Menu.AddItem( - new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ).AddItem( - new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - ) - ) -); -```` - -替换为以下代码: - -````csharp -var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" -); - -context.Menu.AddItem(bookStoreMenu); - -//CHECK the PERMISSION -if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); -} -```` - -你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字并重新整理返回值. 最终的 `ConfigureMainMenuAsync` 方法如下: - -````csharp -private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) -{ - var l = context.GetLocalizer(); - - context.Menu.Items.Insert( - 0, - new ApplicationMenuItem( - "BookStore.Home", - l["Menu:Home"], - "/", - icon: "fas fa-home" - ) - ); - - var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ); - - context.Menu.AddItem(bookStoreMenu); - - //CHECK the PERMISSION - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); - } -} -```` - -{{end}} - -## 下一章 - -查看本教程的[下一章](Part-6.md). diff --git a/docs/zh-Hans/Tutorials/Part-6.md b/docs/zh-Hans/Tutorials/Part-6.md deleted file mode 100644 index 234108a00a..0000000000 --- a/docs/zh-Hans/Tutorials/Part-6.md +++ /dev/null @@ -1,280 +0,0 @@ -# Web应用程序开发教程 - 第六章: 作者: 领域层 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- **Part 6: 作者: 领域层 (本章)** -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -## 简介 - -在前面的章节中, 我们使用 ABP 框架轻松地构建了一些服务; - -* 使用 [CrudAppService](../Application-Services.md) 基类, 而不是为标准的增删改查操作手工开发应用服务. -* 使用 [generic repositories](../Repositories.md) 自动完成数据层功能. - -对于 "作者" 部分; - -* 我们将要展示在需要的情况下, 如何 **手工做一些事情**. -* 我们将要实现一些 **领域驱动设计 (DDD) 最佳实践**. - -> **开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式** - -## 作者实体 - -在 `Acme.BookStore.Domain` 项目中创建 `Authors` 文件夹 (命名空间), 在其中加入 `Author` 类: - -````csharp -using System; -using JetBrains.Annotations; -using Volo.Abp; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore.Authors -{ - public class Author : FullAuditedAggregateRoot - { - public string Name { get; private set; } - public DateTime BirthDate { get; set; } - public string ShortBio { get; set; } - - private Author() - { - /* This constructor is for deserialization / ORM purpose */ - } - - internal Author( - Guid id, - [NotNull] string name, - DateTime birthDate, - [CanBeNull] string shortBio = null) - : base(id) - { - SetName(name); - BirthDate = birthDate; - ShortBio = shortBio; - } - - internal Author ChangeName([NotNull] string name) - { - SetName(name); - return this; - } - - private void SetName([NotNull] string name) - { - Name = Check.NotNullOrWhiteSpace( - name, - nameof(name), - maxLength: AuthorConsts.MaxNameLength - ); - } - } -} -```` - -* 由 `FullAuditedAggregateRoot` 继承使得实体支持[软删除](../Data-Filtering.md) (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 [审计](../Entities.md) 属性. -* `Name` 属性的 `private set` 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证): - * 当新建一个作者时, 通过构造器. - * 使用 `ChangeName` 方法更新名字. -* `构造器` 和 `ChangeName` 方法的访问级别是 `internal`, 强制这些方法只能在领域层由 `AuthorManager` 使用. 稍后将对此进行解释. -* `Check` 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 `ArgumentException`). - -`AuthorConsts` 是一个简单的类, 它位于 `Acme.BookStore.Domain.Shared` 项目的 `Authors` 命名空间 (文件夹)中: - -````csharp -namespace Acme.BookStore.Authors -{ - public static class AuthorConsts - { - public const int MaxNameLength = 64; - } -} -```` - -在 `Acme.BookStore.Domain.Shared` 项目中创建这个类, 因为[数据传输类](../Data-Transfer-Objects.md) (DTOs) 稍后会再一次用到它. - -## AuthorManager: 领域服务 - -`Author` 构造器和 `ChangeName` 方法的访问级别是 `internal`, 所以它们只能在领域层使用. 在 `Acme.BookStore.Domain` 项目中的 `Authors` 文件夹 (命名空间)创建 `AuthorManager` 类: - -````csharp -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Volo.Abp; -using Volo.Abp.Domain.Services; - -namespace Acme.BookStore.Authors -{ - public class AuthorManager : DomainService - { - private readonly IAuthorRepository _authorRepository; - - public AuthorManager(IAuthorRepository authorRepository) - { - _authorRepository = authorRepository; - } - - public async Task CreateAsync( - [NotNull] string name, - DateTime birthDate, - [CanBeNull] string shortBio = null) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var existingAuthor = await _authorRepository.FindByNameAsync(name); - if (existingAuthor != null) - { - throw new AuthorAlreadyExistsException(name); - } - - return new Author( - GuidGenerator.Create(), - name, - birthDate, - shortBio - ); - } - - public async Task ChangeNameAsync( - [NotNull] Author author, - [NotNull] string newName) - { - Check.NotNull(author, nameof(author)); - Check.NotNullOrWhiteSpace(newName, nameof(newName)); - - var existingAuthor = await _authorRepository.FindByNameAsync(newName); - if (existingAuthor != null && existingAuthor.Id != author.Id) - { - throw new AuthorAlreadyExistsException(newName); - } - - author.ChangeName(newName); - } - } -} -```` - -* `AuthorManager` 强制使用一种可控的方式创建作者和修改作者的名字. 应用层 (后面会介绍) 将会使用这些方法. - -> **DDD 提示**: 如非必须并且用于执行核心业务规则, 不要引入领域服务方法. 对于这个场景, 我们使用这个服务保证名字的唯一性. - -两个方法都检查是否存在同名用户, 如果存在, 抛出业务异常 `AuthorAlreadyExistsException`, 这个异常定义在 `Acme.BookStore.Domain` 项目 (`Authors` 文件夹中): - -````csharp -using Volo.Abp; - -namespace Acme.BookStore.Authors -{ - public class AuthorAlreadyExistsException : BusinessException - { - public AuthorAlreadyExistsException(string name) - : base(BookStoreDomainErrorCodes.AuthorAlreadyExists) - { - WithData("name", name); - } - } -} -```` - -`BusinessException` 是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化. `WithData(...)` 方法提供额外的数据给异常对象, 这些数据将会在本地化中或出于其它一些目的被使用. - -打开 `Acme.BookStore.Domain.Shared` 项目中的 `BookStoreDomainErrorCodes` 并修改为: - -````csharp -namespace Acme.BookStore -{ - public static class BookStoreDomainErrorCodes - { - public const string AuthorAlreadyExists = "BookStore:00001"; - } -} -```` - -这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` , 加入以下项: - -````json -"BookStore:00001": "There is already an author with the same name: {name}" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -当 `AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息. - -## IAuthorRepository - -`AuthorManager` 注入了 `IAuthorRepository`, 所以我们需要定义它. 在 `Acme.BookStore.Domain` 项目的 `Authors` 文件夹 (命名空间) 中创建这个新接口: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Authors -{ - public interface IAuthorRepository : IRepository - { - Task FindByNameAsync(string name); - - Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null - ); - } -} -```` - -* `IAuthorRepository` 扩展了标准 `IRepository` 接口, 所以所有的标准 [repository](../Repositories.md) 方法对于 `IAuthorRepository` 都是可用的. -* `FindByNameAsync` 在 `AuthorManager` 中用来根据姓名查询用户. -* `GetListAsync` 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上. - -我们会在下一章实现这个repository. - -> 这两个方法似乎 **看上去没有必要**, 因为标准repositories已经是 `IQueryable`, 你可以直接使用它们, 而不是自定义方法. 在实际应用程序中, 这么做是没问题的. 但在这个 **学习指南**中, 解释如何在需要时创建自定义repository方法是有价值的. - -## 结论 - -这一章覆盖了图书管理程序作者相关功能的领域层. 在这一章中创建/更新的文件在下图中被高亮: - -![bookstore-author-domain-layer](images/bookstore-author-domain-layer.png) - -## 下一章 - -查看本教程的[下一章](Part-7.md). diff --git a/docs/zh-Hans/Tutorials/Part-7.md b/docs/zh-Hans/Tutorials/Part-7.md deleted file mode 100644 index fc3630e628..0000000000 --- a/docs/zh-Hans/Tutorials/Part-7.md +++ /dev/null @@ -1,238 +0,0 @@ -# Web应用程序开发教程 - 第七章: 数据库集成 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- **Part 7: 数据库集成**(本章) -- [Part 8: 作者: 应用服务层](Part-8.md) -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -## 简介 - -这章阐述如何为前一章介绍的 `作者` 实体配置数据库集成. - -{{if DB=="EF"}} - -## DB Context - -打开 `Acme.BookStore.EntityFrameworkCore` 项目中的 `BookStoreDbContext` 加入 `DbSet` 属性: - -````csharp -public DbSet Authors { get; set; } -```` - -定位到相同项目中的 `BookStoreDbContext` 类中的 `OnModelCreating` 方法, 加入以下代码到方法的结尾: - -````csharp -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", - BookStoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Name) - .IsRequired() - .HasMaxLength(AuthorConsts.MaxNameLength); - - b.HasIndex(x => x.Name); -}); -```` - -这和前面的 `Book` 实体做的一样, 所以不再赘述. - -## 创建数据库迁移 - -配置启动解决方案为使用 [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). 因为我们还没有修改数据库映射配置,所以需要创建一个新的迁移并对数据库应用变更. - -打开命令行终端, 切换当前目录为 `Acme.BookStore.EntityFrameworkCore` 项目目录, 输入以下命令: - -````bash -dotnet ef migrations add Added_Authors -```` - -这会在项目中添加一个迁移类: - -![bookstore-efcore-migration-authors](./images/bookstore-efcore-migration-authors.png) - -你可以在同一个命令行终端中使用以下命令对数据库应用更改: - -````bash -dotnet ef database update -```` - -> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* . - -{{else if DB=="Mongo"}} - -## DB Context - -打开 `Acme.BookStore.MongoDB` 项目 `MongoDb 文件夹`中的 `BookStoreMongoDbContext`, 在类中加入以下属性: - -````csharp -public IMongoCollection Authors => Collection(); -```` - -{{end}} - -## 实现 IAuthorRepository - -{{if DB=="EF"}} - -在 `Acme.BookStore.EntityFrameworkCore` 项目 (`Authors` 文件夹)中创建一个新类 `EfCoreAuthorRepository`, 粘贴以下代码: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Threading.Tasks; -using Acme.BookStore.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -namespace Acme.BookStore.Authors -{ - public class EfCoreAuthorRepository - : EfCoreRepository, - IAuthorRepository - { - public EfCoreAuthorRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var dbSet = await GetDbSetAsync(); - return await dbSet - .WhereIf( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } - } -} -```` - -* 继承自 `EfCoreRepository`, 所以继承了标准repository的方法实现. -* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. -* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. - -> 参阅 [EF Core 集成文档](../Entity-Framework-Core.md) 获得基于EF Core的repositories的更多信息. - -{{else if DB=="Mongo"}} - -在 `Acme.BookStore.MongoDB` 项目 (`Authors` 文件夹)中创建一个新类 `MongoDbAuthorRepository`, 粘贴以下代码: - -```csharp -using System; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Collections.Generic; -using System.Threading.Tasks; -using Acme.BookStore.MongoDB; -using MongoDB.Driver; -using MongoDB.Driver.Linq; -using Volo.Abp.Domain.Repositories.MongoDB; -using Volo.Abp.MongoDB; - -namespace Acme.BookStore.Authors -{ - public class MongoDbAuthorRepository - : MongoDbRepository, - IAuthorRepository - { - public MongoDbAuthorRepository( - IMongoDbContextProvider dbContextProvider - ) : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable - .WhereIf>( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .As>() - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } - } -} -``` - -* 继承自 `MongoDbRepository`, 所以继承了标准repository的方法实现. -* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. -* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. - -> 参阅 [MongoDB 集成文档](../MongoDB.md) 获得基于MongoDB的repositories的更多信息. - -{{end}} - -## 下一章 - -查看本教程的[下一章](Part-8.md). diff --git a/docs/zh-Hans/Tutorials/Part-8.md b/docs/zh-Hans/Tutorials/Part-8.md deleted file mode 100644 index 2c12aa1f78..0000000000 --- a/docs/zh-Hans/Tutorials/Part-8.md +++ /dev/null @@ -1,611 +0,0 @@ -# Web应用程序开发教程 - 第八章: 作者: 应用服务层 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- **Part 8: 作者: 应用服务层 (本章)** -- [Part 9: 作者: 用户页面](Part-9.md) -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -## 简介 - -这章阐述如何为前一章介绍的 `作者` 实体创建应用服务层. - -## IAuthorAppService - -我们首先创建 [应用服务](../Application-Services.md) 接口和相关的 [DTO](../Data-Transfer-Objects.md)s. 在 `Acme.BookStore.Application.Contracts` 项目的 `Authors` 命名空间 (文件夹) 创建一个新接口 `IAuthorAppService`: - -````csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore.Authors -{ - public interface IAuthorAppService : IApplicationService - { - Task GetAsync(Guid id); - - Task> GetListAsync(GetAuthorListDto input); - - Task CreateAsync(CreateAuthorDto input); - - Task UpdateAsync(Guid id, UpdateAuthorDto input); - - Task DeleteAsync(Guid id); - } -} -```` - -* `IApplicationService` 是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们. -* 在 `Author` 实体中定义标准方法用于CRUD操作. -* `PagedResultDto` 是一个ABP框架中预定义的 DTO 类. 它拥有一个 `Items` 集合 和一个 `TotalCount` 属性, 用于返回分页结果. -* 优先从 `CreateAsync` 方法返回 `AuthorDto` (新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法. - -这个类使用下面定义的DTOs (为你的项目创建它们). - -### AuthorDto - -````csharp -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Authors -{ - public class AuthorDto : EntityDto - { - public string Name { get; set; } - - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } - } -} -```` - -* `EntityDto` 只有一个类型为指定泛型参数的 `Id` 属性. 你可以自己创建 `Id` 属性, 而不是继承自 `EntityDto`. - -### GetAuthorListDto - -````csharp -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Authors -{ - public class GetAuthorListDto : PagedAndSortedResultRequestDto - { - public string? Filter { get; set; } - } -} -```` - -* `Filter` 用于搜索作者. 它可以是 `null` (或空字符串) 以获得所有用户. -* `PagedAndSortedResultRequestDto` 具有标准分页和排序属性: `int MaxResultCount`, `int SkipCount` 和 `string Sorting`. - -> ABP 框架拥有这些基本的DTO类以简化并标准化你的DTOs. 参阅 [DTO 文档](../Data-Transfer-Objects.md) 获得所有DTO类的详细信息. - -### CreateAuthorDto - -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Authors -{ - public class CreateAuthorDto - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } - - [Required] - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } - } -} -```` - -数据标记特性可以用来验证DTO. 参阅 [验证文档](../Validation.md) 获得详细信息. - -### UpdateAuthorDto - -````csharp -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore.Authors -{ - public class UpdateAuthorDto - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } - - [Required] - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } - } -} -```` - -> 我们可以在创建和更新操作间分享 (重用) 相同的DTO. 虽然可以这么做, 但我们推荐为这些操作创建不同的DTOs, 因为我们发现随着时间的推移, 它们通常会变得有差异. 所以, 与紧耦合相比, 代码重复也是合理的. - -## AuthorAppService - -是时候实现 `IAuthorAppService` 接口了. 在 `Acme.BookStore.Application` 项目的 `Authors` 命名空间 (文件夹) 中创建一个新类 `AuthorAppService` : - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Permissions; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore.Authors -{ - [Authorize(BookStorePermissions.Authors.Default)] - public class AuthorAppService : BookStoreAppService, IAuthorAppService - { - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public AuthorAppService( - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - //...SERVICE METHODS WILL COME HERE... - } -} -```` - -* `[Authorize(BookStorePermissions.Authors.Default)]` 是一个检查权限(策略)的声明式方法, 用来给当前用户授权. 参阅 [授权文档](../Authorization.md) 获得详细信息. `BookStorePermissions` 类在后文会被更新, 现在不需要担心编译错误. -* 由 `BookStoreAppService` 派生, 这个类是一个简单基类, 可以做为模板. 它继承自标准的 `ApplicationService` 类. -* 实现上面定义的 `IAuthorAppService` . -* 注入 `IAuthorRepository` 和 `AuthorManager` 以使用服务方法. - -现在, 我们逐个介绍服务方法. 复制这些方法到 `AuthorAppService` 类. - -### GetAsync - -````csharp -public async Task GetAsync(Guid id) -{ - var author = await _authorRepository.GetAsync(id); - return ObjectMapper.Map(author); -} -```` - -这个方法根据 `Id` 获得 `Author` 实体, 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 转换为 `AuthorDto`. 这需要配置AutoMapper, 后面会介绍. - -### GetListAsync - -````csharp -public async Task> GetListAsync(GetAuthorListDto input) -{ - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Author.Name); - } - - var authors = await _authorRepository.GetListAsync( - input.SkipCount, - input.MaxResultCount, - input.Sorting, - input.Filter - ); - - var totalCount = input.Filter == null - ? await _authorRepository.CountAsync() - : await _authorRepository.CountAsync( - author => author.Name.Contains(input.Filter)); - - return new PagedResultDto( - totalCount, - ObjectMapper.Map, List>(authors) - ); -} -```` - -* 为处理客户端没有设置的情况, 在方法的开头设置默认排序是 "根据作者名". -* 使用 `IAuthorRepository.GetListAsync` 从数据库中获得分页的, 排序的和过滤的作者列表. 我们已经在教程的前一章中实现了它. 再一次强调, 实际上不需要创建这个方法, 因为我们可以从数据库中直接查询, 这里只是演示如何创建自定义repository方法. -* 直接查询 `AuthorRepository` , 得到作者的数量. 如果客户端发送了过滤条件, 会得到过滤后的作者数量. -* 最后, 通过映射 `Author` 列表到 `AuthorDto` 列表, 返回分页后的结果. - -### CreateAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Create)] -public async Task CreateAsync(CreateAuthorDto input) -{ - var author = await _authorManager.CreateAsync( - input.Name, - input.BirthDate, - input.ShortBio - ); - - await _authorRepository.InsertAsync(author); - - return ObjectMapper.Map(author); -} -```` - -* `CreateAsync` 需要 `BookStorePermissions.Authors.Create` 权限 (另外包括 `AuthorAppService` 类声明的 `BookStorePermissions.Authors.Default` 权限). -* 使用 `AuthorManager` (领域服务) 创建新作者. -* 使用 `IAuthorRepository.InsertAsync` 插入新作者到数据库. -* 使用 `ObjectMapper` 返回 `AuthorDto` , 代表新创建的作者. - -> **DDD提示**: 一些开发者可能会发现可以在 `_authorManager.CreateAsync` 插入新实体. 我们认为把它留给应用层是更好的设计, 因为应用层更了解应该何时插入实体到数据库(在插入实体前可能需要额外的工作. 如果在领域层插入, 可能需要额外的更新操作). 但是, 你拥有最终的决定权. - -### UpdateAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Edit)] -public async Task UpdateAsync(Guid id, UpdateAuthorDto input) -{ - var author = await _authorRepository.GetAsync(id); - - if (author.Name != input.Name) - { - await _authorManager.ChangeNameAsync(author, input.Name); - } - - author.BirthDate = input.BirthDate; - author.ShortBio = input.ShortBio; - - await _authorRepository.UpdateAsync(author); -} -```` - -* `UpdateAsync` 需要额外的 `BookStorePermissions.Authors.Edit` 权限. -* 使用 `IAuthorRepository.GetAsync` 从数据库中获得作者实体. 如果给定的id没有找到作者, `GetAsync` 抛出 `EntityNotFoundException`, 这在web应用程序中导致一个 `404` HTTP 状态码. 在更新操作中先获取实体再更新它, 是一个好的实践. -* 如果客户端请求, 使用 `AuthorManager.ChangeNameAsync` (领域服务方法) 修改作者姓名. -* 因为没有任何业务逻辑, 直接更新 `BirthDate` 和 `ShortBio`, 它们可以接受任何值. -* 最后, 调用 `IAuthorRepository.UpdateAsync` 更新实体到数据库. - -{{if DB == "EF"}} - -> **EF Core 提示**: Entity Framework Core 拥有 **change tracking** 系统并在unit of work 结束时 **自动保存** 任何修改到实体 (你可以简单地认为APB框架在方法结束时自动调用 `SaveChanges`). 所以, 即使你在方法结束时没有调用 `_authorRepository.UpdateAsync(...)` , 它依然可以工作. 如果你不考虑以后修改EF Core, 你可以移除这一行. - -{{end}} - -### DeleteAsync - -````csharp -[Authorize(BookStorePermissions.Authors.Delete)] -public async Task DeleteAsync(Guid id) -{ - await _authorRepository.DeleteAsync(id); -} -```` - -* `DeleteAsync` 需要额外的 `BookStorePermissions.Authors.Delete` 权限. -* 直接使用repository的 `DeleteAsync` 方法. - -## 权限定义 - -你还不能编译代码, 因为它需要 `BookStorePermissions` 类定义中一些常数. - -打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissions` 类 (在 `Permissions` 文件夹中), 修改为如下代码: - -````csharp -namespace Acme.BookStore.Permissions -{ - public static class BookStorePermissions - { - public const string GroupName = "BookStore"; - - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } - - // *** ADDED a NEW NESTED CLASS *** - public static class Authors - { - public const string Default = GroupName + ".Authors"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } - } -} -```` - -然后打开同一项目中的 `BookStorePermissionDefinitionProvider`, 在 `Define` 方法的结尾加入以下行: - -````csharp -var authorsPermission = bookStoreGroup.AddPermission( - BookStorePermissions.Authors.Default, L("Permission:Authors")); - -authorsPermission.AddChild( - BookStorePermissions.Authors.Create, L("Permission:Authors.Create")); - -authorsPermission.AddChild( - BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit")); - -authorsPermission.AddChild( - BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete")); -```` - -最后, 在 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` 加入以下项, 用以本地化权限名称: - -````csharp -"Permission:Authors": "Author Management", -"Permission:Authors.Create": "Creating new authors", -"Permission:Authors.Edit": "Editing the authors", -"Permission:Authors.Delete": "Deleting the authors" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -## 对象到对象映射 - -`AuthorAppService` 使用 `ObjectMapper` 将 `Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射. - -打开 `Acme.BookStore.Application` 项目中的 `BookStoreApplicationAutoMapperProfile` 类, 加入以下行到构造函数: - -````csharp -CreateMap(); -```` - -## 数据种子 - -如同图书管理部分所做的, 在数据库中生成一些初始作者实体. 不仅当第一次运行应用程序时是有用的, 对自动化测试也是很有用的. - -打开 `Acme.BookStore.Domain` 项目中的 `BookStoreDataSeederContributor`, 修改文件内容如下: - -````csharp -using System; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } - - // ADDED SEED DATA FOR AUTHORS - - if (await _authorRepository.GetCountAsync() <= 0) - { - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - } - } - } -} -```` - -{{if DB=="EF"}} - -你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. - -{{else if DB=="Mongo"}} - -你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. - -{{end}} - -## 测试作者应用服务 - -最后, 你可以为 `IAuthorAppService` 写一些测试. 在 `Acme.BookStore.Application.Tests` 项目的 `Authors` 命名空间(文件夹)中加入一个名为 `AuthorAppService_Tests` 新类: - -````csharp -using System; -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Modularity; -using Xunit; - -namespace Acme.BookStore.Authors; - -public abstract class AuthorAppService_Tests : BookStoreApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly IAuthorAppService _authorAppService; - - protected AuthorAppService_Tests() - { - _authorAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_All_Authors_Without_Any_Filter() - { - var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); - - result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldContain(author => author.Name == "Douglas Adams"); - } - - [Fact] - public async Task Should_Get_Filtered_Authors() - { - var result = await _authorAppService.GetListAsync( - new GetAuthorListDto {Filter = "George"}); - - result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); - } - - [Fact] - public async Task Should_Create_A_New_Author() - { - var authorDto = await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Edward Bellamy", - BirthDate = new DateTime(1850, 05, 22), - ShortBio = "Edward Bellamy was an American author..." - } - ); - - authorDto.Id.ShouldNotBe(Guid.Empty); - authorDto.Name.ShouldBe("Edward Bellamy"); - } - - [Fact] - public async Task Should_Not_Allow_To_Create_Duplicate_Author() - { - await Assert.ThrowsAsync(async () => - { - await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Douglas Adams", - BirthDate = DateTime.Now, - ShortBio = "..." - } - ); - }); - } - - //TODO: Test other methods... -} -```` - -{{if DB == "EF"}} -添加 `AuthorAppService_Tests` 的实现类,命名为 `EfCoreAuthorAppService_Tests` ,并放置在 `EntityFrameworkCore\Applications\Authors` 命名空间 (文件夹)下,在 `Acme.BookStore.EntityFrameworkCore.Tests` 项目中: - -````csharp -using Acme.BookStore.Authors; -using Xunit; - -namespace Acme.BookStore.EntityFrameworkCore.Applications.Authors; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class EfCoreAuthorAppService_Tests : AuthorAppService_Tests -{ - -} -```` -{{end}} - -{{if DB == "Mongo"}} -添加 `AuthorAppService_Tests` 的实现类,命名为 `MongoDBAuthorAppService_Tests` ,并放置在 `MongoDb\Applications\Authors` 命名空间 (文件夹)下,在 `Acme.BookStore.MongoDB.Tests` 项目中: - -````csharp -using Acme.BookStore.MongoDB; -using Acme.BookStore.Authors; -using Xunit; - -namespace Acme.BookStore.MongoDb.Applications.Authors; - -[Collection(BookStoreTestConsts.CollectionDefinitionName)] -public class MongoDBAuthorAppService_Tests : AuthorAppService_Tests -{ - -} -```` -{{end}} - -完成应用服务方法的测试, 它们应该很容易理解. - -## 下一章 - -查看本教程的[下一章](Part-9.md). diff --git a/docs/zh-Hans/Tutorials/Part-9.md b/docs/zh-Hans/Tutorials/Part-9.md deleted file mode 100644 index 41c0f87320..0000000000 --- a/docs/zh-Hans/Tutorials/Part-9.md +++ /dev/null @@ -1,1255 +0,0 @@ -# Web应用程序开发教程 - 第九章: 作者: 用户页面 -````json -//[doc-params] -{ - "UI": ["MVC","Blazor","BlazorServer","NG"], - "DB": ["EF","Mongo"] -} -```` -## 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: - -* **{{DB_Value}}** 做为ORM提供程序. -* **{{UI_Value}}** 做为UI框架. - -本教程分为以下部分: - -- [Part 1: 创建服务端](Part-1.md) -- [Part 2: 图书列表页面](Part-2.md) -- [Part 3: 创建,更新和删除图书](Part-2.md) -- [Part 4: 集成测试](Part-4.md) -- [Part 5: 授权](Part-5.md) -- [Part 6: 作者: 领域层](Part-6.md) -- [Part 7: 作者: 数据库集成](Part-7.md) -- [Part 8: 作者: 应用服务层](Part-8.md) -- **Part 9: 作者: 用户页面 (本章)** -- [Part 10: 图书到作者的关系](Part-10.md) - -## 下载源码 - -本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: - -* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) -* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) -* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - -> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` - -## 简介 - -这章阐述如何为前一章介绍的 `作者` 实体创建CRUD页面. - -{{if UI == "MVC"}} - -## 作者列表页面 - -在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个新的razor页面, `Index.cshtml`, 修改文件内容如下. - -### Index.cshtml - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Permissions -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.AspNetCore.Authorization -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L -@inject IAuthorizationService AuthorizationService -@model IndexModel - -@section scripts -{ - -} - - - - - - @L["Authors"] - - - @if (await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Create)) - { - - } - - - - - - - -```` - -这是一个简单的页面, 和我们以前创建的图书页面一样. 它导入了一个JavaScript文件, 我们后面会进行介绍这个文件. - -### IndexModel.cshtml.cs - -````csharp -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Acme.BookStore.Web.Pages.Authors -{ - public class IndexModel : PageModel - { - public void OnGet() - { - - } - } -} -```` - -### Index.js - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal'); - - var dataTable = $('#AuthorsTable').DataTable( - abp.libs.datatables.normalizeConfiguration({ - serverSide: true, - paging: true, - order: [[1, "asc"]], - searching: false, - scrollX: true, - ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList), - columnDefs: [ - { - title: l('Actions'), - rowAction: { - items: - [ - { - text: l('Edit'), - visible: - abp.auth.isGranted('BookStore.Authors.Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - visible: - abp.auth.isGranted('BookStore.Authors.Delete'), - confirmMessage: function (data) { - return l( - 'AuthorDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - acme.bookStore.authors.author - .delete(data.record.id) - .then(function() { - abp.notify.info( - l('SuccessfullyDeleted') - ); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { - title: l('Name'), - data: "name" - }, - { - title: l('BirthDate'), - data: "birthDate", - render: function (data) { - return luxon - .DateTime - .fromISO(data, { - locale: abp.localization.currentCulture.name - }).toLocaleString(); - } - } - ] - }) - ); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewAuthorButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -简单来说, 这个JavaScript页面: - -* 创建了一个具有 `操作`, `姓名` 和 `生日` 列的数据表格. - * `Actions` 列用来添加 *编辑* 和 *删除* 操作. - * `生日` 提供了一个 `render` 函数, 使用 [luxon](https://moment.github.io/luxon/) 库格式化 `DateTime` 值. -* 使用 `abp.ModalManager` 打开 *新建* 和 *编辑* 模态表单. - -这块代码与以前创建的图书页面非常相似, 所以我们不再赘述. - -### 本地化 - -这个页面使用了一些需要声明的本地化键. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -注意我们加入了额外的键. 它们会在下面的小节中被使用. - -### 加入主菜单 - -打开 `Acme.BookStore.Web` 项目的 `Menus` 文件夹中的 `BookStoreMenuContributor.cs` , 在 `ConfigureMainMenuAsync` 方法的结尾加入以下代码: - -````csharp -if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Authors", - l["Menu:Authors"], - url: "/Authors" - )); -} -```` - -### 运行应用程序 - -运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作: - -![bookstore-author-permissions](images/bookstore-author-permissions.png) - -如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项: - -![bookstore-authors-page](images/bookstore-authors-page.png) - -页面是完全可以工作的, 除了 *新建作者* 和 *操作/编辑*, 因为它们还没有实现 . - -> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限. - -## 新建模态窗口 - -在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个 razor 页面 `CreateModal.cshtml`, 修改它的内容如下: - -### CreateModal.cshtml - -```html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model CreateModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} -
    - - - - - - - - - -
    -``` - -之前我们已经使用ABP框架的 [动态表单](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md)开发了图书页面. 这里可以使用相同的方法, 但我们希望展示如何手工完成它. 实际上, 没有那么手工化, 因为在这个例子中我们使用了 `abp-input` 标签简化了表单元素的创建. - -你当然可以使用标准Bootstrap HTML结构, 但是这需要写很多代码. `abp-input` 自动添加验证, 本地化和根据数据类型生成标准元素. - -### CreateModal.cshtml.cs - -```csharp -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Authors -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateAuthorViewModel Author { get; set; } - - private readonly IAuthorAppService _authorAppService; - - public CreateModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } - - public void OnGet() - { - Author = new CreateAuthorViewModel(); - } - - public async Task OnPostAsync() - { - var dto = ObjectMapper.Map(Author); - await _authorAppService.CreateAsync(dto); - return NoContent(); - } - - public class CreateAuthorViewModel - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } - - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } - - [TextArea] - public string ShortBio { get; set; } - } - } -} -``` - -这个页面模型类注入和使用 `IAuthorAppService` 创建新作者. 它和图书创建模型类之间主要的区别是这个模型类为视图模型声明了一个新类 `CreateAuthorViewModel`, 而不是重用 `CreateAuthorDto`. - -这么做的主要原因是展示在页面中如何使用不同的模型. 但还有一个好处: 我们为类成员添加了两个不存在于 `CreateAuthorDto` 中的特性: - -* 为 `BirthDate` 添加 `[DataType(DataType.Date)]` 特性, 这会在UI为这个属性显示一个日期选择控件. -* 为 `ShortBio` 添加 `[TextArea]` 特性, 这会显示一个多行文本框, 而不是标准文本框. - -通过这种方式, 可以根据UI需求定制视图模型类, 而无需修改DTO. 这么做的一个结果是: 使用 `ObjectMapper` 将 `CreateAuthorViewModel` 映射到 `CreateAuthorDto`. 为了完成映射, 需要在 `BookStoreWebAutoMapperProfile` 构造函数中加入新的映射代码: - -````csharp -using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - - // ADD a NEW MAPPING - CreateMap(); - } - } -} -```` - -当你重新运行应用程序后, 点击"新建作者" 按钮会打开一个新的模态窗口. - -![bookstore-new-author-modal](images/bookstore-new-author-modal.png) - -## 编辑模态窗口 - -在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个 razor 页面 `EditModal.cshtml`, 修改它的内容如下: - -### EditModal.cshtml - -````html -@page -@using Acme.BookStore.Localization -@using Acme.BookStore.Web.Pages.Authors -@using Microsoft.Extensions.Localization -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@inject IStringLocalizer L -@{ - Layout = null; -} -
    - - - - - - - - - - -
    -```` - -### EditModal.cshtml.cs - -```csharp -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; - -namespace Acme.BookStore.Web.Pages.Authors -{ - public class EditModalModel : BookStorePageModel - { - [BindProperty] - public EditAuthorViewModel Author { get; set; } - - private readonly IAuthorAppService _authorAppService; - - public EditModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } - - public async Task OnGetAsync(Guid id) - { - var authorDto = await _authorAppService.GetAsync(id); - Author = ObjectMapper.Map(authorDto); - } - - public async Task OnPostAsync() - { - await _authorAppService.UpdateAsync( - Author.Id, - ObjectMapper.Map(Author) - ); - - return NoContent(); - } - - public class EditAuthorViewModel - { - [HiddenInput] - public Guid Id { get; set; } - - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } - - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } - - [TextArea] - public string ShortBio { get; set; } - } - } -} -``` - -这个类与 `CreateModal.cshtml.cs` 类似, 主要不同是: - -* 使用 `IAuthorAppService.GetAsync(...)` 方法从应用层获取正在编辑的作者. -* `EditAuthorViewModel` 拥有一个额外的 `Id` 属性, 它被 `[HiddenInput]` 特性标记, 会为这个属性在页面上创建一个隐藏输入框. - -这个类要求在 `BookStoreWebAutoMapperProfile` 类中添加两个对象映射声明: - -```csharp -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - - CreateMap(); - - // ADD THESE NEW MAPPINGS - CreateMap(); - CreateMap(); - } - } -} -``` - -这就是全部了! 你可以运行应用程序并尝试编辑一个作者. - -{{else if UI == "NG"}} - -## 作者管理页面 - -运行下面的命令行在angular应用程序的根目录创建一个名为 `AuthorModule` 新模块: - -```bash -yarn ng generate module author --module app --routing --route authors -``` - -这上命令应该产生以下输出: - -```bash -> yarn ng generate module author --module app --routing --route authors - -yarn run v1.19.1 -$ ng generate module author --module app --routing --route authors -CREATE src/app/author/author-routing.module.ts (344 bytes) -CREATE src/app/author/author.module.ts (349 bytes) -CREATE src/app/author/author.component.html (21 bytes) -CREATE src/app/author/author.component.spec.ts (628 bytes) -CREATE src/app/author/author.component.ts (276 bytes) -CREATE src/app/author/author.component.scss (0 bytes) -UPDATE src/app/app-routing.module.ts (1396 bytes) -Done in 2.22s. -``` - -### AuthorModule - -打开 `/src/app/author/author.module.ts,` 替换成以下内容: - -```js -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { AuthorRoutingModule } from './author-routing.module'; -import { AuthorComponent } from './author.component'; -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - declarations: [AuthorComponent], - imports: [SharedModule, AuthorRoutingModule, NgbDatepickerModule], -}) -export class AuthorModule {} -``` - -- 添加 `SharedModule`. `SharedModule` 导出一些创建用户页面需要的通用模块. -- `SharedModule` 已经导出 `CommonModule`, 所以移除 `CommonModule`. -- 添加 `NgbDatepickerModule`, 后面用于作者创建和编辑表单. - -### 菜单定义 - -打开 `src/app/route.provider.ts` 文件, 加入以下的菜单定义: - -````js -{ - path: '/authors', - name: '::Menu:Authors', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Authors', -} -```` - -最终的 `configureRoutes` 函数声明应该如下: - -```js -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/', - name: '::Menu:Home', - iconClass: 'fas fa-home', - order: 1, - layout: eLayoutType.application, - }, - { - path: '/book-store', - name: '::Menu:BookStore', - iconClass: 'fas fa-book', - order: 2, - layout: eLayoutType.application, - }, - { - path: '/books', - name: '::Menu:Books', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Books', - }, - { - path: '/authors', - name: '::Menu:Authors', - parentName: '::Menu:BookStore', - layout: eLayoutType.application, - requiredPolicy: 'BookStore.Authors', - }, - ]); - }; -} -``` - -### 生成服务代理 - -[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) 提供 `generate-proxy` 命令为HTTP APIs生成客户端代理, 使得使用HTTP APIs更容易. `generate-proxy` 命令运行前, 应用程序必须启动运行. - -在 `angular` 文件夹运行以下命令: - -```bash -abp generate-proxy -t ng -``` - -这个命令为作者服务和相关模型(DTO)类生成服务代理: - -![bookstore-angular-service-proxy-author](images/bookstore-angular-service-proxy-author-2.png) - -### AuthorComponent - -打开 `/src/app/author/author.component.ts` 文件, 替换成以下内容: - -```js -import { Component, OnInit } from '@angular/core'; -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { AuthorService, AuthorDto } from '@proxy/authors'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; - -@Component({ - selector: 'app-author', - templateUrl: './author.component.html', - styleUrls: ['./author.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class AuthorComponent implements OnInit { - author = { items: [], totalCount: 0 } as PagedResultDto; - - isModalOpen = false; - - form: FormGroup; - - selectedAuthor = {} as AuthorDto; - - constructor( - public readonly list: ListService, - private authorService: AuthorService, - private fb: FormBuilder, - private confirmation: ConfirmationService - ) {} - - ngOnInit(): void { - const authorStreamCreator = (query) => this.authorService.getList(query); - - this.list.hookToQuery(authorStreamCreator).subscribe((response) => { - this.author = response; - }); - } - - createAuthor() { - this.selectedAuthor = {} as AuthorDto; - this.buildForm(); - this.isModalOpen = true; - } - - editAuthor(id: string) { - this.authorService.get(id).subscribe((author) => { - this.selectedAuthor = author; - this.buildForm(); - this.isModalOpen = true; - }); - } - - buildForm() { - this.form = this.fb.group({ - name: [this.selectedAuthor.name || '', Validators.required], - birthDate: [ - this.selectedAuthor.birthDate ? new Date(this.selectedAuthor.birthDate) : null, - Validators.required, - ], - }); - } - - save() { - if (this.form.invalid) { - return; - } - - if (this.selectedAuthor.id) { - this.authorService - .update(this.selectedAuthor.id, this.form.value) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } else { - this.authorService.create(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } - } - - delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure') - .subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.authorService.delete(id).subscribe(() => this.list.get()); - } - }); - } -} -``` - -打开 `/src/app/author/author.component.html` 文件, 替换成以下内容: - -````html -
    -
    -
    -
    -
    - {%{{{ '::Menu:Authors' | abpLocalization }}}%} -
    -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    - -
    - - -
    -
    -
    -
    - - - - {%{{{ row.birthDate | date }}}%} - - -
    -
    -
    - - - -

    {%{{{ (selectedAuthor.id ? '::Edit' : '::NewAuthor') | abpLocalization }}}%}

    -
    - - -
    -
    - * - -
    - -
    - * - -
    -
    -
    - - - - - - -
    -```` - -### 本地化 - -这个页面使用了一些需要声明的本地化键. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -### 运行应用程序 - -运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作: - -![bookstore-author-permissions](images/bookstore-author-permissions.png) - -如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项: - -![bookstore-authors-page](images/bookstore-angular-authors-page.png) - -这就是全部了! 这是一个完整的, 可以工作的页面. 你可以新建, 编辑和删除作者. - -> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限. - -{{end}} - -{{if UI == "Blazor" || UI == "BlazorServer"}} - -## 作者管理页面 - -### 作者Razor组件 - -在 `Acme.BookStore.Blazor` 项目中新建一个 Razor组件页面 `/Pages/Authors.razor`, 替换成以下内容: - -````xml -@page "/authors" -@using Acme.BookStore.Authors -@using Acme.BookStore.Localization -@using Volo.Abp.AspNetCore.Components.Web -@inherits BookStoreComponentBase -@inject IAuthorAppService AuthorAppService -@inject AbpBlazorMessageLocalizerHelper LH - - - - -

    @L["Authors"]

    -
    - - - @if (CanCreateAuthor) - { - - } - - -
    -
    - - - - - - - - @L["Actions"] - - - @if (CanEditAuthor) - { - - @L["Edit"] - - } - @if (CanDeleteAuthor) - { - - @L["Delete"] - - } - - - - - - - - @context.BirthDate.ToShortDateString() - - - - - -
    - - - - -
    - - @L["NewAuthor"] - - - - - - - @L["Name"] - - - - - - - - - @L["BirthDate"] - - - - - @L["ShortBio"] - - - - - - - - - - - - - -
    -
    -
    - - - - -
    - - @EditingAuthor.Name - - - - - - - @L["Name"] - - - - - - - - - @L["BirthDate"] - - - - - @L["ShortBio"] - - - - - - - - - - - - - -
    -
    -
    -```` - -* 这些代码类似 `Books.razor`, 除了不继承自 `AbpCrudPageBase`, 它使用自己的实现. -* 注入 `IAuthorAppService` , 从UI使用服务器端的HTTP APIs . 我们可以直接注入应用服务接口并在 [动态 C# HTTP API 客户端代理系统](../API/Dynamic-CSharp-API-Clients.md)的帮助下像使用普通的方法一样使用它们, 动态 C# HTTP API 客户端代理系统会为我们调用REST API. 参考下面的 `Authors` 类获得使用方法. -* 注入 `IAuthorizationService` 检查 [权限](../Authorization.md). -* 注入 `IObjectMapper` 进行 [对象到对象映射](../Object-To-Object-Mapping.md). - -在 `Pages` 文件夹下新建一个代码后置文件 `Authors.razor.cs`, 使用以下代码: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Acme.BookStore.Authors; -using Acme.BookStore.Permissions; -using Blazorise; -using Blazorise.DataGrid; -using Microsoft.AspNetCore.Authorization; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore.Blazor.Pages -{ - public partial class Authors - { - private IReadOnlyList AuthorList { get; set; } - - private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; - private int CurrentPage { get; set; } - private string CurrentSorting { get; set; } - private int TotalCount { get; set; } - - private bool CanCreateAuthor { get; set; } - private bool CanEditAuthor { get; set; } - private bool CanDeleteAuthor { get; set; } - - private CreateAuthorDto NewAuthor { get; set; } - - private Guid EditingAuthorId { get; set; } - private UpdateAuthorDto EditingAuthor { get; set; } - - private Modal CreateAuthorModal { get; set; } - private Modal EditAuthorModal { get; set; } - - private Validations CreateValidationsRef; - - private Validations EditValidationsRef; - - public Authors() - { - NewAuthor = new CreateAuthorDto(); - EditingAuthor = new UpdateAuthorDto(); - } - - protected override async Task OnInitializedAsync() - { - await SetPermissionsAsync(); - await GetAuthorsAsync(); - } - - private async Task SetPermissionsAsync() - { - CanCreateAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Create); - - CanEditAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Edit); - - CanDeleteAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Delete); - } - - private async Task GetAuthorsAsync() - { - var result = await AuthorAppService.GetListAsync( - new GetAuthorListDto - { - MaxResultCount = PageSize, - SkipCount = CurrentPage * PageSize, - Sorting = CurrentSorting - } - ); - - AuthorList = result.Items; - TotalCount = (int)result.TotalCount; - } - - private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) - { - CurrentSorting = e.Columns - .Where(c => c.Direction != SortDirection.None) - .Select(c => c.Field + (c.Direction == SortDirection.Descending ? " DESC" : "")) - .JoinAsString(","); - CurrentPage = e.Page - 1; - - await GetAuthorsAsync(); - - await InvokeAsync(StateHasChanged); - } - - private void OpenCreateAuthorModal() - { - CreateValidationsRef.ClearAll(); - - NewAuthor = new CreateAuthorDto(); - CreateAuthorModal.Show(); - } - - private void CloseCreateAuthorModal() - { - CreateAuthorModal.Hide(); - } - - private void OpenEditAuthorModal(AuthorDto author) - { - EditValidationsRef.ClearAll(); - - EditingAuthorId = author.Id; - EditingAuthor = ObjectMapper.Map(author); - EditAuthorModal.Show(); - } - - private async Task DeleteAuthorAsync(AuthorDto author) - { - var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; - if (!await Message.Confirm(confirmMessage)) - { - return; - } - - await AuthorAppService.DeleteAsync(author.Id); - await GetAuthorsAsync(); - } - - private void CloseEditAuthorModal() - { - EditAuthorModal.Hide(); - } - - private async Task CreateAuthorAsync() - { - if (CreateValidationsRef.ValidateAll()) - { - await AuthorAppService.CreateAsync(NewAuthor); - await GetAuthorsAsync(); - CreateAuthorModal.Hide(); - } - } - - private async Task UpdateAuthorAsync() - { - if (EditValidationsRef.ValidateAll()) - { - await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); - await GetAuthorsAsync(); - EditAuthorModal.Hide(); - } - } - } -} -```` - -这个类定义了 `Authors.razor` 页面使用的属性和方法. - -### 对象映射 - -`Authors` 类使用 `OpenEditAuthorModal` 方法中的 `IObjectMapper`. 所以需要定义这个映射. - -打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreBlazorAutoMapperProfile.cs`, 在构造函数中加入一个映射: - -````csharp -CreateMap(); -```` - -你在文件开头需要 `using Acme.BookStore.Authors;` 声明语句. - -### 加入主菜单 - -打开 `Acme.BookStore.Blazor` 项目的 `Menus` 文件夹中的 `BookStoreMenuContributor.cs` , 在 `ConfigureMainMenuAsync` 方法的结尾加入以下代码: - -````csharp -if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Authors", - l["Menu:Authors"], - url: "/authors" - )); -} -```` - -### 本地化 - -我们需要本地化上面的代码. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目: - -````json -"Menu:Authors": "Authors", -"Authors": "Authors", -"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", -"BirthDate": "Birth date", -"NewAuthor": "New author" -```` - -> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文. - -### 运行应用程序 - -运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作: - -![bookstore-author-permissions](images/bookstore-author-permissions.png) - -如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项: - -![bookstore-authors-page](images/bookstore-authors-blazor-ui.png) - -这就是全部了! 这是一个完整的, 可以工作的页面. 你可以新建, 编辑和删除作者. - -> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限. - -{{end}} - -## 下一章 - -查看本教程的[下一章](Part-10.md). diff --git a/docs/zh-Hans/Tutorials/Todo/Index.md b/docs/zh-Hans/Tutorials/Todo/Index.md deleted file mode 100644 index 9e98eb0a0a..0000000000 --- a/docs/zh-Hans/Tutorials/Todo/Index.md +++ /dev/null @@ -1,830 +0,0 @@ -# 快速入门 - -````json -//[doc-params] -{ - "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] -} -```` - -这是一个由单个部分组成的快速入门教程, 旨在使用ABP框架构建一个简单的待办事项应用程序. 这是最终应用程序的屏幕截图: - -![待办事项列表](todo-list.png) - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)找到已完成的项目源代码. - -## 先决条件 - -* 一个集成开发环境 (比如: [Visual Studio](https://visualstudio.microsoft.com/vs/)) 它需要支持 [.NET 5.0+](https://dotnet.microsoft.com/download/dotnet) 的开发. - -{{if DB=="Mongo"}} - -* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) - -{{end}} - -{{if UI=="NG"}} - -* [Node v16.x](https://nodejs.org/) - -{{end}} - -## 创建新的解决方案 - -我们将使用[ABP CLI](../../CLI.md) 创建带有ABP框架的新解决方案. 你可以在命令行终端中运行以下命令来安装它: - -````bash -dotnet tool install -g Volo.Abp.Cli -```` - -然后创建一个空文件夹, 打开命令行终端并在终端中执行以下命令: - -````bash -abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} -```` - -{{if UI=="NG"}} - -这将创建一个名为*TodoApp*的新解决方案, 其中包含`angular`和`aspnet core`文件夹. 一旦解决方案就绪, 请在你最喜欢的 IDE 中打开 ASP.NET Core 解决方案. - -{{else}} - -这将创建一个名为*TodoApp*的新解决方案. 一旦解决方案就绪, 请在你最喜欢的 IDE 中打开它. - -{{end}} - -### 创建数据库 - -如果你使用的是Visual Studio, 请右键单击`TodoApp.DbMigrator`项目, 选择*设置为启动项目*, 然后按*Ctrl+F5*运行它而不进行调试. 它将创建初始数据库并生成初始数据. - -{{if DB=="EF"}} - -> 一些IDE (例如Rider) 可能会在第一次运行时遇到问题, 因为 *DbMigrator* 添加初始迁移并重新编译项目. 在这种情况下, 在 `.DbMigrator` 项目文件夹中打开命令行终端并执行 `dotnet run` 命令. - -{{end}} - -### 运行应用程序 - -{{if UI=="MVC" || UI=="BlazorServer"}} - -最好在开始开发之前运行一下应用程序. 确保 {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} 是启动项目, 然后运行应用程序(Visual Studio中是Ctrl+F5)来查看初始UI: - -{{else if UI=="Blazor"}} - -最好在开始开发之前运行一下应用程序. 解决方案中有两个主要应用程序 - -* `TodoApp.HttpApi.Host` 承载服务器端的 HTTP API. -* `TodoApp.Blazor` 是客户端的Blazor WebAssembly应用程序. - -确保 `TodoApp.HttpApi.Host` 是启动项目, 然后运行应用程序(Visual Studio中的Ctrl+F5)打开[Swagger UI](https://swagger.io/tools/swagger-ui/)来查看服务器端的 HTTP API: - -![todo-swagger-ui-initial](todo-swagger-ui-initial.png) - -你可以使用此 UI 探索和测试你的 HTTP API. 现在, 我们可以将 `TodoApp.Blazor` 设置为启动项目并运行它来打开实际的 Blazor 应用程序UI: - -{{else if UI=="NG"}} - -最好在开始开发之前运行一下应用程序. 解决方案中有两个主要应用程序; - -* `TodoApp.HttpApi.Host` (在.NET解决方案中)承载服务器端HTTP API. -* `Angular` 文件夹包含 Angular 应用程序. - -确保 `TodoApp.HttpApi.Host` 是启动项目, 然后运行应用程序(Visual Studio中的Ctrl+F5)打开[Swagger UI](https://swagger.io/tools/swagger-ui/)来查看服务器端的 HTTP API: - -![todo-swagger-ui-initial](todo-swagger-ui-initial.png) - -你可以使用此 UI 探索和测试你的 HTTP API. 如果该功能正常, 我们可以运行Angular 客户端应用程序. - -首先, 运行以下命令来还原NPM包: - -````bash -npm install -```` - -安装所有软件包需要一些时间. 然后可以使用以下命令运行应用程序: - -````bash -npm start -```` - -此命令需要时间, 但最终在你的默认浏览器中运行并打开应用程序: - -{{end}} - -![todo-ui-initial](todo-ui-initial.png) - -你可以单击 *登录* 按钮, 以`admin`作为用户名和`1q2w3E*` 作为密码登录到应用程序. - -一切就绪. 我们可以开始编程! - -## 领域层 - -此应用程序只有一个 [实体](../../Entities.md), 接下来我们开始创建它. 在 *TodoApp.Domain* 项目中创建一个新的 `TodoItem` 类: - -````csharp -using System; -using Volo.Abp.Domain.Entities; - -namespace TodoApp -{ - public class TodoItem : BasicAggregateRoot - { - public string Text { get; set; } - } -} -```` - -`BasicAggregateRoot` 是创建根实体的最简单的基础类. `Guid` 是这里实体的主键 (`Id`). - -## 数据库集成 - -{{if DB=="EF"}} - -下一步是设置 [Entity Framework Core](../../Entity-Framework-Core.md)配置 - -### 映射配置 - -打开在 *TodoApp.EntityFrameworkCore* 项目中 `EntityFrameworkCore` 文件夹中的 `TodoAppDbContext` 类, 并向该类添加新的 `DbSet` 属性: - -````csharp -public DbSet TodoItems { get; set; } -```` - -然后在 `TodoAppDbContext` 类中定位到 `OnModelCreating` 方法, 并为 `TodoItem` 实体添加映射代码: - -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - /* Include modules to your migration db context */ - - builder.ConfigurePermissionManagement(); - ... - - /* Configure your own tables/entities inside here */ - builder.Entity(b => - { - b.ToTable("TodoItems"); - }); -} -```` - -我们已经将 `TodoItem` 实体映射到数据库中的 `TodoItems` 表. - -### Code First 迁移 - -解决方案快速模版已经配置为使用Entity Framework Core的 [Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). 由于我们已经更改了数据库映射配置, 因此我们应该创建一个新的迁移并将更改应用于数据库. - -在 *TodoApp.EntityFrameworkCore* 项目目录中打开一个命令行终端并输入以下命令: - -````bash -dotnet ef migrations add Added_TodoItem -```` - -这将向项目添加一个新的迁移类: - -![todo-efcore-migration](todo-efcore-migration.png) - -你可以在同一命令行终端中使用以下命令将更改应用于数据库: - -````bash -dotnet ef database update -```` - -> 如果你使用的是Visual Studio, 则可能希望在 *包管理器控制台 (PMC)* 中使用 `Add-Migration Added_TodoItem` 和 `Update-Database` 命令. 在这种情况下, 请确保 {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} 是启动项目, 并且 `TodoApp.EntityFrameworkCore` 是PMC中的 *默认项目*. - -{{else if DB=="Mongo"}} - -下一步是设置 [MongoDB](../../MongoDB.md) 配置. 打开 *TodoApp.MongoDB* 项目中 `MongoDb` 文件夹的`TodoAppMongoDbContext` 类并做出以下更改; - -1. 向类添加新属性: - -````csharp -public IMongoCollection TodoItems => Collection(); -```` - -2. 在 `CreateModel` 方法中添加以下代码: - -````csharp -modelBuilder.Entity(b => -{ - b.CollectionName = "TodoItems"; -}); -```` - -{{end}} - -现在, 我们可以使用ABP仓库保存和检索待办事项, 如我们在下一节所做的那样. - -## 应用层 - -[应用程序服务](../../Application-Services.md) 用于执行应用程序的用例. 我们需要执行以下用例: - -* 获取待办事项列表 -* 创建新的待办事项 -* 删除现有的待办事项 - -### 应用服务接口 - -我们可以先从为应用程序服务定义接口开始. 在 *TodoApp.Application.Contracts* 项目中创建新的 `ITodoAppService` 接口, 如下所示: - -````csharp -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace TodoApp -{ - public interface ITodoAppService : IApplicationService - { - Task> GetListAsync(); - Task CreateAsync(string text); - Task DeleteAsync(Guid id); - } -} -```` - -### 数据传输对象 - -`GetListAsync` 和`CreateAsync` 方法返回 `TodoItemDto`. `应用程序服务` 通常获取并返回 DTO([数据传输对象](../../Data-Transfer-Objects.md)) 而不是实体. 因此, 我们应该在这里定义DTO类. 在 *TodoApp.Application.Contracts* 项目中创建新的 `TodoItemDto` 类: - -````csharp -using System; - -namespace TodoApp -{ - public class TodoItemDto - { - public Guid Id { get; set; } - public string Text { get; set; } - } -} -```` - -这是一个非常简单的DTO类, 它与我们的 `TodoItem` 实体相对应. 接下来, 我们准备实现 `ITodoAppService`接口. - -### 应用服务实现 - -在 *TodoApp.Application* 项目中创建 `TodoAppService` 类, 如下所示: - -````csharp -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace TodoApp -{ - public class TodoAppService : ApplicationService, ITodoAppService - { - private readonly IRepository _todoItemRepository; - - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... - } -} -```` - -该类继承自ABP框架的`ApplicationService`类, 并实现了之前定义的 `ITodoAppService`接口. ABP为实体提供默认的泛型 [仓储](../../Repositories.md). 我们可以使用它们来执行基本的数据库操作. 此类中 [注入](../../Dependency-Injection.md) 的 `IRepository`, 它就是 `TodoItem` 实体的默认存储库. 我们将使用它来实现之前描述的用例. - -#### 获取待办事项 - -让我们先实现 `GetListAsync` 方法: - -````csharp -public async Task> GetListAsync() -{ - var items = await _todoItemRepository.GetListAsync(); - return items - .Select(item => new TodoItemDto - { - Id = item.Id, - Text = item.Text - }).ToList(); -} -```` - -我们只是从数据库中获取完整的`TodoItem`列表, 将它们映射到`TodoItemDto`对象并作为结果返回. - -#### 创建一个新的待办事项 - -下一个我们可以实现方法是 `CreateAsync` , 如下所示: - -````csharp -public async Task CreateAsync(string text) -{ - var todoItem = await _todoItemRepository.InsertAsync( - new TodoItem {Text = text} - ); - - return new TodoItemDto - { - Id = todoItem.Id, - Text = todoItem.Text - }; -} -```` - -仓储的 `InsertAsync` 方法将给定的`TodoItem`插入数据库, 并返回相同的`TodoItem` 对象. 它还设置了`Id`, 因此我们可以在返回对象上使用它. 我们只是通过从新的 `TodoItem` 实体创建和返回 `TodoItemDto`. - -#### 删除待办事项 - -最后, 我们来实现 `DeleteAsync` 方法, 代码如下: - -````csharp -public async Task DeleteAsync(Guid id) -{ - await _todoItemRepository.DeleteAsync(id); -} -```` - -至此, 应用程序服务已准备好了让UI层来使用. - -## 用户界面层 - -现在是在UI上显示待办事项的时候了! 在开始编写代码之前, 最好记住我们正在尝试构建的内容. 这里是示例程序的最终用户界面的截图: - -![待办事项列表](todo-list.png) - -> **我们将在本教程中保持最简洁的UI端, 以使本教程简单且重点突出. 请参阅 [web应用程序开发教程](../Part-1.md) 来了解构建实际应用各个方面.** - -{{if UI=="MVC"}} - -### Index.cshtml.cs - -打开在 *TodoApp.Web* 项目的 `Pages` 文件夹中`Index.cshtml.cs`文件, 并用以下代码块替换它的默认内容: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TodoApp.Web.Pages -{ - public class IndexModel : TodoAppPageModel - { - public List TodoItems { get; set; } - - private readonly ITodoAppService _todoAppService; - - public IndexModel(ITodoAppService todoAppService) - { - _todoAppService = todoAppService; - } - - public async Task OnGetAsync() - { - TodoItems = await _todoAppService.GetListAsync(); - } - } -} -```` - -此类使用 `ITodoAppService` 获取待办事项列表并将它赋值给 `TodoItems` 属性. 我们将用它来渲染razor页面上的待办事项目列表. - -### Index.cshtml - -打开 *TodoApp.Web* 项目中`Pages`文件夹下的`Index.cshtml`, 并替换为以下内容: - -````xml -@page -@model TodoApp.Web.Pages.IndexModel -@section styles { - -} -@section scripts { - -} -
    - - - - TODO LIST - - - - -
    - - -
    - - -
      - @foreach (var todoItem in Model.TodoItems) - { -
    • - @todoItem.Text -
    • - } -
    -
    -
    -
    -```` - -我们使用 ABP 的 [卡片标签助手](../../UI/AspNetCore/Tag-Helpers/Cards.md) 来创建一个简单的卡片视图. 你可以直接使用标准引导HTML结构, 但ABP [标签助手](../../UI/AspNetCore/Tag-Helpers/Index.md) 使它更容易并且更安全. - -此页面导入一个 CSS 和 JavaScript 文件, 所以我们需要创建它们. - -### Index.js - -打开 *TodoApp.Web* 项目中`Pages`文件夹下的`Index.js`, 并替换为以下内容: - -````js -$(function () { - - // DELETING ITEMS ///////////////////////////////////////// - $('#TodoList').on('click', 'li i', function(){ - var $li = $(this).parent(); - var id = $li.attr('data-id'); - - todoApp.todo.delete(id).then(function(){ - $li.remove(); - abp.notify.info('Deleted the todo item.'); - }); - }); - - // CREATING NEW ITEMS ///////////////////////////////////// - $('#NewItemForm').submit(function(e){ - e.preventDefault(); - - var todoText = $('#NewItemText').val(); - todoApp.todo.create(todoText).then(function(result){ - $('
  • ') - .html(' ' + result.text) - .appendTo($('#TodoList')); - $('#NewItemText').val(''); - }); - }); -}); -```` - -在第一部分中, 我们注册了在待办事项旁边的删除图标的点击事件, 来删除服务器上的相关项目并在UI上显示通知. 此外, 我们会从 DOM 中移除已删除的项目, 所以我们不需要刷新页面. - -在第二部分中, 我们在服务器上创建一个新的待办事项. 如果成功, 我们将操纵DOM来将新的 `
  • ` 元素插入到待办事项列表. 这样我们不需要在创建新的待办事项后刷新整个页面. - -这里有趣的部分是我们如何与服务器通信. 请参阅 *动态JavaScript代理和自动API控制器* 部分来了解其工作原理. 但是现在让我们继续并完成这个应用程序. - -### Index.css - -最后, 请打开 *TodoApp.Web* 项目的 `Pages` 文件夹中的 `Index.css` 文件, 并替换为以下内容: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -这是待办事项页面的简单样式. 我们相信你可以做得更好 :) - -现在, 你可以再次运行应用程序并看到结果. - -### 动态JavaScript代理和自动API控制器 - -在 `Index.js` 文件中, 我们使用了 `todoApp.todo.delete(...)` 和 `todoApp.todo.create(...)` 方法来与服务器通信. 这些函数是由ABP框架动态创建的, 这要归功于 [动态JavaScript客户端代理](../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) 系统. 它们执行 HTTP API 到服务器调用并返回一个Promise对象, 这样你就可以使用 `then` 函数注册一个回调, 像之前所做的那样. - -但是, 你可能会注意到我们还没有创建任何API控制器, 所以服务器如何处理这些请求? 这个问题为我们引出了ABP 框架的 [自动API控制器](../../API/Auto-API-Controllers.md) 功能. 它通过约定自动将应用程序服务转换为 API 控制器. - -如果你在应用程序URL中输入 `/swagger` 来打开 [Swagger UI](https://swagger.io/tools/swagger-ui/) , 就会看到Todo API: - -![todo-api](todo-api.png) - -{{else if UI=="Blazor" || UI=="BlazorServer"}} - -### Index.razor.cs - -打开在*TodoApp.Blazor*项目的`Pages`文件夹中`Index.razor.cs`文件, 并替换为一下内容: - -````csharp -using Microsoft.AspNetCore.Components; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace TodoApp.Blazor.Pages -{ - public partial class Index - { - [Inject] - private ITodoAppService TodoAppService { get; set; } - - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } - - protected async override Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } - - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } - } -} -```` - -此类使用 `ITodoAppService` 来执行待办事项的操作. 它在创建和删除操作后操纵 `TodoItems` 列表. 这样, 我们不需要从服务器刷新整个待办事项列表. - -{{if UI=="Blazor"}} - -请参阅下面的 *动态C#代理和自动API控制器* 部分, 来了解我们如何在浏览器上运行的 Blazor 应用程序中使用注入和应用服务接口的! 但是现在让我们继续并完成这个应用程序. - -{{end # Blazor}} - -### Index.razor - -打开在 *TodoApp.Blazor* 项目的`Pages`文件夹中 `Index.razor`文件, 并替换为以下代码块内容: - -````xml -@page "/" -@inherits TodoAppComponentBase -
    - - - - TODO LIST - - - - -
    - - -
    - - -
      - @foreach (var todoItem in TodoItems) - { -
    • - @todoItem.Text -
    • - } -
    -
    -
    -
    -```` - -### Index.razor.css - -最后, 请打开 *TodoApp.Blazor* 项目的 `Pages` 文件夹中的 `Index.razor.css`文件, 并替换为以下内容: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -这是待办事项页面的简单样式. 我们相信你可以做得更好 :) - -现在, 你可以再次运行应用程序来查看结果. - -{{if UI=="Blazor"}} - -### 动态C#代理和自动API控制器 - -在 `Index.razor.cs` 文件中, 我们已经注入(使用 `[Inject]` 特性)并像使用本地服务一样使用 `ITodoAppService`. 请记住, Blazor应用程序是在浏览器上运行, 但此应用程序服务的实现却是在服务器上运行的. - -这个神奇的过程是由ABP框架的 [动态C#客户端代理](../../API/Dynamic-CSharp-API-Clients.md) 系统完成. 它使用标准的`HttpClient` 并执行HTTP API到远程服务器请求. 它还为我们处理所有标准任务, 包括授权、JSON序列化和异常处理. - -但是, 你可能会问我们还没有创建任何API控制器, 那么服务器如何处理这些请求呢? 这个问题为我们引出了ABP 框架的 [自动API控制器](../../API/Auto-API-Controllers.md) 功能. 它通过约定自动将应用程序服务转换为 API 控制器. - -如果你运行 `TodoApp.HttpApi.Host` 应用程序, 你可以看到Todo API: - -![todo-api](todo-api.png) - -{{end # Blazor}} - -{{else if UI=="NG"}} - -### 服务代理生成 - -ABP提供了一个便捷的功能来自动创建客户端服务, 以方便地使用由服务器提供的HTTP API. - -你首先需要运行 `TodoApp.HttpApi.Host` 项目, 因为代理生成器从服务器应用程序读取API定义. - -> **请注意**: IIS Express有一个问题: 它不允许从另一个进程连接到应用程序. 如果你使用的是Visual Studio, 请在 “运行” 按钮下拉列表中选择`TodoApp.HttpApi.Host` 来替代IIS Express, 如下图所示: - -![运行时不使用 iisexpress](run-without-iisexpress.png) - -当启动 `TodoApp.HttpApi.Host` 项目后, 在`angular`文件夹中打开一个命令行终端并输入以下命令: - -````bash -abp generate-proxy -t ng -```` - -如果一切顺利, 它应该生成如下输出: - -````bash -CREATE src/app/proxy/generate-proxy.json (170978 bytes) -CREATE src/app/proxy/README.md (1000 bytes) -CREATE src/app/proxy/todo.service.ts (794 bytes) -CREATE src/app/proxy/models.ts (66 bytes) -CREATE src/app/proxy/index.ts (58 bytes) -```` - -然后, 我们可以使用 `todoService` 来使用服务器端HTTP API, 就像我们将在下一节中做的那样. - -### home.component.ts - -打开 `/angular/src/app/home/home.component.ts` 文件, 并用以下代码块替换其内容: - -````js -import { ToasterService } from '@abp/ng.theme.shared'; -import { Component, OnInit } from '@angular/core'; -import { TodoItemDto, TodoService } from '@proxy'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'] -}) -export class HomeComponent implements OnInit { - - todoItems: TodoItemDto[]; - newTodoText: string; - - constructor( - private todoService: TodoService, - private toasterService: ToasterService) - { } - - ngOnInit(): void { - this.todoService.getList().subscribe(response => { - this.todoItems = response; - }); - } - - create(): void{ - this.todoService.create(this.newTodoText).subscribe((result) => { - this.todoItems = this.todoItems.concat(result); - this.newTodoText = null; - }); - } - - delete(id: string): void { - this.todoService.delete(id).subscribe(() => { - this.todoItems = this.todoItems.filter(item => item.id !== id); - this.toasterService.info('Deleted the todo item.'); - }); - } -} - -```` - -我们已经实现了使用 `todoService` 来获取待办事项目列表, 并将返回值赋值给 `todoItems` 数组. 我们还添加了 `create` 和 `delete` 方法. 这些方法将在视图端使用. - -### home.component.html - -打开 `/angular/src/app/home/home.component.html` 文件, 并用以下代码块替换其内容: - -````html -
    -
    -
    -
    TODO LIST
    -
    -
    - -
    - - -
    - - -
      -
    • - {%{{{ todoItem.text }}}%} -
    • -
    -
    -
    -
    -```` - -### home.component.scss - -最后, 打开`/angular/src/app/home/home.component.scss` 文件, 并用以下代码块替换其内容: - -````css -#TodoList{ - list-style: none; - margin: 0; - padding: 0; -} - -#TodoList li { - padding: 5px; - margin: 5px 0px; - border: 1px solid #cccccc; - background-color: #f5f5f5; -} - -#TodoList li i -{ - opacity: 0.5; -} - -#TodoList li i:hover -{ - opacity: 1; - color: #ff0000; - cursor: pointer; -} -```` - -这是待办事项页面的简单样式. 我们相信你可以做得更好 :) - -现在, 你可以再次运行应用程序来查看结果. - -{{end}} - -## 总结 - -在这个教程中, 我们已经建立了一个非常简单的应用程序来熟悉 ABP 框架. 如果你想要构建一个实际场景的应用程序, 请查看 [web 应用程序开发教程](../Part-1.md), 该教程涵盖了真实的 Web 应用程序开发的所有方面. - -## 源代码 - -你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)找到已完成的项目源代码. - -## 另请参见 - -* [Web应用程序开发教程](../Part-1.md) diff --git a/docs/zh-Hans/Tutorials/Todo/run-without-iisexpress.png b/docs/zh-Hans/Tutorials/Todo/run-without-iisexpress.png deleted file mode 100644 index 7713daf004..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/run-without-iisexpress.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Todo/todo-api.png b/docs/zh-Hans/Tutorials/Todo/todo-api.png deleted file mode 100644 index e698356e40..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/todo-api.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Todo/todo-efcore-migration.png b/docs/zh-Hans/Tutorials/Todo/todo-efcore-migration.png deleted file mode 100644 index 8c98351e96..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/todo-efcore-migration.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Todo/todo-list.png b/docs/zh-Hans/Tutorials/Todo/todo-list.png deleted file mode 100644 index d5e2d70535..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/todo-list.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Todo/todo-swagger-ui-initial.png b/docs/zh-Hans/Tutorials/Todo/todo-swagger-ui-initial.png deleted file mode 100644 index c0b4ae3596..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/todo-swagger-ui-initial.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/Todo/todo-ui-initial.png b/docs/zh-Hans/Tutorials/Todo/todo-ui-initial.png deleted file mode 100644 index 7d232b7eb6..0000000000 Binary files a/docs/zh-Hans/Tutorials/Todo/todo-ui-initial.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-add-book-button.png b/docs/zh-Hans/Tutorials/images/blazor-add-book-button.png deleted file mode 100644 index a28856cd91..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-add-book-button.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-add-books-component.png b/docs/zh-Hans/Tutorials/images/blazor-add-books-component.png deleted file mode 100644 index ccb414ec4f..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-add-books-component.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list-with-authors.png b/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list-with-authors.png deleted file mode 100644 index e9eb55dc6f..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list-with-authors.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png deleted file mode 100644 index 61cf0ebf66..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png b/docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png deleted file mode 100644 index 258bd29d1d..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png b/docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png deleted file mode 100644 index 3c3fed77ff..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png b/docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png deleted file mode 100644 index 679ce2bdc7..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png b/docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png deleted file mode 100644 index 097175dc64..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png b/docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png deleted file mode 100644 index 08942800dc..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/book-create-modal-with-author.png b/docs/zh-Hans/Tutorials/images/book-create-modal-with-author.png deleted file mode 100644 index 0ecaac08f4..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/book-create-modal-with-author.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png deleted file mode 100644 index 8f495e0cc3..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png deleted file mode 100644 index fd06f3e4e5..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png deleted file mode 100644 index adfc036d0b..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png deleted file mode 100644 index a4760261c6..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png b/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png deleted file mode 100644 index 2c16034ec4..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png deleted file mode 100644 index e033826c2a..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png deleted file mode 100644 index 38dfd300a7..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png b/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png deleted file mode 100644 index ea2aba892c..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-author-selection.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-author-selection.png deleted file mode 100644 index 8518ba521a..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-author-selection.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png deleted file mode 100644 index 3d965f2179..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png deleted file mode 100644 index b311cf50c6..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png deleted file mode 100644 index 5d89973669..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png b/docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png deleted file mode 100644 index 142ef57e22..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png b/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png deleted file mode 100644 index 2256971559..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png b/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png deleted file mode 100644 index bfb4c208ef..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png b/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png deleted file mode 100644 index 477437b525..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png b/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png deleted file mode 100644 index e4b53d6298..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png deleted file mode 100644 index 8305a6caf4..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png deleted file mode 100644 index 2022efbb35..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png deleted file mode 100644 index f30c4eec72..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png deleted file mode 100644 index 431fb2defc..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-books-table.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table.png deleted file mode 100644 index 7254a97566..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-books-table.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-books-with-authorname-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-books-with-authorname-angular.png deleted file mode 100644 index d388a49cd0..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-books-with-authorname-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png deleted file mode 100644 index 86b908e14a..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png deleted file mode 100644 index eb84d88065..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png deleted file mode 100644 index f09f2f394f..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png deleted file mode 100644 index c778baa656..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png deleted file mode 100644 index d7b2a9abec..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png deleted file mode 100644 index 9d168b3c55..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png deleted file mode 100644 index 65fd9da213..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index 3aa5e02a09..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png deleted file mode 100644 index 4b99c50713..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png deleted file mode 100644 index 88e1acc758..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png b/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png deleted file mode 100644 index cfc74b1f0a..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png deleted file mode 100644 index df5b8128f8..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png deleted file mode 100644 index b6083364f7..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png deleted file mode 100644 index 923acba533..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png b/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png deleted file mode 100644 index 60b458ae5b..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png b/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png deleted file mode 100644 index d5f057d66e..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png deleted file mode 100644 index 6168a91fdf..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png deleted file mode 100644 index 94d4e20aef..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png b/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png deleted file mode 100644 index 1a89bfbaeb..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png b/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png deleted file mode 100644 index ef9977e00c..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-homepage.png b/docs/zh-Hans/Tutorials/images/bookstore-homepage.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-homepage.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png deleted file mode 100644 index 2db5ab1a5e..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png deleted file mode 100644 index a3b34c5161..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png deleted file mode 100644 index b7082da979..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png deleted file mode 100644 index a07f488012..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png b/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png deleted file mode 100644 index 141da741dd..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png deleted file mode 100644 index 34f5819fe7..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-menu-items.png b/docs/zh-Hans/Tutorials/images/bookstore-menu-items.png deleted file mode 100644 index ef3c404855..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-menu-items.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png deleted file mode 100644 index 8c14e6e6f5..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png deleted file mode 100644 index 8369cfef8e..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png deleted file mode 100644 index 98e939a264..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png deleted file mode 100644 index 1e49299f1d..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png deleted file mode 100644 index 4a3eac313b..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png deleted file mode 100644 index b4d36d6075..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png deleted file mode 100644 index 9c06825eea..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png deleted file mode 100644 index aecc1d4a1a..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png b/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png deleted file mode 100644 index d77d1938db..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png b/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png deleted file mode 100644 index 7b1cccb748..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png b/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png deleted file mode 100644 index d2259fdedd..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png deleted file mode 100644 index b1bdadbc9c..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png deleted file mode 100644 index cb3b6440c7..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png b/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png deleted file mode 100644 index 7567f0d95f..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png deleted file mode 100644 index 88dcacffed..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png deleted file mode 100644 index c3c9a25beb..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png deleted file mode 100644 index d8e64184b3..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png deleted file mode 100644 index b0e8fac507..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger.png deleted file mode 100644 index 423142e15d..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-swagger.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png deleted file mode 100644 index ffa63dc581..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png deleted file mode 100644 index 7fe3cead35..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png deleted file mode 100644 index 51eb6455de..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png deleted file mode 100644 index 45d08ecea3..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png deleted file mode 100644 index 45d08ecea3..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png deleted file mode 100644 index 81cb3f1440..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-user-management.png b/docs/zh-Hans/Tutorials/images/bookstore-user-management.png deleted file mode 100644 index d7d3429826..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-user-management.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png deleted file mode 100644 index ce821eba72..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/generate-proxy-command.png b/docs/zh-Hans/Tutorials/images/generate-proxy-command.png deleted file mode 100644 index d7f5706d78..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/generate-proxy-command.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies-2.png b/docs/zh-Hans/Tutorials/images/generated-proxies-2.png deleted file mode 100644 index f13aff1ac6..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/generated-proxies-2.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies-3.png b/docs/zh-Hans/Tutorials/images/generated-proxies-3.png deleted file mode 100644 index 8af6942e58..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/generated-proxies-3.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies.png b/docs/zh-Hans/Tutorials/images/generated-proxies.png deleted file mode 100644 index 669e095abb..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/generated-proxies.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png deleted file mode 100644 index 4f411dd748..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png and /dev/null differ diff --git a/docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png b/docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png deleted file mode 100644 index e837ed6ffc..0000000000 Binary files a/docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png and /dev/null differ diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md deleted file mode 100644 index bb54bb6497..0000000000 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ /dev/null @@ -1,546 +0,0 @@ -## 替换组件 - -你可以将一些ABP的组件替换为你自己的自定义组件. - -你可以**替换**但**不能自定义**默认ABP组件的原因是禁用或更改该组件的一部分可能会导致问题. 所以我们把这些组件称为可替换组件. - -### 如何替换组件 - -创建一个你想要使用的新组件,添加到 `AppModule` 中的 `declarations` 和`entryComponents` 中. - -然后打开 `app.component.ts` 使用 `AddReplaceableComponent` 将你的组件替换ABP组件. 如下所示: - -```js -import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action -import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum -import { Store } from '@ngxs/store'; // imported Store -//... - -@Component(/* component metadata */) -export class AppComponent { - constructor( - private store: Store // injected Store - ) - { - // dispatched the AddReplaceableComponent action - this.store.dispatch( - new AddReplaceableComponent({ - component: YourNewRoleComponent, - key: eIdentityComponents.Roles, - }), - ); - } -} -``` - -![Example Usage](./images/component-replacement.gif) - -### 如何替换布局 - -每个ABP主题模块有3个布局,分别是`ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. 这些布局可以用相同的方式替换. - -> 一个布局组件模板应该包含 `` 元素. - -下面的例子解释了如何更换 `ApplicationLayoutComponent`: - -运行以下命令在 `angular` 文件夹中生成布局: - -```bash -yarn ng generate component my-application-layout -``` - -在你的布局模板(`my-application-layout.component.html`)中添加以下代码: - -```html - -``` - -打开 `src/app` 文件夹下的 `app.component.ts` 文件添加以下内容: - -```js -import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys -import { Store } from '@ngxs/store'; // imported Store -import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent - -@Component(/* component metadata */) -export class AppComponent { - constructor( - private store: Store, // injected Store - ) { - // dispatched the AddReplaceableComponent action - this.store.dispatch( - new AddReplaceableComponent({ - component: MyApplicationLayoutComponent, - key: eThemeBasicComponents.ApplicationLayout, - }), - ); - } -} -``` - -### 布局组件 - -![Layout Components](./images/layout-components.png) - -#### 如何替换LogoComponent - -![LogoComponent](./images/logo-component.png) - -在 `angular` 目录下运行以下命令创建新的组件 `LogoComponent`: - -```bash -yarn ng generate component logo --inlineTemplate --inlineStyle --entryComponent - -# You don't need the --entryComponent option in Angular 9 -``` - -打开 `src/app/logo` 目录下生成的 `logo.component.ts` 并使用以下内容替换它: - -```js -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-logo', - template: ` - - - logo - - `, -}) -export class LogoComponent {} -``` - -打开 `src/app` 目录下的 `app.component.ts` 做以下修改: - -```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store -import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - //... - - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ - component: LogoComponent, - key: eThemeBasicComponents.Logo, - }), - ); - } -} -``` - -最终UI如下: - -![New logo](./images/replaced-logo-component.png) - -#### 如何替换RoutesComponent - -![RoutesComponent](./images/routes-component.png) - -在 `angular` 目录下运行以下命令创建新的组件 `RoutesComponent`: - - -```bash -yarn ng generate component routes --entryComponent - -# You don't need the --entryComponent option in Angular 9 -``` - -打开 `src/app/routes` 目录下生成的 `routes.component.ts` 并使用以下内容替换它: - -```js -import { ABP, ReplaceableComponents } from '@abp/ng.core'; -import { - Component, - HostBinding, - Inject, - Renderer2, - TrackByFunction, - AfterViewInit, -} from '@angular/core'; -import { fromEvent } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; - -@Component({ - selector: 'app-routes', - templateUrl: 'routes.component.html', -}) -export class RoutesComponent implements AfterViewInit { - @HostBinding('class.mx-auto') - marginAuto = true; - - smallScreen = window.innerWidth < 992; - - constructor(private renderer: Renderer2) {} - - ngAfterViewInit() { - fromEvent(window, 'resize') - .pipe(debounceTime(150)) - .subscribe(() => { - this.smallScreen = window.innerWidth < 992; - }); - } -} -``` - -打开 `src/app/routes` 目录下生成的 `routes.component.html` 并使用以下内容替换它: - -```html - -``` - -打开 `src/app` 目录下的 `app.component.ts` 做以下修改: - -```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store -import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - //... - - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ - component: RoutesComponent, - key: eThemeBasicComponents.Routes, - }), - ); - } -} -``` - -最终UI如下: - -![New routes](./images/replaced-routes-component.png) - -#### 如何替换NavItemsComponent - -![NavItemsComponent](./images/nav-items-component.png) - -在 `angular` 目录下运行以下命令创建新的组件 `NavItemsComponent`: - -```bash -yarn ng generate component nav-items --entryComponent - -# You don't need the --entryComponent option in Angular 9 -``` - -打开 `src/app/nav-items` 目录下生成的 `nav-items.component.ts` 并使用以下内容替换它: - -```js -import { - ApplicationConfiguration, - AuthService, - ConfigState, - SessionState, - SetLanguage, -} from '@abp/ng.core'; -import { Component, AfterViewInit } from '@angular/core'; -import { Navigate, RouterState } from '@ngxs/router-plugin'; -import { Select, Store } from '@ngxs/store'; -import { Observable, fromEvent } from 'rxjs'; -import { map, debounceTime } from 'rxjs/operators'; -import snq from 'snq'; - -@Component({ - selector: 'app-nav-items', - templateUrl: 'nav-items.component.html', -}) -export class NavItemsComponent implements AfterViewInit { - @Select(ConfigState.getOne('currentUser')) - currentUser$: Observable; - - @Select(ConfigState.getDeep('localization.languages')) - languages$: Observable; - - smallScreen = window.innerWidth < 992; - - get defaultLanguage$(): Observable { - return this.languages$.pipe( - map( - languages => - snq( - () => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName, - ), - '', - ), - ); - } - - get dropdownLanguages$(): Observable { - return this.languages$.pipe( - map( - languages => - snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)), - [], - ), - ); - } - - get selectedLangCulture(): string { - return this.store.selectSnapshot(SessionState.getLanguage); - } - - constructor(private store: Store, private authService: AuthService) {} - - ngAfterViewInit() { - fromEvent(window, 'resize') - .pipe(debounceTime(150)) - .subscribe(() => { - this.smallScreen = window.innerWidth < 992; - }); - } - - onChangeLang(cultureName: string) { - this.store.dispatch(new SetLanguage(cultureName)); - } - - logout() { - this.authService.logout().subscribe(() => { - this.store.dispatch( - new Navigate(['/'], null, { - state: { redirectUrl: this.store.selectSnapshot(RouterState).state.url }, - }), - ); - }); - } -} -``` - -打开 `src/app/nav-items` 目录下生成的 `nav-items.component.html` 并使用以下内容替换它: - -```html - -``` - -打开 `src/app` 目录下的 `app.component.ts` 做以下修改: - -```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store -import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent -import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents -//... - -@Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - //... - - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ - component: NavItemsComponent, - key: eThemeBasicComponents.NavItems, - }), - ); - } -} -``` - -最终UI如下: - -![New nav-items](./images/replaced-nav-items-component.png) - -## 另请参阅 - -- [如何替换PermissionManagementComponent](./Permission-Management-Component-Replacement.md) - -## 下一步是什么? - -- [自定义设置页面](./Custom-Setting-Page.md) diff --git a/docs/zh-Hans/UI/Angular/Config-State.md b/docs/zh-Hans/UI/Angular/Config-State.md deleted file mode 100644 index de645c8968..0000000000 --- a/docs/zh-Hans/UI/Angular/Config-State.md +++ /dev/null @@ -1,295 +0,0 @@ -## 配置状态 - -`ConfigStateService` 是一个单例服务,即在应用程序的根级别提供,用于与 `Store` 中的应用程序配置状态进行交互. - -## 使用前 - -为了使用 `ConfigStateService`,你必须将其注入到你的类中. - -```js -import { ConfigStateService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private config: ConfigStateService) {} -} -``` - -你不必在模块或组件/指令级别提供 `ConfigStateService`,因为它已经在**根中**提供. - -## 选择器方法 - -`ConfigStateService` 有许多选择器方法允许你从 `Store` 获取特定或所有的配置. - -### 如何从Store获取所有的配置 - -你可以使用 `ConfigStateService` 的 `getAll` 方法从Store获取所有的配置对象. 用法如下: - -```js -// this.config is instance of ConfigStateService - -const config = this.config.getAll(); -``` - -### 如何从Store获取特定的配置 - -你可以使用 `ConfigStateService` 的 `getOne` 方法从Store获取特定的配置属性. 你需要将属性名做为参数传递给方法: - -```js -// this.config is instance of ConfigStateService - -const currentUser = this.config.getOne("currentUser"); -``` - -有时你想要获取具体信息,而不是当前用户. 例如你只想获取到 `tenantId`: - -```js -const tenantId = this.config.getDeep("currentUser.tenantId"); -``` - -或通过提供键数组作为参数: - -```js -const tenantId = this.config.getDeep(["currentUser", "tenantId"]); -``` - -`getDeep` 可以执行 `getOne` 的所有操作. 但 `getOne` 的执行效率要高一些. - -#### 配置状态属性 - -请参阅 `Config.State` 类型,你可以通过 `getOne` 和 `getDeep` 获取所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L7)中找到. - -### 如何从Store获取应用程序信息 - -`getApplicationInfo` 方法从存储为配置状态存储的环境变量中获取应用程序信息. 你可以这样使用它: - -```js -// this.config is instance of ConfigStateService - -const appInfo = this.config.getApplicationInfo(); -``` - -该方法不会返回 `undefined` 或 `null`,而是会返回一个空对象(`{}`). 换句话说,当你使用上面代码中的 `appInfo` 属性时,永远不会出现错误. - -#### 应用程序信息属性 - -请参阅 `Config.State` 类型,你可以通过 `getApplicationInfo` 获取所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L21)中找到. - -### 如何从Store获取 - -`getApplicationInfo` 方法从存储为配置状态存储的环境变量中获取特定的API URL. 你可以这样使用它: - -```js -// this.config is instance of ConfigStateService - -const apiUrl = this.config.getApiUrl(); -// environment.apis.default.url - -const searchUrl = this.config.getApiUrl("search"); -// environment.apis.search.url -``` - -该方法返回给定键的特定的API `url`. 如果没有Key,则使用 `default`. - -### 如何从Store获取所有的设置 - -你可以使用 `ConfigStateService` 的 `getSettings` 获取配置状态所有的设置对象. 你可以这样使用它: - -```js -// this.config is instance of ConfigStateService - -const settings = this.config.getSettings(); -``` - -实际上该方法可以通过**传递关键字**来搜索设置. - -```js -const localizationSettings = this.config.getSettings("Localization"); -/* -{ - 'Abp.Localization.DefaultLanguage': 'en' -} -*/ -``` - -请注意, **设置搜索区分大小写**. - -### 如何从Store获取特定的设置 - -你可以使用 `ConfigStateService` 的 `getSetting` 获取配置状态特定的设置. 你可以这样使用它: - -```js -// this.config is instance of ConfigStateService - -const defaultLang = this.config.getSetting("Abp.Localization.DefaultLanguage"); -// 'en' -``` - -### 如何从Store获取特定的权限 - -你可以使用 `ConfigStateService` 的 `getGrantedPolicy` 获取配置状态特定的权限. 你应该将策略key做为参数传递给方法: - -```js -// this.config is instance of ConfigStateService - -const hasIdentityPermission = this.config.getGrantedPolicy("Abp.Identity"); -// true -``` - -你还可以使用 **组合策略key** 来微调你的选择: - -```js -// this.config is instance of ConfigStateService - -const hasIdentityAndAccountPermission = this.config.getGrantedPolicy( - "Abp.Identity && Abp.Account" -); -// false - -const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( - "Abp.Identity || Abp.Account" -); -// true -``` - -创建权限选择器时,请考虑以下**规则**: - -- 最多可组合两个键. -- `&&` 操作符查找两个键. -- `||` 操作符查找任意一个键. -- 空字符串 `''` 做为键将返回 `true` -- 使用没有第二个键的操作符将返回 `false` - -### 如何从Store中获取翻译 - -`ConfigStateService` 的 `getLocalization` 方法用于翻译. 这里有一些示例: - -```js -// this.config is instance of ConfigStateService - -const identity = this.config.getLocalization("AbpIdentity::Identity"); -// 'identity' - -const notFound = this.config.getLocalization("AbpIdentity::IDENTITY"); -// 'AbpIdentity::IDENTITY' - -const defaultValue = this.config.getLocalization({ - key: "AbpIdentity::IDENTITY", - defaultValue: "IDENTITY" -}); -// 'IDENTITY' -``` - -请参阅[本地化文档](./Localization.md)了解详情. - -## 分发方法 - -`ConfigStateService` 有几种分发方法,让你方便地将预定义操作分发到 `Store`. - -### 如何从服务器获取应用程序配置 - -`dispatchGetAppConfiguration` 触发对端点的请求,该端点使用应用程序状态进行响应,然后将此响应作为配置状态放置到 `Store`中. - -```js -// this.config is instance of ConfigStateService - -this.config.dispatchGetAppConfiguration(); -// returns a state stream which emits after dispatch action is complete -``` - -请注意,**你不必在应用程序启动时调用此方法**,因为在启动时已经从服务器收到了应用程序配置. - -### 如何修补路由配置 - -`dispatchPatchRouteByName` 根据名称查找路由, 并将其在 `Store` 中的配置替换为作为第二个参数传递的新配置. - -```js -// this.config is instance of ConfigStateService - -const newRouteConfig: Partial = { - name: "Home", - path: "home", - children: [ - { - name: "Dashboard", - path: "dashboard" - } - ] -}; - -this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig); -// returns a state stream which emits after dispatch action is complete -``` - -### 如何添加新路由配置 - -`dispatchAddRoute` 向 `Store` 的配置状态添加一个新路由. 应该将路由配置做为方法参数传递. - -```js -// this.config is instance of ConfigStateService - -const newRoute: ABP.Route = { - name: "My New Page", - iconClass: "fa fa-dashboard", - path: "page", - invisible: false, - order: 2, - requiredPolicy: "MyProjectName.MyNewPage" -}; - -this.config.dispatchAddRoute(newRoute); -// returns a state stream which emits after dispatch action is complete -``` - -`newRoute` 将被放置在根级别,没有任何父路由,并且其url将存储为 `'/path'`. - -如果你想要**添加一个子路由,你可以这样做:** - -```js -import { eIdentityRouteNames } from '@abp/ng.identity'; -// this.config is instance of ConfigStateService - -const newRoute: ABP.Route = { - parentName: eIdentityRouteNames.IdentityManagement, - name: "My New Page", - iconClass: "fa fa-dashboard", - path: "page", - invisible: false, - order: 2, - requiredPolicy: "MyProjectName.MyNewPage" -}; - -this.config.dispatchAddRoute(newRoute); -// returns a state stream which emits after dispatch action is complete -``` - -`newRoute` 做为 `'AbpAccount::Login'` 父路由的子路由被放置,它的url被设置为 `'/account/login/page'`. - -#### 路由配置属性 - -请参阅 `ABP.Route` 类型,获取可在参数中传递给 `dispatchSetEnvironment` 的所有属性. 你可以在[common.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27)中找到. - -### 如何设置环境 - -`dispatchSetEnvironment` 将传递给它的环境变量放在 `Store` 中的配置状态下. 使用方法如下: - -```js -// this.config is instance of ConfigStateService - -this.config.dispatchSetEnvironment({ - /* environment properties here */ -}); -// returns a state stream which emits after dispatch action is complete -``` - -注意,**你不必在应用程序启动时调用此方法**,因为环境变量已经在启动时存储了. - -#### 环境属性 - -请参阅 `Config.Environment` 类型,获取可在参数中传递给 `dispatchSetEnvironment` 的所有属性. 你可以在[config.ts 文件](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13)中找到. - -## 下一步是什么? - -- [修改菜单](./Modifying-the-Menu.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Confirmation-Service.md b/docs/zh-Hans/UI/Angular/Confirmation-Service.md deleted file mode 100644 index 7f47c7d8a1..0000000000 --- a/docs/zh-Hans/UI/Angular/Confirmation-Service.md +++ /dev/null @@ -1,183 +0,0 @@ -# 确认弹层 - -你可以使用@abp/ng.theme.shared包中提供 `ConfirmationService` 放置在你项目的级别来显示确认弹层 - -## 入门 - -你不必在模块或组件级别提供 `ConfirmationService`,它已经在**根**级别提供,你可以在你的组件,指令或服务直接注入并使用它. - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private confirmation: ConfirmationService) {} -} -``` - -## 用法 - -你可以使用 `ConfirmationService` 的 `success`, `warn`, `error` 和 `info` 方法显示一个确认弹层. - -### 如何显示一个确认弹层 - -```js -const confirmationStatus$ = this.confirmation.success('Message', 'Title'); -``` - -- `ConfirmationService` 方法接收三个参数,分别是 `message`, `title`, 和 `options`. -- `success`, `warn`, `error`, 和 `info` 方法返回一个[RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject)监听确认弹层关闭事件. 事件值类型是 [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24)枚举. - -### 如何监听关闭事件 - -你可以订阅确认弹层关闭事件,例: - -```js -import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; - -constructor(private confirmation: ConfirmationService) {} - -this.confirmation - .warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) - .subscribe((status: Confirmation.Status) => { - // your code here - }); -``` - -- `message` 和 `title` 参数接收字符串,本地化Key或本地化对象. 参阅[本地化文档](./Localization.md) -- `Confirmation.Status` 是一个枚举,具有三个属性; - - `Confirmation.Status.confirm` 是一个关闭事件值,当通过确认按钮关闭弹出窗口时触发此事件. - - `Confirmation.Status.reject` 是一个关闭事件值,当通过取消按钮关闭弹出窗口时触发此事件. - - `Confirmation.Status.dismiss` 是一个关闭事件值,当通过按Escape键关闭弹出窗口时触发此事件. - -如果你对确认状态不感兴趣,则不必订阅返回的observable: - -```js -this.confirmation.error('You are not authorized.', 'Error'); -``` - -### 如何显示具有给定选项的确认弹层 - -选项可以作为第三个参数传递给`success`, `warn`, `error`, 和 `info` 方法: - -```js -const options: Partial = { - hideCancelBtn: false, - hideYesBtn: false, - dismissible: false, - cancelText: 'Close', - yesText: 'Confirm', - messageLocalizationParams: ['Demo'], - titleLocalizationParams: [], -}; - -this.confirmation.warn( - 'AbpIdentity::RoleDeletionConfirmationMessage', - 'Are you sure?', - options, -); -``` - -- `hideCancelBtn` 选项为 `true` 时隐藏取消按钮. 默认值为 `false`. -- `hideYesBtn` 选项为 `true` 时隐藏确认按钮. 默认值为 `false`. -- `dismissible`选项允许通过按Escape键或单击背景来取消确认弹出窗口. 默认值为 `true`. -- `cancelText` 是取消按钮的文本,可以传递本地化键或本地化对象. 默认值为 `AbpUi::Cancel`. -- `yesText` 是确定按钮的文本,可以传递本地化键或本地化对象. 默认值为 `AbpUi::Yes`. -- `messageLocalizationParams`是用于消息本地化的插值参数. -- `titleLocalizationParams` 是标题本地化的插值参数. - -使用以上选项确认弹层窗口如下所示: - -![confirmation](./images/confirmation.png) - -你可以传递HTML字符串作为标题,消息或按钮文本. 例如: - -```js -const options: Partial = { - yesText: 'Yes, delete it', -}; - -this.confirmation.warn( - ` - Role Demo will be deleted -
    - Do you confirm that? - `, - 'Are you sure?', - options, -); -``` - -由于这些值现在是HTML,因此应该手动处理本地化. 参阅[LocalizationService](./Localization#using-the-localization-service)了解如何实现. - -> 注意,Angular会清除所有字符串,并且并非每个HTML字符串都可以使用. 仅显示被Angular视为"安全"的值. - -### 如何删除一个确认弹层 - -打开的确认弹出窗口可以通过 `clear` 方法手动删除: - -```js -this.confirmation.clear(); -``` - -## API - -### success - -```js -success( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -> 请参见[`Config.LocalizationParam`类型](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46)和[Confirmation名称空间](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) - - -### warn - -```js -warn( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### error - -```js -error( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### info - -```js -info( - message: Config.LocalizationParam, - title: Config.LocalizationParam, - options?: Partial, -): Observable -``` - -### clear - -```js -clear( - status: Confirmation.Status = Confirmation.Status.dismiss -): void -``` - -- `status` 参数是确认关闭事件的值. - - -## 下一步是什么? - -- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/zh-Hans/UI/Angular/Container-Strategy.md b/docs/zh-Hans/UI/Angular/Container-Strategy.md deleted file mode 100644 index daf3be01db..0000000000 --- a/docs/zh-Hans/UI/Angular/Container-Strategy.md +++ /dev/null @@ -1,87 +0,0 @@ -# ContainerStrategy - -`ContainerStrategy` 是 @abp/ng.core 包暴露出的抽象类. 有两种扩展容器扩展策略: `ClearContainerStrategy` 和 `InsertIntoContainerStrategy`. 它们实现了相同的方法和属性,这两种策略都可以帮助你定义容器的准备方式和内容的投影位置. - -## API - -`ClearContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影之前清除容器**. - -### 构造函数 - -```js -constructor( - public containerRef: ViewContainerRef, - private index?: number, // works only in InsertIntoContainerStrategy -) -``` - -- `containerRef` 是在投影内容时使用的 `ViewContainerRef`. - -### getIndex - -```js -getIndex(): number -``` - -该方法返回被 `0` 和 `containerRef` `length` 限制的给定索引. 对于没有索引的策略,它返回`0`. - -### prepare - -```js -prepare(): void -``` - -此方法在内容投影之前调用. 基于使用的容器策略,它要么清除容器,要么什么都不做(空操作). - -## ClearContainerStrategy - -`ClearContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影之前清除容器**. - -## InsertIntoContainerStrategy - -`InsertIntoContainerStrategy` 是一个扩展了 `ContainerStrategy` 的类. 它允许你**将内容投影到容器中的特定节点索引上**. - -## 预定义的容器策略 - -可以通过 `CONTAINER_STRATEGY` 常量访问预定义的容器策略. - -### Clear - -```js -CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) -``` - -在内容投影之前清除给定的容器. - - -### Append - -```js -CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) -``` - -将投影内容附加到容器中. - - -### Prepend - -```js -CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) -``` - -将投影的内容预先写入容器中. - -### Insert - -```js -CONTAINER_STRATEGY.Insert( - containerRef: ViewContainerRef, - index: number, -) -``` - -将投影内容按照给定的索引(在`0` 到 `containerRef`的长度之间)插入到容器中. - -## 另请参阅 - -- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/zh-Hans/UI/Angular/Content-Projection-Service.md b/docs/zh-Hans/UI/Angular/Content-Projection-Service.md deleted file mode 100644 index 72fa468a79..0000000000 --- a/docs/zh-Hans/UI/Angular/Content-Projection-Service.md +++ /dev/null @@ -1,77 +0,0 @@ -# 内容投影 - -你可以使用位于@abp/ng.core包中的 `ContentProjectionService` 简单明确的投影内容. - -## 入门 - -你不必在模块或组件级别提供 `ContentProjectionService`,因为它已经在**根中提供了**. 你可以在组件中注入并开始使用它. 为了获得更好的类型支持,你可以将迭代项目的类型传递给它. - -```js -import { ContentProjectionService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private contentProjectionService: ContentProjectionService) {} -} -``` - -## 用法 - -你可以使用 `ContentProjectionService` 的 `projectContent` 方法在你的项目中动态的渲染组件和模板. - -### 如何将组件投影到根级别 - -如果将 `RootComponentProjectionStrategy` 做为 `projectContent` 方法的第一个参数,那么 `ContentProjectionService` 会解析投影组件并放在根级别,它还为组件传递上下文. - -```js -const strategy = PROJECTION_STRATEGY.AppendComponentToBody( - SomeOverlayComponent, - { someOverlayProp: "SOME_VALUE" } -); - -const componentRef = this.contentProjectionService.projectContent(strategy); -``` - -在上面的示例中, `SomeOverlayComponent` 组件放置在 `` 的**末尾**并返回 `ComponentRef`. 另外将应用给定的上下文,因此组件的 `someOverlayProp` 被设置为 `SOME_VALUE`. - -> 你应该总是返回 `ComponentRef` 实例,因为它是对投影组件的引用,在你需要时使用该引用销毁投影视图和组件实例. - -### 如何将组件和模板投影到容器中 - -如果将 `ComponentProjectionStrategy` 或 `TemplateProjectionStrategy` 做为 `projectContent` 方法的第一个参数,并且传递 `ViewContainerRef` 做为策略的第二个参数传递. 那么 `ContentProjectionService` 把组件或模板投影到给定的容器中,它还为组件或模板传递上下文. - -```js -const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( - SomeComponent, - viewContainerRefOfTarget, - { someProp: "SOME_VALUE" } -); - -const componentRef = this.contentProjectionService.projectContent(strategy); -``` - -在上面的示例中,`viewContainerRefOfTarget`(它是一个`ViewContainerRef` 实例)将被清除,并把 `SomeComponent` 组件放在其中. 另外将应用给定的上下文,因此组件的 `someProp` 被设置为 `SOME_VALUE`. - -> 你应该总是返回 `ComponentRef` 或 `EmbeddedViewRef` ,因为它是对投影内容的引用,在你需要时使用该引用销毁它们. - -请参考[ProjectionStrategy](./Projection-Strategy.md)查看所有可用的投影策略以及如何构建自己的投影策略. - -## API - -### projectContent - -```js -projectContent | TemplateRef>( - projectionStrategy: ProjectionStrategy, - injector = this.injector, -): ComponentRef | EmbeddedViewRef -``` - -- `projectionStrategy` 参数是此处的要点,在上面进行了说明. -- `injector` 参数是 `Injector` 实例,你可以传递到投影内容. 在 `TemplateProjectionStrategy` 并没有使用到它. - -## 下一步是什么? - -- [TrackByService](./Track-By-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md b/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md deleted file mode 100644 index 7e419f8ab7..0000000000 --- a/docs/zh-Hans/UI/Angular/Content-Security-Strategy.md +++ /dev/null @@ -1,55 +0,0 @@ -# ContentSecurityStrategy - -`ContentSecurityStrategy` 是@abp/ng.core包暴露出的抽象类. 它可以根据[内容安全策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)帮助你将内联脚本或样式标记为安全. - -## API - -### 构造函数 - -```js -constructor(public nonce?: string) -``` - -- `nonce` 启用将内联脚本或样式列入白名单,避免在[script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script)和[style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles)指令中使用 `unsafe-inline`. - -### applyCSP - -```js -applyCSP(element: HTMLScriptElement | HTMLStyleElement): void -``` - -该方法将上述属性映射到给定`element`. - -## LooseContentSecurityPolicy - -`LooseContentSecurityPolicy` 是扩展了 `ContentSecurityStrategy` 的类. 它需要 `nonce` 和带有给定 `` 元素放置在 ``的末尾, `scriptElement` 类型是一个 `HTMLScriptElement`. - -请参考[ContentStrategy](./Content-Strategy.md)查看所有可用的内容策略以及如何构建自己的内容策略. - -> 重要说明: `DomInsertionService` 不会两次插入相同的内容. 为了再次添加内容你首先应该使用 `removeContent` 方法删除旧内容. - -### 如何插入Styles - -`insertContent` 方法的第一个参数需要一个 `ContentStrategy`. 如果传递 `StyleContentStrategy` 实例, `DomInsertionService` 将创建具有给定内容的 `` 元素放置在 ``的末尾, `styleElement` 类型是一个 `HTMLStyleElement`. - -请参考[ContentStrategy](./Content-Strategy.md)查看所有可用的内容策略以及如何构建自己的内容策略. -. -> 重要说明: `DomInsertionService` 不会两次插入相同的内容. 为了再次添加内容你首先应该使用 `removeContent` 方法删除旧内容. - -### 如何删除已插入的 Scripts & Styles - -如果你传递 `HTMLScriptElement` 或 `HTMLStyleElement` 做为 `removeContent` 方法的第一个参数, `DomInsertionService` 将删除给定的元素. - -```js -import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - private styleElement: HTMLStyleElement; - - constructor(private domInsertionService: DomInsertionService) {} - - ngOnInit() { - this.styleElement = this.domInsertionService.insertContent( - CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') - ); - } - - ngOnDestroy() { - this.domInsertionService.removeContent(this.styleElement); - } -} -``` - -在上面的示例中,销毁组件时,将从 `` 中删除 `` 元素. - -## API - -### insertContent - -```js -insertContent( - contentStrategy: ContentStrategy, -): T -``` - -- `contentStrategy` 是方法的重要参数,已经在上方进行说明. -- 根据给定的策略返回 `HTMLScriptElement` 或 `HTMLStyleElement`. - -### removeContent - -```js -removeContent(element: HTMLScriptElement | HTMLStyleElement): void -``` - -- `element` 参数是已插入的 `HTMLScriptElement` 或 `HTMLStyleElement` 元素,它们应由 `insertContent` 方法返回. - -### has - -```js -has(content: string): boolean -``` - -`has` 返回一个布尔值,用于表示给定的内容是否插入到DOM. - -- `content` 参数是 `HTMLScriptElement` 或 `HTMLStyleElement` 元素的内容. - -## 下一步是什么? - -- [ContentProjectionService](./Content-Projection-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Dom-Strategy.md b/docs/zh-Hans/UI/Angular/Dom-Strategy.md deleted file mode 100644 index c4982853b9..0000000000 --- a/docs/zh-Hans/UI/Angular/Dom-Strategy.md +++ /dev/null @@ -1,80 +0,0 @@ -# DomStrategy - -`DomStrategy` 是@abp/ng.core包暴露出的抽象类. 它的实例定义了如何将元素附加到DOM以及如何被其它类(如`LoadingStrategy`)使用. - -## API - -### 构造函数 - -```js -constructor( - public target?: HTMLElement, - public position?: InsertPosition -) -``` - -- `target` 是一个 HTMLElement (默认值: document.head_). -- `position` 定义将创建的元素放置在何处. 可以在[此处](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement)找到所有可能的 `position` 值(默认值: 'beforeend'_). - -### insertElement - -```js -insertElement(element: HTMLElement): void -``` - -该方法根据 `postion` 将给定 `元素` 插入到目标中. - -## 预定义DOM策略 - -可以通过 `DOM_STRATEGY` 常量访问预定义的dom策略. - - -### AppendToBody - -```js -DOM_STRATEGY.AppendToBody() -``` - -`insertElement` 将给定 `元素` 放在 `` 的末尾. - - -### AppendToHead - -```js -DOM_STRATEGY.AppendToHead() -``` - -`insertElement` 将给定 `元素` 放在 `` 的末尾. - -### PrependToHead - -```js -DOM_STRATEGY.PrependToHead() -``` - -`insertElement` 将给定 `元素` 放在 `` 的头部. - - -### AfterElement - -```js -DOM_STRATEGY.AfterElement(target: HTMLElement) -``` - -`insertElement` 将给定 `元素` 放在 `target` 之后 (做为同级元素). - -### BeforeElement - -```js -DOM_STRATEGY.BeforeElement(target: HTMLElement) -``` - -`insertElement` 将给定 `元素` 放在 `target` 之前 (做为同级元素). - -## 另请参阅 - -- [DomInsertionService](./Dom-Insertion-Service.md) -- [LazyLoadService](./Lazy-Load-Service.md) -- [LoadingStrategy](./Loading-Strategy.md) -- [ContentStrategy](./Content-Strategy.md) -- [ProjectionStrategy](./Projection-Strategy.md) diff --git a/docs/zh-Hans/UI/Angular/Environment.md b/docs/zh-Hans/UI/Angular/Environment.md deleted file mode 100644 index cb82a640d5..0000000000 --- a/docs/zh-Hans/UI/Angular/Environment.md +++ /dev/null @@ -1 +0,0 @@ -TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/HTTP-Requests.md b/docs/zh-Hans/UI/Angular/HTTP-Requests.md deleted file mode 100644 index 47f871d6e6..0000000000 --- a/docs/zh-Hans/UI/Angular/HTTP-Requests.md +++ /dev/null @@ -1,179 +0,0 @@ -## HTTP请求 - -## 关于 HttpClient - -Angular具有很棒的 `HttpClient` 与后端服务进行通信. 它位于顶层,是[XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)的封装. 同时也是Angular建议用于任何HTTP请求的代理,在你的ABP项目中使用 `HttpClient` 是最佳做法. - -但是 `HttpClient` 将错误处理留给调用方,换句话说HTTP错误是通过手动处理的,通过挂接到返回的 `Observable` 的观察者中来处理. - -```js -getConfig() { - this.http.get(this.configUrl).subscribe( - config => this.updateConfig(config), - error => { - // Handle error here - }, - ); -} -``` - -上面的代码尽管清晰灵活,但即使将错误处理委派给Store或任何其他注入. 以这种方式处理错误也是重复性的工作. - -`HttpInterceptor` 能够捕获 `HttpErrorResponse` 并可用于集中的错误处理. 然而,在必须放置错误处理程序(也就是拦截器)的情况下,需要额外的工作以及对Angular内部机制的理解. 检查[这个issue](https://github.com/angular/angular/issues/20203)了解详情. - -## RestService - -ABP核心模块有用于HTTP请求的实用程序服务: `RestService`. 除非另有明确配置,否则它将捕获HTTP错误并调度 `RestOccurError` 操作, 然后由 `ThemeSharedModule` 引入的 `ErrorHandler` 捕获此操作. 你应该已经在应用程序中导入了此模块,在使用 `RestService` 时,默认情况下将自动处理所有HTTP错误. - -### RestService 入门 - -为了使用 `RestService`, 你必须将它注入到你的类中. - -```js -import { RestService } from '@abp/ng.core'; - -@Injectable({ - /* class metadata here */ -}) -class DemoService { - constructor(private rest: RestService) {} -} -``` - -你不必在模块或组件/指令级别提供 `estService`,因为它已经在**根中**中提供了. - -### 如何使用RestService发出请求 - -你可以使用 `RestService` 的 `request` 方法来处理HTTP请求. 示例: - -```js -getFoo(id: number) { - const request: Rest.Request = { - method: 'GET', - url: '/api/some/path/to/foo/' + id, - }; - - return this.rest.request(request); -} -``` - -`request` 方法始终返回 `Observable`. 无论何时使用 `getFoo` 方法,都可以执行以下操作: - -```js -doSomethingWithFoo(id: number) { - this.demoService.getFoo(id).subscribe( - foo => { - // Do something with foo. - } - ) -} -``` - -**你不必担心关于取消订阅**. `RestService` 在内部使用 `HttpClient`,因此它返回的每个可观察对象都是有限的可观察对象,成功或出错后将自动关闭订阅. - -如你所见,`request` 方法获取一个具有 `Rest.Reques` 类型的请求选项对象. 此泛型类型需要请求主体的接口. 当没有正文时,例如在 `GET` 或 `DELETE` 请求中,你可以传递 `null`. 示例: - -```js -postFoo(body: Foo) { - const request: Rest.Request = { - method: 'POST', - url: '/api/some/path/to/foo', - body - }; - - return this.rest.request(request); -} -``` - -你可以在[此处检查](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)完整的 `Rest.Request` 类型,与Angular中的[HttpRequest](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)类相比只有很少的改动. - -### 如何禁用RestService的默认错误处理程序 - -默认 `request` 方法始终处理错误. 让我们看看如何改变这种行为并由自己处理错误: - -```js -deleteFoo(id: number) { - const request: Rest.Request = { - method: 'DELETE', - url: '/api/some/path/to/foo/' + id, - }; - - return this.rest.request(request, { skipHandleError: true }); -} -``` - -`skipHandleError` 配置选项设置为 `true` 时,禁用错误处理程序,并返回 `observable` 引发错误,你可以在订阅中捕获该错误. - -```js -removeFooFromList(id: number) { - this.demoService.deleteFoo(id).subscribe( - foo => { - // Do something with foo. - }, - error => { - // Do something with error. - } - ) -} -``` - -### 如何从应用程序配置获取特定的API端点 - -`request` 方法接收到的另一个配置选项是 `apiName` (在v2.4中可用),它用于从应用程序配置获取特定的模块端点. - -```js -putFoo(body: Foo, id: string) { - const request: Rest.Request = { - method: 'PUT', - url: '/' + id, - body - }; - - return this.rest.request(request, {apiName: 'foo'}); -} -``` - -上面的putFoo将请求 `https://localhost:44305/api/some/path/to/foo/{id}` 当环境变量如下: - -```js -// environment.ts - -export const environment = { - apis: { - default: { - url: 'https://localhost:44305', - }, - foo: { - url: 'https://localhost:44305/api/some/path/to/foo', - }, - }, - - /* rest of the environment variables here */ -} -``` - -### 如何观察响应对象或HTTP事件而不是正文 - -`RestService` 假定你通常对响应的正文感兴趣,默认情况下将 `observe` 属性设置为 `body`. 但是有时你可能对其他内容(例如自定义标头)非常感兴趣. 为此, `request` 方法在 `config` 对象中接收 `watch` 属性. - -```js -getSomeCustomHeaderValue() { - const request: Rest.Request = { - method: 'GET', - url: '/api/some/path/that/sends/some-custom-header', - }; - - return this.rest.request>( - request, - {observe: Rest.Observe.Response}, - ).pipe( - map(response => response.headers.get('Some-Custom-Header')) - ); -} -``` - -你可以在[此处](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10)找到 `Rest.Observe` 枚举. - -## 下一步是什么? - -* [本地化](./Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md b/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md deleted file mode 100644 index 07753f8541..0000000000 --- a/docs/zh-Hans/UI/Angular/Lazy-Load-Service.md +++ /dev/null @@ -1,189 +0,0 @@ -# 如何懒加载 Scripts 与 Styles - -你可以使用@abp/ng.core包中的 `LazyLoadService` 以简单明了的方式延迟加载脚本和样式. - -## 入门 - -你不必在模块或组件/指令级别提供 `LazyLoadService`,因为它已经在**根中**中提供了. 你可以在组件,指令或服务中注入并使用它. - -```js -import { LazyLoadService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private lazyLoadService: LazyLoadService) {} -} -``` - -## 用法 - -你可以使用 `LazyLoadService` 的 `load` 方法在DOM中的所需位置创建 ` - - -```` - -#### 在TagHelpers中添加外部/CDN文件 - -````html - - - - - - - - - - - - - - - -```` - -**输出HTMl:** - -````html - - - - - - - -```` - -### 主题 - -主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). - -### 最佳实践 & 建议 - -建议为应用程序定义多个包, 每个包用于不同的目的. - -* **全局包**: 应用程序中的每个页面都包含全局样式/脚本包. 主题已经定义了全局样式和脚本包. 你的模块可以为他们做出贡献. -* **布局包**: 这是针对单个布局的特定包. 仅包含在所有页面之间共享的资源使用布局. 使用bundling tag helpers创建捆绑包是一种很好的做法. -* **模块包**: 用于单个模块页面之间的共享资源. -* **页面包**: 为每个页面创建的特定包. 使用bundling tag helpers创建捆绑包作为最佳实践. - -在性能,网络带宽使用和捆绑包的数量之间建立平衡. - -### 参见 - -* [客户端包管理](Client-Side-Package-Management.md) -* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md deleted file mode 100644 index f11866404b..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md +++ /dev/null @@ -1,113 +0,0 @@ - -## ASP.NET Core MVC 客户端包管理 - -ABP框架可以与任何类型的客户端包管理系统一起使用. 甚至你可以决定不使用包管理系统并手动管理依赖项. - -但是, ABP框架最适用于**NPM/Yarn**. 默认情况下,内置模块配置为与NPM/Yarn一起使用. - -最后, 我们建议[**Yarn**](https://classic.yarnpkg.com/)而不是NPM,因为它更快,更稳定并且与NPM兼容. - -### @ABP NPM Packages - -ABP是一个模块化平台. 每个开发人员都可以创建模块, 模块应该在**兼容**和**稳定**状态下协同工作. - -一个挑战是依赖NPM包的**版本**. 如果两个不同的模块使用相同的JavaScript库但其不同(并且可能不兼容)的版本会怎样. - -为了解决版本问题, 我们创建了一套**标准包**, 这取决于一些常见的第三方库. 一些示例包是[@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@ abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap)和[@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). 你可以从[Github存储库](https://github.com/volosoft/abp/tree/master/npm/packs)中查看**列表**. - -**标准包**的好处是: - -* 它取决于包装的**标准版本**. 取决于此包是**安全**,因为所有模块都依赖于相同的版本. -* 它包含将库资源(js,css,img...文件)从**node_modules**文件夹复制到**wwwroot/libs**文件夹. 有关更多信息, 请参阅 *映射库资源* 部分. - -依赖标准包装很容易. 只需像往常一样将它添加到**package.json**文件中. 例如: - -```` - { - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } - } -```` - -建议依赖于标准软件包, 而不是直接依赖于第三方软件包. - -#### 安装包 - -依赖于NPM包后, 你应该做的就是从命令行运行**yarn**命令来安装所有包及其依赖项: - -```` -yarn -```` - -虽然你可以使用`npm install`,但如前所述,建议使用[Yarn](https://classic.yarnpkg.com/). - -#### 贡献包 - -如果你需要不在标准软件包中的第三方NPM软件包,你可以在Github[repository](https://github.com/volosoft/abp)上创建Pull请求. 接受遵循这些规则的拉取请求: - -* 对于NPM上的`package-name`, 包名称应该命名为`@abp/package-name`(例如:`bootstrap`包的`@abp/bootstrap`). -* 它应该是**最新的稳定**版本的包. -* 它应该只依赖于**单个**第三方包. 它可以依赖于多个`@abp/*`包. -* 包应包含一个`abp.resourcemapping.js`文件格式,如*映射库资源*部分中所定义. 此文件应仅映射所依赖包的资源. -* 你还需要为你创建的包创建[bundle贡献者](Bundling-Minification.md). - -有关示例, 请参阅当前标准包. - -### 映射库资源 - -使用NPM包和NPM/Yarn工具是客户端库的事实标准. NPM/Yarn工具在Web项目的根文件夹中创建一个**node_modules**文件夹. - -下一个挑战是将所需的资源(js,css,img ...文件)从`node_modules`复制到**wwwroot**文件夹内的文件夹中,以使其可供客户端/浏览器访问. - -ABP的 `install-libs` 命令**将资源**从**node_modules**复制到**wwwroot/libs**文件夹. 每个**标准包**(参见*@ABP NPM Packages*部分)定义了自己文件的映射. 因此, 大多数情况你只配置依赖项. - -**启动模板**已经配置为开箱即用的所有这些. 本节将介绍配置选项. - -#### 资源映射定义文件 - -模块应该定义一个名为`abp.resourcemapping.js`的JavaScript文件,其格式如下例所示: - -````js -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs" - ], - mappings: { - - } -} -```` - -* **aliases**部分定义了可在映射路径中使用的标准别名(占位符). **@node_modules**和 **@libs**是必需的(通过标准包), 你可以定义自己的别名以减少重复. -* **clean**部分是在复制文件之前要清理的文件夹列表. -* **mappings**部分是要复制的文件/文件夹的映射列表.此示例不会复制任何资源本身,但取决于标准包. - -示例映射配置如下所示: - -````js -mappings: { - "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" -} -```` - -#### install-libs 命令 - -正确配置`abp.resourcemapping.js`文件后, 可以从命令行运行ABP CLI命令: - -````bash -abp install-libs -```` - -当你运行这个命令时,所有包都会将自己的资源复制到**wwwroot/libs**文件夹中. 只有在**package.json**文件中对依赖项进行更改时, 才需要运行`abp install-libs`. - -#### 参见 - -* [捆绑 & 压缩](Bundling-Minification.md) -* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md deleted file mode 100644 index f10ccabaff..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md +++ /dev/null @@ -1,467 +0,0 @@ -# ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南 - -本文档解释了如何重写ASP.NET Core MVC / Razor Page 应用程序依赖[应用模块](../../Modules/Index.md)的用户界面. - -## 重写页面 - -本节介绍了[Razor 页面](https://docs.microsoft.com/zh-cn/aspnet/core/razor-pages/)开发,它是ASP.NET Core推荐的服务端渲染用户页面的方法. 预构建的模块通常使用Razor页面替代经典的MVC方式(下一节也介绍MVC模式). - -你通过有三种重写页面的需求: - -* 仅**重写页面模型**(C#)端执行其他逻辑,不更改UI. -* 仅**重写Razor页面**(.cshtml文件),不更改逻辑. -* **完全重写** 页面. - -### 重写页面模型 (C#) - -````csharp -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Identity; -using Volo.Abp.Identity.Web.Pages.Identity.Users; - -namespace Acme.BookStore.Web.Pages.Identity.Users -{ - [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(EditModalModel))] - public class MyEditModalModel : EditModalModel - { - public MyEditModalModel( - IIdentityUserAppService identityUserAppService, - IIdentityRoleAppService identityRoleAppService - ) : base( - identityUserAppService, - identityRoleAppService) - { - } - - public async override Task OnPostAsync() - { - //TODO: Additional logic - await base.OnPostAsync(); - //TODO: Additional logic - } - } -} -```` - -* 这个类继承并替换 `EditModalModel` ,重写了 `OnPostAsync` 方法在基类代码的前后执行附加逻辑 -* 它使用 `ExposeServices` 和 `Dependency` attributes去替换这个类. - -### 重写Razor页面 (.CSHTML) - -同一路径下创建相同的`.cshtml`文件可以实现重写功能(razor page, razor view, view component... 等.) - -#### 示例 - -这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面**UI - -账户模块在 `Pages/Account` 文件夹下定义了 `Login.cshtml` 文件. 所以你可以在同一路径下创建文件覆盖它: -![overriding-login-cshtml](../../images/overriding-login-cshtml.png) - -通常你想要拷贝模块的 `.cshtml` 原文件,然后进行需要的更改. 你可以在[这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到源文件. 不要拷贝 `Login.cshtml.cs` 文件,它是隐藏razor页面的代码,我们不希望覆盖它(见下节). - -这就够了,接下来你可以对文件内容做你想要的更改. - -### 完全重写Razo页面 - -也许你想要完全重写页面,Razor和页面相关的C#文件. - -在这种情况下; - -1. 像上面描述过的那样重写C#页面模型类,但不需要替换已存在的页面模型类. -2. 像上面描述过的那样重写Razor页面,并且更改@model指向新的页面模型 - -#### 示例 - -这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面** - -创建一个继承自 `LoginModel`(定义在`Volo.Abp.Account.Web.Pages.Account`命名空间下)的页面模型类: - -````csharp -public class MyLoginModel : LoginModel -{ - public MyLoginModel( - IAuthenticationSchemeProvider schemeProvider, - IOptions accountOptions - ) : base( - schemeProvider, - accountOptions) - { - - } - - public override Task OnPostAsync(string action) - { - //TODO: Add logic - return base.OnPostAsync(action); - } - - //TODO: Add new methods and properties... -} -```` - -如果需要,你可以重写任何方法或添加新的属性/方法 - -> 注意我们没有使用 `[Dependency(ReplaceServices = true)]` 或 `[ExposeServices(typeof(LoginModel))]`,因为我们不想替换依赖注入中已存在的类,我们定义了一个新的. - -拷贝 `Login.cshtml` 到你们解决方案,更改 **@model** 指定到 `MyLoginModel`: - -````xml -@page -... -@model Acme.BookStore.Web.Pages.Account.MyLoginModel -... -```` - -这就够了,接下来你可以做任何想要更改. - -#### 不使用继承替换页面模型 - -你不需要继承源页面模型类(像之前的示例). 你可以完全**重写实现**你自己的页面. 在这种事情下你可以从 `PageModel`,`AbpPageModel` 或任何你需要的合适的基类派生. - -## 重写视图组件 - -在ABP框架,预构建的模块和主题定义了一些**可重用的视图组件**. 这些视图组件可以像页面一样被替换. - -### 示例 - -下面是应用程序启动模板自带的 **基本主题** 的截图. - -![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) - -[基本主题](../../Themes/Basic.md) 为layout定义了一些视图组件. 例如上面带有红色矩形的突出显示区域称为 **Brand组件**, 你可能想添加自己的**自己的应用程序logo**来自定义此组件. 让我们来看看如何去做. - -首先创建你的logo并且放到你的web应用程序文件夹中,我们使用 `wwwroot/logos/bookstore-logo.png` 路径. 然后在 `Themes/Basic/Components/Brand` 文件夹下复制[Brand组件视图](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml). 结果应该是类似下面的图片: - -![bookstore-added-brand-files](../../images/bookstore-added-brand-files.png) - -然后对 `Default.cshtml` 文件做你想要的更改. 例如内容可以是这样的: - -````xml - - - -```` - -现在你可以运行应用程序看到结果: - -![bookstore-added-logo](../../images/bookstore-added-logo.png) - -如果你需要,你也可以仅使用依赖注入系统替换组件[背后的C#类代码](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) - -### 重写主题 - -正如上所解释的,你可以更改任何组件,layout或c#类. 参阅[主题文档]了解更多关于主题系统的信息. - -## 重写静态资源 - -重写模块的静态资源(像JavaScript,Css或图片文件)是很简单的. 只需要在解决方案的相同路径创建文件,[虚拟文件系统](../../Virtual-File-System.md)会自动处理它. - -## 操作捆绑 - -[捆绑 & 压缩](Bundling-Minification.md) 系统提供了**动态可扩展的** 系统去创建**script**和**style**捆绑. 它允许你扩展和操作现有的包. - -### 示例: 添加全局CSS文件 - -例如APP框架定义了一个**全局样式捆绑**添加到所有的页面(事实上由主题添加layout). 让我们添加一个**自定义样式文件**到这个捆绑文件的最后,我们可以覆盖任何全局样式. - -创建在 `wwwroot` 文件夹下创建一个CSS文件 - -![bookstore-global-css-file](../../images/bookstore-global-css-file.png) - -在CSS文件中定义一些规则. 例如: - -````css -.card-title { - color: orange; - font-size: 2em; - text-decoration: underline; -} - -.btn-primary { - background-color: red; -} -```` - -然后在你的[模块](../../Module-Development-Basics.md) `ConfigureServices` 方法添加这个文件到标准的全局样式捆绑包: - -````csharp -Configure(options => -{ - options.StyleBundles.Configure( - StandardBundles.Styles.Global, //The bundle name! - bundleConfiguration => - { - bundleConfiguration.AddFiles("/styles/my-global-styles.css"); - } - ); -}); -```` - -#### 全局脚本捆绑包 - -就像 `StandardBundles.Styles.Global` 一样,还有一个 `StandardBundles.Scripts.Global`,你可以添加文件或操作现有文件. - -### 示例: 操作捆绑包文件 - -上面的示例中添加了新文件到捆绑包. 如果你创建 **bundle contributor** 类则可以做到更多. 示例: - -````csharp -public class MyGlobalStyleBundleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.Clear(); - context.Files.Add("/styles/my-global-styles.css"); - } -} -```` - -然后你可以添加这个contributor到已存在的捆绑中: - -````csharp -Configure(options => -{ - options.StyleBundles.Configure( - StandardBundles.Styles.Global, - bundleConfiguration => - { - bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); - } - ); -}); -```` - -示例中清除了所有的CSS文件,在现实中这并不是一个好主意,你可以找到某个特定的文件替换成你自己的文件. - -### 示例: 为特定页面添加JavaScript文件 - -上面的示例将全局包添加到布局中. 如果要在依赖模块中为特定页面定义添加CSS/JavaScript文件(或替换文件)怎么做? - -假设你想要用户进入身份模块的**角色管理**页面时运行**JavaScript代码**. - -首先在 `wwwroot`, `Pages` 或 `Views` 文件夹下创建一个标准的JavaScript文件(默认ABP支持这些文件夹下的静态文件). 根据约定我们推荐 `Pages/Identity/Roles` 文件夹: - -![bookstore-added-role-js-file](../../images/bookstore-added-role-js-file.png) - -该文件的内容很简单: - -````js -$(function() { - abp.log.info('My custom role script file has been loaded!'); -}); -```` - -然后将这个文件添加到角色管页面理捆绑包中: - -````csharp -Configure(options => -{ - options.ScriptBundles - .Configure( - typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName, - bundleConfig => - { - bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js"); - }); -}); -```` - -`typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName` 是获取角色管理页面捆绑包名称的安全方式: - -> 请注意并非每个页面都定义了这个页面的捆绑包. 它们仅在需要时定义. - -除了添加新的CSS/JavaScript文件到页面,你也可以以替换(通过捆绑包contributor)已存在. - -## 布局定制 - -布局由主题([参阅主题](Theming.md))定义设计. 它们不包含在下载的应用程序解决方案中. 通过这种方式你可以轻松的**更改**主题并获取新的功能. 你不能**直接更改**应用程序中的布局代码,除非你用自己的布局替换它(在下一部分中说明). - -有一些通用的方法可以**自定义布局**,将在下一节中介绍. - -### 菜单贡献者 - -ABP框架定义了两个**标准菜单**: - -![bookstore-menus-highlighted](../../images/bookstore-menus-highlighted.png) - -* `StandardMenus.Main`: 应用程序的主菜单. -* `StandardMenus.User`: 用户菜单 (通常在屏幕的右上方). - -显示菜单是主题的责任,但**菜单项**由模板和你的应用程序代码决定. 只需要实现 `IMenuContributor` 接口并在 `ConfigureMenuAsync` 方法操作菜单项. - -渲染菜单时需要执行菜单贡献者. **应用程序启动模板** 已经定义了菜单贡献者,所以你可以使用它. 参阅[导航菜单](Navigation-Menu.md)文档了解更多. - -### 工具栏贡献者 - -[工具栏系统](Toolbars.md)用于在用户界面定义 **工具栏** . 模块 (或你的应用程序)可以将 **项** 添加到工具栏, 随后主题将在**布局**上呈现工具栏. - -只有一个 **标准工具栏** (名称为 "Main" - 定义为常量: `StandardToolbars.Main`). 对于基本主题,按如下呈现:![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) - -在上面的屏幕快照中,主工具栏添加了两个项目:语言开关组件和用户菜单. 你可以在此处添加自己的项. - -#### 示例: 添加通知图标 - -在这个示例中,我们会添加一个**通知(响铃)图标**到语言切换项的左侧. 工具栏的项项目是一个**视图组件**. 所以,在你的项目中创建一个新的视图组件: - -![bookstore-notification-view-component](../../images/bookstore-notification-view-component.png) - -**NotificationViewComponent.cs** - -````csharp -public class NotificationViewComponent : AbpViewComponent -{ - public async Task InvokeAsync() - { - return View("/Pages/Shared/Components/Notification/Default.cshtml"); - } -} -```` - -**Default.cshtml** - -````xml -
    - -
    -```` - -现在,我们创建一个类实现 `IToolbarContributor` 接口: - -````csharp -public class MyToolbarContributor : IToolbarContributor -{ - public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) - { - if (context.Toolbar.Name == StandardToolbars.Main) - { - context.Toolbar.Items - .Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); - } - - return Task.CompletedTask; - } -} -```` - -这个类向 `Main` 工具栏的第一项添加了 `NotificationViewComponent`. - -最后你需要将这个贡献者添加到 `AbpToolbarOptions`,在你模块类的 `ConfigureServices` 方法: - -````csharp -Configure(options => -{ - options.Contributors.Add(new MyToolbarContributor()); -}); -```` - -这就够了,当你运行应用程序后会看到工具栏上的通知图标: - -![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) - -示例中的 `NotificationViewComponent` 返回没有任何数据的视图. 在实际场景中,你可能想**查询数据库**(或调用HTTP API)获取通知并传递给视图. 如果需要可以将 `JavaScript` 或 `CSS` 文件添加到工具栏的全局捆绑包中(如前所述). - -参阅[工具栏文档](Toolbars.md)了解更多关于工具栏系统. - -### 布局钩子 - -[布局钩子](Layout-Hooks.md) 系统允许你在布局页面的某些特定部分 **添加代码** . 所有主题的所有布局都应该实现这些钩子. 然后你可以将**视图组件**添加到钩子. - -#### 示例: 添加谷歌统计 - -假设你想要添加谷歌统计脚本到布局(将适用所有的页面). 首先在你的项目中**创建一个视图组件**: - -![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) - -**NotificationViewComponent.cs** - -````csharp -public class GoogleAnalyticsViewComponent : AbpViewComponent -{ - public IViewComponentResult Invoke() - { - return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); - } -} -```` - -**Default.cshtml** - -````html - -```` - -在你自己的代码中更改 `UA-xxxxxx-1` . - -然后你可以在你模块的 `ConfigureServices` 方法将这个组件添加到任何的钩子点: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, //The hook name - typeof(GoogleAnalyticsViewComponent) //The component to add - ); -}); -```` - -现在谷歌统计代码将在页面的 `head` 所为最后一项插入. 你(或你在使用的模块)可以将多个项添加到相同的钩子,它们都会添加到布局. - -在上面我们添加 `GoogleAnalyticsViewComponent` 到所有的布局,你可能只想添加到指定的布局: - -````csharp -Configure(options => -{ - options.Add( - LayoutHooks.Head.Last, - typeof(GoogleAnalyticsViewComponent), - layout: StandardLayouts.Application //Set the layout to add - ); -}); -```` - -参阅下面的布局部分,以了解有关布局系统的更多信息. - -### 布局 - -布局系统允许主题定义标准,命名布局并且允许任何页面选择使用合适的布局. 有三种预定义的布局: - -* "**Application**": 应用程序的主要(和默认)布局. 它通常包含页眉,菜单(侧栏),页脚,工具栏等. -* "**Account**": 登录,注册和其他类似页面使用此布局. 默认它用于 `/Pages/Account` 文件夹下的页面. -* "**Empty**": 空的最小的布局. - -这些名称在 `StandardLayouts` 类定义为常量. 这是标准的布局名称,所有的主题开箱即用的实现. 你也可以创建自己的布局. - -#### 布局位置 - -你可以在[这里](https://github.com/abpframework/abp/blob/dev/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts)找到基本主题的布局文件. 你可以将它们作用构建自己的布局的参考,也可以在必要时覆盖它们. - -#### ITheme - -ABP框架使用 `ITheme` 服务通过局部名称获取布局位置. 你可以替换此服务动态的选择布局位置. - -#### IThemeManager - -`IThemeManager` 用于获取当前主题,并得到了布局路径. 任何页面可以都决定自己的布局. 例: - -````html -@using Volo.Abp.AspNetCore.Mvc.UI.Theming -@inject IThemeManager ThemeManager -@{ - Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); -} -```` - -此页面将使用空白布局. 它使用 `ThemeManager.CurrentTheme.GetEmptyLayout()` 扩展方法. - -如果你设置特定目录下所有页面的布局,可以在该文件夹下的 `_ViewStart.cshtml` 文件编写以上代码. diff --git a/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md b/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md deleted file mode 100644 index 391a63a910..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md +++ /dev/null @@ -1,3 +0,0 @@ -# Dynamic JavaScript HTTP API Proxies - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/JavaScript-API/Index.md b/docs/zh-Hans/UI/AspNetCore/JavaScript-API/Index.md deleted file mode 100644 index b06025cd16..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/JavaScript-API/Index.md +++ /dev/null @@ -1,23 +0,0 @@ -# JavaScript API - -ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. - -## APIs - -* abp.ajax -* [abp.auth] -* abp.currentUser -* abp.dom -* abp.event -* abp.features -* abp.localization -* abp.log -* abp.ModalManager -* abp.notify -* abp.security -* abp.setting -* abp.ui -* abp.utils -* abp.ResourceLoader -* abp.WidgetManager -* Other APIs \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md b/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md deleted file mode 100644 index 49b334c6c0..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md +++ /dev/null @@ -1,3 +0,0 @@ -# Layout Hooks - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Libraries/DatatablesNet.md b/docs/zh-Hans/UI/AspNetCore/Libraries/DatatablesNet.md deleted file mode 100644 index 029691e720..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Libraries/DatatablesNet.md +++ /dev/null @@ -1,3 +0,0 @@ -# ABP ASP.NET Core UI Datatables.Net 集成 - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md b/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md deleted file mode 100644 index 4e0164ec72..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md +++ /dev/null @@ -1,3 +0,0 @@ -# Navigation Menu - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md deleted file mode 100644 index fa70c1c9b0..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md +++ /dev/null @@ -1,80 +0,0 @@ -# Alerts - -## 介绍 - -`abp-alert` 是创建alert的主要元素. - -基本用法: - -````xml - - A simple primary alert—check it out! - -```` - -## Demo - -参阅[alerts demo页面](https://bootstrap-taghelpers.abp.io/Components/Alerts)查看示例. - -## Attributes - -### alert-type - -值做为alert的Type,应为以下值之一: - -* `Default` (默认值) -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` - -示例: - -````xml - - A simple warning alert—check it out! - -```` - -### alert-link - -alert的链接. - -示例: - -````xml - - A simple danger alert with an example link. Give it a click if you like. - -```` - -### dismissible - -使alert可被忽略: - -示例: - -````xml - - Holy guacamole! You should check in on some of those fields below. - -```` - -### Additional content - -`abp-alert`还可以包含其他HTML元素,例如标题,段落和分隔符. - -示例: - -````xml - -

    Well done!

    -

    Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

    -
    -

    Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

    -
    -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Badges.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Badges.md deleted file mode 100644 index b15dd502b8..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Badges.md +++ /dev/null @@ -1,38 +0,0 @@ -# 徽章 - -## 结合扫 - -`abp-badge` 和 `abp-badge-pill` 是abp徽章标签. - -基本用法: - -````csharp -Primary -Info -Danger -```` - -## Demo - -参阅[徽章demo页面](https://bootstrap-taghelpers.abp.io/Components/Badges)查看示例. - -### Values - -* 表示徽章的类型. 应为下列值之一: - - * `_` (默认值) - * `Default` (默认值) - * `Primary` - * `Secondary` - * `Success` - * `Danger` - * `Warning` - * `Info` - * `Light` - * `Dark` - -示例: - -````csharp -Danger -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Borders.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Borders.md deleted file mode 100644 index 5b57ce76c3..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Borders.md +++ /dev/null @@ -1,124 +0,0 @@ -# 边框 - -## 介绍 - -`abp-border` 是边框样式的主要元素. - -基本用法: - -````csharp - - - - - -```` - -## Demo - -参阅[边框demo页面](https://bootstrap-taghelpers.abp.io/Components/Borders)查看示例. - -## Values - -值代表类型,位置和边框的颜色.应为下列值之一: - -* `Default` -* `_0` -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` -* `White` -* `Primary_0` -* `Secondary_0` -* `Success_0` -* `Danger_0` -* `Warning_0` -* `Info_0` -* `Light_0` -* `Dark_0` -* `White_0` -* `Top` -* `Top_0` -* `Top_Primary` -* `Top_Secondary` -* `Top_Success` -* `Top_Danger` -* `Top_Warning` -* `Top_Info` -* `Top_Light` -* `Top_Dark` -* `Top_White` -* `Top_Primary_0` -* `Top_Secondary_0` -* `Top_Success_0` -* `Top_Danger_0` -* `Top_Warning_0` -* `Top_Info_0` -* `Top_Light_0` -* `Top_Dark_0` -* `Top_White_0` -* `Right` -* `Right_0` -* `Right_Primary` -* `Right_Secondary` -* `Right_Success` -* `Right_Danger` -* `Right_Warning` -* `Right_Info` -* `Right_Light` -* `Right_Dark` -* `Right_White` -* `Right_Primary_0` -* `Right_Secondary_0` -* `Right_Success_0` -* `Right_Danger_0` -* `Right_Warning_0` -* `Right_Info_0` -* `Right_Light_0` -* `Right_Dark_0` -* `Right_White_0` -* `Left` -* `Left_0` -* `Left_Primary` -* `Left_Secondary` -* `Left_Success` -* `Left_Danger` -* `Left_Warning` -* `Left_Info` -* `Left_Light` -* `Left_Dark` -* `Left_White` -* `Left_Primary_0` -* `Left_Secondary_0` -* `Left_Success_0` -* `Left_Danger_0` -* `Left_Warning_0` -* `Left_Info_0` -* `Left_Light_0` -* `Left_Dark_0` -* `Left_White_0` -* `Bottom` -* `Bottom_0` -* `Bottom_Primary` -* `Bottom_Secondary` -* `Bottom_Success` -* `Bottom_Danger` -* `Bottom_Warning` -* `Bottom_Info` -* `Bottom_Light` -* `Bottom_Dark` -* `Bottom_White` -* `Bottom_Primary_0` -* `Bottom_Secondary_0` -* `Bottom_Success_0` -* `Bottom_Danger_0` -* `Bottom_Warning_0` -* `Bottom_Info_0` -* `Bottom_Light_0` -* `Bottom_Dark_0` -* `Bottom_White_0` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md deleted file mode 100644 index 10bc729fcb..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md +++ /dev/null @@ -1,25 +0,0 @@ -# 面包屑 - -## Introduction - -`ABP-breadcrumb` 是面包屑项主容器. - -基本用法: - -````csharp - - - - - -```` - -## Demo - -参阅[面包屑demo页面](https://bootstrap-taghelpers.abp.io/Components/Breadcrumbs)查看示例. - -## abp-breadcrumb-item Attributes - -- **title**: 设置面包屑项文本. -- **active**: 设置活动面包屑项. 如果没有其他项是活动的,默认最后一项为活动项. -- **href**: 表示 `abp-breadcrumb-item` 是否有链接. 值应该是字符串链接. \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Button-groups.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Button-groups.md deleted file mode 100644 index b69f39c7b4..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Button-groups.md +++ /dev/null @@ -1,37 +0,0 @@ -# 按钮组 - -## 介绍 - -`abp-button-group` 是创建分组按钮的主要元素. - -基本用法: - -````csharp - - Left - Middle - Right - -```` - -## Demo - -参阅[按钮组demo页面](https://bootstrap-taghelpers.abp.io/Components/Button-groups)查看示例. - -## Attributes - -### direction - -按钮的方向. 应为以下值之一: - -* `Horizontal` (默认值) -* `Vertical` - -### size - -组中按钮的大小. 应为以下值之一: - -* `Default` (默认值) -* `Small` -* `Medium` -* `Large` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md deleted file mode 100644 index f628f17ef4..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md +++ /dev/null @@ -1,86 +0,0 @@ -# 按钮 - -## 介绍 - -`abp-button` 是创建按钮的主要元素. - -基本用法: - -````xml -Click Me -```` - -## Demo - -参阅[按钮Demo页面](https://bootstrap-taghelpers.abp.io/Components/Buttons)查看示例. - -## Attributes - -### `button-type` - -指定按钮的主样式/类型. 应为以下值之一: - -* `Default` (默认值) -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` -* `Outline_Primary` -* `Outline_Secondary` -* `Outline_Success` -* `Outline_Danger` -* `Outline_Warning` -* `Outline_Info` -* `Outline_Light` -* `Outline_Dark` -* `Link` - -### `size` - -指定按钮的大小. 应为以下值之一: - -* `Default` -* `Small` -* `Medium` -* `Large` -* `Block` -* `Block_Small` -* `Block_Medium` -* `Block_Large` - -### `busy-text` - -当按钮busy时显示的文本. - -### `text` - -按钮的文本. 如果你只想为为按钮设置文本,这是一种快捷方式. 例: - -````xml - -```` - -在这个示例中,你可以使用 self-closing 标签将其缩短. - -### `icon` - -设置按钮的图标. 默认情况下它使用[Font Awesome](https://fontawesome.com/)图标库. 例: - -````xml - -```` - -### `icon-type` - -如果你不想使用font-awesome,你有两个选项: - -1. 设置 `icon-type` 为 `Other`,并为你的按钮编写图标样式. -2. 如果你不使用图标,请手动使用opening和closing标签,并在标签内写任何代码. - -### `disabled` - -设置为 `true` 禁用按钮. \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md deleted file mode 100644 index 4cb75ea3b0..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md +++ /dev/null @@ -1,180 +0,0 @@ -# 卡片 - -## 介绍 - -`abp-card` 是从bootstrap card元素派生的内容容器. - -基本用法: - -````xml - - - - Card Title - Some quick example text to build on the card title and make up the bulk of the card's content. - Go somewhere - - -```` - -##### 使用标题,文本和链接: - -`abp-card` 可以使用以下标签 - -* `abp-card-title` -* `abp-card-subtitle` -* `a abp-card-link` - -示例: - -````xml - - - Card title - Card subtitle - Some quick example text to build on the card title and make up the bulk of the card's content. - Card link - Another link - - -```` - -##### 使用列表组: - -* `abp-list-group flush="true"` : `flush` 属性渲染到 bootstrap `list-group-flush` 的 calss 中,该类用于删除边界和圆角以使列表组项在父容器中并排显示. -* `abp-list-group-item` - -示例: - -````xml - - - - Card Title - Some quick example text to build on the card title and make up the bulk of the card's content. - - - Cras justo odio - Dapibus ac facilisis in - Vestibulum at eros - - - Card link - Another link - - -```` - -##### 使用页眉,页脚和块引用: - -* `abp-card-header` -* `abp-card-footer` -* `abp-blockquote` - -示例: - -```xml - - Featured - - Special title treatment - With supporting text below as a natural lead-in to additional content. - Go somewhere - - -``` - -引用示例: - -```xml - - Quote - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

    -
    Someone famous in Source Title
    -
    -
    -
    -``` - -页脚示例: - -```xml - - Featured - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

    -
    Someone famous in Source Title
    -
    -
    - 2 days ago -
    -``` - -## Demo - -参阅[卡片demo页面](https://bootstrap-taghelpers.abp.io/Components/Cards)查看示例. - -## abp-card Attributes - -- **background:** 值指定卡片背景的颜色. -- **text-color**: 值指定卡片内文本的颜色. -- **border:** 值指定卡片边框的颜色. - -应为以下值之一: - -* `Default` (默认值) -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` - -示例: - -````xml - -```` - -### sizing - -卡片的默认值为100%,可以使用自定义CSS,栅格类,栅格Sass mixins或[utilities](https://getbootstrap.com/docs/4.0/utilities/sizing/)进行更改. - -````xml - -```` - -### card-deck 和 card-columns - -`abp-card` 可以在 `card-deck` 或 `card-columns` 里使用. - -````xml -
    - - First Deck - - Ace - Here is the content for Ace. - - - - Second Deck - - Beta - Beta content. - - - - Third Deck - - Epsilon - Content for Epsilon. - - -
    -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Carousel.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Carousel.md deleted file mode 100644 index f230428c15..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Carousel.md +++ /dev/null @@ -1,71 +0,0 @@ -# 轮播 - -## 介绍 - -`abp-carousel` 是abp标签轮播元素 - -基本用法: - -````csharp - - - - - -```` - -## Demo - -参阅[轮播demo页面](https://bootstrap-taghelpers.abp.io/Components/Carousel)查看示例. - -## Attributes - -### id - -轮播的ID. 如果未设置则会生成一个ID. - -### controls - -用于启用轮播上的控件(previous和next按钮). 应为以下值之一: - -* `false` -* `true` - -### indicators - -启用轮播指标. 应为以下值之一: - -* `false` -* `true` - -### crossfade - -用于启用淡入淡出动画而不是在轮播上滑动. 应为以下值之一: - -* `false` -* `true` - -## abp-carousel-item Attributes - -### caption-title - -设置轮播项的标题 - -### caption - -设置轮播项的说明. - -### src - -链接值设置显示在轮播项上的图像的来源. - -### active - -设置活动轮播项. 应为以下值之一: - -* `false` -* `true` - -### alt - -当无法显示图像时,该值设置轮播项目图像的替代文本. \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md deleted file mode 100644 index 65327de438..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md +++ /dev/null @@ -1,90 +0,0 @@ -# Collapse - -## 介绍 - -`abp-collapse-body` 是显示和隐藏内容的主要容器. `abp-collapse-id` 用于显示和隐藏内容容器. 可以通过 `abp-button` 和 `a` 标签触发. - -基本用法: - -````xml - - Link with href - - - Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. - -```` - -## Demo - -参阅[collapse demo 页面](https://bootstrap-taghelpers.abp.io/Components/Collapse)查看示例. - -## Attributes - -### show - -指定折叠主体初始化时是折叠还是展开. 应为以下值之一: - -* `false` (默认值) -* `true` - -### multi - -指定 `abp-collapse-body` 是否可以通过显示/隐藏多个折叠体的元素显示或隐藏. 此属性做为 "multi-collapse" class 添加到 `abp-collapse-body`. 应为以下值之一: - -* `false` (默认值) -* `true` - -示例: - -````xml - Toggle first element - - - - - - - Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit - - - - - Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. - - - -```` - -## 手风琴示例 - -`abp-accordion` 是手风琴项的主容器. - -基本用法: - -````xml - - - Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. - - - Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. - - - Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. - - -```` - -## Attributes - -### active - -指定手风琴项目在初始化时是显示还是隐藏. 应为以下值之一: - -* `false` (默认值) -* `true` - -### title - -指定手风琴项的标题. 应为字符串类型的值. diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md deleted file mode 100644 index 971dce7a1c..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md +++ /dev/null @@ -1,93 +0,0 @@ -# 下拉菜单 - -## 介绍 - -`abp-dropdown` 是下拉菜单的主要容器. - -基本用法: - -````xml - - - - Action - Another action - Something else here - - -```` - -## Demo - -参阅 [下拉菜单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. - -## Attributes - -### direction - -指定下拉菜单的方向. 应为以下值之一: - -* `Down` (默认值) -* `Up` -* `Right` -* `Left` - -### dropdown-style - -指定 `abp-dropdown-button` 是否具有用于拆分的拆分图标. 应为以下值之一: - -* `Single` (默认值) -* `Split` - -## Menu items - -`abp-dropdown-menu` 下拉菜单项的主要容器. - -基本用法: - -````xml - - - - Dropdown Header - Action - Active action - Disabled action - - Dropdown Item Text - Something else here - - -```` - -## Attributes - -### align - -指定 `abp-dropdown-menu` 在哪个方向对齐. 应为以下值之一: - -* `Left` (默认值) -* `Right` - -### Additional content - -`abp-dropdown-menu` 也可以包含其他HTML元素,例如标题,段落,分隔符或form元素. - -示例: - -````xml - - - -
    - - - - - - - New around here? Sign up - Forgot password? -
    -
    -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md deleted file mode 100644 index 5672eebe09..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ /dev/null @@ -1,281 +0,0 @@ -# 动态表单 - -`提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档. - -## 介绍 - -`abp-dynamic-form` 为给定c#模型创建bootstrap表单. - -基本用法: - -````xml - -```` - -Model: - -````csharp -public class DynamicFormsModel : PageModel -{ - [BindProperty] - public DetailedModel MyDetailedModel { get; set; } - - public List CountryList { get; set; } = new List - { - new SelectListItem { Value = "CA", Text = "Canada"}, - new SelectListItem { Value = "US", Text = "USA"}, - new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} - }; - - public void OnGet() - { - MyDetailedModel = new DetailedModel - { - Name = "", - Description = "Lorem ipsum dolor sit amet.", - IsActive = true, - Age = 65, - Day = DateTime.Now, - MyCarType = CarType.Coupe, - YourCarType = CarType.Sedan, - Country = "RU", - NeighborCountries = new List() { "UK", "CA" } - }; - } - - public class DetailedModel - { - [Required] - [Placeholder("Enter your name...")] - [Display(Name = "Name")] - public string Name { get; set; } - - [TextArea(Rows = 4)] - [Display(Name = "Description")] - [InputInfoText("Describe Yourself")] - public string Description { get; set; } - - [Required] - [DataType(DataType.Password)] - [Display(Name = "Password")] - public string Password { get; set; } - - [Display(Name = "Is Active")] - public bool IsActive { get; set; } - - [Required] - [Display(Name = "Age")] - public int Age { get; set; } - - [Required] - [Display(Name = "My Car Type")] - public CarType MyCarType { get; set; } - - [Required] - [AbpRadioButton(Inline = true)] - [Display(Name = "Your Car Type")] - public CarType YourCarType { get; set; } - - [DataType(DataType.Date)] - [Display(Name = "Day")] - public DateTime Day { get; set; } - - [SelectItems(nameof(CountryList))] - [Display(Name = "Country")] - public string Country { get; set; } - - [SelectItems(nameof(CountryList))] - [Display(Name = "Neighbor Countries")] - public List NeighborCountries { get; set; } - } - - public enum CarType - { - Sedan, - Hatchback, - StationWagon, - Coupe - } -} -```` - -## Demo - -参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. - -## Attributes - -### abp-model - -为动态表单设置c#模型,模型的属性以表单形式转化为输入. - -### column-size - -此处使用 `col-sm` 来设置大小。当设置该属性是会同时添加 `col-12` . - -### submit-button - -可以为 `True` 或 `False`. - -如果为 `True`,则会在表单底部生成一个提交按钮. - -默认值是 `False`. - -### required-symbols - -可以为 `True` 或 `False`. - -如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的. - -默认值是 `True`. - -## 表单内容布局 - -默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用``标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变. - -用法: - -````xml - -
    - Some content.... -
    -
    - -
    -
    - Some more content.... -
    -
    -```` - -## 输入排序 - -`abp-dynamic-form` 通过 `DisplayOrder` attribute对属性进行排序,然后按模型类中的属性顺序进行排序. - -默认每个属性的 `DisplayOrder` attribute值是10000. - -参见以下示例: - -````csharp -public class OrderExampleModel -{ - [DisplayOrder(10004)] - public string Name{ get; set; } - - [DisplayOrder(10005)] - public string Surname{ get; set; } - - //Default 10000 - public string EmailAddress { get; set; } - - [DisplayOrder(10003)] - public string PhoneNumber { get; set; } - - [DisplayOrder(9999)] - public string City { get; set; } -} -```` - -在这个示例中,inputs字段顺序为: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. - -## 忽略属性 - -默认情况下, `abp-dynamic-form` 会为模型类中的每个属性生成输入. 如果要忽略属性请使用 `DynamicFormIgnore` attribute. - -参见以下示例: - -````csharp -public class MyModel -{ - public string Name { get; set; } - - [DynamicFormIgnore] - public string Surname { get; set; } -} -```` - -在这个示例中,不会为 `Surname` 属性生成输入. - -## 指示文本框,单选按钮组和组合框 - -如果你已经阅读了[表单元素文档](Form-elements.md),你会注意到在c#模型上 `abp-radio` 和 `abp-select` 标签非常相. 我们必须使用 `[AbpRadioButton()]` attribute来告诉 `abp-dynamic-form` 你的哪些属性是单选按钮组,哪些属性是组合框. - -参见以下示例: - -````xml - -```` - -Model: - -````csharp -public class DynamicFormsModel : PageModel -{ - [BindProperty] - public DetailedModel MyDetailedModel { get; set; } - - public List CountryList { get; set; } = new List - { - new SelectListItem { Value = "CA", Text = "Canada"}, - new SelectListItem { Value = "US", Text = "USA"}, - new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} - }; - - public void OnGet() - { - MyDetailedModel = new DetailedModel - { - ComboCarType = CarType.Coupe, - RadioCarType = CarType.Sedan, - ComboCountry = "RU", - RadioCountry = "UK" - }; - } - - public class DetailedModel - { - public CarType ComboCarType { get; set; } - - [AbpRadioButton(Inline = true)] - public CarType RadioCarType { get; set; } - - [SelectItems(nameof(CountryList))] - public string ComboCountry { get; set; } - - [AbpRadioButton()] - [SelectItems(nameof(CountryList))] - public string RadioCountry { get; set; } - } - - public enum CarType - { - Sedan, - Hatchback, - StationWagon, - Coupe - } -} -```` - -正如你上面的例子中看到: - -* 如果在**Enum**属性上使用 `[AbpRadioButton()]`,它将是一个单选按钮组. 否则它是组合框. -* 如果在属性上使用 `[SelectItems()]` 和 `[AbpRadioButton()]`,那么它将是一个单选按钮组. -* 如果只在属性上使用 `[SelectItems()]`,它将是一个组合框. -* 如果一个属性没有使用这些属性,它将是一个文本框. - -## 本地化 - -`abp-dynamic-form` 会处理本地化. - -默认情况下, 它将尝试查找 "DisplayName:{PropertyName}" 或 "{PropertyName}" 定位本地化键,并将定位值设置为label. - -你可以使用Asp.Net Core的 `[Display()]` attribute自行设置. 可以在此属性中使用本地化密钥. 请参阅以下示例: - -````csharp -[Display(Name = "Name")] -public string Name { get; set; } -```` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Form-elements.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Form-elements.md deleted file mode 100644 index 57e66cc888..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Form-elements.md +++ /dev/null @@ -1,267 +0,0 @@ - -# 表单元素 - -## 简介 - -Abp提供表单输入标签助手以便更轻松地构建表单。 - -## 演示 - -请查看[表单元素演示页面](https://bootstrap-taghelpers.abp.io/Components/FormElements)。 - -## abp-input - -`abp-input`标签为给定的C#属性创建一个Bootstrap表单输入。它在后台使用了[Asp.Net Core Input Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper),所以Asp.Net Core的每个input标签助手的数据注释属性对于`abp-input`也有效。 - -用法: - -````xml - - - - -```` - -Model: - -````csharp - public class FormElementsModel : PageModel - { - public SampleModel MyModel { get; set; } - - public void OnGet() - { - MyModel = new SampleModel(); - } - - public class SampleModel - { - [Required] - [Placeholder("Enter your name...")] - [InputInfoText("What is your name?")] - public string Name { get; set; } - - [Required] - [FormControlSize(AbpFormControlSize.Large)] - public string SurName { get; set; } - - [TextArea(Rows = 4)] - public string Description { get; set; } - - [Required] - [DataType(DataType.Password)] - public string Password { get; set; } - - public bool IsActive { get; set; } - } - } -```` - -### 特性(Attributes) - -你可以在你的c#属性上设置一些特性,或者直接在html标签上设置。如果您要在[abp-dynamic-form](Dynamic-Forms.md)中使用此属性,则只能通过属性特性设置这些属性。 - - -#### 属性特性(Property Attributes) - -- `[TextArea()]`: 将输入转换为文本区域。 - -* `[Placeholder()]`: 为输入设置占位符。您可以直接使用本地化键。 -* `[InputInfoText()]`: 为输入设置小型信息文本。您可以直接使用本地化键。 -* `[FormControlSize()]`: 设置表单控件包装器元素的大小。可用的值为 - - `AbpFormControlSize.Default` - - `AbpFormControlSize.Small` - - `AbpFormControlSize.Medium` - - `AbpFormControlSize.Large` -* `[DisabledInput]` : 输入被禁用。 -* `[ReadOnlyInput]`: 输入为只读。 - -#### 标签属性(Tag Attributes) - -* `info`: 为输入设置小型信息文本。您可以直接使用本地化键。 -* `auto-focus`: 如果为true,则浏览器会自动聚焦在该元素上。 -* `size`: 设置表单控件包装器元素的大小。可用的值为 - - `AbpFormControlSize.Default` - - `AbpFormControlSize.Small` - - `AbpFormControlSize.Medium` - - `AbpFormControlSize.Large` -* `disabled`: 输入被禁用。 -* `readonly`: 输入为只读。 -* `label`: 为输入设置标签。 -* `display-required-symbol`: 如果输入为必填项,则向标签添加必需符号(*)。默认为`True`。 - -[Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper)的`asp-format`、`name`和`value`属性也对`abp-input`标签助手有效。 - -### 标签和本地化 - -您可以通过不同的方式设置输入的标签: - -- 您可以使用`Label`属性直接设置标签。 但是,它不会自动本地化您的本地化键。 因此,请使用`label = "@L [“ {LocalizationKey}”] .Value"`。 -- 您可以使用Asp.Net Core的`[Display(name = "{LocalizationKey}")]`属性设置标签。 -- 您可以让 **abp** 查找属性的本地化键。 如果未设置`label`或`[DisplayName]`属性,则会尝试查找“DisplayName:{PropertyName}”或“{PropertyName}”本地化键。 - -## abp-select - -`abp-select` 标签为给定的 C# 属性创建了一个 Bootstrap 表单选择器。它在后台使用 [Asp.Net Core 选择标签助手](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper),因此 `Asp.Net Core` 的 `select` 标签助手的每个数据注释属性也适用于 `abp-select`。 - -`abp-select` 标签需要一个 `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem` 的列表来工作。它可以通过标签上的 `asp-items` 属性或 C# 属性上的 `[SelectItems()]` 属性来提供(如果您使用的是 [abp-dynamic-form](Dynamic-Forms.md),则只能使用 C# 属性的方式。) - -`abp-select` 支持多重选择。 - -`abp-select` 自动为 **枚举(Enum)** 属性创建选择列表。不需要额外的数据。如果属性是可空的,则会在自动生成的列表顶部添加一个空键和值。 - -使用: - -````xml - - - - - - - - - -```` - -Model: - -````csharp - public class FormElementsModel : PageModel - { - public SampleModel MyModel { get; set; } - - public List CityList { get; set; } - - public void OnGet() - { - MyModel = new SampleModel(); - - CityList = new List - { - new SelectListItem { Value = "NY", Text = "New York"}, - new SelectListItem { Value = "LDN", Text = "London"}, - new SelectListItem { Value = "IST", Text = "Istanbul"}, - new SelectListItem { Value = "MOS", Text = "Moscow"} - }; - } - - public class SampleModel - { - public string City { get; set; } - - [SelectItems(nameof(CityList))] - public string AnotherCity { get; set; } - - public List MultipleCities { get; set; } - - public CarType MyCarType { get; set; } - - public CarType? MyNullableCarType { get; set; } - } - - public enum CarType - { - Sedan, - Hatchback, - StationWagon, - Coupe - } - } -```` - -### 属性 - -您可以在 C# 属性上或直接在 HTML 标签上设置一些属性。如果您将在 [abp-dynamic-form](Dynamic-Forms.md) 中使用此属性,则只能通过属性属性设置这些属性。 - -#### 属性特性(Property Attributes) - -* `[SelectItems()]`: 设置选择数据。参数应为数据列表的名称。(见上面的示例) - -- `[InputInfoText()]`:设置输入的小信息文本。您可以直接使用本地化键。 -- `[FormControlSize()]`:设置表单控件包装元素的大小。可用值为 - - `AbpFormControlSize.Default` - - `AbpFormControlSize.Small` - - `AbpFormControlSize.Medium` - - `AbpFormControlSize.Large` - -#### 标签属性(Tag Attributes) - -- `asp-items`: 设置选择数据。这应该是一个 `SelectListItem` 列表。 -- `info`: 为输入设置一个小的信息文本。您可以直接使用本地化键。 -- `size`: 设置表单控件包装元素的大小。可用值为 - - `AbpFormControlSize.Default` - - `AbpFormControlSize.Small` - - `AbpFormControlSize.Medium` - - `AbpFormControlSize.Large` -- `label`: 为输入设置标签。 -- `display-required-symbol`: 如果输入是必需的,则向标签添加必需符号 (*)。默认为 `True`。 -- `floating-label`: 设置输入的标签是否应该是浮动的。默认为 `False`。 - - -### 标签和本地化 - -您可以以不同的方式设置输入的标签: - -- 您可以使用 `Label` 属性并直接设置标签。但是它不会自动本地化您的本地化键。因此,请将其用作 `label="@L["{LocalizationKey}"].Value"`。 -- 您可以使用 Asp.Net Core 的 `[Display(name="{LocalizationKey}")]` 属性进行设置。 -- 您可以让 **abp** 查找属性的本地化键。它将尝试查找“DisplayName:{PropertyName}”或“{PropertyName}”本地化键。 - -对于**枚举**属性,`abp-select`会设置下拉框的本地化值。它会查找"{EnumTypeName}.{EnumPropertyName}"或"{EnumPropertyName}"的本地化键。例如,在上面的示例中,当它本地化下拉框的值时,它将使用"CarType.StationWagon"或"StationWagon"键。 - -## abp-radio - -`abp-radio`标签为给定的C#属性创建一个Bootstrap表单单选框组。使用方式与`abp-select`标签非常相似。 - -Usage: - -````xml - - - -```` - -Model: - -````csharp - public class FormElementsModel : PageModel - { - public SampleModel MyModel { get; set; } - - public List CityList { get; set; } = new List - { - new SelectListItem { Value = "NY", Text = "New York"}, - new SelectListItem { Value = "LDN", Text = "London"}, - new SelectListItem { Value = "IST", Text = "Istanbul"}, - new SelectListItem { Value = "MOS", Text = "Moscow"} - }; - - public void OnGet() - { - MyModel = new SampleModel(); - MyModel.CityRadio = "IST"; - MyModel.CityRadio2 = "MOS"; - } - - public class SampleModel - { - public string CityRadio { get; set; } - - [SelectItems(nameof(CityList))] - public string CityRadio2 { get; set; } - } - } -```` - -### 特性(attributes) - -您可以在C#属性或直接在HTML标签上设置一些属性。如果您将在[abp-dynamic-form](Dynamic-Forms.md)中使用此属性,则只能通过属性属性设置这些属性。 - -#### 属性特性(Property Attributes) - -- `[SelectItems()]`:设置选择数据。参数应为数据列表的名称。 (见上面的示例) - -#### 标签特性(Tag Attributes) - -- `asp-items`: 设置选择数据。这应该是一个SelectListItem列表。 -- `Inline`: 如果为true,则单选按钮将在单行中,相互紧挨着。如果为false,则它们将在彼此下面。 diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md deleted file mode 100644 index 0605e893f0..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md +++ /dev/null @@ -1,273 +0,0 @@ -# 栅格 - -## 介绍 - -ABP标签助手基于bootstrap栅格系统. - -## Demo - -参阅[网络demo页面](https://bootstrap-taghelpers.abp.io/Components/Grids)查看示例. - -### Sizing - -**等宽:** 创建等宽的列. - -示例: - -````xml - - - 1 of 2 - 2 of 2 - - - 1 of 3 - 2 of 3 - 3 of 3 - - -```` - -**列中断:** `abp-column-breaker` 用于中断当前行的自动放置宽度,然后在新行中开始. - -示例: - -````xml - - - column - column - - column - column - - -```` - -**设置列宽度:** size属性用于设置特定列的宽度. - -示例: - -```xml - - - 1 of 3 - 2 of 3 (wider) - 3 of 3 - - - 1 of 3 - 2 of 3 (wider) - 3 of 3 - - -``` - -**可变宽度内容:** 根据内容自动调整列的大小. - -```xml - - - 1 of 3 - Contrary to popular belief, Lorem Ipsum is not simply random text. - 3 of 3 - - - 1 of 3 - Variable width content - 3 of 3 - - -``` - -### 响应类 - -响应式类可以在abp标签中强类型使用. - -```xml - - col-sm-8 - col-sm-4 - - - col-sm - col-sm - col-sm - col-sm - - - - .col-12 .col-md-8 - .col-6 .col-md-4 - - - - - .col-6 .col-md-4 - .col-6 .col-md-4 - .col-6 .col-md-4 - - - - - .col-6 - .col-6 - -``` - -### 对齐 - -可以使用强类型的abp标签在垂直和水平方向上进行列对齐. - -**垂直对齐**: `v-align` 属性值用于对垂直齐列. - -示例: - -```xml - - - column - column - column - - - column - column - column - - - column - column - column - - -``` - -**水平对齐**: `h-align` 属性值用于对水平齐列. - -示例: - -```xml - - - One of two columns - One of two columns - - - One of two columns - One of two columns - - - One of two columns - One of two columns - - - One of two columns - One of two columns - - - One of two columns - One of two columns - - -``` - -**无间隙**: 可以使用 `gutters="false"` 删除预定义栅格类中列之间的栅格线. 这会从 `abp-row` 中消除负边距,并从所有直接子列中消除水平边距. - -示例: - -```xml - - One of two columns - One of two columns - -``` - -**列包装**: 如果在一行中放置超过12列,则将每组额外的列作为一个单元包装到新行上. - -示例: - -```xml - - .col-9 - .col-4
    Since 9 + 4 = 13 > 12, this 4-column-wide div gets wrapped onto a new line as one contiguous unit.
    - .col-6
    Subsequent columns continue along the new line.s
    -
    -``` - -### 重新排序 - -**Order类**: `order` 属性用于控制内容的视觉顺序. - -示例: - -```xml - - - First, but Last - Second, but unordered - Third, but Second - - -``` - -**偏移列**: `offset` 属性用于设置栅格列的偏移量. - -示例: - -```xml - - - .col-md-4 - .col-md-4 .offset-md-4 - - - .col-md-3 .offset-md-3 - .col-md-3 .offset-md-3 - - - .col-md-6 .offset-md-3 - - - .col-sm-5 .col-md-6 - .col-sm-5 .offset-sm-2 .col-md-6 .offset-md-0 - - - col-sm-6 .col-md-5 .col-lg-6 - .col-sm-6 .col-md-5 .offset-md-2 .col-lg-6 .offset-lg-0 - - -``` - -## abp-row Attributes - -- **v-align:** 指定包含列的垂直位置. 应为以下值之一: - * `Default` (默认值) - * `Start` - * `Center` - * `End` - -- **h-align**: 指定包含列的水平位置. 应为以下值之一: - * `Default` (默认值) - * `Start` - * `Center` - * `Around` - * `Between` - * `End` -- **gutter**: 指定是否将从所有子列中删除负边距和水平填充. 如果未设置,默认为 `true`. 应为以下值之一: - * `true` - * `false` - -## abp-column Attributes - -- **size:** 指定列的宽度: `_`, `Undefined`, `_1`..`_12`, `Auto`. 或者可以与预定义值一起使用,例如: - - `size-sm` - - `size-md` - - `size-lg` - - `size-xl` -- **order**: 指定列的顺序: `Undefined`, `_1`..`_12`, `First` 和 `Last`. -- **offset:** 指定列的偏移量: `_`, `Undefined`, `_1`..`_12`, `Auto`. 或者可以与预定义值一起使用,例如: - - `offset-sm` - - `offset-md` - - `offset-lg` - - `offset-xl` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md deleted file mode 100644 index 296d2e210f..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md +++ /dev/null @@ -1,38 +0,0 @@ -# ABP Tag Helpers - -ABP框架定义了一组**标签助手组件**. 简化开发ASP.NET Core (MVC / Razor Pages) 应用程序界面. - -## bootstrap 组件包装 - -大多数标签助手是[Bootstrap](https://getbootstrap.com/) (v5+)的包装. 编写bootstrap代码并不是那么简单,其中包含太多的重复HTML标签并且也没有类型安全. ABP标签助手使其 **简单** 并且 **类型安全**. - -我们的目标并不是100%的包装bootstrap组件. 仍然可以编写 **原生bootstrap代码** (实际上标签助手生成的也是原生的bootstrap代码), 但我们建议尽量使用标签助手. - -ABP框架还向标准bootstrap组件添加了一些**实用的功能**. - -这里是ABP框架包装的组件列表: - -* [Alerts](Alerts.md) -* [Buttons](Buttons.md) -* [Cards](Cards.md) -* [Collapse](Collapse.md) -* [Dropdowns](Dropdowns.md) -* [Grids](Grids.md) -* [List Groups](List-Groups.md) -* [Modals](Modals.md) -* [Paginator](Paginator.md) -* [Popovers](Popovers.md) -* [Progress Bars](Progress-Bars.md) -* [Tabs](Tabs.md) -* [Tooltips](Tooltips.md) -* ... - -> 在为所有的标签助手完成文档之前,你可以访问 https://bootstrap-taghelpers.abp.io/ 查看在线示例. - -## 表单元素 - -参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). - -## 动态表单 - -参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md deleted file mode 100644 index 32c28381db..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md +++ /dev/null @@ -1,76 +0,0 @@ -# 列表组 - -## 介绍 - -`abp-list-group` 是列表组内容的主要容器. - -基本用法: - -````xml - - Cras justo odio - Dapibus ac facilisis in - Morbi leo risus - Vestibulum at eros - -```` - -## Demo - -参阅[列表组demo页面](https://bootstrap-taghelpers.abp.io/Components/ListGroup)查看示例. - -## Attributes - -### flush - -指定 `abp-list-group` 项可删除某些边框和圆角,以在父容器中无边框呈现列表组项. 应为以下值之一: - -* `false` (默认值) -* `true` - -### active - -指定 `abp-list-group-item` 是否处于active. 应为以下值之一: - -* `false` (默认值) -* `true` - -### disabled - -指定 `abp-list-group-item` 是否被禁用. 应为以下值之一: - -* `false` (默认值) -* `true` - -### href - -指定 `abp-list-group-item` 是否含有链接. 应该是一个字符串链接值. - -### type - -指定 `abp-list-group-item` 具有状态背景和颜色的样式类. 应为以下值之一: - -* `Default` (默认值) -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` -* `Link` - -### Additional content - -`abp-list-group-item` 还可以包含其他HTML元素(例如span). - -示例: - -````xml - - Cras justo odio 14 - Dapibus ac facilisis in 2 - Morbi leo risus 1 - -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md deleted file mode 100644 index 3abebdde0c..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md +++ /dev/null @@ -1,86 +0,0 @@ -# 模态框 - -## 介绍 - -`abp-modal` 是创建模态框的主要元素. - -基本用法: - -````xml -Launch modal - - - - - Woohoo, you're reading this text in a modal! - - - -```` - -## Demo - -参阅[模态框demo页面](https://bootstrap-taghelpers.abp.io/Components/Modals)查看示例. - -## Attributes - -### centered - -指定模态框的位置. 应为以下值之一: - -* `false` (默认值) -* `true` - -### Scrollable - -指定模态框滚动. 应为以下值之一: - -* `false` (默认值) -* `true` - -### size - -指定模态框的大小. 应为以下值之一: - -* `Default` (默认值) -* `Small` -* `Large` -* `ExtraLarge` - -### static - -指定模态框是否是静态的. 应为以下值之一: - -* `false` (默认值) -* `true` - -### Additional content - -`abp-modal-footer` 可以有多个带有对齐选项的按钮. - -添加 `@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal` 到你的页面. - -示例: - -````xml -Launch modal - - - - - Woohoo, you're reading this text in a modal! - - - -```` - -### button-alignment - -指定模态页脚按钮的位置. 应为以下值之一: - -* `Default` (默认值) -* `Start` -* `Center` -* `Around` -* `Between` -* `End` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Navs.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Navs.md deleted file mode 100644 index 6edbc1b96f..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Navs.md +++ /dev/null @@ -1,114 +0,0 @@ -# 导航 - -## 介绍 - -`abp-nav` 是从bootstrap nav元素派生的基本标签助手. - -基本用法: - -````csharp - - -Active - - -Longer nav link - - -link - - -disabled - - -```` - -## Demo - -参阅[导航demo页面](https://bootstrap-taghelpers.abp.io/Components/Navs)查看示例. - -## abp-nav Attributes - -- **nav-style**: 指示包含项的位置和样式. 应为以下值之一: - * `Default` (默认值) - * `Vertical` - * `Pill` - * `PillVertical` -- **align:** 指示包含项的对齐方式: - * `Default` (默认值) - * `Start` - * `Center` - * `End` - -### abp-nav-bar Attributes - -- **nav-style**: 指示基本导航栏的颜色布局. 应为以下值之一: - * `Default` (默认值) - * `Dark` - * `Light` - * `Dark_Primary` - * `Dark_Secondary` - * `Dark_Success` - * `Dark_Danger` - * `Dark_Warning` - * `Dark_Info` - * `Dark_Dark` - * `Dark_Link` - * `Light_Primary` - * `Light_Secondary` - * `Light_Success` - * `Light_Danger` - * `Light_Warning` - * `Light_Info` - * `Light_Dark` - * `Light_Link` -- **size:** 指示基本导航栏的大小. 应为以下值之一: - * `Default` (默认值) - * `Sm` - * `Md` - * `Lg` - * `Xl` - -### abp-nav-item Attributes - -**dropdown**: 将导航项设置为下拉菜单(如果提供的话). 可以是下列值之一: - -* `false` (默认值) -* `true` - -示例: - -````csharp - - Navbar - - - - Home (current) - - - Link - - - - - - Dropdown header - Action - Another disabled action - Something else here - - Separated link - - - - - Disabled - - - - Sample Text - - - -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md deleted file mode 100644 index 945da6fa67..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md +++ /dev/null @@ -1,55 +0,0 @@ -# 分页器 - -## 介绍 - -`abp-paginator` 是分页器的abp标签. 需要 `Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` 类型的模型. - -基本用法: - -````xml - -```` - -模型: - -````csharp -using Microsoft.AspNetCore.Mvc.RazorPages; -using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components -{ - public class PaginatorModel : PageModel - { - public PagerModel PagerModel { get; set; } - - public void OnGet(int currentPage, string sort) - { - PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); - } - } -} -```` - -## Demo - -参阅[分页器demo页面](https://bootstrap-taghelpers.abp.io/Components/Paginator)查看示例. - -## Attributes - -### model - -`Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` 类型模型可以用以下数据初始化: - -* `totalCount` -* `shownItemsCount` -* `currentPage` -* `pageSize` -* `pageUrl` -* `sort` (默认值为null) - -### show-info - -指定是否显示开始,结束和总记录的其他信息. 应为以下值之一: - -* `false` (默认值) -* `true` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md deleted file mode 100644 index 18c896a90e..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md +++ /dev/null @@ -1,68 +0,0 @@ -# 进度条 - -## 介绍 - -`abp-progress-bar` 是进度条状态的abp标签. - -基本用法: - -````xml - - - %25 - - - - %50 - - - - %10 - - -```` - -## Demo - -参阅[进度条demo页面](https://bootstrap-taghelpers.abp.io/Components/Progressbars)查看示例. - -## Attributes - -### value - -指定当前进度条的进度. - -### type - -指定进度条的背景颜色. 应为下列值之一: - -* `Default` (默认值) -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` - -### min-value - -进度条的最小值. 默认值是0. - -### max-value - -进度条的最大值. 默认值是100. - -### strip - -指定进度条的背景样式是否被去除. 应为以下值之一: - -* `false` (默认值) -* `true` - -### animation - -指定进度条的背景样式是否为动画. 应为以下值之一: - -* `false` (默认值) -* `true` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tables.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tables.md deleted file mode 100644 index 24c61d28fc..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tables.md +++ /dev/null @@ -1,59 +0,0 @@ -# 表格 - -## 介绍 - -`ABP-table` 在ABP中用于表格的基本标签组件. - -基本用法: - -````csharp - - - - # - First - Last - Handle - - - - - 1 - Mark - Otto - mdo - - - 2 - Jacob - Thornton - fat - - - 3 - Larry - the Bird - twitter - - - -```` - -## Demo - -参阅[表格demo页面](https://bootstrap-taghelpers.abp.io/Components/Tables)查看示例. - -## abp-table Attributes - -- **responsive**: 用于创建直至特定断点的响应表. 请参阅[特定断点](https://getbootstrap.com/docs/4.1/content/tables/#breakpoint-specific)获取更多信息. -- **responsive-sm**: 如果没有设置为false,则为小屏幕设备设置表响应性. -- **responsive-md**: 如果未设置为false,则为中等屏幕设备设置表响应性. -- **responsive-lg**: 如果未设置为false,则为大屏幕设备设置表响应性. -- **responsive-xl**: 如果未设置为false,则为超大屏幕设备设置表响应性. -- **dark-theme**: 如果设置为true,则将表格颜色主题设置为黑暗. -- **striped-rows**: 如果设置为true,则将斑马条纹添加到表行中. -- **hoverable-rows**: 如果设置为true,则将悬停状态添加到表行. -- **border-style**: 设置表格的边框样式. 应为以下值之一: - - `Default` (默认) - - `Bordered` - - `Borderless` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md deleted file mode 100644 index 3aacbb5e0d..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md +++ /dev/null @@ -1,91 +0,0 @@ -# 标签页 - -## 介绍 - -`abp-tab` 是从引导标签元素派生的基本标签导航内容容器. - -基本用法: - -````xml - - - Content_Home - - - - Content_Profile - - - - Content_1_Content - - - Content_2_Content - - - -```` - -## Demo - -参阅[标签页demo页面](https://bootstrap-taghelpers.abp.io/Components/Tabs)查看示例. - -## abp-tab Attributes - -- **title**: 设置标签页菜单文字. -- **name:** 设置生成元素的"id"属性. 默认值为Guid. 除非使用Jquery更改或修改了选项卡,否则不需要. -- **active**: 设置active标签页. - -示例: - -````xml - - - Content_Home - - - Content_Profile - - - Content_Contact - - -```` - -### Pills - -示例: - -````xml - - - Content_Home - - - Content_Profile - - - Content_Contact - - -```` - -### Vertical - -**vertical-header-size**: 设置标签标题的列宽. - -示例: - -````xml - - - Content_Home - - - Content_Profile - - - Content_Contact - - -```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md deleted file mode 100644 index 0f404911b7..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md +++ /dev/null @@ -1,33 +0,0 @@ -# 提示工具 - -## 介绍 - -`abp-tooltip` 是提示工具的abp标签. - -基本用法: - -````xml - - Tooltip Default - - - - Tooltip on top - - - - Tooltip on right - - - - Tooltip on bottom - - - - Disabled button Tooltip - -```` - -## Demo - -参阅[提示工具demo页面](https://bootstrap-taghelpers.abp.io/Components/Tooltips)查看示例. diff --git a/docs/zh-Hans/UI/AspNetCore/Testing.md b/docs/zh-Hans/UI/AspNetCore/Testing.md deleted file mode 100644 index 1dfdbe367e..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Testing.md +++ /dev/null @@ -1,207 +0,0 @@ -# ASP.NET Core MVC / Razor Pages: 测试 - -> 你可以参考[ASP.NET Core集成测试文档](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests)了解ASP.NET Core集成测试的详细内容. 本文档解释了ABP框架提供的附加测试基础设施. - -## 应用程序启动模板 - -应用程序启动模板的`.Web`项目其中包含应用程序的UI视图/页面/组件, 并提供`.Web.Tests`项目来测试这些内容. - -![aspnetcore-web-tests-in-solution](../../images/aspnetcore-web-tests-in-solution.png) - -## 测试Razor页面 - -假设你已经创建了一个名为`Issues.cshtml`的Razor页面, 包含以下内容; - -**Issues.cshtml.cs** - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.RazorPages; -using MyProject.Issues; - -namespace MyProject.Web.Pages -{ - public class IssuesModel : PageModel - { - public List Issues { get; set; } - - private readonly IIssueAppService _issueAppService; - - public IssuesModel(IIssueAppService issueAppService) - { - _issueAppService = issueAppService; - } - - public async Task OnGetAsync() - { - Issues = await _issueAppService.GetListAsync(); - } - } -} -```` - -**Issues.cshtml** - -````html -@page -@model MyProject.Web.Pages.IssuesModel -

    Issue List

    - - - - - - - - - @foreach (var issue in Model.Issues) - { - - - - - } - -
    IssueClosed?
    @issue.Title - @if (issue.IsClosed) - { - Closed - } - else - { - Open - } -
    -```` - -本页仅创建一个包含issue的表格: - -![issue-list](../../images/issue-list.png) - -你可以在`.Web.Tests`项目中编写一个测试类如下所示: - -````csharp -using System.Threading.Tasks; -using HtmlAgilityPack; -using Shouldly; -using Xunit; - -namespace MyProject.Pages -{ - public class Issues_Tests : MyProjectWebTestBase - { - [Fact] - public async Task Should_Get_Table_Of_Issues() - { - // Act - - var response = await GetResponseAsStringAsync("/Issues"); - - //Assert - - var htmlDocument = new HtmlDocument(); - htmlDocument.LoadHtml(response); - - var tableElement = htmlDocument.GetElementbyId("IssueTable"); - tableElement.ShouldNotBeNull(); - - var trNodes = tableElement.SelectNodes("//tbody/tr"); - trNodes.Count.ShouldBeGreaterThan(0); - } - } -} -```` - -`GetResponseAsStringAsync`是一个快捷方法, 它来自执行HTTP GET请求的基类, 检查生成的HTTP状态是否为`200`, 并将响应作为`string`返回. - -> 你可以使用`Client`对象(类型为`HttpClient`)对服务器执行任何类型的请求, 并读取响应.`GetResponseAsStringAsync`只是一种快捷方法. - -本例使用[HtmlAgilityPack](https://html-agility-pack.net/)库来解析传入的HTML并测试它是否包含issue表格. - -> 本例假设的数据库中存在一些初始issue. 请参阅[测试文档](../../Testing.md)的*种子数据*部分, 了解如何设置种子数据, 以便可以假定数据库中有一些可用的初始数据. - -## 控制器测试 - -测试控制器也不例外. 只需使用正确的URL向服务器执行请求, 获取响应并做出断言. - -### 查看结果 - -如果控制器返回一个视图, 你可以使用类似的代码来测试返回的HTML. 参见上面的Razor页面示例. - -### 对象结果 - -如果控制器返回对象结果, 则可以使用`GetResponseAsObjectAsync`方法. - -假设你有一个如下定义的控制器: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using MyProject.Issues; -using Volo.Abp.AspNetCore.Mvc; - -namespace MyProject.Web.Controllers -{ - [Route("api/issues")] - public class IssueController : AbpController - { - private readonly IIssueAppService _issueAppService; - - public IssueController(IIssueAppService issueAppService) - { - _issueAppService = issueAppService; - } - - [HttpGet] - public async Task> GetAsync() - { - return await _issueAppService.GetListAsync(); - } - } -} -```` - -你可以编写测试代码来调用API并获得结果: - -````csharp -using System.Collections.Generic; -using System.Threading.Tasks; -using MyProject.Issues; -using Shouldly; -using Xunit; - -namespace MyProject.Pages -{ - public class Issues_Tests : MyProjectWebTestBase - { - [Fact] - public async Task Should_Get_Issues_From_Api() - { - var issues = await GetResponseAsObjectAsync>("/api/issues"); - - issues.ShouldNotBeNull(); - issues.Count.ShouldBeGreaterThan(0); - } - } -} -```` - -## 测试JavaScript代码 - -ABP框架不提供任何基础设施来测试JavaScript代码. 你可以使用任何测试框架和工具来测试JavaScript代码. - -## 测试基础设施 - -[Volo.Abp.AspNetCore.TestBase](https://www.nuget.org/packages/Volo.Abp.AspNetCore.TestBase) 提供了集成到ABP框架和ASP.NET Core的测试基础设施. - -> Volo.Abp.AspNetCore.TestBase 已经安装在 `.Web.Tests` 项目中. - -此包提供的`AbpWebApplicationFactoryIntegratedTest`作为派生测试类的基类. 它继承自ASP.NET Core提供的[WebApplicationFactory](https://learn.microsoft.com/zh-cn/aspnet/core/test/integration-tests)类。 -上面使用的`MyProjectWebTestBase`继承自`AbpWebApplicationFactoryIntegratedTest`, 因此我们间接继承了`AbpWebApplicationFactoryIntegratedTest`. - -另请参阅 - -* [ASP.NET Core 中的集成测试](https://learn.microsoft.com/zh-cn/aspnet/core/test/integration-tests) -* [总览/服务器端测试](../../Testing.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Theming.md b/docs/zh-Hans/UI/AspNetCore/Theming.md deleted file mode 100644 index 10486dd6c5..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Theming.md +++ /dev/null @@ -1,3 +0,0 @@ -# ASP.NET Core MVC / Razor Pages 主题 - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Toolbars.md b/docs/zh-Hans/UI/AspNetCore/Toolbars.md deleted file mode 100644 index cc3bcd95be..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Toolbars.md +++ /dev/null @@ -1,3 +0,0 @@ -# Toolbars - -TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Widgets.md b/docs/zh-Hans/UI/AspNetCore/Widgets.md deleted file mode 100644 index 5f86595ba2..0000000000 --- a/docs/zh-Hans/UI/AspNetCore/Widgets.md +++ /dev/null @@ -1,274 +0,0 @@ -# 小部件 - -ABP为创建**可重用的部件**提供了模型和基础设施. 部件系统是[ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components)的扩展. 在你有以下需求时,小部件会非常有用; - -* 在可复用的 **[模块](../../Module-Development-Basics.md)** 中定义部件. -* 在部件中引用 **scripts & styles** 脚本. -* 使用部件创建 **仪表盘**. -* 支持 **[授权](../../Authorization.md)** 与 **[捆绑`bundling`](Bundling-Minification.md)** 的部件 - -## 基本部件定义 - -### 创建一个视图组件 - -第一部,创建一个新的ASP.NET Core View Component: - -![widget-basic-files](../../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -继承 `AbpViewComponent` 不是必需的. 你也可以继承ASP.NET Core的 `ViewComponent`. `AbpViewComponent` 只是定义了一些基本的实用属性. - -**Default.cshtml**: - -```xml -
    -

    My Simple Widget

    -

    This is a simple widget!

    -
    -``` - -### 定义部件 - -添加 `Widget` attribute 到 `MySimpleWidgetViewComponent` 类,将此视图组件标记为部件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 渲染部件 - -渲染部件的用法是ASP.NET Core的标准用法. 在razor view/page中使用 `Component.InvokeAsync` 方法, 就像渲染一个View Component一样. 例如: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -第一行代码使用名称渲染了部件,第二行代码使用type渲染了View Comonent. - -## 部件名称 - -默认下名称是根据View Conponent组件的名称计算的, 比如你的视图组件名是 `MySimpleWidgetViewComponent`, 那么部件的名称就是 `MySimpleWidget` (删除`ViewComponent`后缀). 这与ASP.NET Core的默认视图组件名称的方式一样. - -想要自定义组件名称,只需要使用ASP.NET Core的 `ViewComponent` attribute: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP会通过自定义的名称去处理部件. - -> 如果视图组件名与视图组件的文件夹名称不匹配,那么需要像本例中那样去手动编写视图路径. - -### 显示名称 - -你还可以定义对于使用者友好的本地化显示名称. 需要时在UI中使用显示名称. 显示名称是可选的,在 `Widget` attribute 的`DisplayName`属性中定义: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", //Localization key - DisplayNameResource = typeof(DashboardDemoResource) //localization resource - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -参阅 [本地化文档](../../Localization.md) 学习关于本地化资源的更多内容. - -## 引用 Style & Script - -当部件含有样式和scirpt文件时,会存在一些挑战; - -* 使用部件的页面应该将 **script & styles** 文件引用到页面中. -* 页面还需要解析部件的 `依赖库/文件`. - -将资源与部件正确的关联在一起时,ABP会解决这些问题. 使用正确的方法,就不用担心部件的依赖关系. - -### 定义一个简单的文件路径 - -下面的示例中部件添加了样式和scirpt文件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP会考虑到这些依赖关系, 在view/page中使用正确的方法添加部件 . 样式和script可以是物理文件也可以是虚拟文件. 它于[虚拟文件系统](../../Virtual-File-System.md)完全集成]. - -### 定义 Bundle - -页面中使用的组件的所有资源都做为捆绑包添加(如果没有其他配置,会在生产中合并和压缩). 除了简单的添加文件,你还可以充分的利用捆绑功能. - -下面的示例与上面的代码相同,但是在添加文件时文件路径替换成了 `BundleContributor`: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -捆绑系统非常强大,如果你的部件使用了JavaScript库来呈现图表, 你可以将它声明为依赖项, 如果之前未添加JavaScript库. 则会自动添加到页面中. 使用这种方式让页面使用部件时不用关心依赖项. - -参阅 [捆包&压缩 文档](Bundling-Minification.md) 了解更多内容. - -## 授权 - -某些组件可能只对通过身份验证或授权的用户可用,这时可以使用 `Widget` attribute 的以下属性: - -* `RequiresAuthentication` (`bool`): 设置为true,只有通过身份验证的用户(登录用户)可用. -* `RequiredPolicies` (`List`): 授权用户的策略名称列表. 有关策略的详细信息请参阅[授权文档](../../Authorization.md). - -示例: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 部件选项 - -`AbpWidgetOptions` 是 `Widget` attribute 替代, 你可以使用它去配置部件: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -将上面的代码写到[模块](../../Module-Development-Basics.md)的 `ConfigureServices` 方法中. `AbpWidgetOptions` 可以完成 `Widget` attribute 的所有功能. 比如为组件添加样式: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> 提示: `AbpWidgetOptions` 还可以更改现有的部件配置. 如果要修改应用程序使用的模块内的组件配置,这会很有用. 使用 `options.Widgets.Find` 获取现有的 `WidgetDefinition`. \ No newline at end of file diff --git a/docs/zh-Hans/UI/Common/Utils/Linked-List.md b/docs/zh-Hans/UI/Common/Utils/Linked-List.md deleted file mode 100644 index 216e2d6a2d..0000000000 --- a/docs/zh-Hans/UI/Common/Utils/Linked-List.md +++ /dev/null @@ -1,1555 +0,0 @@ -# 链表 (双向) - -@abp/utils包提供了称为[双链表](https://en.wikipedia.org/wiki/Doubly_linked_list)的实用数据结构. 简而言之双向链表是一系列记录(又称节点),这些记录具有上一个节点,下一个节点及其自身值(或数据)的信息. - -## 入门 - -要创建一个双向链表,你需要做的就是导入和创建它的一个新的实例: - -```js -import { LinkedList } from '@abp/utils'; - -var list = new LinkedList(); -``` - -MVC: - -```js -var list = new abp.utils.common.LinkedList(); -``` - -构造函数没有任何参数. - -## 用法 - -### 如何添加新节点 - -有几种方法可以在链表中创建新节点,这些方法都可以单独使用,也可以通过 `add` 和 `addMany` 方法. - - -#### addHead(value) - -```js -addHead(value: T): ListNode -``` - -将给定值添加到链表的第一个节点: - -```js -list.addHead('a'); - -// "a" - -list.addHead('b'); - -// "b" <-> "a" - -list.addHead('c'); - -// "c" <-> "b" <-> "a" -``` - - - -#### addManyHead(values) - -```js -addManyHead(values: T[]): ListNode[] -``` - -将给定的多个值添加到链表的第一个节点: - -```js -list.addManyHead(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -list.addManyHead(['x', 'y', 'z']); - -// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c" -``` - - - -#### addTail(value) - -```js -addTail(value: T): ListNode -``` - -将给定值添加到链表的最后一个节点: - -```js -list.addTail('a'); - -// "a" - -list.addTail('b'); - -// "a" <-> "b" - -list.addTail('c'); - -// "a" <-> "b" <-> "c" -``` - - - -#### addManyTail(values) - -```js -addManyTail(values: T[]): ListNode[] -``` - -将给定多个值添加到链表的最后一个节点: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -list.addManyTail(['x', 'y', 'z']); - -// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z" -``` - -#### addAfter(value, previousValue [, compareFn]) - -```js -addAfter(value: T, previousValue: T, compareFn?: ListComparisonFn): ListNode -``` - -添加给定值到previousValue节点后: - -```js -list.addTail('a'); -list.addTail('b'); -list.addTail('b'); -list.addTail('c'); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addAfter('x', 'b'); - -// "a" <-> "b" <-> "x" <-> "b" <-> "c" -``` - - -你可以自定义比较器: - -```js -list.addTail({ x: 1 }); -list.addTail({ x: 2 }); -list.addTail({ x: 3 }); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list.addAfter( - { x: 0 }, - 2, - (value, searchedValue) => value.x === searchedValue -); - -// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} -``` - - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - -#### addManyAfter(values, previousValue [, compareFn]) - -```js -addManyAfter(values: T[], previousValue: T, compareFn?: ListComparisonFn): ListNode[] -``` - -添加给定的多个值到previousValue节点后: - -```js -list.addManyTail(['a', 'b', 'b', 'c']); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addManyAfter(['x', 'y'], 'b'); - -// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c" -``` - -你可以自定义比较器: - -```js -list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list.addManyAfter( - [{ x: 4 }, { x: 5 }], - 2, - (value, searchedValue) => value.x === searchedValue -); - -// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3} -``` - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - -#### addBefore(value, nextValue [, compareFn]) - -```js -addBefore(value: T, nextValue: T, compareFn?: ListComparisonFn): ListNode -``` - -添加给值到previousValue节点前: - -```js -list.addTail('a'); -list.addTail('b'); -list.addTail('b'); -list.addTail('c'); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addBefore('x', 'b'); - -// "a" <-> "x" <-> "b" <-> "b" <-> "c" -``` - -你可以自定义比较器: - -```js -list.addTail({ x: 1 }); -list.addTail({ x: 2 }); -list.addTail({ x: 3 }); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list.addBefore( - { x: 0 }, - 2, - (value, searchedValue) => value.x === searchedValue -); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} -``` - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### addManyBefore(values, nextValue [, compareFn]) - -```js -addManyBefore(values: T[], nextValue: T, compareFn?: ListComparisonFn): ListNode[] -``` - - -添加给定的多个值到previousValue节点前: - -```js -list.addManyTail(['a', 'b', 'b', 'c']); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addManyBefore(['x', 'y'], 'b'); - -// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list.addManyBefore( - [{ x: 4 }, { x: 5 }], - 2, - (value, searchedValue) => value.x === searchedValue -); - -// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3} -``` - - - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### addByIndex(value, position) - -```js -addByIndex(value: T, position: number): ListNode -``` - -在链表的指定位置添加节点: - -```js -list.addTail('a'); -list.addTail('b'); -list.addTail('c'); - -// "a" <-> "b" <-> "c" - -list.addByIndex('x', 2); - -// "a" <-> "b" <-> "x" <-> "c" -``` - -它也适用于负索引: - -```js -list.addTail('a'); -list.addTail('b'); -list.addTail('c'); - -// "a" <-> "b" <-> "c" - -list.addByIndex('x', -1); - -// "a" <-> "b" <-> "x" <-> "c" -``` - - - -#### addManyByIndex(values, position) - -```js -addManyByIndex(values: T[], position: number): ListNode[] -``` - -添加多个节点到链表的指定位置: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -list.addManyByIndex(['x', 'y'], 2); - -// "a" <-> "b" <-> "x" <-> "y" <-> "c" -``` - -它也适用于负索引: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -list.addManyByIndex(['x', 'y'], -1); - -// "a" <-> "b" <-> "x" <-> "y" <-> "c" -``` - - - -#### add(value).head() - -```js -add(value: T).head(): ListNode -``` - -将添加的节点移动到链表头: - -```js -list.add('a').head(); - -// "a" - -list.add('b').head(); - -// "b" <-> "a" - -list.add('c').head(); - -// "c" <-> "b" <-> "a" -``` - - - -> 它是 `addHead` 的替代API. - - - -#### add(value).tail() - -```js -add(value: T).tail(): ListNode -``` - -将添加的节点移动到链表尾: - -```js -list.add('a').tail(); - -// "a" - -list.add('b').tail(); - -// "a" <-> "b" - -list.add('c').tail(); - -// "a" <-> "b" <-> "c" -``` - - - -> 它是 `addTail` 的替代API. - - - -#### add(value).after(previousValue [, compareFn]) - -```js -add(value: T).after(previousValue: T, compareFn?: ListComparisonFn): ListNode -``` - -将添加的节点移动到指定节点后: - -```js -list.add('a').tail(); -list.add('b').tail(); -list.add('b').tail(); -list.add('c').tail(); - -// "a" <-> "b" <-> "b" <-> "c" - -list.add('x').after('b'); - -// "a" <-> "b" <-> "x" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.add({ x: 1 }).tail(); -list.add({ x: 2 }).tail(); -list.add({ x: 3 }).tail(); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list - .add({ x: 0 }) - .after(2, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} -``` - - - -> 它是 `addAfter` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### add(value).before(nextValue [, compareFn]) - -```js -add(value: T).before(nextValue: T, compareFn?: ListComparisonFn): ListNode -``` - -将添加的节点移动到指定节点前: - -```js -list.add('a').tail(); -list.add('b').tail(); -list.add('b').tail(); -list.add('c').tail(); - -// "a" <-> "b" <-> "b" <-> "c" - -list.add('x').before('b'); - -// "a" <-> "x" <-> "b" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.add({ x: 1 }).tail(); -list.add({ x: 2 }).tail(); -list.add({ x: 3 }).tail(); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list - .add({ x: 0 }) - .before(2, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} -``` - - - -> 它是 `addBefore` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### add(value).byIndex(position) - -```js -add(value: T).byIndex(position: number): ListNode -``` - -将添加的节点移动到链表指定位置: - -```js -list.add('a').tail(); -list.add('b').tail(); -list.add('c').tail(); - -// "a" <-> "b" <-> "c" - -list.add('x').byIndex(2); - -// "a" <-> "b" <-> "x" <-> "c" -``` - - - -它也适用于负索引: - -```js -list.add('a').tail(); -list.add('b').tail(); -list.add('c').tail(); - -// "a" <-> "b" <-> "c" - -list.add('x').byIndex(-1); - -// "a" <-> "b" <-> "x" <-> "c" -``` - - - -> 它是 `addByIndex` 的替代API. - - - -#### addMany(values).head() - -```js -addMany(values: T[]).head(): ListNode[] -``` - -将添加的多个节点移动到链表头: - -```js -list.addMany(['a', 'b', 'c']).head(); - -// "a" <-> "b" <-> "c" - -list.addMany(['x', 'y', 'z']).head(); - -// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c" -``` - - - -> 它是 `addManyHead` 的替代API. - - - -#### addMany(values).tail() - -```js -addMany(values: T[]).tail(): ListNode[] -``` - -将添加的多个节点移动到链表尾: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.addMany(['x', 'y', 'z']).tail(); - -// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z" -``` - - - -> 它是 `addManyTail` 的替代API. - - - -#### addMany(values).after(previousValue [, compareFn]) - -```js -addMany(values: T[]).after(previousValue: T, compareFn?: ListComparisonFn): ListNode[] -``` - -将添加的多个节点移动到指定节点后: - -```js -list.addMany(['a', 'b', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addMany(['x', 'y']).after('b'); - -// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list - .addMany([{ x: 4 }, { x: 5 }]) - .after(2, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3} -``` - - - -> 它是 `addManyAfter` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### addMany(values).before(nextValue [, compareFn]) - -```js -addMany(values: T[]).before(nextValue: T, compareFn?: ListComparisonFn): ListNode[] -``` - -将添加的多个节点移动到指定节点前: - -```js -list.addMany(['a', 'b', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "b" <-> "c" - -list.addMany(['x', 'y']).before('b'); - -// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":2} <-> {"x":3} - -list - .addMany([{ x: 4 }, { x: 5 }]) - .before(2, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3} -``` - - - -> 它是 `addManyBefore` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### addMany(values).byIndex(position) - -```js -addMany(values: T[]).byIndex(position: number): ListNode[] -``` - -将添加的多个节点移动到链表的指定位置: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.addMany(['x', 'y']).byIndex(2); - -// "a" <-> "b" <-> "x" <-> "y" <-> "c" -``` - - -它也适用于负索引: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.addMany(['x', 'y']).byIndex(-1); - -// "a" <-> "b" <-> "x" <-> "y" <-> "c" -``` - - - -> 它是 `addManyByIndex` 的替代API. - - - -### 如何删除节点 - -有几种方法可以在链表中删除节点,这些方法都可以单独使用,也可以通过 `drop` 方法. - - - -#### dropHead() - -```js -dropHead(): ListNode | undefined -``` - -删除链表的第一个节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropHead(); - -// "b" <-> "c" -``` - - - -#### dropManyHead(count) - -```js -dropManyHead(count: number): ListNode[] -``` - -删除指定数量的链表的头节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropManyHead(2); - -// "c" -``` - - - -#### dropTail() - -```js -dropTail(): ListNode | undefined -``` - -删除链表的最后一个节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropTail(); - -// "a" <-> "b" -``` - - - -#### dropManyTail(count) - -```js -dropManyTail(count: number): ListNode[] -``` - -删除指定数量的链表的尾节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropManyTail(2); - -// "a" -``` - - - -#### dropByIndex(position) - -```js -dropByIndex(position: number): ListNode | undefined -``` - -删除链表中给定位置的节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropByIndex(1); - -// "a" <-> "c" -``` - - -它也适用于负索引: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropByIndex(-2); - -// "a" <-> "c" -``` - - - -#### dropManyByIndex(count, position) - -```js -dropManyByIndex(count: number, position: number): ListNode[] -``` - -删除链表中给定位置与数量的多个节点: - -```js -list.addMany(['a', 'b', 'c', 'd']).tail(); - -// "a" <-> "b" <-> "c" <-> "d - -list.dropManyByIndex(2, 1); - -// "a" <-> "d" -``` - - - -它也适用于负索引: - -```js -list.addMany(['a', 'b', 'c', 'd']).tail(); - -// "a" <-> "b" <-> "c" <-> "d - -list.dropManyByIndex(2, -2); - -// "a" <-> "d" -``` - - - -#### dropByValue(value [, compareFn]) - -```js -dropByValue(value: T, compareFn?: ListComparisonFn): ListNode | undefined -``` - -删除链表中含有给定值的第一个节点: - -```js -list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); - -// "a" <-> "x" <-> "b" <-> "x" <-> "c" - -list.dropByValue('x'); - -// "a" <-> "b" <-> "x" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} - -list.dropByValue(0, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} -``` - - - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### dropByValueAll(value [, compareFn]) - -```js -dropByValueAll(value: T, compareFn?: ListComparisonFn): ListNode[] -``` - -删除链表中含有给定值的所有节点: - -```js -list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); - -// "a" <-> "x" <-> "b" <-> "x" <-> "c" - -list.dropByValueAll('x'); - -// "a" <-> "b" <-> "c" -``` - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} - -list.dropByValueAll(0, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":3} -``` - - - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### drop().head() - -```js -drop().head(): ListNode | undefined -``` - -删除链表的头节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.drop().head(); - -// "b" <-> "c" -``` - - - -> 它是 `dropHead` 的替代API. - - - -#### drop().tail() - -```js -drop().tail(): ListNode | undefined -``` - -删除链表的尾节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.drop().tail(); - -// "a" <-> "b" -``` - - - -> 它是 `dropTail` 的替代API. - - - -#### drop().byIndex(position) - -```js -drop().byIndex(position: number): ListNode | undefined -``` - -删除链表指定位置的节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.drop().byIndex(1); - -// "a" <-> "c" -``` - - - -它也适用于负索引: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.drop().byIndex(-2); - -// "a" <-> "c" -``` - - - -> 它是 `dropByIndex` 的替代API. - - - -#### drop().byValue(value [, compareFn]) - -```js -drop().byValue(value: T, compareFn?: ListComparisonFn): ListNode | undefined -``` - -删除链表中含有给定值的第一个节点: - -```js -list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); - -// "a" <-> "x" <-> "b" <-> "x" <-> "c" - -list.drop().byValue('x'); - -// "a" <-> "b" <-> "x" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} - -list - .drop() - .byValue(0, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} -``` - - - -> 它是 `dropByValue` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### drop().byValueAll(value [, compareFn]) - -```js -drop().byValueAll(value: T, compareFn?: ListComparisonFn): ListNode[] -``` - -删除链表中含有给定值的所有节点: - -```js -list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); - -// "a" <-> "x" <-> "b" <-> "x" <-> "c" - -list.drop().byValueAll('x'); - -// "a" <-> "b" <-> "c" -``` - - - -你可以自定义比较器 - -```js -list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} - -list - .drop() - .byValueAll(0, (value, searchedValue) => value.x === searchedValue); - -// {"x":1} <-> {"x":2} <-> {"x":3} -``` - - - -> 它是 `dropByValueAll` 的替代API. -> -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -#### dropMany(count).head() - -```js -dropMany(count: number).head(): ListNode[] -``` - -删除链表中指定数量的头节点: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropMany(2).head(); - -// "c" -``` - - - -> 它是 `dropManyHead` 的替代API. - - - -#### dropMany(count).tail() - -```js -dropMany(count: number).tail(): ListNode[] -``` - -删除链表中指定数量的尾节点:: - -```js -list.addMany(['a', 'b', 'c']).tail(); - -// "a" <-> "b" <-> "c" - -list.dropMany(2).tail(); - -// "a" -``` - - - -> 它是 `dropManyTail` 的替代API. - - - -#### dropMany(count).byIndex(position) - -```js -dropMany(count: number).byIndex(position: number): ListNode[] -``` - -删除链表中指定位置和数量的节点: - -```js -list.addMany(['a', 'b', 'c', 'd']).tail(); - -// "a" <-> "b" <-> "c" <-> "d - -list.dropMany(2).byIndex(1); - -// "a" <-> "d" -``` - - - -它也适用于负索引: - -```js -list.addMany(['a', 'b', 'c', 'd']).tail(); - -// "a" <-> "b" <-> "c" <-> "d - -list.dropMany(2).byIndex(-2); - -// "a" <-> "d" -``` - - - -> 它是 `dropManyByIndex` 的替代API. - - - -### 如何查找节点 - -有几个方法找到链表特定节点. - -#### head - -```js -head: ListNode | undefined; -``` - -链表中的第一个节点. - -#### tail - -```js -tail: ListNode | undefined; -``` - -链表中的最后一个节点. - -#### length - -```js -length: number; -``` - -链表的节点总数. - -#### find(predicate) - -```js -find(predicate: ListIteratorFunction): ListNode | undefined -``` - -从链表中找到与给定谓词匹配的第一个节点: - -```js -list.addManyTail(['a', 'b', 'b', 'c']); - -// "a" <-> "b" <-> "b" <-> "c" - -var found = list.find(node => node.value === 'b'); - -/* -found.value === "b" -found.previous.value === "a" -found.next.value === "b" -*/ -``` - -#### findIndex(predicate) - -```js -findIndex(predicate: ListIteratorFunction): number -``` - -从链表中找到与给定谓词匹配的第一个节点的位置: - -```js -list.addManyTail(['a', 'b', 'b', 'c']); - -// "a" <-> "b" <-> "b" <-> "c" - -var i0 = list.findIndex(node => node.next && node.next.value === 'b'); -var i1 = list.findIndex(node => node.value === 'b'); -var i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); -var i3 = list.findIndex(node => node.value === 'x'); - -/* -i0 === 0 -i1 === 1 -i2 === 2 -i3 === -1 -*/ -``` - - - -#### get(position) - -```js -get(position: number): ListNode | undefined -``` - -查找并返回链表中特定位置的节点: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -var found = list.get(1); - -/* -found.value === "b" -found.previous.value === "a" -found.next.value === "c" -*/ -``` - - - -#### indexOf(value [, compareFn]) - -```js -indexOf(value: T, compareFn?: ListComparisonFn): number -``` - -在链表中找到匹配给定值的第一个节点位置: - -```js -list.addManyTail(['a', 'b', 'b', 'c']); - -// "a" <-> "b" <-> "b" <-> "c" - -var i0 = list.indexOf('a'); -var i1 = list.indexOf('b'); -var i2 = list.indexOf('c'); -var i3 = list.indexOf('x'); - -/* -i0 === 0 -i1 === 1 -i2 === 3 -i3 === -1 -*/ -``` - - - -你可以自定义比较器 - -```js -list.addManyTail([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]); - -// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} - -var i0 = indexOf(1, (value, searchedValue) => value.x === searchedValue); -var i1 = indexOf(2, (value, searchedValue) => value.x === searchedValue); -var i2 = indexOf(3, (value, searchedValue) => value.x === searchedValue); -var i3 = indexOf(0, (value, searchedValue) => value.x === searchedValue); -var i4 = indexOf(4, (value, searchedValue) => value.x === searchedValue); - -/* -i0 === 0 -i1 === 2 -i2 === 4 -i3 === 1 -i4 === -1 -*/ -``` - - - -> 默认的比较函数检查深度相等性,因此你几乎不需要传递该参数. - - - -### 如何检查所有节点 - -有几种方法来遍历或显示一个链表. - - - -#### forEach(iteratorFn) - -```js -forEach(iteratorFn: ListIteratorFn): void -``` - -从头到尾在链表中的所有节点上运行回调函数: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -list.forEach((node, index) => console.log(node.value + index)); - -// 'a0' -// 'b1' -// 'c2' -``` - -#### \*\[Symbol.iterator\]\(\) - -链表是可迭代的. 换句话说你可以使用诸如`for ... of`之类的方法. - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -for(var node of list) { /* ES6 for...of statement */ - console.log(node.value); -} - -// 'a' -// 'b' -// 'c' -``` - - - -#### toArray() - -```js -toArray(): T[] -``` - -转换链表值为数组: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -var arr = list.toArray(); - -/* -arr === ['a', 'b', 'c'] -*/ -``` - - - -#### toNodeArray() - -```js -toNodeArray(): T[] -``` - -转换链表节点为数组: - -```js -list.addManyTail(['a', 'b', 'c']); - -// "a" <-> "b" <-> "c" - -var arr = list.toNodeArray(); - -/* -arr[0].value === 'a' -arr[1].value === 'a' -arr[2].value === 'a' -*/ -``` - -#### toString(mapperFn) - -```js -toString(mapperFn: ListMapperFn = JSON.stringify): string -``` - -将链表转换为节点及其关系的字符串表示形式: - -```js -list.addManyTail(['a', 2, 'c', { k: 4, v: 'd' }]); - -// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} - -var str = list.toString(); - -/* -str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}' -*/ -``` - -你可以在对值进行字符串化之前通过自定义映射器函数来映射值: - -```js -list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }]).tail(); - -// {"x":1} <-> {"x":2} <-> {"x":3} <-> {"x":4} <-> {"x":5} - -var str = list.toString(value => value.x); - -/* -str === '1 <-> 2 <-> 3 <-> 4 <-> 5' -*/ -``` - -## API - -### Classes - -#### LinkedList - -```js -export class LinkedList { - - // properties and methods are explained above - -} -``` - -#### ListNode - -```js -export class ListNode { - next: ListNode | undefined; - - previous: ListNode | undefined; - - constructor(public readonly value: T) {} -} -``` - -`ListNode` 是存储在 `LinkedList` 中的每个记录的节点. - -- `value` value是存储在节点中的值,通过构造函数传递. -- `next` 引用列表中的下一个节点. -- `previous`引用列表中的上一个节点. - -```js -list.addManyTail([ 0, 1, 2 ]); - -console.log( - list.head.value, // 0 - list.head.next.value, // 1 - list.head.next.next.value, // 2 - list.head.next.next.previous.value, // 1 - list.head.next.next.previous.previous.value, // 0 - list.tail.value, // 2 - list.tail.previous.value, // 1 - list.tail.previous.previous.value, // 0 - list.tail.previous.previous.next.value, // 1 - list.tail.previous.previous.next.next.value, // 2 -); -``` - - -### Types - -#### ListMapperFn - -```js -type ListMapperFn = (value: T) => any; -``` - -该函数在 `toString` 方法中用于在生成列表的字符串形式之前映射节点值. - -#### ListComparisonFn - -```js -type ListComparisonFn = (nodeValue: T, comparedValue: any) => boolean; -``` - -该函数用于根据比较值添加,删除和查找节点. - -#### ListIteratorFn - -```js -type ListIteratorFn = ( - node: ListNode, - index?: number, - list?: LinkedList, -) => R; -``` - -该函数在遍历列表时使用,可以对每个节点执行某些操作,也可以查找某个节点. \ No newline at end of file diff --git a/docs/zh-Hans/Unit-Of-Work.md b/docs/zh-Hans/Unit-Of-Work.md deleted file mode 100644 index b937c86ac0..0000000000 --- a/docs/zh-Hans/Unit-Of-Work.md +++ /dev/null @@ -1,355 +0,0 @@ -# 工作单元 - -ABP框架的工作单元(UOW)实现提供了对应用程序中的**数据库连接和事务范围**的抽象和控制. - -一旦一个新的UOW启动,它将创建一个**环境作用域**,当前作用域中执行的**所有数据库操作**都将参与该作用域并将其视为单个事务边界. 操作一起**提交**(成功时)或**回滚**(异常时). - -ABP的UOW系统是; - -* **按约定工作**, 所以大部分情况下你不需要处理UOW. -* **数据库提供者独立**. -* **Web独立**, 这意味着你可以在Web应用程序/服务之外的任何类型的应用程序中创建工作单元作用域. - -## 约定 - -以下方法类型被认为是一个工作单元: - -* ASP.NET Core MVC **Controller Actions**. -* ASP.NET Core Razor **Page Handlers**. -* **应用程序** 方法. -* **仓储方法**. - -UOW自动针对这些方法开始,除非**周围已经有一个(环境)**UOW在运行.示例; - -* 如果你调用一个[仓储](Repositories.md)方法,但还没有启动UOW,它将自动**启动一个新的事务UOW**,其中包括在仓储方法中完成的所有操作,如果仓储方法没有抛出任何异常,则**提交事务**. 仓储方法根本不知道UOW或事务. 它只在一个常规的数据库对象上工作(例如用于[EF Core](Entity-Framework-Core.md)的`DbContext`),而UOW由ABP框架处理. -* 如果调用[应用服务](Application-Services.md)方法,则相同的UOW系统将按上述说明工作. 如果应用服务方法使用某些仓储,这些仓储**不会开始新的UOW**,而是**参与由ABP框架为应用程序服务方法启动的当前工作单元中**. -* ASP.NET Core控制器操作也是如此. 如果操作以控制器action开始,**UOW范围是控制器action的方法主体**. - -所有这些都是由ABP框架自动处理的. - -### 数据库事务行为 - -虽然上一节解释了UOW是数据库事务,但实际上UOW不必是事务性的. 默认情况下; - -* **HTTP GET**请求不会启动事务性UOW. 它们仍然启动UOW,但**不创建数据库事务**. -* 如果底层数据库提供程序支持数据库事务,那么所有其他HTTP请求类型都使用数据库事务启动UOW. - -这是因为HTTP GET请求不会(也不应该)在数据库中进行任何更改. 你可以使用下面解释的选项来更改此行为. - -## 默认选项 - -`AbpUnitOfWorkDefaultOptions` 用于配置工作单元系统的默认选项.在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中配置选项. - -**示例: 完全禁用数据库事务** - -````csharp -Configure(options => -{ - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; -}); -```` - -### 选项属性 - -* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). 配置事务行为的全局点. 默认值为 `Auto` ,按照上面"*数据库事务行为"*一节的说明工作. 你可以使用此选项启用(甚至对于HTTP GET请求)或禁用事务. -* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值是 `null`** 并使用基础数据库提供程序的默认值. -* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务性的用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). - -## 控制工作单元 - -在某些情况下你可能希望更改常规事务作用域,创建内部作用域或精细控制事务行为. 下面几节将介绍这些可能性. - -### IUnitOfWorkEnabled 接口 - -这是为不是按照上面解释的约定作为工作单元的类(或类的层次结构)启用UOW的一种简单方法. - -**示例: 为任意服务实现 `IUnitOfWorkEnabled`** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace AbpDemo -{ - public class MyService : ITransientDependency, IUnitOfWorkEnabled - { - public virtual async Task FooAsync() - { - //this is a method with a UOW scope - } - } -} -```` - -然后 `MyService`(和它的派生类)方法都将是UOW. - -但是为了使它工作,**有些规则应该被遵守**; - -* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作). -* 仅异步方法(返回`Task`或`Task`的方法)被拦截. 因此同步方法无法启动UOW. - -> 注意,如果 `FooAsync` 在UOW作用域内被调用,那么它已经参与了UOW,不需要 `IUnitOfWorkEnabled` 或其他配置. - -### UnitOfWorkAttribute - -`UnitOfWork` attribute提供了更多的可能性,比如启用或禁用UOW和控制事务行为. - -`UnitOfWork` attribute可以用于**类**或**方法**级别. - -**示例: 为类的特定方法启用UOW** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - [UnitOfWork] - public virtual async Task FooAsync() - { - //this is a method with a UOW scope - } - - public virtual async Task BarAsync() - { - //this is a method without UOW - } - } -} -```` - -**示例: 为类的所有方法启用UOW** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace AbpDemo -{ - [UnitOfWork] - public class MyService : ITransientDependency - { - public virtual async Task FooAsync() - { - //this is a method with a UOW scope - } - - public virtual async Task BarAsync() - { - //this is a method with a UOW scope - } - } -} -```` - -**同样的规则**也适用于此: - -* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作). -* 仅异步方法(返回`Task`或`Task`的方法)被拦截. 因此同步方法无法启动UOW. - -#### UnitOfWorkAttribute 属性 - -* `IsTransactional` (`bool?`): 用于设置UOW是否是事务性的. **默认值为 `null`**. 如果你让它为 `null`,它会通过约定和配置自动确定. -* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值. -* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值. -* `IsDisabled` (`bool`): 用于禁用当前方法/类的UOW. - -> 如果在环境UOW作用域内调用方法,将忽略 `UnitOfWork` 属性,并且该方法参与周围的事务. - -**示例: 为控制器action禁用UOW** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Uow; - -namespace AbpDemo.Web -{ - public class MyController : AbpController - { - [UnitOfWork(IsDisabled = true)] - public virtual async Task FooAsync() - { - //... - } - } -} -```` - -## IUnitOfWorkManager - -`IUnitOfWorkManager` 是用于控制工作单元系统的主要服务. 下面的部分解释了如何使用此服务(大多数时候你并不需要). - -### 开始新的工作单元 - -`IUnitOfWorkManager.Begin` 方法用于创建一个新的UOW作用域. - -**示例: 创建一个新的非事务性UOW作用域** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace AbpDemo -{ - public class MyService : ITransientDependency - { - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public MyService(IUnitOfWorkManager unitOfWorkManager) - { - _unitOfWorkManager = unitOfWorkManager; - } - - public virtual async Task FooAsync() - { - using (var uow = _unitOfWorkManager.Begin( - requiresNew: true, isTransactional: false - )) - { - //... - - await uow.CompleteAsync(); - } - } - } -} -```` - -`Begin` 方法有以下可选参数: - -* `requiresNew` (`bool`): 设置为 `true` 可忽略周围的工作单元,并使用提供的选项启动新的UOW. **默认值为`false`. 如果为`false`,并且周围有UOW,则 `Begin` 方法实际上不会开始新的UOW,而是以静默方式参与现有的UOW**. -* `isTransactional` (`bool`). 默认为 `false`. -* `isolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值. -* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值. - -### 当前工作单元 - -如上所述UOW是环境的. 如果需要访问当前的工作单元,可以使用 `IUnitOfWorkManager.Current` 属性. - -**示例: 获取当前UOW** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace AbpDemo -{ - public class MyProductService : ITransientDependency - { - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public MyProductService(IUnitOfWorkManager unitOfWorkManager) - { - _unitOfWorkManager = unitOfWorkManager; - } - - public async Task FooAsync() - { - var uow = _unitOfWorkManager.Current; - //... - } - } -} -```` - -`Current` 属性返回一个 `IUnitOfWork` 对象. - -> 如果没有周围的工作单元,则**当前工作单元可以为`null`**. 如上所述,如果你的类是常规的UOW类,你将其手动设置为UOW或在UOW作用域内调用它,那么该值就不会为 `null`. - -#### SaveChangesAsync - -`IUnitOfWork.SaveChangesAsync()` 方法将到目前为止的所有更改保存到数据库中. 如果你正在使用EF Core,它的行为完全相同. 如果当前UOW是事务性的,即使已保存的更改也可以在错误时回滚(对于支持的数据库提供程序). - -**示例: 插入实体后保存更改以获取其自动增量ID** - -````csharp -using System.Threading.Tasks; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace AbpDemo -{ - public class CategoryAppService : ApplicationService, ICategoryAppService - { - private readonly IRepository _categoryRepository; - - public CategoryAppService(IRepository categoryRepository) - { - _categoryRepository = categoryRepository; - } - - public async Task CreateAsync(string name) - { - var category = new Category {Name = name}; - await _categoryRepository.InsertAsync(category); - - //Saving changes to be able to get the auto increment id - await UnitOfWorkManager.Current.SaveChangesAsync(); - - return category.Id; - } - } -} -```` - -示例的 `Category` [实体](Entities.md)使用自动递增的 `int` 主键. 自动增量PK需要将实体保存到数据库中来获得新实体的ID. - -示例是从基类 `ApplicationService` 派生的[应用服务](Application-Services.md), `IUnitOfWorkManager` 服务已经作为 `UnitOfWorkManager` 属性注入,所以无需手动注入. - -获取当前UOW非常常见,所以还有一个 `UnitOfWorkManager.Current` 的快捷属性 `CurrentUnitOfWork`. 所以可以对上面的例子进行以下更改: - -````csharp -await CurrentUnitOfWork.SaveChangesAsync(); -```` - -##### SaveChanges() 的替代方法 - -由于经常需要在插入,更新或删除实体后保存更改,相应的[仓储](Repositories.md)方法有一个可选的 `autoSave` 参数. 可以将上面的 `CreateAsync` 方法按如下重写: - -````csharp -public async Task CreateAsync(string name) -{ - var category = new Category {Name = name}; - await _categoryRepository.InsertAsync(category, autoSave: true); - return category.Id; -} -```` - -如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`. - -> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()` 和设置 `autoSave` 为 `true`. -> -> **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用. - -#### IUnitOfWork 其他属性/方法 - -* `OnCompleted` 方法获得一个回调动作,当工作单元成功完成时调用(在这里你可以确保所有更改都保存了). -* `Failed` 和 `Disposed` 事件可以用于UOW失败和被销毁的通知. -* `Complete` 和 `Rollback` 方法用于完成(提交)或回滚当前 UOW, 通常ABP框架在内部使用,如果你使用 `IUnitOfWorkManager.Begin` 方法手动启动事务,那么你可以手动使用这些方法. -* `Options` 可用于获取启动UOW时使用的选项. -* `Items` 字典可用于在同一工作单元内存储和获取任意对象,可以实现自定义逻辑. - -## ASP.NET Core 集成 - -工作单元系统已完全集成到ASP.NET Core. 它为UOW系统定义了动作过滤器和页面过滤器. 当你使用ASP.NET Core MVC控制器或Razor页面时,它可以正常工作. - -> 使用ASP.NET Core时,通常你不需要做任何操作配置UOW. - -### 工作单元中间件 - -`AbpUnitOfWorkMiddleware` 是可以在ASP.NET Core请求管道中启用UOW的中间件. 如果你需要扩大UOW范围以涵盖其他一些中间件,可以这样做. - -**示例:** - -````csharp -app.UseUnitOfWork(); -app.UseConfiguredEndpoints(); -```` diff --git a/docs/zh-Hans/Upgrading.md b/docs/zh-Hans/Upgrading.md deleted file mode 100644 index dd5bec7b19..0000000000 --- a/docs/zh-Hans/Upgrading.md +++ /dev/null @@ -1,59 +0,0 @@ -# 升级 ABP 框架 - -本文档解释当ABP框架发布一个新版本时,怎样升级你已存在的解决方案. - -## ABP UPDATE 命令 - -ABP 框架 & 模块生态系统由数百个 NuGet 和 NPM 包组成. 手动更新所有这些软件包以升级您的应用程序会很麻烦。 - -[ABP CLI](CLI.md) 提供了一个方便的命令,可以使用单个命令更新解决方案中所有与 ABP 相关的 NuGet 和 NPM 包: -````bash -abp update -```` -在终端里,解决方案的根文件夹中运行此命令。 - -> 如果您的解决方案有 Angular UI,您的解决方案中可能有 `aspnet-core` 和 `angular` 文件夹。在这两个文件夹的父文件夹中运行此命令。 - -### 数据库迁移 - -> 警告:迁移数据库时要小心,因为在某些情况下可能会丢失数据。在执行之前仔细检查生成的迁移代码。建议备份您当前的数据库。 - -当您升级到新版本时,如果您的数据库提供者是 **Entity Framework Core**,最好检查是否有数据库架构更改并升级您的数据库架构; - - -* 在包管理器控制台 (PMC) 中使用 `Add-Migration "Upgraded_To_Abp_4_1"` 或类似命令创建新迁移(在 Visual Studio 中将 `EntityFrameworkCore` 设置为 PMC 中的默认项目,并将 `.DbMigrator` 在解决方案资源管理器中设置为启动项目)。 -* 运行 `.DbMigrator` 应用程序以升级数据库并播种初始数据。 - -如果 `Add-Migration` 生成一个空迁移,您可以在执行 `.DbMigrator` 之前使用 `Remove-Migration` 将其删除。 - -## 博客文章和指南 - -每当您升级您的解决方案时,强烈建议您查看 [ABP BLOG](https://blog.abp.io/) 以了解新版本的新功能和更改。我们会定期发布帖子并编写此类更改。 - -### 迁移指南 - -如果新版本为现有应用程序带来重大更改,我们会准备迁移指南。所有指南请参见 [迁移指南](Migration-Guides/Index.md) 页面。 - -### 升级 Startup 模板 - -有时我们会引入需要**在启动模板中进行更改**的新功能/更改。我们已经在新应用程序的启动模板中实现了更改。但是,在某些情况下,您需要手动对现有解决方案进行一些小的更改。 - -逐行记录必要的更改是不切实际的。在这种情况下,我们建议您创建一个示例解决方案,一个是现有版本,一个是新版本,并使用差异工具进行比较。您可以 [查看本指南](Migration-Guides/Upgrading-Startup-Template.md) 了解如何使用 WinMerge 应用程序进行操作。 - -## 语义版本控制和重大更改 - -我们正在努力保持语义版本控制规则,因此您不会对 3.1、3.2、3.3 等次要(功能)版本进行重大更改...... - -但是,在某些情况下,我们也可能会在功能版本中引入重大更改; - -* ABP 有许多集成包,有时集成库/框架会发布主要版本并进行重大更改。在这种情况下,我们会仔细检查这些更改并决定是否升级集成包。如果变更的影响比较小,我们会更新集成包,并在发布博文中说明变更。在这种情况下,如果您使用过此集成包,则应按照博文中的说明进行操作。如果更改可能会破坏许多应用程序并且不容易修复,我们决定等到下一个主要的 ABP 框架版本发布时升级。 -* 有时我们必须进行重大更改以修复主要错误或使用问题。在这种情况下,我们认为开发人员已经无法正确使用该功能,因此通过重大更改修复它没有问题。在这种情况下,该功能通常是很少使用的功能。同样,我们尝试将影响保持在最低限度。 - -## 预览版和每日构建 - -预览版和夜间构建可以帮助您在新的稳定版本之前尝试新功能并调整您的解决方案。 - -* [预览版](Previews.md) 通常在次要(功能)版本之前约 2 周发布(我们的次要版本开发周期约为 4 周)。 -* [每日构建](Nightly-Builds.md) 每天晚上(周末除外)从开发分支发布。这意味着您可以尝试前一天的开发。 - -请参阅他们的文档以了解有关此类版本的详细信息。 diff --git a/docs/zh-Hans/Validation.md b/docs/zh-Hans/Validation.md deleted file mode 100644 index 6e46b07aed..0000000000 --- a/docs/zh-Hans/Validation.md +++ /dev/null @@ -1,164 +0,0 @@ -# 验证 - -验证系统用于验证对于特定的控制器操作或服务的方法的用户输入或客户端请求. - -ABP与ASP.NET Core模型验证系统系统兼容,[模型验证文档](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation)中的内容对于基于ABP应用程序同样有效.所以本文主要集中在ABP特征,而不是重复微软文档. - -ABP增加了以下优点: - -* 定义 `IValidationEnabled` 向任意类添加自动验证. 所有的[应用服务](Application-Services.md)都实现了该接口,所以它们会被自动验证. -* 自动将数据注解属性的验证错误信息本地化. -* 提供可扩展的服务来验证方法调用或对象的状态. -* 提供[FluentValidation](https://fluentvalidation.net/)的集成. - -## 验证DTO - -本节简要介绍了验证系统.有关详细信息请参阅[ASP.NET Core验证文档](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation). - -### 数据注解 Attribute - -使用数据注解是一种以声明式对[DTO](Data-Transfer-Objects.md)进行验证的简单方法. -示例 : - -````csharp -public class CreateBookDto -{ - [Required] - [StringLength(100)] - public string Name { get; set; } - - [Required] - [StringLength(1000)] - public string Description { get; set; } - - [Range(0, 999.99)] - public decimal Price { get; set; } -} -```` - -当使用该类作为[应用服务](Application-Services.md)或控制器的参数时,将对其自动验证并抛出本地化异常(由ABP框架[处理](Exception-Handling.md)). - -### IValidatableObject - -`IValidatableObject` can be implemented by a DTO to perform custom validation logic. `CreateBookDto` in the following example implements this interface and checks if the `Name` is equals to the `Description` and returns a validation error in this case. -可以将DTO实现 `IValidatableObject` 接口进行自定义验证逻辑. 下面的示例中 `CreateBookDto` 实现了这个接口,并检查 `Name` 是否等于 `Description` 并返回一个验证错误. - -````csharp -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateBookDto : IValidatableObject - { - [Required] - [StringLength(100)] - public string Name { get; set; } - - [Required] - [StringLength(1000)] - public string Description { get; set; } - - [Range(0, 999.99)] - public decimal Price { get; set; } - - public IEnumerable Validate( - ValidationContext validationContext) - { - if (Name == Description) - { - yield return new ValidationResult( - "Name and Description can not be the same!", - new[] { "Name", "Description" } - ); - } - } - } -} -```` - -#### 解析服务 - -如果你需要从[依赖注入系统](Dependency-Injection.md)解析服务,可以使用 `ValidationContext` 对象. -例: - -````csharp -var myService = validationContext.GetRequiredService(); -```` - -> 虽然可以在 `Validate` 方法中解析服务实现任何可能性,但在DTO中实现领域验证逻辑不是一个很好的做法. 应保持简单的DTO,他们的目的是传输数据(DTO:数据传输对象). - -## 验证基础设施 - -本节介绍了ABP框架提供的一些额外的服务. - -### IValidationEnabled 接口 - -`IValidationEnabled` 是可以由任何类来实现的空标记接口(注册到[DI](Dependency-Injection.md)并从中解析),让ABP框架为该类执行验证系统. -示例 : - -````csharp -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Validation; - -namespace Acme.BookStore -{ - public class MyService : ITransientDependency, IValidationEnabled - { - public virtual async Task DoItAsync(MyInput input) - { - //... - } - } -} -```` - -> ABP框架使用[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统来执行验证.为了使其工作,你的方法应该是 **virtual** 的,服务应该被注入并通过接口(如`IMyService`)使用. - -### AbpValidationException - -一旦ABP确定了一个验证错误,它就会抛出类型为 `AbpValidationException` 的异常. 你的应用程序代码可以抛出 `AbpValidationException`,但大多数情况不会使用它. - -* `ValidationErrors` 是 `AbpValidationException` 的属性,它包含了验证错误列表. -* `AbpValidationException` 的日志级别设置为 `Warning`. 它将所有验证错误记录到[日志系统](Logging.md). -* `AbpValidationException` 由ABP框架自动捕获并将HTTP状态码设置为400转换成可用的错误响应. 参阅[异常处理](Exception-Handling.md)文档了解更多. - -## 高级主题 - -### IObjectValidator - -除了自动验证你可能需要手动验证对象,这种情况下[注入](Dependency-Injection.md)并使用 `IObjectValidator` 服务: - -* `ValidateAsync` 方法根据验证​​规则验证给定对象,如果对象没有被验证通过会抛出 `AbpValidationException` 异常. -* `GetErrorsAsync` 不会抛出异常,只返回验证错误. - -`IObjectValidator` 默认由 `ObjectValidator` 实现. `ObjectValidator`是可扩展的; 可以实现`IObjectValidationContributor`接口提供自定义逻辑. -示例 : - -````csharp -public class MyObjectValidationContributor - : IObjectValidationContributor, ITransientDependency -{ - public Task AddErrorsAsync(ObjectValidationContext context) - { - //Get the validating object - var obj = context.ValidatingObject; - - //Add the validation errors if available - context.Errors.Add(...); - return Task.CompletedTask; - } -} -```` - -* 记录将类注册到[DI](Dependency-Injection.md)(实现`ITransientDependency` 如同本例) -* ABP会自动发现验证类,并用于任何类型的对象验证(包括自动方法调用验证). - -### IMethodInvocationValidator - -`IMethodInvocationValidator` 用于验证方法调用. 它在内部使用 `IObjectValidator` 来验证传递给方法调用的对象. 由于框架会自动使用此服务,通常你并不需要此服务,但在少数情况下你可能在应用程序中重用或替换它. - -## FluentValidation Integration - -Volo.Abp.FluentValidation 包将FluentValidation库集成到了验证系统(通过实现 `IObjectValidationContributor`). 请参阅[FluentValidation集成文档](FluentValidation.md)了解更多信息. \ No newline at end of file diff --git a/docs/zh-Hans/Value-Objects.md b/docs/zh-Hans/Value-Objects.md deleted file mode 100644 index c5dd929ff2..0000000000 --- a/docs/zh-Hans/Value-Objects.md +++ /dev/null @@ -1,75 +0,0 @@ -## 值对象 - -> 一个对象,表示领域的描述方面,没有概念上的身份被称为 值对象. -> -> (Eric Evans) - -属性相同但`Id`不同的两个[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities) 被视为不同的实体.但是,值对象没有`Id` - -## 值对象的类 - -值对象是一个抽象类,可以继承它来创建值对象类 - -**示例: An Address class** - -```csharp -public class Address : ValueObject -{ - public Guid CityId { get; private set; } - - public string Street { get; private set; } - - public int Number { get; private set; } - - private Address() - { - - } - - public Address( - Guid cityId, - string street, - int number) - { - CityId = cityId; - Street = street; - Number = number; - } - - protected override IEnumerable GetAtomicValues() - { - yield return Street; - yield return CityId; - yield return Number; - } -} -``` - -- 值对象类必须实现 `GetAtomicValues()`方法来返回原始值 - -### ValueEquals - -`ValueObject.ValueEquals(...)` 用于检测两个值是否相等 - -**示例: Check if two addresses are equals** - -```csharp -Address address1 = ... -Address address2 = ... - -if (address1.ValueEquals(address2)) //Check equality -{ - ... -} -``` - -## 最佳实践 - -以下是使用值对象时的一些最佳实践: - -- 如果没有充分的理由将值对象设计为可变的,则将其设计为**不可变**(如上面的地址). -- 构成一个值对象的属性应该形成一个概念整体.例如:CityId,Street和Number不应是个人实体的单独属性.这也使Person实体更简单. - -## 另请参阅 - -- [实体](Entities.md) diff --git a/docs/zh-Hans/Virtual-File-System.md b/docs/zh-Hans/Virtual-File-System.md deleted file mode 100644 index 17ec91b2f9..0000000000 --- a/docs/zh-Hans/Virtual-File-System.md +++ /dev/null @@ -1,185 +0,0 @@ -## 虚拟文件系统 - -虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们像物理文件一样使用. - -## 安装 - -> 大多数情况下你不需要手动安装这个包,因为[应用程序启动模板](Startup-Templates/Application.md)已经预先安装. - -[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) 是虚拟文件系统的NuGet主页. - -使用ABP CLIi添加包到你的项目: - -* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. -* 在你想要添加 `Volo.Abp.VirtualFileSystem` 包的项目的 `.csproj` 文件目录打开命令行(终端). -* 运行 `abp add-package Volo.Abp.VirtualFileSystem` 命令. - -如果你想要手动安装,安装[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem)NuGet包到你的项目并且添加`[DependsOn(typeof(AbpVirtualFileSystemModule))]`到你项目的[ABP Module](Module-Development-Basics.md)类. - -## 与嵌入式文件工作 - -### 嵌入文件 - -要将文件嵌入到程序集中, 首先需要把该文件标记为**嵌入式资源**. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**. -例如: - -![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png) - -如果需要添加多个文件, 这样做会很乏味. 作为选择, 你可以直接编辑 **.csproj** 文件: - -````C# - - - - -```` - -此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件). - -如果文件名包含一些特殊字符,在项目/程序集中嵌入文件可能会导致问题. 为了克服这个限制; - -1. 将[Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet包添加到包含嵌入式资源的项目中. -2. 添加 `true` 到 `.csproj` 文件的 `...` 部分中. - -> 尽管这两个步骤是可选的,并且ABP无需这些配置即可工作,但强烈建议你这样做. - -### 配置AbpVirtualFileSystemOptions - -使用 `AbpVirtualFileSystemOptions` [选项类](Options.md)可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中将嵌入式文件注册到虚拟文件系统. - -**示例: 添加嵌入式文件到虚拟文件系统** - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded(); -}); -```` - -`AddEmbedded` 扩展方法需要一个类, 从给定**类的程序集中查找所有嵌入文件**, 并将它们注册到虚拟文件系统. 通常将模块类作为通用参数传递. - -`AddEmbedded` 有两个可选参数; - -* `baseNamespace`: 仅在你没有配置上面说明的 `GenerateEmbeddedFilesManifest` 并且你的根名称空间不为空时,才可能需要这样做. 在这种情况下,请在此处设置你的根名称空间. -* `baseFolder`: 如果你不想公开项目中的所有嵌入式文件,只希望公开特定的文件夹(和子文件夹/文件),可以相对于项目根页面设置基本文件夹. - -**示例: 添加项目中 `MyFiles` 目录下的文件** - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded( - baseNamespace: "Acme.BookStore.MyFiles", - baseFolder: "/MyFiles" - ); -}); -```` - -这个例子假设; - -* 你的项目根(default)命令空间是 `Acme.BookStore`. -* 你的项目有一个名为 `MyFiles` 的目录. -* 你只想添加 `MyFiles` 目录到虚拟文件系统. - -## IVirtualFileProvider - -将文件嵌入到程序集中并注册到虚拟文件系统后,可以使用 `IVirtualFileProvider` 接口来获取文件或目录内容: - -````C# -public class MyService : ITransientDependency -{ - private readonly IVirtualFileProvider _virtualFileProvider; - - public MyService(IVirtualFileProvider virtualFileProvider) - { - _virtualFileProvider = virtualFileProvider; - } - - public void Test() - { - //Getting a single file - var file = _virtualFileProvider - .GetFileInfo("/MyResources/js/test.js"); - - var fileContent = file.ReadAsString(); - - //Getting all files/directories under a directory - var directoryContents = _virtualFileProvider - .GetDirectoryContents("/MyResources/js"); - } -} -```` - -### ASP.NET Core 集成 - -虚拟文件系统与 ASP.NET Core 无缝集成: - -* 虚拟文件可以像Web应用程序上的物理(静态)文件一样使用. -* Js, css, 图像文件和所有其他Web内容可以嵌入到程序集中并像物理文件一样使用. -* 应用程序(或其他模块)可以**覆盖模块的虚拟文件**, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样. - -#### 静态虚拟文件夹 - -默认情况下,ASP.NET Core仅允许 `wwwroot` 文件夹包含客户端使用的静态文件. 当你使用虚拟文件系统时以下文件夹也可以包含静态文件: - -* Pages -* Views -* Components -* Themes - -这允许你可以在 `.cshtml` 文件附近添加 `.js`, `.css`... 文件,更易于开发和维护你的项目. - -#### 在开发过程中处理嵌入式文件 - -将文件嵌入到模块程序集中并能够通过引用程序集(或添加nuget包)在另一个项目中使用它对于创建可重用模块非常有价值. 但是, 这使得开发模块本身变得有点困难. - -假设你正在开发一个包含嵌入式JavaScript文件的模块. 当你更改文件时, 你必须重新编译项目, 重新启动应用程序并刷新浏览器页面以使更改生效. 显然, 这是非常耗时和乏味的. - -我们需要的是应用程序在开发时直接使用物理文件的能力, 让浏览器刷新时同步JavaScript文件的任何更改. `ReplaceEmbeddedByPhysical` 方法使其成为可能. - -下面的示例展示了应用程序依赖于包含嵌入文件的模块("MyModule"), 并且应用程序可以在开发过程中直接使用模块的源代码. - -````C# -[DependsOn(typeof(MyModule))] -public class MyWebAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - var hostingEnvironment = context.Services.GetHostingEnvironment(); - - if (hostingEnvironment.IsDevelopment()) //only for development time - { - Configure(options => - { - options.FileSets.ReplaceEmbeddedByPhysical( - Path.Combine( - hostingEnvironment.ContentRootPath, - string.Format( - "..{0}MyModuleProject", - Path.DirectorySeparatorChar - ) - ) - ); - }); - } - } -} -```` - -上面的代码假设`MyWebAppModule`和`MyModule`是Visual Studio解决方案中的两个不同的项目, `MyWebAppModule`依赖于`MyModule`. - -> [应用程序启动模板]已经为本地化文件应用这个方法,所以当你更改一个本地化文件时,它会自动检测到更改. - -## 替换/重写虚拟文件 - -虚拟文件系统在运行时创建一个统一的文件系统,其中实际的文件在开发时被分配到不同的模块中. - -如果两个模块将文件添加到相同的虚拟路径(如`my-path/my-file.css`),之后添加的模块将替换/替换前一个([模块依赖](Module-Development-Basics.md)顺序决定了添加文件的顺序). - -此功能允许你的应用程序可以覆盖/替换定义应用程序所使用的模块的任何虚拟文件. 这是ABP框架的基本可扩展性功能之一. - -因此,如果需要替换模块的文件,只需在模块/应用程序中完全相同的路径中创建该文件. - -### 物理文件 - -物理文件总是覆盖虚拟文件. 这意味着如果你把一个文件放在 `/wwwroot/my-folder/my-file.css`,它将覆盖虚拟文件系统相同位置的文件.因此你需要知道在模块中定义的文件路径来覆盖它们. diff --git a/docs/zh-Hans/_resources/Diagrams.docx b/docs/zh-Hans/_resources/Diagrams.docx deleted file mode 100644 index 424e281470..0000000000 Binary files a/docs/zh-Hans/_resources/Diagrams.docx and /dev/null differ diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json deleted file mode 100644 index 13cb219ba1..0000000000 --- a/docs/zh-Hans/docs-nav.json +++ /dev/null @@ -1,894 +0,0 @@ -{ - "items": [ - { - "text": "快速入门", - "path": "Tutorials/Todo/Index.md" - }, - { - "text": "入门教程", - "items": [ - { - "text": "Web应用程序", - "path": "Getting-Started.md", - "items": [ - { - "text": "1: 设置你的开发环境", - "path": "Getting-Started-Setup-Environment.md" - }, - { - "text": "2: 创建新的解决方案", - "path": "Getting-Started-Create-Solution.md" - }, - { - "text": "3: 运行解决方案", - "path": "Getting-Started-Running-Solution.md" - } - ] - }, - { - "text": "控制台应用程序", - "path": "Startup-Templates/Console.md" - }, - { - "text": "WPF应用程序", - "path": "Startup-Templates/WPF.md" - }, - { - "text": "空Web应用程序", - "path": "Getting-Started-AspNetCore-Application.md" - } - ] - }, - { - "text": "启动模板", - "items": [ - { - "text": "概述", - "path": "Startup-Templates/Index.md" - }, - { - "text": "应用程序", - "path": "Startup-Templates/Application.md" - }, - { - "text": "模块", - "path": "Startup-Templates/Module.md" - }, - { - "text": "控制台", - "path": "Startup-Templates/Console.md" - }, - { - "text": "WPF", - "path": "Startup-Templates/WPF.md" - }, - { - "text": "MAUI", - "path": "Startup-Templates/MAUI.md" - } - ] - }, - { - "text": "教程", - "items": [ - { - "text": "应用开发", - "items": [ - { - "text": "1: 创建应用服务端", - "path": "Tutorials/Part-1.md" - }, - { - "text": "2: 图书列表页面", - "path": "Tutorials/Part-2.md" - }, - { - "text": "3: 创建,更新和删除书籍", - "path": "Tutorials/Part-3.md" - }, - { - "text": "4: 集成测试", - "path": "Tutorials/Part-4.md" - }, - { - "text": "5: 授权", - "path": "Tutorials/Part-5.md" - }, - { - "text": "6: 作者:领域层", - "path": "Tutorials/Part-6.md" - }, - { - "text": "7: 作者:数据库集成", - "path": "Tutorials/Part-7.md" - }, - { - "text": "8: 作者:应用服务层", - "path": "Tutorials/Part-8.md" - }, - { - "text": "9: 作者:用户界面", - "path": "Tutorials/Part-9.md" - }, - { - "text": "10: 图书-作者关系", - "path": "Tutorials/Part-10.md" - } - ] - }, - { - "text": "社区文章", - "path": "https://community.abp.io/articles" - }, - { - "text": "从ASP.NET Boilerplate迁移", - "path": "AspNet-Boilerplate-Migration-Guide.md" - } - ] - }, - { - "text": "基础知识", - "items": [ - { - "text": "配置", - "path": "Configuration.md" - }, - { - "text": "选项", - "path": "Options.md" - }, - { - "text": "依赖注入", - "path": "Dependency-Injection.md", - "items": [ - { - "text": "AutoFac 集成", - "path": "Autofac-Integration.md" - } - ] - }, - { - "text": "本地化", - "path": "Localization.md" - }, - { - "text": "异常处理", - "path": "Exception-Handling.md" - }, - { - "text": "验证", - "path": "Validation.md", - "items": [ - { - "text": "FluentValidation集成", - "path": "FluentValidation.md" - } - ] - }, - { - "text": "授权", - "path": "Authorization.md" - }, - { - "text": "缓存", - "path": "Caching.md", - "items": [ - { - "text": "Redis 缓存", - "path": "Redis-Cache.md" - } - ] - }, - { - "text": "日志", - "path": "Logging.md" - }, - { - "text": "设置管理", - "path": "Settings.md" - }, - { - "text": "连接字符串", - "path": "Connection-Strings.md" - }, - { - "text": "对象扩展", - "path": "Object-Extensions.md" - } - ] - }, - { - "text": "基础设施", - "items":[ - { - "text": "后台服务", - "items": [ - { - "text": "后台作业", - "path": "Background-Jobs.md", - "items": [ - { - "text": "Hangfire 集成", - "path": "Background-Jobs-Hangfire.md" - }, - { - "text": "RabbitMQ 集成", - "path": "Background-Jobs-RabbitMq.md" - }, - { - "text": "Quartz 集成", - "path": "Background-Jobs-Quartz.md" - } - ] - }, - { - "text": "后台工作者", - "path": "Background-Workers.md", - "items": [ - { - "text": "Quartz 集成", - "path": "Background-Workers-Quartz.md" - }, - { - "text": "Hangfire 集成", - "path": "Background-Workers-Hangfire.md" - } - ] - } - ] - }, - { - "text": "事件总线", - "items": [ - { - "text": "概述", - "path": "Event-Bus.md" - }, - { - "text": "本地 Event Bus", - "path": "Local-Event-Bus.md" - }, - { - "text": "分布式 Event Bus", - "path": "Distributed-Event-Bus.md", - "items": [ - { - "text": "RabbitMQ 集成", - "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" - }, - { - "text": "Kafka 集成", - "path": "Distributed-Event-Bus-Kafka-Integration.md" - }, - { - "text": "Rebus 集成", - "path": "Distributed-Event-Bus-Rebus-Integration.md" - } - ] - } - ] - }, - { - "text": "种子数据", - "path": "Data-Seeding.md" - }, - { - "text": "分布式锁", - "path": "Distributed-Locking.md" - }, - { - "text": "虚拟文件系统", - "path": "Virtual-File-System.md" - }, - { - "text": "审计日志", - "path": "Audit-Logging.md" - }, - { - "text": "数据过滤", - "path": "Data-Filtering.md" - }, - { - "text": "当前用户", - "path": "CurrentUser.md" - }, - { - "text": "对象到对象映射", - "path": "Object-To-Object-Mapping.md" - - }, - { - "text": "邮件发送", - "items": [ - { - "text": "邮件发送系统", - "path": "Emailing.md" - }, - { - "text": "MailKit集成", - "path": "MailKit.md" - } - ] - }, - { - "text": "BLOB存储", - "items": [ - { - "text": "BLOB存储系统", - "path": "Blob-Storing.md" - }, - { - "text": "存储提供程序", - "items": [ - { - "text": "文件系统提供程序", - "path": "Blob-Storing-File-System.md" - }, - { - "text": "数据库系统提供程序", - "path": "Blob-Storing-Database.md" - }, - { - "text": "Azure提供程序", - "path": "Blob-Storing-Azure.md" - }, - { - "text": "Aliyun提供程序", - "path": "Blob-Storing-Aliyun.md" - }, - { - "text": "Minio提供程序", - "path": "Blob-Storing-Minio.md" - }, - { - "text": "AWS提供程序", - "path": "Blob-Storing-Aws.md" - }, - { - "text": "创建自定义提供程序", - "path": "Blob-Storing-Custom-Provider.md" - } - ] - } - ] - }, - { - "text": "文本模板", - "path": "Text-Templating.md" - }, - { - "text": "GUID 生成", - "path": "Guid-Generation.md" - }, - { - "text": "时钟", - "path": "Timing.md" - } - ] - }, - { - "text": "架构", - "items":[ - { - "text": "模块化", - "items": [ - { - "text": "基础", - "path": "Module-Development-Basics.md" - }, - { - "text": "模块插件", - "path": "PlugIn-Modules.md" - }, - { - "text": "自定义应用模块", - "path": "Customizing-Application-Modules-Guide.md", - "items": [ - { - "text": "扩展实体", - "path": "Customizing-Application-Modules-Extending-Entities.md" - }, - { - "text": "重写服务", - "path": "Customizing-Application-Modules-Overriding-Services.md" - }, - { - "text": "重写用户界面", - "path": "Customizing-Application-Modules-Overriding-User-Interface.md" - } - ] - }, - { - "text": "最佳实践", - "items": [ - { - "text": "总体", - "path": "Best-Practices/Index.md" - }, - { - "text": "模块架构", - "path": "Best-Practices/Module-Architecture.md" - }, - { - "text": "领域层", - "items": [ - { - "text": "实体", - "path": "Best-Practices/Entities.md" - }, - { - "text": "仓储", - "path": "Best-Practices/Repositories.md" - }, - { - "text": "领域服务", - "path": "Best-Practices/Domain-Services.md" - } - ] - }, - { - "text": "应用程序层", - "items": [ - { - "text": "应用程序服务", - "path": "Best-Practices/Application-Services.md" - }, - { - "text": "数据传输对象", - "path": "Best-Practices/Data-Transfer-Objects.md" - } - ] - }, - { - "text": "数据访问", - "items": [ - { - "text": "Entity Framework Core 集成", - "path": "Best-Practices/Entity-Framework-Core-Integration.md" - }, - { - "text": "MongoDB 集成", - "path": "Best-Practices/MongoDB-Integration.md" - } - ] - } - ] - } - ] - }, - { - "text": "领域驱动设计", - "items": [ - { - "text": "概述", - "path": "Domain-Driven-Design.md" - }, - { - "text": "领域层", - "items": [ - { - "text": "实体&聚合根", - "path": "Entities.md" - }, - { - "text": "值对象", - "path": "Value-Objects.md" - }, - { - "text": "仓储", - "path": "Repositories.md" - }, - { - "text": "领域服务", - "path": "Domain-Services.md" - }, - { - "text": "规约", - "path": "Specifications.md" - } - ] - }, - { - "text": "应用服务层", - "items": [ - { - "text": "应用服务", - "path": "Application-Services.md" - }, - { - "text": "数据传输对象(DTO)", - "path": "Data-Transfer-Objects.md" - }, - { - "text": "工作单元", - "path": "Unit-Of-Work.md" - } - ] - }, - { - "text": "指南: 实现DDD", - "path": "Domain-Driven-Design-Implementation-Guide.md" - } - ] - }, - { - "text": "多租户", - "path": "Multi-Tenancy.md" - }, - { - "text": "微服务架构", - "path": "Microservice-Architecture.md" - } - ] - }, - { - "text": "API", - "items": [ - { - "text": "自动API控制器", - "path": "API/Auto-API-Controllers.md" - }, - { - "text": "动态C# API客户端", - "path": "API/Dynamic-CSharp-API-Clients.md" - }, - { - "text": "ABP端点", - "items": [ - { - "text": "应用程序配置", - "path": "API/Application-Configuration.md" - } - ] - }, - { - "text": "API版本控制", - "path": "API/API-Versioning.md" - } - ] - }, - { - "text": "用户界面", - "items": [ - { - "text": "ASP.NET Core", - "items": [ - { - "text": "客户端包管理", - "path": "UI/AspNetCore/Client-Side-Package-Management.md" - }, - { - "text": "捆绑&压缩", - "path": "UI/AspNetCore/Bundling-Minification.md" - }, - { - "text": "Tag Helpers", - "path": "UI/AspNetCore/Tag-Helpers/Index.md", - "items": [ - { - "text": "Form元素", - "path": "UI/AspNetCore/Tag-Helpers/Form-elements.md" - }, - { - "text": "动态表单", - "path": "UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md" - } - ] - }, - { - "text": "仪表板和小部件(Widget)系统", - "path": "UI/AspNetCore/Widgets.md" - }, - { - "text": "自定义/扩展UI", - "path": "UI/AspNetCore/Customization-User-Interface.md" - }, - { - "text": "测试", - "path": "UI/AspNetCore/Testing.md" - }, - { - "text": "主题化", - "path": "UI/AspNetCore/Theming.md" - } - ] - }, - { - "text": "Angular", - "items": [ - { - "text": "v2.x 到 v3 迁移指南", - "path": "UI/Angular/Migration-Guide-v3.md" - }, - { - "text": "环境", - "path": "UI/Angular/Environment.md" - }, - { - "text": "服务代理", - "path": "UI/Angular/Service-Proxies.md" - }, - { - "text": "单元测试", - "path": "UI/Angular/Testing.md" - }, - { - "text": "HTTP请求", - "path": "UI/Angular/HTTP-Requests.md" - }, - { - "text": "本地化", - "path": "UI/Angular/Localization.md" - }, - { - "text": "权限管理", - "path": "UI/Angular/Permission-Management.md" - }, - { - "text": "多租户", - "path": "UI/Angular/Multi-Tenancy.md" - }, - { - "text": "确认弹层", - "path": "UI/Angular/Confirmation-Service.md" - }, - { - "text": "Toast Overlay", - "path": "UI/Angular/Toaster-Service.md" - }, - { - "text": "配置状态", - "path": "UI/Angular/Config-State.md" - }, - { - "text": "修改菜单", - "path": "UI/Angular/Modifying-the-Menu.md" - }, - { - "text": "替换组件", - "path": "UI/Angular/Component-Replacement.md" - }, - { - "text": "懒加载Scripts与Styles", - "path": "UI/Angular/Lazy-Load-Service.md" - }, - { - "text": "DomInsertionService", - "path": "UI/Angular/Dom-Insertion-Service.md" - }, - { - "text": "ContentProjectionService", - "path": "UI/Angular/Content-Projection-Service.md" - }, - { - "text": "TrackByService", - "path": "UI/Angular/Track-By-Service.md" - }, - { - "text": "SubscriptionService", - "path": "UI/Angular/Subscription-Service.md" - }, - { - "text": "ListService", - "path": "UI/Angular/List-Service.md" - } - ] - }, - { - "text": "React Native", - "items": [ - { - "text": "入门", - "path": "Getting-Started-React-Native.md" - } - ] - }, - { - "text": "通用", - "items": [ - { - "text": "Utilities", - "items": [ - { - "text": "链表 (双向)", - "path": "UI/Common/Utils/Linked-List.md" - } - ] - } - ] - } - ] - }, - { - "text": "数据访问", - "items": [ - { - "text": "概述", - "path": "Data-Access.md" - }, - { - "text": "Entity Framework Core", - "path": "Entity-Framework-Core.md", - "items": [ - { - "text": "数据库迁移", - "path": "Entity-Framework-Core-Migrations.md" - }, - { - - "text": "切换DMBS", - "path": "Entity-Framework-Core-Other-DBMS.md", - "items":[ - { - "text": "到MySql", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "到PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": " Oracle", - "path": "Entity-Framework-Core-Oracle.md" - }, - { - "text": "到SQLite", - "path": "Entity-Framework-Core-SQLite.md" - } - ] - } - ] - }, - { - "text": "MongoDB", - "path": "MongoDB.md" - }, - { - "text": "Dapper", - "path": "Dapper.md" - } - ] - }, - { - "text": "实时", - "items": [ - { - "text": "SignalR集成", - "path": "SignalR-Integration.md" - } - ] - }, - { - "text": "测试", - "path": "Testing.md" - }, - { - "text": "部署", - "path": "Deployment/Index.md", - "items": [ - { - "text": "部署到集群环境", - "path": "Deployment/Clustered-Environment.md" - } - ] - }, - { - "text": "示例", - "items": [ - { - "text": "所有示例", - "path": "Samples/Index.md" - }, - { - "text": "微服务示例", - "path": "Samples/Microservice-Demo.md" - } - ] - }, - { - "text": "应用模块", - "items":[ - { - "text": "概述", - "path": "Modules/Index.md" - }, - { - "text": "账户", - "path": "Modules/Account.md" - }, - { - "text": "审计日志", - "path": "Modules/Audit-Logging.md" - }, - { - "text": "后台作业", - "path": "Modules/Background-Jobs.md" - }, - { - "text": "CMS Kit", - "path": "Modules/Cms-Kit/Index.md" - }, - { - "text": "文档", - "path": "Modules/Docs.md" - }, - { - "text": "功能管理", - "path": "Modules/Feature-Management.md" - }, - { - "text": "Identity", - "path": "Modules/Identity.md" - }, - { - "text": "IdentityServer", - "path": "Modules/IdentityServer.md" - }, - { - "text": "权限管理", - "path": "Modules/Permission-Management.md" - }, - { - "text": "设置管理", - "path": "Modules/Setting-Management.md" - }, - { - "text": "租户管理", - "path": "Modules/Tenant-Management.md" - }, - { - "text": "虚拟文件浏览器", - "path": "Modules/Virtual-File-Explorer.md" - } - ] - }, - { - "text": "发布信息", - "items": [ - { - "text": "升级", - "path": "Upgrading.md" - }, - { - "text": "官方包", - "path": "https://abp.io/packages" - }, - { - "text": "预览版本", - "path": "Previews.md" - }, - { - "text": "每日构建", - "path": "Nightly-Builds.md" - }, - { - "text": "路线图", - "path": "Road-Map.md" - } - ] - }, - { - "text": "参考", - "items": [ - { - "text": "CLI", - "path": "CLI.md", - "items": [ - { - "text": "新命令示例", - "path": "CLI-New-Command-Samples.md" - } - ] - }, - { - "text": "API文档", - "path": "{ApiDocumentationUrl}" - } - ] - }, - { - "text": "贡献指南", - "path": "Contribution/Index.md" - } - ] -} diff --git a/docs/zh-Hans/docs-params.json b/docs/zh-Hans/docs-params.json deleted file mode 100644 index 46e496f248..0000000000 --- a/docs/zh-Hans/docs-params.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "parameters": [ - { - "name": "UI", - "displayName": "UI", - "values": { - "MVC": "MVC / Razor Pages", - "Blazor": "Blazor WebAssembly", - "BlazorServer": "Blazor Server", - "NG": "Angular" - } - }, - { - "name": "DB", - "displayName": "Database", - "values": { - "EF": "Entity Framework Core", - "Mongo": "MongoDB" - } - }, - { - "name": "Tiered", - "displayName": "Tiered", - "values": { - "No": "Not Tiered", - "Yes": "Tiered" - } - } - ] -} \ No newline at end of file diff --git a/docs/zh-Hans/images/MonthlyProfitWidgetFiles.png b/docs/zh-Hans/images/MonthlyProfitWidgetFiles.png deleted file mode 100644 index c3e4d6f1ab..0000000000 Binary files a/docs/zh-Hans/images/MonthlyProfitWidgetFiles.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-book.png b/docs/zh-Hans/images/abp-book.png deleted file mode 100644 index 7fd6c34c90..0000000000 Binary files a/docs/zh-Hans/images/abp-book.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-dynamic-form-result.png b/docs/zh-Hans/images/abp-dynamic-form-result.png deleted file mode 100644 index 10048c9156..0000000000 Binary files a/docs/zh-Hans/images/abp-dynamic-form-result.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png b/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png deleted file mode 100644 index d8906aec46..0000000000 Binary files a/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error-french.png b/docs/zh-Hans/images/abp-form-input-validation-error-french.png deleted file mode 100644 index 6fbe26b81b..0000000000 Binary files a/docs/zh-Hans/images/abp-form-input-validation-error-french.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error.png b/docs/zh-Hans/images/abp-form-input-validation-error.png deleted file mode 100644 index 518cdc8fdf..0000000000 Binary files a/docs/zh-Hans/images/abp-form-input-validation-error.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-select2-multiple.png b/docs/zh-Hans/images/abp-select2-multiple.png deleted file mode 100644 index 057bde4b57..0000000000 Binary files a/docs/zh-Hans/images/abp-select2-multiple.png and /dev/null differ diff --git a/docs/zh-Hans/images/abp-select2-single.png b/docs/zh-Hans/images/abp-select2-single.png deleted file mode 100644 index 0690b83834..0000000000 Binary files a/docs/zh-Hans/images/abp-select2-single.png and /dev/null differ diff --git a/docs/zh-Hans/images/account-module-forgot-password.png b/docs/zh-Hans/images/account-module-forgot-password.png deleted file mode 100644 index ddb4fa4a66..0000000000 Binary files a/docs/zh-Hans/images/account-module-forgot-password.png and /dev/null differ diff --git a/docs/zh-Hans/images/account-module-login.png b/docs/zh-Hans/images/account-module-login.png deleted file mode 100644 index 2e91d01622..0000000000 Binary files a/docs/zh-Hans/images/account-module-login.png and /dev/null differ diff --git a/docs/zh-Hans/images/account-module-manage-account.png b/docs/zh-Hans/images/account-module-manage-account.png deleted file mode 100644 index 28fdf80342..0000000000 Binary files a/docs/zh-Hans/images/account-module-manage-account.png and /dev/null differ diff --git a/docs/zh-Hans/images/account-module-register.png b/docs/zh-Hans/images/account-module-register.png deleted file mode 100644 index 6c04039366..0000000000 Binary files a/docs/zh-Hans/images/account-module-register.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png b/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png deleted file mode 100644 index fe43ab0d4c..0000000000 Binary files a/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-propert-to-user-database-field.png b/docs/zh-Hans/images/add-new-propert-to-user-database-field.png deleted file mode 100644 index 713d33237f..0000000000 Binary files a/docs/zh-Hans/images/add-new-propert-to-user-database-field.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-property-enum.png b/docs/zh-Hans/images/add-new-property-enum.png deleted file mode 100644 index 0f8427764e..0000000000 Binary files a/docs/zh-Hans/images/add-new-property-enum.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png deleted file mode 100644 index fea1cfb02b..0000000000 Binary files a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png deleted file mode 100644 index 79c0c7cff2..0000000000 Binary files a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form.png b/docs/zh-Hans/images/add-new-property-to-user-form.png deleted file mode 100644 index 8e42823a2d..0000000000 Binary files a/docs/zh-Hans/images/add-new-property-to-user-form.png and /dev/null differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-table.png b/docs/zh-Hans/images/add-new-property-to-user-table.png deleted file mode 100644 index 19f4fea22d..0000000000 Binary files a/docs/zh-Hans/images/add-new-property-to-user-table.png and /dev/null differ diff --git a/docs/zh-Hans/images/ajax-error.png b/docs/zh-Hans/images/ajax-error.png deleted file mode 100644 index ffc456cd41..0000000000 Binary files a/docs/zh-Hans/images/ajax-error.png and /dev/null differ diff --git a/docs/zh-Hans/images/angular-folder-structure.png b/docs/zh-Hans/images/angular-folder-structure.png deleted file mode 100644 index c202a7aaa3..0000000000 Binary files a/docs/zh-Hans/images/angular-folder-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/angular-module-dev-app-project.png b/docs/zh-Hans/images/angular-module-dev-app-project.png deleted file mode 100644 index 94eeecefaf..0000000000 Binary files a/docs/zh-Hans/images/angular-module-dev-app-project.png and /dev/null differ diff --git a/docs/zh-Hans/images/angular-module-folder-structure.png b/docs/zh-Hans/images/angular-module-folder-structure.png deleted file mode 100644 index e92114041b..0000000000 Binary files a/docs/zh-Hans/images/angular-module-folder-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/angular-template-structure-diagram.png b/docs/zh-Hans/images/angular-template-structure-diagram.png deleted file mode 100644 index 269c54984c..0000000000 Binary files a/docs/zh-Hans/images/angular-template-structure-diagram.png and /dev/null differ diff --git a/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png b/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png deleted file mode 100644 index 7196f6aad2..0000000000 Binary files a/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/auditlog-object-diagram.png b/docs/zh-Hans/images/auditlog-object-diagram.png deleted file mode 100644 index f89c110147..0000000000 Binary files a/docs/zh-Hans/images/auditlog-object-diagram.png and /dev/null differ diff --git a/docs/zh-Hans/images/authorization-new-permission-ui-hierarcy.png b/docs/zh-Hans/images/authorization-new-permission-ui-hierarcy.png deleted file mode 100644 index 07abfc7132..0000000000 Binary files a/docs/zh-Hans/images/authorization-new-permission-ui-hierarcy.png and /dev/null differ diff --git a/docs/zh-Hans/images/authorization-new-permission-ui-localized.png b/docs/zh-Hans/images/authorization-new-permission-ui-localized.png deleted file mode 100644 index 948fd618d9..0000000000 Binary files a/docs/zh-Hans/images/authorization-new-permission-ui-localized.png and /dev/null differ diff --git a/docs/zh-Hans/images/authorization-new-permission-ui.png b/docs/zh-Hans/images/authorization-new-permission-ui.png deleted file mode 100644 index 1190f04b70..0000000000 Binary files a/docs/zh-Hans/images/authorization-new-permission-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-console-application-solution.png b/docs/zh-Hans/images/basic-console-application-solution.png deleted file mode 100644 index b8ed5c51e6..0000000000 Binary files a/docs/zh-Hans/images/basic-console-application-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-maui-application-solution.png b/docs/zh-Hans/images/basic-maui-application-solution.png deleted file mode 100644 index f1db8affe2..0000000000 Binary files a/docs/zh-Hans/images/basic-maui-application-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-theme-account-layout.png b/docs/zh-Hans/images/basic-theme-account-layout.png deleted file mode 100644 index 6436bee756..0000000000 Binary files a/docs/zh-Hans/images/basic-theme-account-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout-blazor.png b/docs/zh-Hans/images/basic-theme-application-layout-blazor.png deleted file mode 100644 index 9d7f122c1d..0000000000 Binary files a/docs/zh-Hans/images/basic-theme-application-layout-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout-parts.png b/docs/zh-Hans/images/basic-theme-application-layout-parts.png deleted file mode 100644 index db1773ca79..0000000000 Binary files a/docs/zh-Hans/images/basic-theme-application-layout-parts.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout.png b/docs/zh-Hans/images/basic-theme-application-layout.png deleted file mode 100644 index 6124d58cb5..0000000000 Binary files a/docs/zh-Hans/images/basic-theme-application-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/basic-wpf-application-solution.png b/docs/zh-Hans/images/basic-wpf-application-solution.png deleted file mode 100644 index f4c6864e6d..0000000000 Binary files a/docs/zh-Hans/images/basic-wpf-application-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-generic-exception-message.png b/docs/zh-Hans/images/blazor-generic-exception-message.png deleted file mode 100644 index f414fd1ce5..0000000000 Binary files a/docs/zh-Hans/images/blazor-generic-exception-message.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-message-confirm.png b/docs/zh-Hans/images/blazor-message-confirm.png deleted file mode 100644 index a161579588..0000000000 Binary files a/docs/zh-Hans/images/blazor-message-confirm.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-message-error.png b/docs/zh-Hans/images/blazor-message-error.png deleted file mode 100644 index eb8abf2e0d..0000000000 Binary files a/docs/zh-Hans/images/blazor-message-error.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-message-success.png b/docs/zh-Hans/images/blazor-message-success.png deleted file mode 100644 index 8dca567eb2..0000000000 Binary files a/docs/zh-Hans/images/blazor-message-success.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-notification-bell-component.png b/docs/zh-Hans/images/blazor-notification-bell-component.png deleted file mode 100644 index 719ae436aa..0000000000 Binary files a/docs/zh-Hans/images/blazor-notification-bell-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-notification-success.png b/docs/zh-Hans/images/blazor-notification-success.png deleted file mode 100644 index ff5926cef9..0000000000 Binary files a/docs/zh-Hans/images/blazor-notification-success.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-page-alert-example.png b/docs/zh-Hans/images/blazor-page-alert-example.png deleted file mode 100644 index 3e80ef5a92..0000000000 Binary files a/docs/zh-Hans/images/blazor-page-alert-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/blazor-user-friendly-exception.png b/docs/zh-Hans/images/blazor-user-friendly-exception.png deleted file mode 100644 index 32118280c3..0000000000 Binary files a/docs/zh-Hans/images/blazor-user-friendly-exception.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-added-brand-files.png b/docs/zh-Hans/images/bookstore-added-brand-files.png deleted file mode 100644 index e23a3a0a7b..0000000000 Binary files a/docs/zh-Hans/images/bookstore-added-brand-files.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-added-logo.png b/docs/zh-Hans/images/bookstore-added-logo.png deleted file mode 100644 index 8f2c722d6c..0000000000 Binary files a/docs/zh-Hans/images/bookstore-added-logo.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-added-role-js-file.png b/docs/zh-Hans/images/bookstore-added-role-js-file.png deleted file mode 100644 index 850f6146ad..0000000000 Binary files a/docs/zh-Hans/images/bookstore-added-role-js-file.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-apis.png b/docs/zh-Hans/images/bookstore-apis.png deleted file mode 100644 index b7928c9637..0000000000 Binary files a/docs/zh-Hans/images/bookstore-apis.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-brand-area-highlighted.png b/docs/zh-Hans/images/bookstore-brand-area-highlighted.png deleted file mode 100644 index 10fb0ccbf1..0000000000 Binary files a/docs/zh-Hans/images/bookstore-brand-area-highlighted.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-branding-blazor.png b/docs/zh-Hans/images/bookstore-branding-blazor.png deleted file mode 100644 index db6b5fc8aa..0000000000 Binary files a/docs/zh-Hans/images/bookstore-branding-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-create-template.png b/docs/zh-Hans/images/bookstore-create-template.png deleted file mode 100644 index bae34a3b64..0000000000 Binary files a/docs/zh-Hans/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-efcore-dependencies.png b/docs/zh-Hans/images/bookstore-efcore-dependencies.png deleted file mode 100644 index cb77601880..0000000000 Binary files a/docs/zh-Hans/images/bookstore-efcore-dependencies.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-global-css-file.png b/docs/zh-Hans/images/bookstore-global-css-file.png deleted file mode 100644 index 0c388140bb..0000000000 Binary files a/docs/zh-Hans/images/bookstore-global-css-file.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-google-analytics-view-component.png b/docs/zh-Hans/images/bookstore-google-analytics-view-component.png deleted file mode 100644 index cc236c5e36..0000000000 Binary files a/docs/zh-Hans/images/bookstore-google-analytics-view-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-home.png b/docs/zh-Hans/images/bookstore-home.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/zh-Hans/images/bookstore-home.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-homepage.png b/docs/zh-Hans/images/bookstore-homepage.png deleted file mode 100644 index dc015aa67d..0000000000 Binary files a/docs/zh-Hans/images/bookstore-homepage.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-login.png b/docs/zh-Hans/images/bookstore-login.png deleted file mode 100644 index 02b5a823c5..0000000000 Binary files a/docs/zh-Hans/images/bookstore-login.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-logo-blazor.png b/docs/zh-Hans/images/bookstore-logo-blazor.png deleted file mode 100644 index 2c3d814902..0000000000 Binary files a/docs/zh-Hans/images/bookstore-logo-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-menus-highlighted.png b/docs/zh-Hans/images/bookstore-menus-highlighted.png deleted file mode 100644 index 584ba4b5f6..0000000000 Binary files a/docs/zh-Hans/images/bookstore-menus-highlighted.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png b/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png deleted file mode 100644 index a3c8f7d485..0000000000 Binary files a/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-notification-view-component.png b/docs/zh-Hans/images/bookstore-notification-view-component.png deleted file mode 100644 index b258a699f9..0000000000 Binary files a/docs/zh-Hans/images/bookstore-notification-view-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-second-database.png b/docs/zh-Hans/images/bookstore-second-database.png deleted file mode 100644 index f08bece185..0000000000 Binary files a/docs/zh-Hans/images/bookstore-second-database.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-swagger-ui-host.png b/docs/zh-Hans/images/bookstore-swagger-ui-host.png deleted file mode 100644 index 7ebd8d8e37..0000000000 Binary files a/docs/zh-Hans/images/bookstore-swagger-ui-host.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-toolbar-highlighted.png b/docs/zh-Hans/images/bookstore-toolbar-highlighted.png deleted file mode 100644 index ce300df5b7..0000000000 Binary files a/docs/zh-Hans/images/bookstore-toolbar-highlighted.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-user-management-v2.png b/docs/zh-Hans/images/bookstore-user-management-v2.png deleted file mode 100644 index cd66010e05..0000000000 Binary files a/docs/zh-Hans/images/bookstore-user-management-v2.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-visual-studio-solution-for-spa.png b/docs/zh-Hans/images/bookstore-visual-studio-solution-for-spa.png deleted file mode 100644 index d114ed188c..0000000000 Binary files a/docs/zh-Hans/images/bookstore-visual-studio-solution-for-spa.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-visual-studio-solution-tiered.png b/docs/zh-Hans/images/bookstore-visual-studio-solution-tiered.png deleted file mode 100644 index 9affe841aa..0000000000 Binary files a/docs/zh-Hans/images/bookstore-visual-studio-solution-tiered.png and /dev/null differ diff --git a/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png deleted file mode 100644 index 0ee4fd274b..0000000000 Binary files a/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png and /dev/null differ diff --git a/docs/zh-Hans/images/branding-appname.png b/docs/zh-Hans/images/branding-appname.png deleted file mode 100644 index d6af568a65..0000000000 Binary files a/docs/zh-Hans/images/branding-appname.png and /dev/null differ diff --git a/docs/zh-Hans/images/branding-nobrand.png b/docs/zh-Hans/images/branding-nobrand.png deleted file mode 100644 index 78feb97c38..0000000000 Binary files a/docs/zh-Hans/images/branding-nobrand.png and /dev/null differ diff --git a/docs/zh-Hans/images/breadcrumbs-example.png b/docs/zh-Hans/images/breadcrumbs-example.png deleted file mode 100644 index b5df91353a..0000000000 Binary files a/docs/zh-Hans/images/breadcrumbs-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/build-action-embedded-resource-sample.png b/docs/zh-Hans/images/build-action-embedded-resource-sample.png deleted file mode 100644 index 700e9921f4..0000000000 Binary files a/docs/zh-Hans/images/build-action-embedded-resource-sample.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-add-widget-on-page.png b/docs/zh-Hans/images/cmskit-add-widget-on-page.png deleted file mode 100644 index 85ed3b73e9..0000000000 Binary files a/docs/zh-Hans/images/cmskit-add-widget-on-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-example-output-on-page.png b/docs/zh-Hans/images/cmskit-example-output-on-page.png deleted file mode 100644 index 8ee046d4f2..0000000000 Binary files a/docs/zh-Hans/images/cmskit-example-output-on-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-blog-post-edit.png b/docs/zh-Hans/images/cmskit-module-blog-post-edit.png deleted file mode 100644 index 5ff460739a..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-blog-post-edit.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-blog-posts-page.png b/docs/zh-Hans/images/cmskit-module-blog-posts-page.png deleted file mode 100644 index 217ead0997..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-blog-posts-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-edit.png b/docs/zh-Hans/images/cmskit-module-blogs-edit.png deleted file mode 100644 index 810ab8b033..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-blogs-edit.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png b/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png deleted file mode 100644 index 97d522bbe0..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-page.png b/docs/zh-Hans/images/cmskit-module-blogs-page.png deleted file mode 100644 index 89bdcf3402..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-blogs-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-comment-page.png b/docs/zh-Hans/images/cmskit-module-comment-page.png deleted file mode 100644 index b6cd793846..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-comment-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-comments-detail.png b/docs/zh-Hans/images/cmskit-module-comments-detail.png deleted file mode 100644 index 3ee20e534c..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-comments-detail.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-features-dialog-2.png b/docs/zh-Hans/images/cmskit-module-features-dialog-2.png deleted file mode 100644 index 08d456b9b3..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-features-dialog-2.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-features-dialog.png b/docs/zh-Hans/images/cmskit-module-features-dialog.png deleted file mode 100644 index 5cefedfe58..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-features-dialog.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-features-scroll-index.png b/docs/zh-Hans/images/cmskit-module-features-scroll-index.png deleted file mode 100644 index 6f22f456f6..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-features-scroll-index.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-global-resources-page.png b/docs/zh-Hans/images/cmskit-module-global-resources-page.png deleted file mode 100644 index ffd64b4ec0..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-global-resources-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-menus-page.png b/docs/zh-Hans/images/cmskit-module-menus-page.png deleted file mode 100644 index 2367c9154b..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-menus-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-menus-public.png b/docs/zh-Hans/images/cmskit-module-menus-public.png deleted file mode 100644 index a3e1def4af..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-menus-public.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-pages-edit.png b/docs/zh-Hans/images/cmskit-module-pages-edit.png deleted file mode 100644 index 5c3933849f..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-pages-edit.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-pages-page.png b/docs/zh-Hans/images/cmskit-module-pages-page.png deleted file mode 100644 index e2ca54b647..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-pages-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-ratings.png b/docs/zh-Hans/images/cmskit-module-ratings.png deleted file mode 100644 index 6d16c954e9..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-ratings.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-reactions.png b/docs/zh-Hans/images/cmskit-module-reactions.png deleted file mode 100644 index 6840f12297..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-reactions.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-tag-edit.png b/docs/zh-Hans/images/cmskit-module-tag-edit.png deleted file mode 100644 index 1bc1cb2035..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-tag-edit.png and /dev/null differ diff --git a/docs/zh-Hans/images/cmskit-module-tags-page.png b/docs/zh-Hans/images/cmskit-module-tags-page.png deleted file mode 100644 index 39f515b9f4..0000000000 Binary files a/docs/zh-Hans/images/cmskit-module-tags-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/create-aspnet-core-application.png b/docs/zh-Hans/images/create-aspnet-core-application.png deleted file mode 100644 index afd1447ba8..0000000000 Binary files a/docs/zh-Hans/images/create-aspnet-core-application.png and /dev/null differ diff --git a/docs/zh-Hans/images/create-new-aspnet-core-application-v2.png b/docs/zh-Hans/images/create-new-aspnet-core-application-v2.png deleted file mode 100644 index d2bce84775..0000000000 Binary files a/docs/zh-Hans/images/create-new-aspnet-core-application-v2.png and /dev/null differ diff --git a/docs/zh-Hans/images/create-new-aspnet-core-application.png b/docs/zh-Hans/images/create-new-aspnet-core-application.png deleted file mode 100644 index 2c38289810..0000000000 Binary files a/docs/zh-Hans/images/create-new-aspnet-core-application.png and /dev/null differ diff --git a/docs/zh-Hans/images/create-new-net-core-console-application.png b/docs/zh-Hans/images/create-new-net-core-console-application.png deleted file mode 100644 index 0c2b3dbcb8..0000000000 Binary files a/docs/zh-Hans/images/create-new-net-core-console-application.png and /dev/null differ diff --git a/docs/zh-Hans/images/custom-settings.png b/docs/zh-Hans/images/custom-settings.png deleted file mode 100644 index afd1f58549..0000000000 Binary files a/docs/zh-Hans/images/custom-settings.png and /dev/null differ diff --git a/docs/zh-Hans/images/dashboard1.png b/docs/zh-Hans/images/dashboard1.png deleted file mode 100644 index 8c542b8786..0000000000 Binary files a/docs/zh-Hans/images/dashboard1.png and /dev/null differ diff --git a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png deleted file mode 100644 index b52c8f7feb..0000000000 Binary files a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png deleted file mode 100644 index 06768ebe06..0000000000 Binary files a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-custom-render-date.png b/docs/zh-Hans/images/datatables-custom-render-date.png deleted file mode 100644 index d2d59e3a1c..0000000000 Binary files a/docs/zh-Hans/images/datatables-custom-render-date.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-default-render-date.png b/docs/zh-Hans/images/datatables-default-render-date.png deleted file mode 100644 index a3c6c53c21..0000000000 Binary files a/docs/zh-Hans/images/datatables-default-render-date.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-example.png b/docs/zh-Hans/images/datatables-example.png deleted file mode 100644 index 177431debb..0000000000 Binary files a/docs/zh-Hans/images/datatables-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-row-actions-confirmation.png b/docs/zh-Hans/images/datatables-row-actions-confirmation.png deleted file mode 100644 index f1854cac89..0000000000 Binary files a/docs/zh-Hans/images/datatables-row-actions-confirmation.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-row-actions-icon.png b/docs/zh-Hans/images/datatables-row-actions-icon.png deleted file mode 100644 index 60032c0e10..0000000000 Binary files a/docs/zh-Hans/images/datatables-row-actions-icon.png and /dev/null differ diff --git a/docs/zh-Hans/images/datatables-row-actions.png b/docs/zh-Hans/images/datatables-row-actions.png deleted file mode 100644 index 3605b0d671..0000000000 Binary files a/docs/zh-Hans/images/datatables-row-actions.png and /dev/null differ diff --git a/docs/zh-Hans/images/db-migrator-app.png b/docs/zh-Hans/images/db-migrator-app.png deleted file mode 100644 index d2248d4588..0000000000 Binary files a/docs/zh-Hans/images/db-migrator-app.png and /dev/null differ diff --git a/docs/zh-Hans/images/db-migrator-output.png b/docs/zh-Hans/images/db-migrator-output.png deleted file mode 100644 index d2248d4588..0000000000 Binary files a/docs/zh-Hans/images/db-migrator-output.png and /dev/null differ diff --git a/docs/zh-Hans/images/db-options.png b/docs/zh-Hans/images/db-options.png deleted file mode 100644 index 368377f5a3..0000000000 Binary files a/docs/zh-Hans/images/db-options.png and /dev/null differ diff --git a/docs/zh-Hans/images/ddd-microservice-simple.png b/docs/zh-Hans/images/ddd-microservice-simple.png deleted file mode 100644 index f4195bff9d..0000000000 Binary files a/docs/zh-Hans/images/ddd-microservice-simple.png and /dev/null differ diff --git a/docs/zh-Hans/images/deployment-clustered.png b/docs/zh-Hans/images/deployment-clustered.png deleted file mode 100644 index f640d55957..0000000000 Binary files a/docs/zh-Hans/images/deployment-clustered.png and /dev/null differ diff --git a/docs/zh-Hans/images/deployment-single-instance.png b/docs/zh-Hans/images/deployment-single-instance.png deleted file mode 100644 index 50b904baf8..0000000000 Binary files a/docs/zh-Hans/images/deployment-single-instance.png and /dev/null differ diff --git a/docs/zh-Hans/images/docs-create-project-v4.4.0.png b/docs/zh-Hans/images/docs-create-project-v4.4.0.png deleted file mode 100644 index 723860e3c5..0000000000 Binary files a/docs/zh-Hans/images/docs-create-project-v4.4.0.png and /dev/null differ diff --git a/docs/zh-Hans/images/docs-create-project.jpg b/docs/zh-Hans/images/docs-create-project.jpg deleted file mode 100644 index d2baa3242a..0000000000 Binary files a/docs/zh-Hans/images/docs-create-project.jpg and /dev/null differ diff --git a/docs/zh-Hans/images/docs-module_download-new-abp-project.png b/docs/zh-Hans/images/docs-module_download-new-abp-project.png deleted file mode 100644 index 5afb0d3b47..0000000000 Binary files a/docs/zh-Hans/images/docs-module_download-new-abp-project.png and /dev/null differ diff --git a/docs/zh-Hans/images/docs-module_download-sample-navigation-menu.png b/docs/zh-Hans/images/docs-module_download-sample-navigation-menu.png deleted file mode 100644 index 8d8eb42d52..0000000000 Binary files a/docs/zh-Hans/images/docs-module_download-sample-navigation-menu.png and /dev/null differ diff --git a/docs/zh-Hans/images/docs-module_solution-explorer.png b/docs/zh-Hans/images/docs-module_solution-explorer.png deleted file mode 100644 index 63d943a4f2..0000000000 Binary files a/docs/zh-Hans/images/docs-module_solution-explorer.png and /dev/null differ diff --git a/docs/zh-Hans/images/docs-section-ui.png b/docs/zh-Hans/images/docs-section-ui.png deleted file mode 100644 index 1c63d1ad3a..0000000000 Binary files a/docs/zh-Hans/images/docs-section-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png b/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png deleted file mode 100644 index d10f236282..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-clean-architecture.png b/docs/zh-Hans/images/domain-driven-design-clean-architecture.png deleted file mode 100644 index c4c680fc23..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-clean-architecture.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png b/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png deleted file mode 100644 index e5b8dfa41d..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png b/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png deleted file mode 100644 index aa45d9b702..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png b/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png deleted file mode 100644 index 2f847d61e1..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png b/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png deleted file mode 100644 index fca830f21d..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-layers.png b/docs/zh-Hans/images/domain-driven-design-layers.png deleted file mode 100644 index c9e64d569a..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-layers.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-multiple-applications.png b/docs/zh-Hans/images/domain-driven-design-multiple-applications.png deleted file mode 100644 index 112335df7c..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-multiple-applications.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-project-relations.png b/docs/zh-Hans/images/domain-driven-design-project-relations.png deleted file mode 100644 index 738edf59f1..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-project-relations.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png b/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png deleted file mode 100644 index 4d277f40ee..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-vs-solution.png b/docs/zh-Hans/images/domain-driven-design-vs-solution.png deleted file mode 100644 index 12305891d4..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-vs-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/domain-driven-design-web-request-flow.png b/docs/zh-Hans/images/domain-driven-design-web-request-flow.png deleted file mode 100644 index 63d5fd0d6b..0000000000 Binary files a/docs/zh-Hans/images/domain-driven-design-web-request-flow.png and /dev/null differ diff --git a/docs/zh-Hans/images/email-settings.png b/docs/zh-Hans/images/email-settings.png deleted file mode 100644 index 23458abc99..0000000000 Binary files a/docs/zh-Hans/images/email-settings.png and /dev/null differ diff --git a/docs/zh-Hans/images/example-global-styles.png b/docs/zh-Hans/images/example-global-styles.png deleted file mode 100644 index 48ad321016..0000000000 Binary files a/docs/zh-Hans/images/example-global-styles.png and /dev/null differ diff --git a/docs/zh-Hans/images/extension-navigation-property-form.png b/docs/zh-Hans/images/extension-navigation-property-form.png deleted file mode 100644 index 2bc5b99daa..0000000000 Binary files a/docs/zh-Hans/images/extension-navigation-property-form.png and /dev/null differ diff --git a/docs/zh-Hans/images/extension-navigation-property-table.png b/docs/zh-Hans/images/extension-navigation-property-table.png deleted file mode 100644 index 5a84150193..0000000000 Binary files a/docs/zh-Hans/images/extension-navigation-property-table.png and /dev/null differ diff --git a/docs/zh-Hans/images/features-action.png b/docs/zh-Hans/images/features-action.png deleted file mode 100644 index f1c53ce6f4..0000000000 Binary files a/docs/zh-Hans/images/features-action.png and /dev/null differ diff --git a/docs/zh-Hans/images/features-modal.png b/docs/zh-Hans/images/features-modal.png deleted file mode 100644 index 477295e677..0000000000 Binary files a/docs/zh-Hans/images/features-modal.png and /dev/null differ diff --git a/docs/zh-Hans/images/features-module-opening.png b/docs/zh-Hans/images/features-module-opening.png deleted file mode 100644 index 9c91a596e5..0000000000 Binary files a/docs/zh-Hans/images/features-module-opening.png and /dev/null differ diff --git a/docs/zh-Hans/images/generated-static-client-proxies.png b/docs/zh-Hans/images/generated-static-client-proxies.png deleted file mode 100644 index 608ec08dbd..0000000000 Binary files a/docs/zh-Hans/images/generated-static-client-proxies.png and /dev/null differ diff --git a/docs/zh-Hans/images/github-access-token-private-repo.jpg b/docs/zh-Hans/images/github-access-token-private-repo.jpg deleted file mode 100644 index cb74f1eea3..0000000000 Binary files a/docs/zh-Hans/images/github-access-token-private-repo.jpg and /dev/null differ diff --git a/docs/zh-Hans/images/github-access-token-public-repo.jpg b/docs/zh-Hans/images/github-access-token-public-repo.jpg deleted file mode 100644 index d091a6d511..0000000000 Binary files a/docs/zh-Hans/images/github-access-token-public-repo.jpg and /dev/null differ diff --git a/docs/zh-Hans/images/github-myusername.jpg b/docs/zh-Hans/images/github-myusername.jpg deleted file mode 100644 index a723c17713..0000000000 Binary files a/docs/zh-Hans/images/github-myusername.jpg and /dev/null differ diff --git a/docs/zh-Hans/images/hello-template-razor.png b/docs/zh-Hans/images/hello-template-razor.png deleted file mode 100644 index 45e0699843..0000000000 Binary files a/docs/zh-Hans/images/hello-template-razor.png and /dev/null differ diff --git a/docs/zh-Hans/images/hello-template.png b/docs/zh-Hans/images/hello-template.png deleted file mode 100644 index 56927aa538..0000000000 Binary files a/docs/zh-Hans/images/hello-template.png and /dev/null differ diff --git a/docs/zh-Hans/images/identity-module-menu.png b/docs/zh-Hans/images/identity-module-menu.png deleted file mode 100644 index 594a5f9913..0000000000 Binary files a/docs/zh-Hans/images/identity-module-menu.png and /dev/null differ diff --git a/docs/zh-Hans/images/identity-module-permissions.png b/docs/zh-Hans/images/identity-module-permissions.png deleted file mode 100644 index 2b6e6787f7..0000000000 Binary files a/docs/zh-Hans/images/identity-module-permissions.png and /dev/null differ diff --git a/docs/zh-Hans/images/identity-module-roles.png b/docs/zh-Hans/images/identity-module-roles.png deleted file mode 100644 index a0fd412742..0000000000 Binary files a/docs/zh-Hans/images/identity-module-roles.png and /dev/null differ diff --git a/docs/zh-Hans/images/identity-module-users.png b/docs/zh-Hans/images/identity-module-users.png deleted file mode 100644 index 2ec7e1b332..0000000000 Binary files a/docs/zh-Hans/images/identity-module-users.png and /dev/null differ diff --git a/docs/zh-Hans/images/implementing-domain-driven-design-book.png b/docs/zh-Hans/images/implementing-domain-driven-design-book.png deleted file mode 100644 index 6568b46f66..0000000000 Binary files a/docs/zh-Hans/images/implementing-domain-driven-design-book.png and /dev/null differ diff --git a/docs/zh-Hans/images/issue-first-test.png b/docs/zh-Hans/images/issue-first-test.png deleted file mode 100644 index 6bea1cd0f3..0000000000 Binary files a/docs/zh-Hans/images/issue-first-test.png and /dev/null differ diff --git a/docs/zh-Hans/images/issue-list.png b/docs/zh-Hans/images/issue-list.png deleted file mode 100644 index cce7a468fe..0000000000 Binary files a/docs/zh-Hans/images/issue-list.png and /dev/null differ diff --git a/docs/zh-Hans/images/issuemanagement-module-solution.png b/docs/zh-Hans/images/issuemanagement-module-solution.png deleted file mode 100644 index d5f64b01d2..0000000000 Binary files a/docs/zh-Hans/images/issuemanagement-module-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/js-message-confirm.png b/docs/zh-Hans/images/js-message-confirm.png deleted file mode 100644 index f91affbdac..0000000000 Binary files a/docs/zh-Hans/images/js-message-confirm.png and /dev/null differ diff --git a/docs/zh-Hans/images/js-message-error.png b/docs/zh-Hans/images/js-message-error.png deleted file mode 100644 index 0ed13dde5e..0000000000 Binary files a/docs/zh-Hans/images/js-message-error.png and /dev/null differ diff --git a/docs/zh-Hans/images/js-message-success.png b/docs/zh-Hans/images/js-message-success.png deleted file mode 100644 index f87c44d78a..0000000000 Binary files a/docs/zh-Hans/images/js-message-success.png and /dev/null differ diff --git a/docs/zh-Hans/images/js-notify-success.png b/docs/zh-Hans/images/js-notify-success.png deleted file mode 100644 index d5300f3468..0000000000 Binary files a/docs/zh-Hans/images/js-notify-success.png and /dev/null differ diff --git a/docs/zh-Hans/images/layered-project-dependencies-module.png b/docs/zh-Hans/images/layered-project-dependencies-module.png deleted file mode 100644 index de3b7a412f..0000000000 Binary files a/docs/zh-Hans/images/layered-project-dependencies-module.png and /dev/null differ diff --git a/docs/zh-Hans/images/layered-project-dependencies.png b/docs/zh-Hans/images/layered-project-dependencies.png deleted file mode 100644 index ed3e03fe4d..0000000000 Binary files a/docs/zh-Hans/images/layered-project-dependencies.png and /dev/null differ diff --git a/docs/zh-Hans/images/lepton-theme-account-layout.png b/docs/zh-Hans/images/lepton-theme-account-layout.png deleted file mode 100644 index 96e7fb7718..0000000000 Binary files a/docs/zh-Hans/images/lepton-theme-account-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/lepton-theme-application-layout.png b/docs/zh-Hans/images/lepton-theme-application-layout.png deleted file mode 100644 index 9da4d0f198..0000000000 Binary files a/docs/zh-Hans/images/lepton-theme-application-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/lepton-theme-blazor-layout.png b/docs/zh-Hans/images/lepton-theme-blazor-layout.png deleted file mode 100644 index 4236ca0ac0..0000000000 Binary files a/docs/zh-Hans/images/lepton-theme-blazor-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/localization-resource-json-files.png b/docs/zh-Hans/images/localization-resource-json-files.png deleted file mode 100644 index 1a1d43403c..0000000000 Binary files a/docs/zh-Hans/images/localization-resource-json-files.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-authserver-home.png b/docs/zh-Hans/images/microservice-sample-authserver-home.png deleted file mode 100644 index 684fdb1a5a..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-authserver-home.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-authserver-login.png b/docs/zh-Hans/images/microservice-sample-authserver-login.png deleted file mode 100644 index 99d898ccd2..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-authserver-login.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-backend-ui-permissions.png b/docs/zh-Hans/images/microservice-sample-backend-ui-permissions.png deleted file mode 100644 index 3e1610294f..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-backend-ui-permissions.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-backend-ui.png b/docs/zh-Hans/images/microservice-sample-backend-ui.png deleted file mode 100644 index e649c8da59..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-backend-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-blogservice-permission-in-database.png b/docs/zh-Hans/images/microservice-sample-blogservice-permission-in-database.png deleted file mode 100644 index 45d7f2b115..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-blogservice-permission-in-database.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-diagram-2.png b/docs/zh-Hans/images/microservice-sample-diagram-2.png deleted file mode 100644 index 414a942aca..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-diagram-2.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-diagram-3.png b/docs/zh-Hans/images/microservice-sample-diagram-3.png deleted file mode 100644 index 9daeea2b41..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-diagram-3.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-diagram.png b/docs/zh-Hans/images/microservice-sample-diagram.png deleted file mode 100644 index 47e6443852..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-diagram.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-kibana-1.png b/docs/zh-Hans/images/microservice-sample-kibana-1.png deleted file mode 100644 index 51777f6bd7..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-kibana-1.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-kibana-2.png b/docs/zh-Hans/images/microservice-sample-kibana-2.png deleted file mode 100644 index cb1d0b748c..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-kibana-2.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-product-module-in-solution.png b/docs/zh-Hans/images/microservice-sample-product-module-in-solution.png deleted file mode 100644 index 2c07ad4b99..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-product-module-in-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-public-product-list.png b/docs/zh-Hans/images/microservice-sample-public-product-list.png deleted file mode 100644 index 932b0b531c..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-public-product-list.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-solution-2.png b/docs/zh-Hans/images/microservice-sample-solution-2.png deleted file mode 100644 index f0acb01424..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-solution-2.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-solution.png b/docs/zh-Hans/images/microservice-sample-solution.png deleted file mode 100644 index d1497d9be2..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-update-database-authserver.png b/docs/zh-Hans/images/microservice-sample-update-database-authserver.png deleted file mode 100644 index 094fd20fa6..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-update-database-authserver.png and /dev/null differ diff --git a/docs/zh-Hans/images/microservice-sample-update-database-products.png b/docs/zh-Hans/images/microservice-sample-update-database-products.png deleted file mode 100644 index 32a0927c1f..0000000000 Binary files a/docs/zh-Hans/images/microservice-sample-update-database-products.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-example-product-create.png b/docs/zh-Hans/images/modal-example-product-create.png deleted file mode 100644 index a2c4b3a8bb..0000000000 Binary files a/docs/zh-Hans/images/modal-example-product-create.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-example-product-info.png b/docs/zh-Hans/images/modal-example-product-info.png deleted file mode 100644 index 5d037c1125..0000000000 Binary files a/docs/zh-Hans/images/modal-example-product-info.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-manager-cancel-warning.png b/docs/zh-Hans/images/modal-manager-cancel-warning.png deleted file mode 100644 index a237e247c6..0000000000 Binary files a/docs/zh-Hans/images/modal-manager-cancel-warning.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-manager-example-modal.png b/docs/zh-Hans/images/modal-manager-example-modal.png deleted file mode 100644 index a413340c52..0000000000 Binary files a/docs/zh-Hans/images/modal-manager-example-modal.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-manager-validation.png b/docs/zh-Hans/images/modal-manager-validation.png deleted file mode 100644 index 8749ab8e5c..0000000000 Binary files a/docs/zh-Hans/images/modal-manager-validation.png and /dev/null differ diff --git a/docs/zh-Hans/images/modal-page-on-rider.png b/docs/zh-Hans/images/modal-page-on-rider.png deleted file mode 100644 index f59a93c344..0000000000 Binary files a/docs/zh-Hans/images/modal-page-on-rider.png and /dev/null differ diff --git a/docs/zh-Hans/images/module-layers-and-packages.jpg b/docs/zh-Hans/images/module-layers-and-packages.jpg deleted file mode 100644 index f71a91eb8d..0000000000 Binary files a/docs/zh-Hans/images/module-layers-and-packages.jpg and /dev/null differ diff --git a/docs/zh-Hans/images/module-tenant-management-actions.png b/docs/zh-Hans/images/module-tenant-management-actions.png deleted file mode 100644 index 2c6c396f3f..0000000000 Binary files a/docs/zh-Hans/images/module-tenant-management-actions.png and /dev/null differ diff --git a/docs/zh-Hans/images/module-tenant-management-new-tenant.png b/docs/zh-Hans/images/module-tenant-management-new-tenant.png deleted file mode 100644 index eb82cc9b88..0000000000 Binary files a/docs/zh-Hans/images/module-tenant-management-new-tenant.png and /dev/null differ diff --git a/docs/zh-Hans/images/module-tenant-management-page.png b/docs/zh-Hans/images/module-tenant-management-page.png deleted file mode 100644 index 14611867b5..0000000000 Binary files a/docs/zh-Hans/images/module-tenant-management-page.png and /dev/null differ diff --git a/docs/zh-Hans/images/multiple-database-usage.png b/docs/zh-Hans/images/multiple-database-usage.png deleted file mode 100644 index aa5d7595a7..0000000000 Binary files a/docs/zh-Hans/images/multiple-database-usage.png and /dev/null differ diff --git a/docs/zh-Hans/images/multiple-file-template-razor.png b/docs/zh-Hans/images/multiple-file-template-razor.png deleted file mode 100644 index efa58107d3..0000000000 Binary files a/docs/zh-Hans/images/multiple-file-template-razor.png and /dev/null differ diff --git a/docs/zh-Hans/images/multiple-file-template.png b/docs/zh-Hans/images/multiple-file-template.png deleted file mode 100644 index 75e514975c..0000000000 Binary files a/docs/zh-Hans/images/multiple-file-template.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-blazor.png b/docs/zh-Hans/images/my-setting-group-blazor.png deleted file mode 100644 index 8094c8c99e..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-component-contributor.png b/docs/zh-Hans/images/my-setting-group-component-contributor.png deleted file mode 100644 index 9595aaf67a..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-component-contributor.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-component.png b/docs/zh-Hans/images/my-setting-group-component.png deleted file mode 100644 index d737d3c287..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-page-contributor.png b/docs/zh-Hans/images/my-setting-group-page-contributor.png deleted file mode 100644 index 44b31c07db..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-page-contributor.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-ui.png b/docs/zh-Hans/images/my-setting-group-ui.png deleted file mode 100644 index 6bb268f46d..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/my-setting-group-view-component.png b/docs/zh-Hans/images/my-setting-group-view-component.png deleted file mode 100644 index 74c1730735..0000000000 Binary files a/docs/zh-Hans/images/my-setting-group-view-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/nav-main-menu-administration.png b/docs/zh-Hans/images/nav-main-menu-administration.png deleted file mode 100644 index 950d91102a..0000000000 Binary files a/docs/zh-Hans/images/nav-main-menu-administration.png and /dev/null differ diff --git a/docs/zh-Hans/images/nav-main-menu.png b/docs/zh-Hans/images/nav-main-menu.png deleted file mode 100644 index 3a348fd988..0000000000 Binary files a/docs/zh-Hans/images/nav-main-menu.png and /dev/null differ diff --git a/docs/zh-Hans/images/night-build-add-nuget-package.png b/docs/zh-Hans/images/night-build-add-nuget-package.png deleted file mode 100644 index f475d3aaa0..0000000000 Binary files a/docs/zh-Hans/images/night-build-add-nuget-package.png and /dev/null differ diff --git a/docs/zh-Hans/images/night-build-add-nuget-source.png b/docs/zh-Hans/images/night-build-add-nuget-source.png deleted file mode 100644 index df3176aa12..0000000000 Binary files a/docs/zh-Hans/images/night-build-add-nuget-source.png and /dev/null differ diff --git a/docs/zh-Hans/images/optimistic-concurrency.png b/docs/zh-Hans/images/optimistic-concurrency.png deleted file mode 100644 index 17d6a9b5df..0000000000 Binary files a/docs/zh-Hans/images/optimistic-concurrency.png and /dev/null differ diff --git a/docs/zh-Hans/images/overriding-login-cshtml.png b/docs/zh-Hans/images/overriding-login-cshtml.png deleted file mode 100644 index 79fdaeccf7..0000000000 Binary files a/docs/zh-Hans/images/overriding-login-cshtml.png and /dev/null differ diff --git a/docs/zh-Hans/images/package-manager-console-update-database.png b/docs/zh-Hans/images/package-manager-console-update-database.png deleted file mode 100644 index 72d02e9186..0000000000 Binary files a/docs/zh-Hans/images/package-manager-console-update-database.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-alert-account-layout.png b/docs/zh-Hans/images/page-alert-account-layout.png deleted file mode 100644 index 966accc01c..0000000000 Binary files a/docs/zh-Hans/images/page-alert-account-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-alert-example.png b/docs/zh-Hans/images/page-alert-example.png deleted file mode 100644 index 5aed46f278..0000000000 Binary files a/docs/zh-Hans/images/page-alert-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-header-toolbar-blazor.png b/docs/zh-Hans/images/page-header-toolbar-blazor.png deleted file mode 100644 index 40834d9dc5..0000000000 Binary files a/docs/zh-Hans/images/page-header-toolbar-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-toolbar-button-blazor.png b/docs/zh-Hans/images/page-toolbar-button-blazor.png deleted file mode 100644 index a901d59b58..0000000000 Binary files a/docs/zh-Hans/images/page-toolbar-button-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-toolbar-button.png b/docs/zh-Hans/images/page-toolbar-button.png deleted file mode 100644 index 15b7a3901d..0000000000 Binary files a/docs/zh-Hans/images/page-toolbar-button.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png b/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png deleted file mode 100644 index 13ab2b21b2..0000000000 Binary files a/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/page-toolbar-custom-component.png b/docs/zh-Hans/images/page-toolbar-custom-component.png deleted file mode 100644 index 76ce86015f..0000000000 Binary files a/docs/zh-Hans/images/page-toolbar-custom-component.png and /dev/null differ diff --git a/docs/zh-Hans/images/pcm-update-database-v2.png b/docs/zh-Hans/images/pcm-update-database-v2.png deleted file mode 100644 index 72d02e9186..0000000000 Binary files a/docs/zh-Hans/images/pcm-update-database-v2.png and /dev/null differ diff --git a/docs/zh-Hans/images/pcm-update-database.png b/docs/zh-Hans/images/pcm-update-database.png deleted file mode 100644 index a9379d2571..0000000000 Binary files a/docs/zh-Hans/images/pcm-update-database.png and /dev/null differ diff --git a/docs/zh-Hans/images/permissions-module-dialog.png b/docs/zh-Hans/images/permissions-module-dialog.png deleted file mode 100644 index 28dd6e5f0c..0000000000 Binary files a/docs/zh-Hans/images/permissions-module-dialog.png and /dev/null differ diff --git a/docs/zh-Hans/images/permissions-module-open-dialog.png b/docs/zh-Hans/images/permissions-module-open-dialog.png deleted file mode 100644 index 7556d2de09..0000000000 Binary files a/docs/zh-Hans/images/permissions-module-open-dialog.png and /dev/null differ diff --git a/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png b/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png deleted file mode 100644 index 25408f5d84..0000000000 Binary files a/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png and /dev/null differ diff --git a/docs/zh-Hans/images/pmc-add-migration-role-title.png b/docs/zh-Hans/images/pmc-add-migration-role-title.png deleted file mode 100644 index 38ded69241..0000000000 Binary files a/docs/zh-Hans/images/pmc-add-migration-role-title.png and /dev/null differ diff --git a/docs/zh-Hans/images/product-create-modal-page-on-rider.png b/docs/zh-Hans/images/product-create-modal-page-on-rider.png deleted file mode 100644 index 23d7300fbd..0000000000 Binary files a/docs/zh-Hans/images/product-create-modal-page-on-rider.png and /dev/null differ diff --git a/docs/zh-Hans/images/react-native-folder-structure.png b/docs/zh-Hans/images/react-native-folder-structure.png deleted file mode 100644 index 10aaf1c18b..0000000000 Binary files a/docs/zh-Hans/images/react-native-folder-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/react-native-introduction.gif b/docs/zh-Hans/images/react-native-introduction.gif deleted file mode 100644 index 15963556aa..0000000000 Binary files a/docs/zh-Hans/images/react-native-introduction.gif and /dev/null differ diff --git a/docs/zh-Hans/images/react-native-navigation-structure.png b/docs/zh-Hans/images/react-native-navigation-structure.png deleted file mode 100644 index 22303f1dd4..0000000000 Binary files a/docs/zh-Hans/images/react-native-navigation-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/react-native-store-folder.png b/docs/zh-Hans/images/react-native-store-folder.png deleted file mode 100644 index a07705526f..0000000000 Binary files a/docs/zh-Hans/images/react-native-store-folder.png and /dev/null differ diff --git a/docs/zh-Hans/images/replace-email-layout-razor.png b/docs/zh-Hans/images/replace-email-layout-razor.png deleted file mode 100644 index 1a21382173..0000000000 Binary files a/docs/zh-Hans/images/replace-email-layout-razor.png and /dev/null differ diff --git a/docs/zh-Hans/images/replace-email-layout.png b/docs/zh-Hans/images/replace-email-layout.png deleted file mode 100644 index 6ea9669879..0000000000 Binary files a/docs/zh-Hans/images/replace-email-layout.png and /dev/null differ diff --git a/docs/zh-Hans/images/rn-environment-local-ip.png b/docs/zh-Hans/images/rn-environment-local-ip.png deleted file mode 100644 index a25bffc629..0000000000 Binary files a/docs/zh-Hans/images/rn-environment-local-ip.png and /dev/null differ diff --git a/docs/zh-Hans/images/rn-expo-interface.png b/docs/zh-Hans/images/rn-expo-interface.png deleted file mode 100644 index e33392237c..0000000000 Binary files a/docs/zh-Hans/images/rn-expo-interface.png and /dev/null differ diff --git a/docs/zh-Hans/images/rn-host-local-ip.png b/docs/zh-Hans/images/rn-host-local-ip.png deleted file mode 100644 index 2fb03ef21f..0000000000 Binary files a/docs/zh-Hans/images/rn-host-local-ip.png and /dev/null differ diff --git a/docs/zh-Hans/images/rn-login-iphone.png b/docs/zh-Hans/images/rn-login-iphone.png deleted file mode 100644 index c7767f27a5..0000000000 Binary files a/docs/zh-Hans/images/rn-login-iphone.png and /dev/null differ diff --git a/docs/zh-Hans/images/rn-tiered-local-ip.png b/docs/zh-Hans/images/rn-tiered-local-ip.png deleted file mode 100644 index d1dd6ed91f..0000000000 Binary files a/docs/zh-Hans/images/rn-tiered-local-ip.png and /dev/null differ diff --git a/docs/zh-Hans/images/select-empty-web-application-v2.png b/docs/zh-Hans/images/select-empty-web-application-v2.png deleted file mode 100644 index 9bfd2ec6a8..0000000000 Binary files a/docs/zh-Hans/images/select-empty-web-application-v2.png and /dev/null differ diff --git a/docs/zh-Hans/images/select-empty-web-application.png b/docs/zh-Hans/images/select-empty-web-application.png deleted file mode 100644 index f4b884140d..0000000000 Binary files a/docs/zh-Hans/images/select-empty-web-application.png and /dev/null differ diff --git a/docs/zh-Hans/images/set-as-startup-project.png b/docs/zh-Hans/images/set-as-startup-project.png deleted file mode 100644 index 8a5445bf38..0000000000 Binary files a/docs/zh-Hans/images/set-as-startup-project.png and /dev/null differ diff --git a/docs/zh-Hans/images/setting-management-email-ui.png b/docs/zh-Hans/images/setting-management-email-ui.png deleted file mode 100644 index ab26a1d180..0000000000 Binary files a/docs/zh-Hans/images/setting-management-email-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/signal-js-file.png b/docs/zh-Hans/images/signal-js-file.png deleted file mode 100644 index c15071e90b..0000000000 Binary files a/docs/zh-Hans/images/signal-js-file.png and /dev/null differ diff --git a/docs/zh-Hans/images/signalr-demo-chat.png b/docs/zh-Hans/images/signalr-demo-chat.png deleted file mode 100644 index 97ac7d3e4f..0000000000 Binary files a/docs/zh-Hans/images/signalr-demo-chat.png and /dev/null differ diff --git a/docs/zh-Hans/images/simple-plug-in-dll-file.png b/docs/zh-Hans/images/simple-plug-in-dll-file.png deleted file mode 100644 index 5fa4a9eb60..0000000000 Binary files a/docs/zh-Hans/images/simple-plug-in-dll-file.png and /dev/null differ diff --git a/docs/zh-Hans/images/simple-plugin-library.png b/docs/zh-Hans/images/simple-plugin-library.png deleted file mode 100644 index 05c730b954..0000000000 Binary files a/docs/zh-Hans/images/simple-plugin-library.png and /dev/null differ diff --git a/docs/zh-Hans/images/simple-plugin-output.png b/docs/zh-Hans/images/simple-plugin-output.png deleted file mode 100644 index cb9cda3e8d..0000000000 Binary files a/docs/zh-Hans/images/simple-plugin-output.png and /dev/null differ diff --git a/docs/zh-Hans/images/simple-razor-plug-in-dll-file.png b/docs/zh-Hans/images/simple-razor-plug-in-dll-file.png deleted file mode 100644 index 2059560dbc..0000000000 Binary files a/docs/zh-Hans/images/simple-razor-plug-in-dll-file.png and /dev/null differ diff --git a/docs/zh-Hans/images/simple-razor-plugin.png b/docs/zh-Hans/images/simple-razor-plugin.png deleted file mode 100644 index 311080fd7f..0000000000 Binary files a/docs/zh-Hans/images/simple-razor-plugin.png and /dev/null differ diff --git a/docs/zh-Hans/images/single-database-usage.png b/docs/zh-Hans/images/single-database-usage.png deleted file mode 100644 index 4015cd1d75..0000000000 Binary files a/docs/zh-Hans/images/single-database-usage.png and /dev/null differ diff --git a/docs/zh-Hans/images/solution-files-mvc.png b/docs/zh-Hans/images/solution-files-mvc.png deleted file mode 100644 index dbbdc7e661..0000000000 Binary files a/docs/zh-Hans/images/solution-files-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/images/solution-files-non-mvc.png b/docs/zh-Hans/images/solution-files-non-mvc.png deleted file mode 100644 index 700c4b2de7..0000000000 Binary files a/docs/zh-Hans/images/solution-files-non-mvc.png and /dev/null differ diff --git a/docs/zh-Hans/images/solution-test-projects.png b/docs/zh-Hans/images/solution-test-projects.png deleted file mode 100644 index eab61f0a5a..0000000000 Binary files a/docs/zh-Hans/images/solution-test-projects.png and /dev/null differ diff --git a/docs/zh-Hans/images/static-js-proxy-example.png b/docs/zh-Hans/images/static-js-proxy-example.png deleted file mode 100644 index 1173a277e2..0000000000 Binary files a/docs/zh-Hans/images/static-js-proxy-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/swagger-ui.png b/docs/zh-Hans/images/swagger-ui.png deleted file mode 100644 index 31f25c53a6..0000000000 Binary files a/docs/zh-Hans/images/swagger-ui.png and /dev/null differ diff --git a/docs/zh-Hans/images/table-column-extension-example-blazor.png b/docs/zh-Hans/images/table-column-extension-example-blazor.png deleted file mode 100644 index f2668f8f7a..0000000000 Binary files a/docs/zh-Hans/images/table-column-extension-example-blazor.png and /dev/null differ diff --git a/docs/zh-Hans/images/table-column-extension-example.png b/docs/zh-Hans/images/table-column-extension-example.png deleted file mode 100644 index 81c3748834..0000000000 Binary files a/docs/zh-Hans/images/table-column-extension-example.png and /dev/null differ diff --git a/docs/zh-Hans/images/tiered-solution-applications.png b/docs/zh-Hans/images/tiered-solution-applications.png deleted file mode 100644 index df8d2b5f4a..0000000000 Binary files a/docs/zh-Hans/images/tiered-solution-applications.png and /dev/null differ diff --git a/docs/zh-Hans/images/tiered-solution-servers.png b/docs/zh-Hans/images/tiered-solution-servers.png deleted file mode 100644 index 68e72990d7..0000000000 Binary files a/docs/zh-Hans/images/tiered-solution-servers.png and /dev/null differ diff --git a/docs/zh-Hans/images/ui-busy.png b/docs/zh-Hans/images/ui-busy.png deleted file mode 100644 index 7d2fe7a89a..0000000000 Binary files a/docs/zh-Hans/images/ui-busy.png and /dev/null differ diff --git a/docs/zh-Hans/images/ui-options.png b/docs/zh-Hans/images/ui-options.png deleted file mode 100644 index 6f0d19b73e..0000000000 Binary files a/docs/zh-Hans/images/ui-options.png and /dev/null differ diff --git a/docs/zh-Hans/images/upgrade-diff-empty-folders.png b/docs/zh-Hans/images/upgrade-diff-empty-folders.png deleted file mode 100644 index 6cbfeca744..0000000000 Binary files a/docs/zh-Hans/images/upgrade-diff-empty-folders.png and /dev/null differ diff --git a/docs/zh-Hans/images/user-action-blazor-extension-click-me.png b/docs/zh-Hans/images/user-action-blazor-extension-click-me.png deleted file mode 100644 index 1540bb0541..0000000000 Binary files a/docs/zh-Hans/images/user-action-blazor-extension-click-me.png and /dev/null differ diff --git a/docs/zh-Hans/images/user-action-extension-click-me.png b/docs/zh-Hans/images/user-action-extension-click-me.png deleted file mode 100644 index f8b1c505d0..0000000000 Binary files a/docs/zh-Hans/images/user-action-extension-click-me.png and /dev/null differ diff --git a/docs/zh-Hans/images/user-action-extension-on-blazor-project.png b/docs/zh-Hans/images/user-action-extension-on-blazor-project.png deleted file mode 100644 index 0d59ccdacf..0000000000 Binary files a/docs/zh-Hans/images/user-action-extension-on-blazor-project.png and /dev/null differ diff --git a/docs/zh-Hans/images/user-action-extension-on-solution.png b/docs/zh-Hans/images/user-action-extension-on-solution.png deleted file mode 100644 index 7962bfcbf0..0000000000 Binary files a/docs/zh-Hans/images/user-action-extension-on-solution.png and /dev/null differ diff --git a/docs/zh-Hans/images/user-menu.png b/docs/zh-Hans/images/user-menu.png deleted file mode 100644 index e30bc375a6..0000000000 Binary files a/docs/zh-Hans/images/user-menu.png and /dev/null differ diff --git a/docs/zh-Hans/images/virtual-file-explorer.png b/docs/zh-Hans/images/virtual-file-explorer.png deleted file mode 100644 index 6436c21284..0000000000 Binary files a/docs/zh-Hans/images/virtual-file-explorer.png and /dev/null differ diff --git a/docs/zh-Hans/images/volodocs-iis-add-website.png b/docs/zh-Hans/images/volodocs-iis-add-website.png deleted file mode 100644 index aa7da8095b..0000000000 Binary files a/docs/zh-Hans/images/volodocs-iis-add-website.png and /dev/null differ diff --git a/docs/zh-Hans/images/volodocs-iis-application-pool.png b/docs/zh-Hans/images/volodocs-iis-application-pool.png deleted file mode 100644 index 28ccfa5c42..0000000000 Binary files a/docs/zh-Hans/images/volodocs-iis-application-pool.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-app-solution-structure-mongodb.png b/docs/zh-Hans/images/vs-app-solution-structure-mongodb.png deleted file mode 100644 index 619e63b1f6..0000000000 Binary files a/docs/zh-Hans/images/vs-app-solution-structure-mongodb.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-app-solution-structure-tiered.png b/docs/zh-Hans/images/vs-app-solution-structure-tiered.png deleted file mode 100644 index f09c5e97c7..0000000000 Binary files a/docs/zh-Hans/images/vs-app-solution-structure-tiered.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-app-solution-structure.png b/docs/zh-Hans/images/vs-app-solution-structure.png deleted file mode 100644 index 5931da61ac..0000000000 Binary files a/docs/zh-Hans/images/vs-app-solution-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-run-tests-in-parallel.png b/docs/zh-Hans/images/vs-run-tests-in-parallel.png deleted file mode 100644 index b13245394a..0000000000 Binary files a/docs/zh-Hans/images/vs-run-tests-in-parallel.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-spa-app-backend-structure-mongodb.png b/docs/zh-Hans/images/vs-spa-app-backend-structure-mongodb.png deleted file mode 100644 index 56dd6e46bd..0000000000 Binary files a/docs/zh-Hans/images/vs-spa-app-backend-structure-mongodb.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-spa-app-backend-structure.png b/docs/zh-Hans/images/vs-spa-app-backend-structure.png deleted file mode 100644 index 6bde8164ac..0000000000 Binary files a/docs/zh-Hans/images/vs-spa-app-backend-structure.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-startup-template-tests.png b/docs/zh-Hans/images/vs-startup-template-tests.png deleted file mode 100644 index d939936b6e..0000000000 Binary files a/docs/zh-Hans/images/vs-startup-template-tests.png and /dev/null differ diff --git a/docs/zh-Hans/images/vs-test-explorer.png b/docs/zh-Hans/images/vs-test-explorer.png deleted file mode 100644 index b313bacf4c..0000000000 Binary files a/docs/zh-Hans/images/vs-test-explorer.png and /dev/null differ diff --git a/docs/zh-Hans/images/widget-basic-files.png b/docs/zh-Hans/images/widget-basic-files.png deleted file mode 100644 index c692abd9e0..0000000000 Binary files a/docs/zh-Hans/images/widget-basic-files.png and /dev/null differ diff --git a/docs/zh-Hans/images/winmerge-comparison-result.png b/docs/zh-Hans/images/winmerge-comparison-result.png deleted file mode 100644 index 9240fdf075..0000000000 Binary files a/docs/zh-Hans/images/winmerge-comparison-result.png and /dev/null differ diff --git a/docs/zh-Hans/images/winmerge-file-diff.png b/docs/zh-Hans/images/winmerge-file-diff.png deleted file mode 100644 index 6ec98b2c30..0000000000 Binary files a/docs/zh-Hans/images/winmerge-file-diff.png and /dev/null differ diff --git a/docs/zh-Hans/images/winmerge-open-folders.png b/docs/zh-Hans/images/winmerge-open-folders.png deleted file mode 100644 index f6e784b8a8..0000000000 Binary files a/docs/zh-Hans/images/winmerge-open-folders.png and /dev/null differ diff --git a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.webp b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.webp index 8d61a9ffda..c31bc80a82 100644 Binary files a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.webp and b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.webp differ