@ -1,34 +1,23 @@ |
|||
# ABP |
|||
# ABP Framework |
|||
|
|||
 |
|||
[](https://www.nuget.org/packages/Volo.Abp.Core) |
|||
[](https://docs.abp.io/en/abp/latest/Nightly-Builds) |
|||
[](https://www.nuget.org/packages/Volo.Abp.Core) |
|||
|
|||
This project is the next generation of the [ASP.NET Boilerplate](https://aspnetboilerplate.com/) web application framework. See [the announcement](https://blog.abp.io/abp/Abp-vNext-Announcement). |
|||
ABP is an **open source application framework** focused on ASP.NET Core based web application development, but also supports developing other type of applications. |
|||
|
|||
See the official [web site (abp.io)](https://abp.io/) for more information. |
|||
## Links |
|||
|
|||
### Documentation |
|||
* <a href="https://abp.io/" target="_blank">Official Web Site</a> |
|||
* <a href="https://abp.io/get-started" target="_blank">Get Started</a> |
|||
* <a href="https://abp.io/features" target="_blank">Features</a> |
|||
* <a href="https://docs.abp.io/" target="_blank">Documentation</a> |
|||
* <a href="https://docs.abp.io/en/abp/latest/Samples/Index" target="_blank">Samples</a> |
|||
* <a href="https://blog.abp.io/" target="_blank">Blog</a> |
|||
* <a href="https://stackoverflow.com/questions/tagged/abp" target="_blank">Stack overflow</a> |
|||
* <a href="https://twitter.com/abpframework" target="_blank">Twitter</a> |
|||
|
|||
See the <a href="https://docs.abp.io/" target="_blank">documentation</a>. |
|||
|
|||
### Development |
|||
|
|||
#### Pre Requirements |
|||
|
|||
- Visual Studio 2019 16.4.0+ |
|||
|
|||
#### Framework |
|||
|
|||
Framework solution is located under the `framework` folder. It has no external dependency. |
|||
|
|||
#### Modules/Templates |
|||
|
|||
[Modules](modules/) and [Templates](templates/) have their own solutions and have **local references** to the framework and each other. |
|||
|
|||
Visual Studio can not work properly with the local references out of the solution folder. When you open a module/sample solution in the Visual Studio, you may get some errors related to the dependencies. In this case, run the `dotnet restore` on the command prompt for the related solution's folder. You need to run it after you first open the solution or change a dependency. |
|||
|
|||
### Contribution |
|||
## Contribution |
|||
|
|||
ABP is an open source platform. Check [the contribution guide](docs/en/Contribution/Index.md) if you want to contribute to the project. |
|||
|
|||
@ -1,5 +1,14 @@ |
|||
{ |
|||
"culture": "tr", |
|||
"texts": { |
|||
"Account": "ABP Hesabı - Giriş Yap & Kayıt Ol | ABP.IO", |
|||
"Welcome": "Hoş Geldiniz", |
|||
"UseOneOfTheFollowingLinksToContinue": "Devam etmek için linkleri kullanabilirsiniz", |
|||
"FrameworkHomePage": "Framework ana sayfa", |
|||
"FrameworkDocumentation": "Framework Dokümantasyon", |
|||
"OfficialBlog": "Resmi blog", |
|||
"CommercialHomePage": "Kurumsal ana sayfa", |
|||
"CommercialSupportWebSite": "Kurumsal destek web sitesi", |
|||
"CommunityWebSite": "ABP topluluk web sitesi" |
|||
} |
|||
} |
|||
@ -1,5 +1,161 @@ |
|||
{ |
|||
"culture": "tr", |
|||
"texts": { |
|||
"Permission:Organizations": "Organizasyonlar", |
|||
"Permission:Manage": "Organizasyonları Yönet", |
|||
"Permission:DiscountRequests": "İndirim Talepleri", |
|||
"Permission:DiscountManage": "İndirim Taleplerini Yönet", |
|||
"Permission:Disable": "Devre Dışı Bırak", |
|||
"Permission:Enable": "Etkinleşir", |
|||
"Permission:EnableSendEmail": "E-Posta Göndermeyi Etkinleştir", |
|||
"Permission:SendEmail": "E-Posta Gönder", |
|||
"Permission:NpmPackages": "NPM Paketleri", |
|||
"Permission:NugetPackages": "Nuget Paketleri", |
|||
"Permission:Maintenance": "Bakım", |
|||
"Permission:Maintain": "Bakım Yap", |
|||
"Permission:ClearCaches": "Önbelleği temizle", |
|||
"Permission:Modules": "Modüller", |
|||
"Permission:Packages": "Paketler", |
|||
"Permission:Edit": "Güncelle", |
|||
"Permission:Delete": "Sil", |
|||
"Permission:Create": "Oluştur", |
|||
"Permission:Accounting": "Muhasebe", |
|||
"Permission:Accounting:Quotation": "Fiyatlandırma", |
|||
"Permission:Accounting:Invoice": "Fatura", |
|||
"Menu:Organizations": "Organizasyonlar", |
|||
"Menu:Accounting": "Muhasebe", |
|||
"Menu:Packages": "Paketler", |
|||
"Menu:DiscountRequests": "İndirim Talepleri", |
|||
"NpmPackageDeletionWarningMessage": "Bu NPM Paketi silinecektir. Onaylıyor musunuz?", |
|||
"NugetPackageDeletionWarningMessage": "Bu Nuget Paketi silinecektir. Onaylıyor musunuz?", |
|||
"ModuleDeletionWarningMessage": "Bu Modül silinecektir. Onaylıyor musunuz?", |
|||
"Name": "İsim", |
|||
"DisplayName": "Görüntülenen isim", |
|||
"ShortDescription": "Kısa açıklama", |
|||
"NameFilter": "İsim", |
|||
"CreationTime": "Oluşturma zamanı", |
|||
"IsPro": "Is pro", |
|||
"ShowOnModuleList": "Modül listesinde göster", |
|||
"EfCoreConfigureMethodName": "Metot adını yapılandır", |
|||
"IsProFilter": "Is pro", |
|||
"ApplicationType": "Uygulama tipi", |
|||
"Target": "Hedef", |
|||
"TargetFilter": "Hedef", |
|||
"ModuleClass": "Modül sınıfı", |
|||
"NugetPackageTarget.DomainShared": "Domain Shared", |
|||
"NugetPackageTarget.Domain": "Domain", |
|||
"NugetPackageTarget.Application": "Application", |
|||
"NugetPackageTarget.ApplicationContracts": "Application Contracts", |
|||
"NugetPackageTarget.HttpApi": "Http Api", |
|||
"NugetPackageTarget.HttpApiClient": "Http Api Client", |
|||
"NugetPackageTarget.Web": "Web", |
|||
"NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core", |
|||
"NugetPackageTarget.MongoDB": "MongoDB", |
|||
"Edit": "Güncelle", |
|||
"Delete": "Sil", |
|||
"Refresh": "Yenile", |
|||
"NpmPackages": "NPM Paketleri", |
|||
"NugetPackages": "Nuget Paketleri", |
|||
"NpmPackageCount": "NPM Paket Sayısı", |
|||
"NugetPackageCount": "Nuget Paket Sayısı", |
|||
"Module": "Modüller", |
|||
"ModuleInfo": "Modül bilgisi", |
|||
"CreateANpmPackage": "Bir NPM paketi oluştur", |
|||
"CreateAModule": "Bir modül oluştur", |
|||
"CreateANugetPackage": "Bir Nuget paketi oluştur", |
|||
"AddNew": "Yenisini Ekle", |
|||
"PackageAlreadyExist{0}": "\"{0}\"isimli paket zaten eklendi.", |
|||
"ModuleAlreadyExist{0}": "\"{0}\" isimli modül zaten eklendi.", |
|||
"ClearCache": "Önbelleği Temizle", |
|||
"SuccessfullyCleared": "Başarıyla temizlendi", |
|||
"Menu:NpmPackages": "NPM Paketleri", |
|||
"Menu:Modules": "Modüller", |
|||
"Menu:Maintenance": "Bakım", |
|||
"Menu:NugetPackages": "Nuget Paketleri", |
|||
"CreateAnOrganization": "Bir organizasyon oluştur", |
|||
"Organizations": "Organizasyonlar", |
|||
"LongName": "Uzun isim", |
|||
"LicenseType": "Lisans tipi", |
|||
"MissingLicenseTypeField": "Lisans tipi alanı zorunludur.", |
|||
"LicenseStartTime": "Lisans başlama zamanı", |
|||
"LicenseEndTime": "Lisans bitiş zamanı", |
|||
"AllowedDeveloperCount": "İzin verilen developer sayısı", |
|||
"UserNameOrEmailAddress": "Kullanıcı adı veya e-posta adresi", |
|||
"AddOwner": "Owner ekle", |
|||
"UserName": "Kullanıcı Adı", |
|||
"Email": "E-Posta", |
|||
"Developers": "Developers", |
|||
"AddDeveloper": "Developer Ekle", |
|||
"Create": "Oluştur", |
|||
"UserNotFound": "Kullanıcı bulunamadı", |
|||
"{0}WillBeRemovedFromDevelopers": "{0} kullanıcı adlı developer silinecektir, Onaylıyor musunuz?", |
|||
"{0}WillBeRemovedFromOwners": "{0} kullanıcı adlı owner silinecektir, Onaylıyor musunuz?", |
|||
"Computers": "Bilgisayarlar", |
|||
"UniqueComputerId": "Özgün bilgisayar id", |
|||
"LastSeenDate": "Son görülme tarihi", |
|||
"{0}Computer{1}WillBeRemovedFromRecords": "{0} kullanıcı isimli kullanıcının bilgisayarı ({1}) kayıtlardan kaldırılacaktır", |
|||
"OrganizationDeletionWarningMessage": "Organizasyon silinecektir.", |
|||
"DeletingLastOwnerWarningMessage": "Bir organizasyon en az bir ownera sahip olmalıdır! Bu nedenle bu ownerı kaldıramazsınız", |
|||
"This{0}AlreadyExistInThisOrganization": "{0} zaten bu organizasyonda bulunmaktadır", |
|||
"AreYouSureYouWantToDeleteAllComputers": "Tüm bilgisayaları silmek istediğinize emin misiniz?", |
|||
"DeleteAll": "Tümünü sil", |
|||
"DoYouWantToCreateNewUser": "Yeni kullanıcı oluşturmak istiyor musunuz?", |
|||
"MasterModules": "Master Modüller", |
|||
"OrganizationName": "Organizasyon adı", |
|||
"OrganizationNamePlaceholder": "Organizasyon adı...", |
|||
"UsernameOrEmail": "Kullanıcı adı veya e-posta", |
|||
"UsernameOrEmailPlaceholder": "Kullanıcı adı veya e-posta", |
|||
"Member": "Üye", |
|||
"PurchaseOrderNo": "Satın alma sipariş no", |
|||
"QuotationDate": "Fiyatlandırma tarihi", |
|||
"CompanyName": "Şirket adı", |
|||
"CompanyAddress": "Şirket adresi", |
|||
"Price": "Fiyat", |
|||
"DiscountText": "İndirim metni", |
|||
"DiscountQuantity": "İndirim miktarı", |
|||
"DiscountPrice": "İndirim fiyatı", |
|||
"Quotation": "Fiyatlandırma", |
|||
"ExtraText": "Eksta metin", |
|||
"ExtraAmount": "Eksta miktar", |
|||
"DownloadQuotation": "Fiyatlandırmayı İndir", |
|||
"Invoice": "Fatura", |
|||
"TaxNumber": "Vergi Numarası", |
|||
"InvoiceNumber": "Fatura Numarası", |
|||
"InvoiceDate": "Fatura Tarihi", |
|||
"InvoiceNote": "Fatura Notu", |
|||
"Quantity": "Miktar", |
|||
"AddProduct": "Ürün Ekle", |
|||
"AddProductWarning": "Ürün eklemelisiniz!", |
|||
"TotalPrice": "Toplam Fiyat", |
|||
"Generate": "Üret", |
|||
"MissingQuantityField": "Miktar alanı zorunludur!", |
|||
"MissingPriceField": "Fiyat alanı zorunludur!", |
|||
"CodeUsageStatus": "Statü", |
|||
"Country": "Ülke", |
|||
"DeveloperCount": "Developer Sayısı", |
|||
"RequestCode": "Talep Kodu", |
|||
"WebSite": "Web Sitesi", |
|||
"GithubUsername": "Github Kullanıcı adı", |
|||
"PhoneNumber": "Telefon Numarası", |
|||
"ProjectDescription": "Proje Açıklaması", |
|||
"Referrer": "Yönlendiren", |
|||
"DiscountRequests": "İndirim Talebi", |
|||
"Copylink": "Kopyalama Linki", |
|||
"Disable": "Devre Dışı Bırak", |
|||
"Enable": "Etkinleştir", |
|||
"EnableSendEmail": "E-Posta Göndermeyi Etkinleştir", |
|||
"SendEmail": "E-Posta Gönder", |
|||
"SuccessfullyDisabled": "Başarıyla Devre Dışı Bırakıldı", |
|||
"SuccessfullyEnabled": "Başarıyla Etkinleştirildi", |
|||
"EmailSent": "E-Posta Gönderildi", |
|||
"SuccessfullySent": "Başarıyla Gönderildi", |
|||
"SuccessfullyDeleted": "Başarıyla Silindi", |
|||
"DiscountRequestDeletionWarningMessage": "İndirim talebi silinecektir", |
|||
"BusinessType": "İş tipi", |
|||
"TotalQuestionCount": "Toplam soru sayısı", |
|||
"RemainingQuestionCount": "Kalan soru sayısı", |
|||
"TotalQuestionMustBeGreaterWarningMessage": "Toplam soru sayısı kalan soru sayısından büyük olmalıdır!", |
|||
"QuestionCountsMustBeGreaterThanZero": "Toplam soru sayısı ve kalan soru sayısı sıfır veya sıfırdan daha büyük olmalıdır!", |
|||
"UnlimitedQuestionCount": "Sınırsız soru sayısı" |
|||
} |
|||
} |
|||
@ -1,5 +1,161 @@ |
|||
{ |
|||
"culture": "tr", |
|||
"texts": { |
|||
"GetStarted": "Başlamak - Başlangıç Templateleri", |
|||
"Create": "Oluştur", |
|||
"NewProject": "Yeni Proje", |
|||
"DirectDownload": "Doğrudan İndir", |
|||
"ProjectName": "Proje ismi", |
|||
"ProjectType": "Proje tipi", |
|||
"DatabaseProvider": "Veritabanı sağlayacısı", |
|||
"NTier": "N-Tier", |
|||
"IncludeUserInterface": "Kullanıcı arayüzünü dahil et", |
|||
"CreateNow": "Şimdi oluştur", |
|||
"TheStartupProject": "Başlangıç projesi", |
|||
"Tutorial": "Öğretici", |
|||
"UsingCLI": "CLI Kullanmak", |
|||
"SeeDetails": "Detayları Görüntüle", |
|||
"AbpShortDescription": "ABP modern web uygulamaları geliştirmek için eksiksiz bir mimari ve günlü bir altyapıdır! Size SOLID geliştirme tecrübelerini sunmak için en iyi uygulama ve kuralları takip eder.", |
|||
"SourceCodeUpper": "KAYNAK KOD", |
|||
"LatestReleaseLogs": "Son release kayıtları", |
|||
"Infrastructure": "Altyapı", |
|||
"Architecture": "Mimari", |
|||
"Modular": "Modüler", |
|||
"DontRepeatYourself": "Kendini Tekrar Etme", |
|||
"DeveloperFocused": "Developer Odaklı", |
|||
"FullStackApplicationInfrastructure": "Full stack uygulama altyapısı", |
|||
"DomainDrivenDesign": "Domain Driven Design", |
|||
"DomainDrivenDesignExplanation": "DDD paterni ve prensiplerinden yola çıkarak dizayn edildi ve geliştirildi. Uygulamanız için katmanlı bir model sunmaktadır.", |
|||
"Authorization": "Yetkilendirme", |
|||
"AuthorizationExplanation": "Kullanıcı, rol ve ayrıntılı izin sistemi ile modern yetkilendirme. Microsoft Identity library üzerine kurulmuştur.", |
|||
"MultiTenancy": "Multi-Tenancy", |
|||
"MultiTenancyExplanationShort": "SaaS uygulamaları kolaylaştı! Veritababından kullanıcı arayüzüne entegre edilmiş multi-tenancy", |
|||
"CrossCuttingConcerns": "Cross Cutting Concerns", |
|||
"CrossCuttingConcernsExplanationShort": "Yetkilendirme, validasyon, hata yakalama, caching, audit logging, işlem yönetimi ve bunun gibi konular için eksiksiz altyapı.", |
|||
"BuiltInBundlingMinification": "Hazır Paketleme & Küçültme", |
|||
"BuiltInBundlingMinificationExplanation": "Paketleme ve küçültmek için external araçları kullanmayı bırakın. ABP daha basit, dinamik, güçlü, modüler ve hazır yolları öneriyor.", |
|||
"VirtualFileSystem": "Sanal Dosya Sistemi", |
|||
"VirtualFileSystemExplanation": "Sayfaları, scriptleri, stilleri, resimleri... paketlere/kütüpanelere gömün ve farklı uygulamalarda yeniden kullanın. ", |
|||
"Theming": "Theming", |
|||
"ThemingExplanationShort": "Bootstrap tabanlı standart kullanıcı arayüzlerini kullan ve kişiselleştir veya kendin yeni bir tane oluştur.", |
|||
"BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dinamik Formlar", |
|||
"BootstrapTagHelpersDynamicFormsExplanation": "Bootstrap komponentlerinin tekrar eden detaylarını manuel olarak yazmak yerine, Bu işlemi basitleştirmek ve iyileştirme avantajından faydalanmak için ABP'nin tag helperlarını kullanın. Dinamik form bir C# sınıfından model olarak eksiksik form oluşturabilir.", |
|||
"HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies", |
|||
"HTTPAPIsDynamicProxiesExplanation": "Application servislerini otomatik olarak Rest stil Http API olarak ayarlayın ve dinamaik Javascript & C# proxyler ile kullanın.", |
|||
"CompleteArchitectureInfo": "Bakım yapılabilir yazılım çözümleri üretmek için modern mimari.", |
|||
"DomainDrivenDesignBasedLayeringModelExplanation": "DDD tabanlı bir katmanlı mimari geliştirmek ve bakım yapılabilir bir kod altyapısı inşaa etmek için size yardım eder.", |
|||
"DomainDrivenDesignBasedLayeringModelExplanationCont": "DDD patern ve prensiplerinden yola çıkarak uygulamanızı geliştirmeye yardımcı olmak için başlanıç templateler, soyutlamalar, base sınıflar, servisler, dokümantasyon ve rehberlik sağlar. ", |
|||
"MicroserviceCompatibleModelExplanation": "Core framework & pre-build modüller mikroservis mimari göz önünde bulundurularak dizayn edildi.", |
|||
"MicroserviceCompatibleModelExplanationCont": "Microservice çözümlerini daha kolay geliştirmek için altyapı, entegrasyon, örnekler ve dokümantasyon sunarken eğer bir tek parça uygulama istiyorsanız ek karmaşıklık getirmez.", |
|||
"ModularInfo": "ABP yeniden kullanılabilir uygulama modülleri geliştirebilmenize izin veren eksiksiz modüler sistem sunar.", |
|||
"PreBuiltModulesThemes": "Pre-Built Modüller & Temalar", |
|||
"PreBuiltModulesThemesExplanation": "Açık kaynak ve ticari modüller & temalar iş uygulamanızda kullanıma hazırdır.", |
|||
"NuGetNPMPackages": "NUGET & NPM Packages", |
|||
"NuGetNPMPackagesExplanation": "NUGET & NPM paketleri olarak dağıtılmıştır. Yüklemek ve güncellemek kolaydır.", |
|||
"ExtensibleReplaceable": "Genişletilebilir/Değiştirilebilir", |
|||
"ExtensibleReplaceableExplanation": "Tüm sevisler & modüller genişletilebilirlik göz önünde bulundurularak dizayn edildi. Servislerin, sayfaların stillerin, komponentlerin vb. yerlerini değiştirebilirsizinz.", |
|||
"CrossCuttingConcernsExplanation2": "Kodunu daha temiz tut ve kendi uygulama koduna odaklan.", |
|||
"CrossCuttingConcernsExplanation3": "Ortak uygulama isterlerini tekrar ve tekrar geliştirmek için zaman harcamayın.", |
|||
"AuthenticationAuthorization": "Kimlik Doğrulama & Yetkilendirme", |
|||
"ExceptionHandling": "Hata yakalama", |
|||
"Validation": "Validasyon", |
|||
"DatabaseConnection": "Veritabanı bağlantısı", |
|||
"TransactionManagement": "İşlem yönetimi", |
|||
"AuditLogging": "Audit Logging", |
|||
"Caching": "Caching", |
|||
"Multitenancy": "Multitenancy", |
|||
"DataFiltering": "Date filtreleme", |
|||
"ConventionOverConfiguration": "Yapılandırma Üzerinde Kurallar", |
|||
"ConventionOverConfigurationExplanation": "ABP minimal veya sıfır yapılandırma ile ortak uygulama kurallarını varsayılan olarak uygular.", |
|||
"ConventionOverConfigurationExplanationList1": "Dependency injection için bilinen servisler otomatik olarak kaydedilir.", |
|||
"ConventionOverConfigurationExplanationList2": "Application servisler isimlerdirme kuralları ile HTTP API ler olarak uygulanır.", |
|||
"ConventionOverConfigurationExplanationList3": "C# ve JavaScript için dinamik HTTP istemci proxyleri yaratır.", |
|||
"ConventionOverConfigurationExplanationList4": "Entityleriniz için varsayılaan repositoriler sunar.", |
|||
"ConventionOverConfigurationExplanationList5": "Her web request veya application servis metodu için Unit of Work işlemini yönetir.", |
|||
"ConventionOverConfigurationExplanationList6": "Entityleriniz için oluşturma, güncelleme & silme işlemlerini yayınlar.", |
|||
"BaseClasses": "Ana Sınıflar", |
|||
"BaseClassesExplanation": "Ortak uygulama paternleri için pre-built ana sınıflar.", |
|||
"DeveloperFocusedExplanation": "ABP developerlar içindir.", |
|||
"DeveloperFocusedExplanationCont": "İhtiyacınız olduğunda düşük seviyede çalışmanızı kısıtlamadan günlük yazılım geliştirmenizi basitleştirmeyi amaçlar.", |
|||
"SeeAllFeatures": "Tüm Özellikleri Görüntüle", |
|||
"CLI_CommandLineInterface": "CLI (Command Line Interface)", |
|||
"CLI_CommandLineInterfaceExplanation": "CLI yeni proje oluşturma ve uygulamanıza modüller ekleme işlemlerini otomatik hale getirir.", |
|||
"StartupTemplates": "Başlangıç Templateler", |
|||
"StartupTemplatesExplanation": "Çeşitli başlangıç templateleri size geliştirme başlatmak için tam yapılandırılmış bir çözüm sağlar.", |
|||
"BasedOnFamiliarTools": "Bilinen Araçlara Dayalı ", |
|||
"BasedOnFamiliarToolsExplanation": "Zaten bildiğiniz popüler araçlar ile geliştirilme ve egtegre edilmiştir. Düşük öğrenme eğrisi, koaly adaptasyon, rahat geliştirme.", |
|||
"ORMIndependent": "ORM Bağımsız", |
|||
"ORMIndependentExplanation": "", |
|||
"Features": "ABP Framework Özelliklerini Keşfet", |
|||
"ABPCLI": "ABP CLI", |
|||
"Modularity": "Modülerlik", |
|||
"BootstrapTagHelpers": "Bootstrap Tag Helpers", |
|||
"DynamicForms": "Dinamik Formlar", |
|||
"BundlingMinification": "Paketleme & Küçültme", |
|||
"BackgroundJobs": "Arkaplan İşleri", |
|||
"DDDInfrastructure": "DDD altyapısı", |
|||
"DomainDrivenDesignInfrastructure": "Domain Driven Design Altyapısı", |
|||
"AutoRESTAPIs": "Otomatik REST APIler", |
|||
"DynamicClientProxies": "Dinamik Client Proxies", |
|||
"DistributedEventBus": "Dağıtılmış Event Bus", |
|||
"DistributedEventBusWithRabbitMQIntegration": "RabbitMQ Entegrasyonu ile Dağıtılmış Event Bus", |
|||
"TestInfrastructure": "Test ALtyapısı", |
|||
"AuditLoggingEntityHistories": "Audit Logging & Entity Histories", |
|||
"ObjectToObjectMapping": "Object to Object Mapping", |
|||
"EmailSMSAbstractions": "E-Posta & SMS Soyutlamaları", |
|||
"EmailSMSAbstractionsWithTemplatingSupport": "Template Destekli E-Posta & SMS Soyutlamaları", |
|||
"Localization": "Localization", |
|||
"SettingManagement": "Ayar Yönetimi", |
|||
"ExtensionMethods": "Extension Methods", |
|||
"ExtensionMethodsHelpers": "Extension Methods & Helpers", |
|||
"AspectOrientedProgramming": "Aspect Oriented Programming", |
|||
"DependencyInjection": "Dependency Injection", |
|||
"DependencyInjectionByConventions": "Dependency Injection by Conventions", |
|||
"ABPCLIExplanation": "ABP CLI (Command Line Interface) ABP tabanlı çözümler ortak işlemleri gerçekleştiren bir komut satırı aracıdır.", |
|||
"ModularityExplanation": "ABP, entityleri, servisleri, veritabanı entegrasyonu, APIleri, UI komponentleri ve bunun gibi özelliklere sahip olabilecek kendi uygulama modüllerini geliştirmeniz için eksiksiz bir altyapı sağlar. ", |
|||
"MultiTenancyExplanation": "ABP framework sadece multi-tenant uygulama geliştirmenizi desteklemekle kalmaz aynı zamanda kodunuzun çoğunlukla tenantların birbirinden haberi olmaycak şekilde olmasını sağlar.", |
|||
"MultiTenancyExplanation2": "Anlık tenant'ı otomatik olarak belirleybilir, farklı tenantların verilerini birbirlerinden izole edebilir.", |
|||
"MultiTenancyExplanation3": "Tek bir veritabanını, her tenant için ayrı bir veritabanını ve hibrid yaklaşımları destekler.", |
|||
"MultiTenancyExplanation4": "Sen kendi uygulama kodunu odaklan ve bırak framework sizin adınıza multi-tenancy üstesinden gelsin.", |
|||
"BootstrapTagHelpersExplanation": "Bootstrap komponentlerinin tekrar eden detaylarını manuel olarak yazmak yerine, Bu işlemi basitleştirmek ve iyileştirme avantajından faydalanmak için ABP'nin tag helperlarını kullanın. Dinamik form bir C# sınıfından model olarak eksiksik form oluşturabilir.", |
|||
"DynamicFormsExplanation": "Dinamik form & input tag helpers bir C# sınıfından model olarak eksiksik form oluşturabilir.", |
|||
"AuthenticationAuthorizationExplanation": "ASP.NET Core Identity & IdentityServer4 ile entegre edilmiş zengin kimlik doğrulama ve yetkilendirme opsiyonları. Genişletilebilir ve detaylandırılabilr bir izin sistemi sunar.", |
|||
"CrossCuttingConcernsExplanation": "Tüm bu ortak şeyleri geliştirmek için kendini sürekli tekrar etme. Kendi iş koduna odaklan ve bırak ABP bunları kurallar ile otomatik hale getirsin.", |
|||
"DatabaseConnectionTransactionManagement": "Veritabanı Bğlantısı & İşlem Yönetimi", |
|||
"CorrelationIdTracking": "Correlation-Id Tracking", |
|||
"BundlingMinificationExplanation": "ABP daha basit, dinamik, güçlü, modüler ve hazır paketlenmiş ve küçültülmüş sistemi öneriyor.", |
|||
"VirtualFileSystemnExplanation": "Sanal Dosya Sistemi fiziksel olarak disk üzerinde var olmayan dosyalarını yönetmeyi mümkün kılmaktadır. Bunlar genellikle önceden assemblyler içerisinde gömülü olan(js,css,image,cshtml..) dosyalardır ve bunlar fiziksel dosylar gibi runtimeda kullanılır.", |
|||
"ThemingExplanation": "Theming sistem son Bootstrap Framework tabanlı ortak bir kütüphane ve layout tanımlayarak uygulamanızı & modüllerini bağımsız olarak geliştirmenizi sağlamaktadır.", |
|||
"DomainDrivenDesignInfrastructureExplanation": "Domain Driven Design pattern ve prensiplerine dayalı katmanlı uygulama geliştirmek için eksiksiz bit altyapı", |
|||
"Specification": "Specification", |
|||
"Repository": "Repository", |
|||
"DomainService": "Domain Service", |
|||
"ValueObject": "Value Object", |
|||
"ApplicationService": "Application Service", |
|||
"DataTransferObject": "Data Transfer Object", |
|||
"AggregateRootEntity": "Aggregate Root, Entity", |
|||
"AutoRESTAPIsExplanation": "ABP, application servislerinizi otomatik olarak API Controller olarak kurallı bir şekilde yapılandırabilir.", |
|||
"DynamicClientProxiesExplanation": "Apilerinizi, JavaScript ve C# clients tarafından kolaylıkla kullanın.", |
|||
"DistributedEventBusWithRabbitMQIntegrationExplanation": "Easily publish & consume distributed events using built-in Distributed Event Bus with RabbitMQ integration available.", |
|||
"TestInfrastructureExplanation": "The framework has been developed unit & integration testing in mind. Provides you base classes to make it easier. Startup templates come with pre-configured for testing.", |
|||
"AuditLoggingEntityHistoriesExplanation": "", |
|||
"EmailSMSAbstractionsWithTemplatingSupportExplanation": "", |
|||
"LocalizationExplanation": "", |
|||
"SettingManagementExplanation": "", |
|||
"ExtensionMethodsHelpersExplanation": "", |
|||
"AspectOrientedProgrammingExplanation": "", |
|||
"DependencyInjectionByConventionsExplanation": "", |
|||
"DataFilteringExplanation": "", |
|||
"PublishEvents": "Publish Events", |
|||
"HandleEvents": "Handle Events", |
|||
"AndMore": "", |
|||
"Code": "Code", |
|||
"Result": "Sonuç", |
|||
"SeeTheDocumentForMoreInformation": "See the <a href=\"{1}\">{0} document</a> for more information", |
|||
"IndexPageHeroSection": "<span class=\"first-line shine\"><strong>open source</strong></span><span class=\"second-line text-uppercase\">Web Application<br />Framework </span><span class=\"third-line shine2\"><strong>for asp.net core</strong></span>", |
|||
"UiFramework": "UI Framework", |
|||
"EmailAddress": "E-Posta Adresi", |
|||
"Mobile": "Mobil", |
|||
"ReactNative": "React Native" |
|||
} |
|||
} |
|||
@ -1,8 +1,8 @@ |
|||
<Project> |
|||
<ItemGroup> |
|||
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" /> |
|||
<PackageReference Include="Fody" Version="6.0.6"> |
|||
<PrivateAssets>all</PrivateAssets> |
|||
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" PrivateAssets="All" /> |
|||
<PackageReference Include="Fody" Version="6.2.0"> |
|||
<PrivateAssets>All</PrivateAssets> |
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> |
|||
</PackageReference> |
|||
</ItemGroup> |
|||
|
|||
@ -1,3 +1,134 @@ |
|||
# Distributed Event Bus RabbitMQ Integration |
|||
|
|||
TODO |
|||
> 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 [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) properties as the connection properties. |
|||
|
|||
**Example: Specify the connection port** |
|||
|
|||
````csharp |
|||
{ |
|||
"RabbitMQ": { |
|||
"Connections": { |
|||
"Default": { |
|||
"HostName": "123.123.123.123", |
|||
"Port": "5672" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### 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<AbpRabbitMqOptions>(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 and exchange names** |
|||
|
|||
````csharp |
|||
Configure<AbpRabbitMqEventBusOptions>(options => |
|||
{ |
|||
options.ClientName = "TestApp1"; |
|||
options.ExchangeName = "TestMessages"; |
|||
}); |
|||
```` |
|||
|
|||
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. |
|||
@ -1,3 +1,299 @@ |
|||
# Distributed Event Bus |
|||
|
|||
TODO |
|||
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 two 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. |
|||
* `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. |
|||
|
|||
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. |
|||
|
|||
### IDistributedEventBus |
|||
|
|||
`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 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 |
|||
{ |
|||
[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, 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 **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. |
|||
|
|||
### 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<Guid> |
|||
{ |
|||
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<TEvent>` 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<StockCountChangedEto>, |
|||
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<TEvent>` interface for each event type. |
|||
|
|||
> 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. |
|||
|
|||
## 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<T>` is published when an entity of type `T` was created. |
|||
* `EntityUpdatedEto<T>` is published when an entity of type `T` was updated. |
|||
* `EntityDeletedEto<T>` 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<EntityUpdatedEto<ProductEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData) |
|||
{ |
|||
var productId = eventData.Entity.Id; |
|||
//TODO |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `MyHandler` implements the `IDistributedEventHandler<EntityUpdatedEto<ProductEto>>`. |
|||
|
|||
### 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<AbpDistributedEntityEventOptions>(options => |
|||
{ |
|||
//Enable for all entities |
|||
options.AutoEventSelectors.AddAll(); |
|||
|
|||
//Enable for a single entity |
|||
options.AutoEventSelectors.Add<IdentityUser>(); |
|||
|
|||
//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.") |
|||
); |
|||
}); |
|||
```` |
|||
|
|||
* 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<EntityUpdatedEto<EntityEto>>` to subscribe the events. However, it is not a good approach to subscribe to such a generic event. You can define the corresponding ETO for the entity type. |
|||
|
|||
**Example: Declare to use `ProductEto` for the `Product` entity** |
|||
|
|||
````csharp |
|||
Configure<AbpDistributedEntityEventOptions>(options => |
|||
{ |
|||
options.AutoEventSelectors.Add<Product>(); |
|||
options.EtoMappings.Add<Product, ProductEto>(); |
|||
}); |
|||
```` |
|||
|
|||
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 mapping. You can check the object to object mapping document for all options, but the following example shows how to configure it with the [AutoMapper](https://automapper.org/) library. |
|||
|
|||
**Example: Configure `Product` to `ProductEto` mapping using the AutoMapper** |
|||
|
|||
````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; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example uses the `AutoMap` attribute of the AutoMapper to configure the mapping. You could create a profile class instead. Please refer to the AutoMapper document for more options. |
|||
@ -0,0 +1,63 @@ |
|||
# 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. |
|||
|
|||
## 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*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. |
|||
|
|||
|
|||
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block |
|||
|
|||
```csharp |
|||
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|||
``` |
|||
|
|||
with this one |
|||
```csharp |
|||
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
|||
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().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.DbMigrations` 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.DbMigrations` 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. |
|||
|
|||
## Run the Application |
|||
|
|||
It is ready. Just run the application and enjoy coding. |
|||
@ -0,0 +1,58 @@ |
|||
# 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. |
|||
|
|||
## 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*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. |
|||
|
|||
|
|||
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block |
|||
|
|||
```csharp |
|||
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|||
``` |
|||
|
|||
with this one (just changes `UseSqlServer(...)` to `UseOracle(...)`) |
|||
```csharp |
|||
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
|||
.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.DbMigrations` 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.DbMigrations` 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. |
|||
|
|||
## Run the Application |
|||
|
|||
It is ready. Just run the application and enjoy coding. |
|||
@ -1,3 +1,10 @@ |
|||
# Event Bus |
|||
|
|||
TODO |
|||
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. |
|||
@ -0,0 +1,72 @@ |
|||
# 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**. |
|||
|
|||
When you **create a new application** as described in the [getting started document](Getting-Started.md), the solution includes the React Native application in the `react-native` folder as default. |
|||
|
|||
## Configure Your Local IP Address |
|||
|
|||
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"}} |
|||
 |
|||
|
|||
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address. |
|||
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
|||
|
|||
{{ else if Tiered == "Yes" }} |
|||
|
|||
 |
|||
|
|||
* Open the `appsettings.json` in the `.IdentityServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address. |
|||
* Open the `launchSettings.json` in the `.IdentityServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
|||
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address. |
|||
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
|||
|
|||
{{ end }} |
|||
|
|||
## Run the Server Application |
|||
|
|||
Run the backend application as described in the [getting started document](Getting-Started.md). |
|||
|
|||
> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. |
|||
|
|||
Go to the `react-native` 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 |
|||
``` |
|||
|
|||
* 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: |
|||
|
|||
 |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> Make sure that `issuer` matches the running address of the `.IdentityServer` 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 }} |
|||
|
|||
Once all node modules are loaded, execute `yarn start` (or `npm start`) command: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address. |
|||
|
|||
 |
|||
|
|||
In the above management interface, you can start the application with an Android emulator, an iOS simulator or a physical phone by the scan the QR code with the [Expo Client](https://expo.io/tools#client). |
|||
|
|||
> See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/), [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) documents on expo.io. |
|||
|
|||
 |
|||
|
|||
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. |
|||
@ -1,3 +1,227 @@ |
|||
# Local Event Bus |
|||
|
|||
TODO |
|||
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. |
|||
|
|||
### 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). |
|||
|
|||
### 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<Guid> |
|||
{ |
|||
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. |
|||
|
|||
> 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<TEvent>` 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<StockCountChangedEvent>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(StockCountChangedEvent eventData) |
|||
{ |
|||
//TODO: your code that does somthing 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 here. |
|||
|
|||
* **Zero or more handlers** can subscribe to the same event. |
|||
* A single event handler class can **subscribe to multiple events** but implementing the `ILocalEventHandler<TEvent>` interface for each event type. |
|||
|
|||
> 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. |
|||
|
|||
## Transaction & Exception Behavior |
|||
|
|||
When an event published, subscribed event handlers are immediately executed. So; |
|||
|
|||
* If a handler **throws an exception**, it effects the code that published the event. That means it gets the exception on the `PublishAsync` call. So, **use try-catch yourself** in the event handler if you want to hide the error. |
|||
* If the event publishing code is being executed inside a [Unit Of Work](Unit-Of-Work.md) scope, the event handlers also covered by the unit of work. That means if your UOW is transactional and a handler throws an exception, the transaction is rolled back. |
|||
|
|||
## 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<EntityCreatedEventData<IdentityUser>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync( |
|||
EntityCreatedEventData<IdentityUser> eventData) |
|||
{ |
|||
var userName = eventData.Entity.UserName; |
|||
var email = eventData.Entity.Email; |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class subscribes to the `EntityCreatedEventData<IdentityUser>`, which is published just after a user was created. You may want to send a "Welcome" email to the new user. |
|||
|
|||
There are two types of these events: events with past tense and events with continuous tense. |
|||
|
|||
### Events with Past Tense |
|||
|
|||
Events with past tense are published when the related unit of work completed and the entity change successfully saved to the database. If you throw an exception on these event handlers, it **can not rollback** the transaction since it was already committed. |
|||
|
|||
The event types are; |
|||
|
|||
* `EntityCreatedEventData<T>` is published just after an entity was successfully created. |
|||
* `EntityUpdatedEventData<T>` is published just after an entity was successfully updated. |
|||
* `EntityDeletedEventData<T>` is published just after an entity was successfully deleted. |
|||
* `EntityChangedEventData<T>` 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. |
|||
|
|||
### Events with Continuous Tense |
|||
|
|||
Events with continuous tense are published before completing the transaction (if database transaction is supported by the database provider being used). If you throw an exception on these event handlers, it **can rollback** the transaction since it is not completed yet and the change is not saved to the database. |
|||
|
|||
The event types are; |
|||
|
|||
* `EntityCreatingEventData<T>` is published just before saving a new entity to the database. |
|||
* `EntityUpdatingEventData<T>` is published just before an existing entity is being updated. |
|||
* `EntityDeletingEventData<T>` is published just before an entity is being deleted. |
|||
* `EntityChangingEventData<T>` is published just before an entity is being 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). |
|||
@ -1,4 +1,6 @@ |
|||
# Identity Management Module |
|||
|
|||
Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library. |
|||
|
|||
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... |
|||
|
|||
|
|||
@ -0,0 +1,47 @@ |
|||
# Organization Unit Management |
|||
|
|||
Organization units (OU) is a part of **Identity Module** and 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. |
|||
|
|||
The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class. |
|||
|
|||
#### 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 OrganizationUnit Manager. 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: |
|||
|
|||
- It must be **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 **changeable** if you move an OU. |
|||
- You must reference an OU by Id, not Code. |
|||
|
|||
### 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. |
|||
|
|||
#### Multi-Tenancy |
|||
|
|||
The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default. |
|||
@ -0,0 +1,3 @@ |
|||
# RabbitMQ |
|||
|
|||
TODO! |
|||
@ -0,0 +1,248 @@ |
|||
# Web Application Development Tutorial - Part 4: Integration Tests |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
{{ |
|||
if UI == "MVC" |
|||
UI_Text="mvc" |
|||
else if UI == "NG" |
|||
UI_Text="angular" |
|||
else |
|||
UI_Text="?" |
|||
end |
|||
if DB == "EF" |
|||
DB_Text="Entity Framework Core" |
|||
else if DB == "Mongo" |
|||
DB_Text="MongoDB" |
|||
else |
|||
DB_Text="?" |
|||
end |
|||
}} |
|||
|
|||
## 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_Text}}** as the ORM provider. |
|||
* **{{UI_Value}}** as the UI Framework. |
|||
|
|||
This tutorial is organized as the following parts; |
|||
|
|||
- [Part 1: Creating the project and book list page](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) |
|||
|
|||
### Download the Source Code |
|||
|
|||
This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two 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) |
|||
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
## Test Projects in the Solution |
|||
|
|||
This part covers the **server side** tests. There are several test projects in the solution: |
|||
|
|||
 |
|||
|
|||
Each project is used to test the related project. Test projects use the following libraries for testing: |
|||
|
|||
* [Xunit](https://xunit.github.io/) as the main test framework. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 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) to prepare a fresh database for every test. |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)** library is used to mock the MongoDB database. A separate database instance is created and seeded (with the data seed system) 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 |
|||
|
|||
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore.Books |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[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"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `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 = System.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<AbpValidationException>(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.Validation; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore.Books |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[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 = 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<AbpValidationException>(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: |
|||
|
|||
 |
|||
|
|||
Congratulations, the **green icons** indicates that the tests have been successfully passed! |
|||
|
|||
## The Next Part |
|||
|
|||
See the [next part](part-5.md) of this tutorial. |
|||
@ -0,0 +1,401 @@ |
|||
# Web Application Development Tutorial - Part 5: Authorization |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
{{ |
|||
if UI == "MVC" |
|||
UI_Text="mvc" |
|||
else if UI == "NG" |
|||
UI_Text="angular" |
|||
else |
|||
UI_Text="?" |
|||
end |
|||
if DB == "EF" |
|||
DB_Text="Entity Framework Core" |
|||
else if DB == "Mongo" |
|||
DB_Text="MongoDB" |
|||
else |
|||
DB_Text="?" |
|||
end |
|||
}} |
|||
|
|||
## 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_Text}}** as the ORM provider. |
|||
* **{{UI_Value}}** as the UI Framework. |
|||
|
|||
This tutorial is organized as the following parts; |
|||
|
|||
- [Part 1: Creating the project and book list page](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)** |
|||
|
|||
### Download the Source Code |
|||
|
|||
This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two 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) |
|||
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
## 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 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`. |
|||
|
|||
### Permission Definitions |
|||
|
|||
You should define permissions before using them. |
|||
|
|||
Open the `BookStorePermissionDefinitionProvider` class inside the `Acme.BookStore.Application.Contracts` project 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<BookStoreResource>(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 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: |
|||
|
|||
 |
|||
|
|||
Grant the permissions you want and save the modal. |
|||
|
|||
## Authorization |
|||
|
|||
Now, you can use the permissions to authorize the book management. |
|||
|
|||
### Application Layer & HTTP API |
|||
|
|||
Open the `BookAppService` class and add 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<Book, Guid> 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)). |
|||
|
|||
{{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<RazorPagesOptions>(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. |
|||
|
|||
 |
|||
|
|||
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<BookStoreResource> L |
|||
@inject IAuthorizationService AuthorizationService |
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/Index.js"/> |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<abp-card-title>@L["Books"]</abp-card-title> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-right"> |
|||
@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create)) |
|||
{ |
|||
<abp-button id="NewBookButton" |
|||
text="@L["NewBook"].Value" |
|||
icon="plus" |
|||
button-type="Primary"/> |
|||
} |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"></abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
```` |
|||
|
|||
* 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: |
|||
|
|||
 |
|||
|
|||
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 |
|||
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 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. |
|||
|
|||
 |
|||
|
|||
Open the `/src/app/book/book.component.html` file and replace the create button HTML content as shown below: |
|||
|
|||
````html |
|||
<!-- Add the abpPermission directive --> |
|||
<button abpPermission="BookStore.Books.Create" id="create" class="btn btn-primary" type="button" (click)="createBook()"> |
|||
<i class="fa fa-plus mr-1"></i> |
|||
<span>{%{{{ '::NewBook' | abpLocalization }}}%}</span> |
|||
</button> |
|||
```` |
|||
|
|||
* 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: |
|||
|
|||
 |
|||
|
|||
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 |
|||
<!-- Add the abpPermission directive --> |
|||
<button abpPermission="BookStore.Books.Edit" ngbDropdownItem (click)="editBook(row.id)"> |
|||
{%{{{ '::Edit' | abpLocalization }}}%} |
|||
</button> |
|||
|
|||
<!-- Add the abpPermission directive --> |
|||
<button abpPermission="BookStore.Books.Delete" ngbDropdownItem (click)="delete(row.id)"> |
|||
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%} |
|||
</button> |
|||
```` |
|||
|
|||
* 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. |
|||
|
|||
{{end}} |
|||
|
|||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,477 @@ |
|||
# v2.9 to v3.0 Angular UI Migration Guide |
|||
|
|||
## What Changed in v3.0? |
|||
|
|||
### Angular 10 |
|||
|
|||
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
Open a terminal at your root folder and run the following command: |
|||
|
|||
```sh |
|||
yarn ng update @angular/cli @angular/core --force |
|||
``` |
|||
|
|||
This will make the following modifications: |
|||
|
|||
- Update your package.json and install new packages |
|||
- Revise tsconfig.json files to create a "Solution Style" configuration |
|||
- Rename `browserlist` as `.browserlistrc` |
|||
|
|||
On the other hand, it would be better if you check which packages to update first with `yarn ng update` command alone. Angular will give you a list of packages to update. |
|||
|
|||
 |
|||
|
|||
When Angular reports the packages above, your command would look like this: |
|||
|
|||
```sh |
|||
yarn ng update @angular/cli @angular/core ng-zorro-antd --force |
|||
``` |
|||
|
|||
> If Angular complains about uncommited changes in your repo, you can either commit/stash them or add `--allow-dirty` parameter to the command. |
|||
|
|||
|
|||
### Config Modules |
|||
|
|||
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account.config'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
AccountConfigModule.forRoot({ redirectUrl: '/' }), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
...and in app-routing.module.ts... |
|||
|
|||
```js |
|||
const routes: Routes = [ |
|||
// other route configuration |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import( |
|||
'./lazy-libs/account-wrapper.module' |
|||
).then(m => m.AccountWrapperModule), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
Although working, this had a few disadvantages: |
|||
|
|||
- Every module came in two independent packages, but in reality, those packages were interdependent. |
|||
- Configuring lazy loaded modules required a wrapper module. |
|||
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size. |
|||
|
|||
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account/config'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
AccountConfigModule.forRoot(), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
...and in app-routing.module.ts... |
|||
|
|||
```js |
|||
const routes: Routes = [ |
|||
// other route configuration |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import('@abp/ng.account') |
|||
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps. |
|||
|
|||
#### A Better Example |
|||
|
|||
AppModule: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account/config'; |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { IdentityConfigModule } from '@abp/ng.identity/config'; |
|||
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config'; |
|||
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config'; |
|||
import { ThemeBasicModule } from '@abp/ng.theme.basic'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { BrowserModule } from '@angular/platform-browser'; |
|||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
|||
import { NgxsModule } from '@ngxs/store'; |
|||
import { environment } from '../environments/environment'; |
|||
import { AppRoutingModule } from './app-routing.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
BrowserModule, |
|||
BrowserAnimationsModule, |
|||
AppRoutingModule, |
|||
CoreModule.forRoot({ |
|||
environment, |
|||
sendNullsAsQueryParam: false, |
|||
skipGetAppConfiguration: false, |
|||
}), |
|||
ThemeSharedModule.forRoot(), |
|||
AccountConfigModule.forRoot(), |
|||
IdentityConfigModule.forRoot(), |
|||
TenantManagementConfigModule.forRoot(), |
|||
SettingManagementConfigModule.forRoot(), |
|||
ThemeBasicModule.forRoot(), |
|||
NgxsModule.forRoot(), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
AppRoutingModule: |
|||
|
|||
```js |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
loadChildren: () => import('./home/home.module').then(m => m.HomeModule), |
|||
}, |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => |
|||
import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
|||
}, |
|||
{ |
|||
path: 'identity', |
|||
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()), |
|||
}, |
|||
{ |
|||
path: 'tenant-management', |
|||
loadChildren: () => |
|||
import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()), |
|||
}, |
|||
{ |
|||
path: 'setting-management', |
|||
loadChildren: () => |
|||
import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()), |
|||
}, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forRoot(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class AppRoutingModule {} |
|||
``` |
|||
|
|||
AppComponent: |
|||
|
|||
```js |
|||
import { Component } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-root', |
|||
template: ` |
|||
<abp-loader-bar></abp-loader-bar> |
|||
<abp-dynamic-layout></abp-dynamic-layout> |
|||
`, |
|||
}) |
|||
export class AppComponent {} |
|||
``` |
|||
|
|||
|
|||
> You may have noticed that we used `<abp-dynamic-layout>` instead of `<router-outlet>` in the AppComponent template. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your AppComponent. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
- Remove config packages from your project using `yarn remove`. |
|||
- Import config modules from secondary entry points (e.g. `@abp/ng.identity/config`). |
|||
- Call static `forRoot` method of all new config modules, even if a configuration is not passed. |
|||
- Call static `forRoot` method of `ThemeBasicModule` (or `ThemeLeptonModule` if commercial) and remove `SharedModule` from imports (unless you have added anything that is necessary for your root module in it). |
|||
- Import lazy ABP modules directly in app routing module (e.g. `() => import('@abp/ng.identity').then(...)`). |
|||
- Call static `forLazy` method of all lazy modules inside `then`, even if a configuration is not passed. |
|||
- [OPTIONAL] Add the `<abp-dynamic-layout></abp-dynamic-layout>` to the AppComponent template and remove the `<router-outlet></router-outlet>` for better performance and UX. |
|||
|
|||
|
|||
### RoutesService |
|||
|
|||
In ABP v2.x, adding a route to the menu was done by one of two ways: |
|||
|
|||
- [Via `routes` Property in `AppRoutingModule`](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule) |
|||
- [Via ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate) |
|||
|
|||
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in `ConfigState` (breaking change). Instead, there is a new service called `RoutesService` which is used for adding, patching, or removing menu items. Please check [the documentation](./Modifying-the-Menu.md) for details. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
- Check if you have ever used `ConfigState` or `ConfigStateService` to add any routes. If so, replace them with `add` method of `RoutesService`. |
|||
- Check if you have ever patched a route. If so, convert them to `patch` method of `RoutesService`. |
|||
- Double-check that you are using absolute paths and providing a `parentName` instead of `children` property for sub-menu items in `add` or `patch` method calls. |
|||
|
|||
|
|||
### NavItemsService |
|||
|
|||
In ABP v2.x, adding a navigation element was done [via LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu) |
|||
|
|||
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check [the documentation](./Modifying-the-Menu.md) for details. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
- Replace all `dispatchAddNavigationElement` calls with `addItems` method of `NavItemsService`. |
|||
|
|||
|
|||
### ngx-datatable |
|||
|
|||
Until v3, we had been using a custom component, `abp-table`, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues. |
|||
|
|||
As of ABP v3, we have switched to a battle-tested, well-executed data grid: [ngx-datatable](https://github.com/swimlane/ngx-datatable). All ABP modules will come with ngx-datatable already implemented in them. `ThemeSharedModule` already exports `NgxDatatableModule`. So, if you install the package by running `yarn add @swimlane/ngx-datatable` in your terminal, it will be available for use in all modules of your app. |
|||
|
|||
For proper styling, you need to add the following in the styles section of your angular.json file (above all others): |
|||
|
|||
```json |
|||
"styles": [ |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/index.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-index" |
|||
}, |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-icons" |
|||
}, |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-material" |
|||
}, |
|||
// other styles |
|||
] |
|||
``` |
|||
|
|||
Since `abp-table` is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the [ListService](./List-Service.md) to work well with ngx-datatable and [introduced](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) two new directives: `NgxDatatableListDirective` and `NgxDatatableDefaultDirective`. |
|||
|
|||
The usage of those directives is rather simple: |
|||
|
|||
```js |
|||
@Component({ |
|||
providers: [ListService], |
|||
}) |
|||
export class SomeComponent { |
|||
data$ = this.list.hookToQuery( |
|||
query => this.dataService.get(query) |
|||
); |
|||
|
|||
constructor( |
|||
public readonly list: ListService, |
|||
public readonly dataService: SomeDataService, |
|||
) {} |
|||
} |
|||
``` |
|||
|
|||
...and in component template... |
|||
|
|||
```html |
|||
<ngx-datatable |
|||
[rows]="(data$ | async)?.items || []" |
|||
[count]="(data$ | async)?.totalCount || 0" |
|||
[list]="list" |
|||
default |
|||
> |
|||
<!-- column templates here --> |
|||
</ngx-datatable> |
|||
``` |
|||
|
|||
Once you bind the injected `ListService` instance through `NgxDatatableListDirective`, you no longer need to worry about pagination or sorting. Similarly, `NgxDatatableDefaultDirective` gets rid of several property bindings to make ngx-datatable fit our styles. |
|||
|
|||
#### A Better Example |
|||
|
|||
```html |
|||
<ngx-datatable |
|||
[rows]="items" |
|||
[count]="count" |
|||
[list]="list" |
|||
default |
|||
> |
|||
<!-- the grid actions column --> |
|||
<ngx-datatable-column |
|||
name="" |
|||
[maxWidth]="150" |
|||
[width]="150" |
|||
[sortable]="false" |
|||
> |
|||
<ng-template |
|||
ngx-datatable-cell-template |
|||
let-row="row" |
|||
let-i="rowIndex" |
|||
> |
|||
<abp-grid-actions |
|||
[index]="i" |
|||
[record]="row" |
|||
text="AbpUi::Actions" |
|||
></abp-grid-actions> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
|
|||
<!-- a basic column --> |
|||
<ngx-datatable-column |
|||
prop="someProp" |
|||
[name]="'::SomeProp' | abpLocalization" |
|||
[width]="200" |
|||
></ngx-datatable-column> |
|||
|
|||
<!-- a column with a custom template --> |
|||
<ngx-datatable-column |
|||
prop="someOtherProp" |
|||
[name]="'::SomeOtherProp' | abpLocalization" |
|||
[width]="250" |
|||
> |
|||
<ng-template |
|||
ngx-datatable-cell-template |
|||
let-row="row" |
|||
let-i="index" |
|||
> |
|||
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
</ngx-datatable> |
|||
``` |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
- Install `@swimlane/ngx-datatable` package. |
|||
- Add ngx-datatable styles in the angular.json file. |
|||
- If you can, update your modules according to the example above. |
|||
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the [breaking change described here](./List-Service.md). |
|||
|
|||
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable. |
|||
|
|||
|
|||
### Extensions System [COMMERCIAL] |
|||
|
|||
The extensions system is open sourced now and is publicly available from `@abp/ng.theme.shared/extensions` package instead of `@volo/abp.commercial.ng.ui`. Also, according to the new structure of config packages, the configuration is given through `forLazy` static methods as described above. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
If you have never used the extensions system before, you do not have to do anything. If you have, then please check the documentation again to see what changed. Extension system itself works the same as before. The only changes are the package you import from and the static method and the module you pass your contributors to. |
|||
|
|||
|
|||
### Lepton Theme Logos [COMMERCIAL] |
|||
|
|||
In ABP v2.x, Lepton had one light and one dark logo per color theme. We have realized we could make it work with only one light and one dark logo. So, we have changed how Lepton looks up logo images and now you just need to have a `logo-light.png` and a `logo-dark.png` in your project. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
If you have switched template logo PNGs before, the change is simple: |
|||
|
|||
- Go to `/assets/images/logo` folder. |
|||
- Rename `theme1.png` as `logo-light.png` and `theme1-reverse.png` as `logo-dark.png`. |
|||
- Delete all other `theme*.png` files. |
|||
|
|||
If you have replaced the logo component(s), the change is a little bit different, but still simple. The `LayoutStateService` has a two new members: `primaryLogoColor` and `secondaryLogoColor`. They have an observable stream of `'light'` and `'dark'` strings as value. You can consume their value in your custom logo component templates with the `async` pipe. Here is a complete example which covers both primary and secondary (account) layout logos. |
|||
|
|||
```js |
|||
import { AddReplaceableComponent } from '@abp/ng.core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { APP_INITIALIZER, Component, Injector, NgModule } from '@angular/core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { eAccountComponents } from '@volo/abp.ng.account'; |
|||
import { |
|||
AccountLayoutComponent, |
|||
eThemeLeptonComponents, |
|||
LayoutStateService, |
|||
} from '@volo/abp.ng.theme.lepton'; |
|||
import { Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<div class="account-brand p-4 text-center mb-1" *ngIf="isAccount; else link"> |
|||
<ng-template [ngTemplateOutlet]="link"></ng-template> |
|||
</div> |
|||
|
|||
<ng-template #link> |
|||
<a [style.background-image]="logoUrl | async" class="navbar-brand" routerLink="/"></a> |
|||
</ng-template> |
|||
`, |
|||
}) |
|||
export class LogoComponent { |
|||
isAccount: boolean; |
|||
|
|||
logoColor: Observable<'dark' | 'light'>; |
|||
|
|||
get logoUrl() { |
|||
return this.logoColor.pipe(map(color => `url(/assets/images/logo/logo-${color}.png)`)); |
|||
} |
|||
|
|||
constructor(injector: Injector) { |
|||
const layout = injector.get(LayoutStateService); |
|||
this.isAccount = Boolean(injector.get(AccountLayoutComponent, false)); |
|||
this.logoColor = this.isAccount ? layout.secondaryLogoColor : layout.primaryLogoColor; |
|||
} |
|||
} |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule], |
|||
declarations: [LogoComponent], |
|||
exports: [LogoComponent], |
|||
}) |
|||
export class LogoModule {} |
|||
|
|||
export const APP_LOGO_PROVIDER = [ |
|||
{ provide: APP_INITIALIZER, useFactory: switchLogos, multi: true, deps: [Store] }, |
|||
]; |
|||
|
|||
export function switchLogos(store: Store) { |
|||
return () => { |
|||
store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: LogoComponent, |
|||
key: eThemeLeptonComponents.Logo, |
|||
}), |
|||
); |
|||
|
|||
store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: LogoComponent, |
|||
key: eAccountComponents.Logo, |
|||
}), |
|||
); |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
Just add `APP_LOGO_PROVIDER` to the providers of your root module (usually `AppModule`) and you will have a custom logo component adjusting to the theme colors. |
|||
|
|||
|
|||
### Deprecated Interfaces |
|||
|
|||
Some interfaces have long been marked as deprecated and now they are removed. |
|||
|
|||
#### What to Do When Migrating? |
|||
|
|||
- Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281) |
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
* [Service Proxies](./Service-Proxies.md) |
|||
|
|||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 152 KiB |
@ -1,3 +1,356 @@ |
|||
## Unit of Work |
|||
# Unit of Work |
|||
|
|||
ABP Framework's Unit Of Work (UOW) implementation provides an abstraction and control on a **database connection and transaction** scope in an application. |
|||
|
|||
Once a new UOW started, it creates an **ambient scope** that is participated by **all the database operations** performed in the current scope and considered as a **single transaction boundary**. The operations are **committed** (on success) or **rolled back** (on exception) all together. |
|||
|
|||
ABP's UOW system is; |
|||
|
|||
* **Works conventional**, so most of the times you don't deal with UOW at all. |
|||
* **Database provider independent**. |
|||
* **Web independent**, that means you can create unit of work scopes in any type of applications beside web applications/services. |
|||
|
|||
## Conventions |
|||
|
|||
The following method types are considered as a unit of work: |
|||
|
|||
* ASP.NET Core MVC **Controller Actions**. |
|||
* ASP.NET Core Razor **Page Handlers**. |
|||
* **Application service** methods. |
|||
* **Repository methods**. |
|||
|
|||
A UOW automatically begins for these methods **except** if there is already a **surrounding (ambient)** UOW in action. Examples; |
|||
|
|||
* If you call a [repository](Repositories.md) method and there is no UOW started yet, it automatically **begins a new transactional UOW** that involves all the operations done in the repository method and **commits the transaction** if the repository method **doesn't throw any exception.** The repository method doesn't know about UOW or transaction at all. It just works on a regular database objects (`DbContext` for [EF Core](Entity-Framework-Core.md), for example) and the UOW is handled by the ABP Framework. |
|||
* If you call an [application service](Application-Services.md) method, the same UOW system works just as explained above. If the application service method uses some repositories, the repositories **don't begin a new UOW**, but **participates to the current unit of work** started by the ABP Framework for the application service method. |
|||
* The same is true for an ASP.NET Core controller action. If the operation has started with a controller action, then the **UOW scope is the controller action's method body**. |
|||
|
|||
All of these are automatically handled by the ABP Framework. |
|||
|
|||
### Database Transaction Behavior |
|||
|
|||
While the section above explains the UOW as it is database transaction, actually a UOW doesn't have to be transactional. By default; |
|||
|
|||
* **HTTP GET** requests don't start a transactional UOW. They still starts a UOW, but **doesn't create a database transaction**. |
|||
* All other HTTP request types start a UOW with a database transaction, if database level transactions are supported by the underlying database provider. |
|||
|
|||
This is because an HTTP GET request doesn't (and shouldn't) make any change in the database. You can change this behavior using the options explained below. |
|||
|
|||
## Default Options |
|||
|
|||
`AbpUnitOfWorkDefaultOptions` is used to configure the default options for the unit of work system. Configure the options in the `ConfigureServices` method of your [module](Module-Development-Basics.md). |
|||
|
|||
**Example: Completely disable the database transactions** |
|||
|
|||
````csharp |
|||
Configure<AbpUnitOfWorkDefaultOptions>(options => |
|||
{ |
|||
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; |
|||
}); |
|||
```` |
|||
|
|||
### Option Properties |
|||
|
|||
* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). A global point to configure the transaction behavior. Default value is `Auto` and work as explained in the "*Database Transaction Behavior*" section above. You can enable (even for HTTP GET requests) or disable transactions with this option. |
|||
* `TimeOut` (`int?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider. |
|||
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. |
|||
|
|||
## Controlling the Unit Of Work |
|||
|
|||
In some cases, you may want to change the conventional transaction scope, create inner scopes or fine control the transaction behavior. The following sections cover these possibilities. |
|||
|
|||
### IUnitOfWorkEnabled Interface |
|||
|
|||
This is an easy way to enable UOW for a class (or a hierarchy of classes) that is not unit of work by the conventions explained above. |
|||
|
|||
**Example: Implement `IUnitOfWorkEnabled` for an arbitrary service** |
|||
|
|||
````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 |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then `MyService` (and any class derived from it) methods will be UOW. |
|||
|
|||
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<T>`) are intercepted. So, sync methods can not start a UOW. |
|||
|
|||
> Notice that if `FooAsync` is called inside a UOW scope, then it already participates to the UOW without needing to the `IUnitOfWorkEnabled` or any other configuration. |
|||
|
|||
### UnitOfWorkAttribute |
|||
|
|||
`UnitOfWork` attribute provides much more possibility like enabling or disabling UOW and controlling the transaction behavior. |
|||
|
|||
`UnitOfWork` attribute can be used for a **class** or a **method** level. |
|||
|
|||
**Example: Enable UOW for a specific method of a class** |
|||
|
|||
````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 |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**Example: Enable UOW for all the methods of a class** |
|||
|
|||
````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 |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Again, the **same rules** are valid here: |
|||
|
|||
* 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<T>`) are intercepted. So, sync methods can not start a UOW. |
|||
|
|||
#### UnitOfWorkAttribute Properties |
|||
|
|||
* `IsTransactional` (`bool?`): Used to set whether the UOW should be transactional or not. **Default value is `null`**. if you leave it `null`, it is determined automatically based on the conventions and the configuration. |
|||
* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value. |
|||
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value. |
|||
* `IsDisabled` (`bool`): Used to disable the UOW for the current method/class. |
|||
|
|||
> If a method is called in an ambient UOW scope, then the `UnitOfWork` attribute is ignored and the method participates to the surrounding transaction in any way. |
|||
|
|||
**Example: Disable UOW for a controller action** |
|||
|
|||
````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` is the main service that is used to control the unit of work system. The following sections explains how to directly work with this service (while most of the times you won't need). |
|||
|
|||
### Begin a New Unit Of Work |
|||
|
|||
`IUnitOfWorkManager.Begin` method is used to create a new UOW scope. |
|||
|
|||
**Example: Create a new non-transactional UOW scope** |
|||
|
|||
````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` method gets the following optional parameters: |
|||
|
|||
* `requiresNew` (`bool`): Set `true` to ignore the surrounding unit of work and start a new UOW with the provided options. **Default value is `false`. If it is `false` and there is a surrounding UOW, `Begin` method doesn't actually begin a new UOW, but silently participates to the existing UOW.** |
|||
* `isTransactional` (`bool`). Default value is `false`. |
|||
* `isolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value. |
|||
* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value. |
|||
|
|||
### The Current Unit Of Work |
|||
|
|||
UOW is ambient, as explained before. If you need to access to the current unit of work, you can use the `IUnitOfWorkManager.Current` property. |
|||
|
|||
**Example: Get the 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` property returns a `IUnitOfWork` object. |
|||
|
|||
> **Current Unit Of Work can be `null`** if there is no surrounding unit of work. It won't be `null` if your class is a conventional UOW class, you manually made it UOW or it was called inside a UOW scope, as explained before. |
|||
|
|||
#### SaveChangesAsync |
|||
|
|||
`IUnitOfWork.SaveChangesAsync()` method can be needed to save all the changes until now to the database. If you are using EF Core, it behaves exactly same. If the current UOW is transactional, even saved changes can be rolled back on an error (for the supporting database providers). |
|||
|
|||
**Example: Save changes after inserting an entity to get its auto-increment 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<Category, int> _categoryRepository; |
|||
|
|||
public CategoryAppService(IRepository<Category, int> categoryRepository) |
|||
{ |
|||
_categoryRepository = categoryRepository; |
|||
} |
|||
|
|||
public async Task<int> 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; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example uses auto-increment `int` primary key for the `Category` [entity](Entities.md). Auto-increment PKs require to save the entity to the database to get the id of the new entity. |
|||
|
|||
This example is an [application service](Application-Services.md) derived from the base `ApplicationService` class, which already has the `IUnitOfWorkManager` service injected as the `UnitOfWorkManager` property. So, no need to inject it manually. |
|||
|
|||
Since getting the current UOW is pretty common, there is also a `CurrentUnitOfWork` property as a shortcut to the `UnitOfWorkManager.Current`. So, the example above can be changed to use it: |
|||
|
|||
````csharp |
|||
await CurrentUnitOfWork.SaveChangesAsync(); |
|||
```` |
|||
|
|||
##### Alternative to the SaveChanges() |
|||
|
|||
Since saving changes after inserting, updating or deleting an entity can be frequently needed, corresponding [repository](Repositories.md) methods has an optional `autoSave` parameter. So, the `CreateAsync` method above could be re-written as shown below: |
|||
|
|||
````csharp |
|||
public async Task<int> CreateAsync(string name) |
|||
{ |
|||
var category = new Category {Name = name}; |
|||
await _categoryRepository.InsertAsync(category, autoSave: true); |
|||
return category.Id; |
|||
} |
|||
```` |
|||
|
|||
If your intent is just to save the changes after creating/updating/deleting an entity, it is suggested to use the `autoSave` option instead of manually using the `CurrentUnitOfWork.SaveChangesAsync()`. |
|||
|
|||
> **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` and don't set `autoSave` to `true` unless you really need it. |
|||
> |
|||
> **Note-2**: If you use `Guid` as the primary key, you never need to save changes on insert to just get the generated id, because `Guid` keys are set in the application and are immediately available once you create a new entity. |
|||
|
|||
#### Other IUnitOfWork Properties/Methods |
|||
|
|||
* `OnCompleted` method gets a callback action which is called when the unit of work successfully completed (where you can be sure that all changes are saved). |
|||
* `Failed` and `Disposed` events can be used to be notified if the UOW fails or when it is disposed. |
|||
* `Complete` and `Rollback` methods are used to complete (commit) or roll backs the current UOW, which are normally used internally by the ABP Framework but can be used if you manually start a transaction using the `IUnitOfWorkManager.Begin` method. |
|||
* `Options` can be used to get options that was used while starting the UOW. |
|||
* `Items` dictionary can be used to store and get arbitrary objects inside the same unit of work, which can be a point to implement custom logics. |
|||
|
|||
## ASP.NET Core Integration |
|||
|
|||
Unit of work system is fully integrated to the ASP.NET Core. It properly works when you use ASP.NET Core MVC Controllers or Razor Pages. It defines action filters and page filters for the UOW system. |
|||
|
|||
> You typically do nothing to configure the UOW when you use ASP.NET Core. |
|||
|
|||
### Unit Of Work Middleware |
|||
|
|||
`AbpUnitOfWorkMiddleware` is a middleware that can enable UOW in the ASP.NET Core request pipeline. This might be needed if you need to enlarge the UOW scope to cover some other middleware(s). |
|||
|
|||
**Example:** |
|||
|
|||
````csharp |
|||
app.UseUnitOfWork(); |
|||
app.UseConfiguredEndpoints(); |
|||
```` |
|||
|
|||
TODO |
|||
@ -0,0 +1,134 @@ |
|||
# 分布式事件总线RabbitMQ集成 |
|||
|
|||
> 本文解释了**如何配置[RabbitMQ](https://www.rabbitmq.com/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统. |
|||
|
|||
## 安装 |
|||
|
|||
使用ABP CLI添加[Volo.Abp.EventBus.RabbitMQ[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" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### 选项类 |
|||
|
|||
`AbpRabbitMqOptions` 和 `AbpRabbitMqEventBusOptions` 类用于配置RabbitMQ的连接字符串和事件总线选项. |
|||
|
|||
你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项. |
|||
|
|||
**示例: 配置连接** |
|||
|
|||
````csharp |
|||
Configure<AbpRabbitMqOptions>(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<AbpRabbitMqEventBusOptions>(options => |
|||
{ |
|||
options.ClientName = "TestApp1"; |
|||
options.ExchangeName = "TestMessages"; |
|||
}); |
|||
```` |
|||
|
|||
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值. |
|||
@ -0,0 +1,300 @@ |
|||
# 分布式事件总线 |
|||
|
|||
分布式事件总线系统允许**发布**和**订阅跨应用/服务边界**传输的事件. 你可以使用分布式事件总线在**微服务**或**应用程序**之间异步发送和接收消息. |
|||
|
|||
## 提供程序 |
|||
|
|||
分布式事件总线系统提供了一个可以被任何提供程序实现的**抽象**. 有两种开箱即用的提供程序: |
|||
|
|||
* `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**. |
|||
* `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-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 StockCountChangedEvent |
|||
{ |
|||
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<Guid> |
|||
{ |
|||
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<TEvent>` 来处理事件. |
|||
|
|||
**示例: 处理上面定义的`StockCountChangedEto`** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyHandler |
|||
: IDistributedEventHandler<StockCountChangedEto>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(StockCountChangedEto eventData) |
|||
{ |
|||
var productId = eventData.ProductId; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这就是全部. |
|||
|
|||
* `MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEto` 事件时调用 `HandleEventAsync`. |
|||
* 如果你使用的是分布式消息代理,比如RabbitMQ,ABP会自动**订阅消息代理上的事件**,获取消息执行处理程序. |
|||
* 如果事件处理程序成功执行(没有抛出任何异常),它将向消息代理发送**确认(ACK)**. |
|||
|
|||
你可以在处理程序注入任何服务来执行所需的逻辑. 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `IDistributedEventHandler<TEvent>` 接口. |
|||
|
|||
> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项. |
|||
|
|||
## 预定义的事件 |
|||
|
|||
如果你配置,ABP框架会为[实体](Entities.md)**自动发布创建,更新和删除**分布式事件. |
|||
|
|||
### 事件类型 |
|||
|
|||
有三种预定义的事件类型: |
|||
|
|||
* `EntityCreatedEto<T>` 是实体 `T` 创建后发布. |
|||
* `EntityUpdatedEto<T>` 是实体 `T` 更新后发布. |
|||
* `EntityDeletedEto<T>` 是实体 `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<EntityUpdatedEto<ProductEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData) |
|||
{ |
|||
var productId = eventData.Entity.Id; |
|||
//TODO |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `MyHandler` 实现了 `IDistributedEventHandler<EntityUpdatedEto<ProductEto>>`. |
|||
|
|||
### 配置 |
|||
|
|||
你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 中配置 `AbpDistributedEntityEventOptions`添加选择器. |
|||
|
|||
**示例: 配置示例** |
|||
|
|||
````csharp |
|||
Configure<AbpDistributedEntityEventOptions>(options => |
|||
{ |
|||
//Enable for all entities |
|||
options.AutoEventSelectors.AddAll(); |
|||
|
|||
//Enable for a single entity |
|||
options.AutoEventSelectors.Add<IdentityUser>(); |
|||
|
|||
//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<EntityUpdatedEto<EntityEto>>` 订阅事件. 但是订阅这样的通用事件不是一个好方法,你可以为实体类型定义对应的ETO. |
|||
|
|||
**示例: 为 `Product` 声明使用 `ProductDto`** |
|||
|
|||
````csharp |
|||
Configure<AbpDistributedEntityEventOptions>(options => |
|||
{ |
|||
options.AutoEventSelectors.Add<Product>(); |
|||
options.EtoMappings.Add<Product, ProductEto>(); |
|||
}); |
|||
```` |
|||
|
|||
在这个示例中; |
|||
|
|||
* 添加选择器允许发布 `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文档了解更多选项. |
|||
@ -0,0 +1,60 @@ |
|||
# 切换到 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<YourProjectNameMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|||
``` |
|||
|
|||
替换为: |
|||
|
|||
```csharp |
|||
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
|||
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().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` 项目创建数据库和初始种子数据. |
|||
|
|||
## 运行应用程序 |
|||
|
|||
它已准备就绪, 只需要运行该应用程序与享受编码. |
|||
@ -0,0 +1,67 @@ |
|||
# 切换到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<YourProjectNameMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|||
``` |
|||
|
|||
与这个 |
|||
|
|||
```csharp |
|||
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
|||
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().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` 项目创建数据库和初始种子数据. |
|||
|
|||
## 运行应用程序 |
|||
|
|||
它已准备就绪, 只需要运行该应用程序与享受编码. |
|||
@ -1,67 +1,10 @@ |
|||
# 切换到EF Core Oracle提供程序 |
|||
# 切换到EF Core Oracle 提供程序 |
|||
|
|||
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 |
|||
|
|||
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 |
|||
ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个: |
|||
|
|||
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 |
|||
* **[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/)公司提供的商业(付费)驱动. |
|||
|
|||
解决方案中的 `.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` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. |
|||
|
|||
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: |
|||
|
|||
```csharp |
|||
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("Default")); |
|||
``` |
|||
|
|||
与这个 |
|||
|
|||
```csharp |
|||
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>) |
|||
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().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` 项目创建数据库和初始种子数据. |
|||
|
|||
## 运行应用程序 |
|||
|
|||
它已准备就绪, 只需要运行该应用程序与享受编码. |
|||
> 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持. |
|||
@ -1,3 +1,10 @@ |
|||
# Event Bus |
|||
# 事件总线 |
|||
|
|||
TODO |
|||
事件总线是将消息从发送方传输到接收方的中介. 它在对象,服务和应用程序之间提供了一种松散耦合的通信方式. |
|||
|
|||
## 事件总线类型 |
|||
|
|||
ABP框架提供了两种事件总线类型; |
|||
|
|||
* **[本地事件总线](Local-Event-Bus.md)** 适合进程内消息传递. |
|||
* **[分布式事件总线](Distributed-Event-Bus.md)** 适合进程间消息传递,如微服务发布和订阅分布式事件. |
|||
@ -0,0 +1,227 @@ |
|||
# 本地事件总线 |
|||
|
|||
本地事件总线允许服务发布和订阅**进程内事件**. 这意味着如果两个服务(发布者和订阅者)在同一个进程中运行,那么它是合适的. |
|||
|
|||
## 发布事件 |
|||
|
|||
以下介绍了两种发布本地事件的方法. |
|||
|
|||
### 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<Guid> |
|||
{ |
|||
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<TEvent>` 来处理事件. |
|||
|
|||
**示例: 处理上面定义的`StockCountChangedEvent`** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyHandler |
|||
: ILocalEventHandler<StockCountChangedEvent>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(StockCountChangedEvent eventData) |
|||
{ |
|||
//TODO: your code that does somthing on the event |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这就是全部,`MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEvent` 事件时调用 `HandleEventAsync`. |
|||
|
|||
* 事件可以由**0个或多个处理程序**订阅. |
|||
* 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `ILocalEventHandler<TEvent>` 接口. |
|||
|
|||
> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项. |
|||
|
|||
## 事务和异常行为 |
|||
|
|||
当一个事件发布,订阅的事件处理程序将立即执行.所以; |
|||
|
|||
* 如果处理程序**抛出一个异常**,它会影响发布该事件的代码. 这意味着它在 `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<EntityCreatedEventData<IdentityUser>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync( |
|||
EntityCreatedEventData<IdentityUser> eventData) |
|||
{ |
|||
var userName = eventData.Entity.UserName; |
|||
var email = eventData.Entity.Email; |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这个类订阅 `EntityCreatedEventData<IdentityUser>`,它在用户创建后发布. 你可能需要向新用户发送一封"欢迎"电子邮件. |
|||
|
|||
这些事件有两种类型:过去时态的事件和进行时态的事件. |
|||
|
|||
### 用过去时态事件 |
|||
|
|||
当相关工作单元完成且实体更改成功保存到数据库时,将发布带有过去时态的事件. 如果在这些事件处理程序上抛出异常,则**无法回滚**事务,因为事务已经提交. |
|||
|
|||
事件类型; |
|||
|
|||
* `EntityCreatedEventData<T>` 当实体创建创建成功后发布. |
|||
* `EntityUpdatedEventData<T>` 当实体创建更新成功后发布. |
|||
* `EntityDeletedEventData<T>` 当实体创建删除成功后发布. |
|||
* `EntityChangedEventData<T>` 当实体创建,更新,删除后发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件. |
|||
|
|||
### 用于进行时态事件 |
|||
|
|||
带有进行时态的事件在完成事务之前发布(如果数据库事务由所使用的数据库提供程序支持). 如果在这些事件处理程序上抛出异常,它**会回滚**事务,因为事务还没有完成,更改也没有保存到数据库中. |
|||
|
|||
事件类型; |
|||
|
|||
* `EntityCreatingEventData<T>` 当新实体保存到数据库前发布. |
|||
* `EntityUpdatingEventData<T>` 当已存在实体更新到数据库前发布. |
|||
* `EntityDeletingEventData<T>` 删除实体前发布. |
|||
* `EntityChangingEventData<T>` 当实体创建,更新,删除前发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件. |
|||
|
|||
#### 它是如何实现的? |
|||
|
|||
在将更改保存到数据库时发布预构建事件; |
|||
|
|||
* 对于 EF Core, 他们在 `DbContext.SaveChanges` 发布. |
|||
* 对于 MongoDB, 在你调用仓储的 `InsertAsync`, `UpdateAsync` 或 `DeleteAsync` 方法发布(因为MongoDB没有更改追踪系统). |
|||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,363 @@ |
|||
# Angular UI v2.9 迁移到 v3.0 指南 |
|||
|
|||
## 在v3.0改变了什么? |
|||
|
|||
### Angular 10 |
|||
|
|||
新的ABP Angular UI基于Angular 10和TypeScript 3.9,我们已经放弃了对Angular 8的支持. 不过ABP模块将继续与Angular 9兼容使用. 因此如果你的项目是Angular 9,则无需更新为 Angular10. 更新通常很容易. |
|||
|
|||
#### 如何迁移? |
|||
|
|||
在你的根文件夹中打开一个终端,然后运行以下命令: |
|||
|
|||
```sh |
|||
yarn ng update @angular/cli @angular/core --force |
|||
``` |
|||
|
|||
这会做如下修改: |
|||
|
|||
- 更新你的package.json并安装新的软件包 |
|||
- 修改tsconfig.json文件创建一个"Solution Style"配置 |
|||
- 重命名 `browserlist` 为 `.browserlistrc` |
|||
|
|||
另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表. |
|||
|
|||
 |
|||
|
|||
当Angular报告上面的包后,运行命令: |
|||
|
|||
```sh |
|||
yarn ng update @angular/cli @angular/core ng-zorro-antd --force |
|||
``` |
|||
|
|||
> 如果Angular提示你的仓库有中未提交的更改,可以提交/存储它,也可以在命令中添加 `--allow-dirty` 参数. |
|||
|
|||
### 配置模块 |
|||
|
|||
在ABP v2.x中,每个延迟加载的模块都有一个可通过单独的程序包使用的配置模块,模块配置如下: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account.config'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
AccountConfigModule.forRoot({ redirectUrl: '/' }), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
...在app-routing.module.ts... |
|||
|
|||
```js |
|||
const routes: Routes = [ |
|||
// other route configuration |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import( |
|||
'./lazy-libs/account-wrapper.module' |
|||
).then(m => m.AccountWrapperModule), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
虽然有效,但有一些缺点: |
|||
|
|||
- 每个模块都有两个独立的程序包,但实际上这些程序包是相互依赖的. |
|||
- 配置延迟加载的模块需要包装模块. |
|||
- ABP Commercial具有可扩展性系统,在根模块上配置可扩展模块会增加 bundle 的大小. |
|||
|
|||
在ABP v3.0中,我们为每个配置模块引入了辅助入口点,并且提供了一种在没有包装的情况下配置延迟加载的模块的新方法. 现在模块配置如下所示: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account/config'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
AccountConfigModule.forRoot(), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
... 在app-routing.module.ts... |
|||
|
|||
```js |
|||
const routes: Routes = [ |
|||
// other route configuration |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import('@abp/ng.account') |
|||
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
这项更改帮助我们减少了捆绑包的大小并大大缩短了构建时间. 我们相信你会注意到你的应用程序有所不同. |
|||
|
|||
#### 一个更好的例子 |
|||
|
|||
AppModule: |
|||
|
|||
```js |
|||
import { AccountConfigModule } from '@abp/ng.account/config'; |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { IdentityConfigModule } from '@abp/ng.identity/config'; |
|||
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config'; |
|||
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config'; |
|||
import { ThemeBasicModule } from '@abp/ng.theme.basic'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { BrowserModule } from '@angular/platform-browser'; |
|||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
|||
import { NgxsModule } from '@ngxs/store'; |
|||
import { environment } from '../environments/environment'; |
|||
import { AppRoutingModule } from './app-routing.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
BrowserModule, |
|||
BrowserAnimationsModule, |
|||
AppRoutingModule, |
|||
CoreModule.forRoot({ |
|||
environment, |
|||
sendNullsAsQueryParam: false, |
|||
skipGetAppConfiguration: false, |
|||
}), |
|||
ThemeSharedModule.forRoot(), |
|||
AccountConfigModule.forRoot(), |
|||
IdentityConfigModule.forRoot(), |
|||
TenantManagementConfigModule.forRoot(), |
|||
SettingManagementConfigModule.forRoot(), |
|||
ThemeBasicModule.forRoot(), |
|||
NgxsModule.forRoot(), |
|||
], |
|||
// providers, declarations, and bootstrap |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
AppRoutingModule: |
|||
|
|||
```js |
|||
import { DynamicLayoutComponent } from '@abp/ng.core'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: '', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
loadChildren: () => import('./home/home.module') |
|||
.then(m => m.HomeModule), |
|||
}, |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import('@abp/ng.account') |
|||
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), |
|||
}, |
|||
{ |
|||
path: 'identity', |
|||
loadChildren: () => import('@abp/ng.identity') |
|||
.then(m => m.IdentityModule.forLazy()), |
|||
}, |
|||
{ |
|||
path: 'tenant-management', |
|||
loadChildren: () => import('@abp/ng.tenant-management') |
|||
.then(m => m.TenantManagementModule.forLazy()), |
|||
}, |
|||
{ |
|||
path: 'setting-management', |
|||
loadChildren: () => import('@abp/ng.setting-management') |
|||
.then(m => m.SettingManagementModule.forLazy()), |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forRoot(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class AppRoutingModule {} |
|||
``` |
|||
|
|||
> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情. |
|||
|
|||
#### 如何迁移? |
|||
|
|||
- 使用 `yarn remove` 删除你的项目的配置包. |
|||
- 从辅助入口点(例如`@abp/ng.identity/config`)导入配置模块. |
|||
- 调用所有新配置模块的静态 `forRoot`方法,即使配置没有被传递. |
|||
- 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容). |
|||
- 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`). |
|||
- 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递. |
|||
- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX. |
|||
|
|||
### RoutesService |
|||
|
|||
在ABP v2.x中,通过以下两种方式之一将路由添加到菜单: |
|||
|
|||
- [通过 `AppRoutingModule` 的 `routes` 属性](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule) |
|||
- [通过 ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate) |
|||
|
|||
从v3.0开始,我们更改了添加和修改路由的方式. 我们不再将路由存储在 `ConfigState`中(破坏性更改). 而是有一个名为 `RoutesService` 的新服务,该服务用于添加,修补或删除菜单项. 详情请查看[文档](Modifying-the-Menu.md). |
|||
|
|||
#### 如何迁移? |
|||
|
|||
- 检查你是否曾经使用 `ConfigState` 或 `ConfigStateService` 添加路由. 请用 `RoutesService` 的 `add` 方法替换它们. |
|||
- 检查你是否曾经修补的路由. 将其替换为 `RoutesService` 的 `patch` 方法. |
|||
- 仔细检查你是否使用绝对路径,并在 `add` 或 `patch` 方法调用中为子菜单项提供 `parentName` 而不是 `children` 属性. |
|||
|
|||
### NavItemsService |
|||
|
|||
在ABP v2.x中,[通过LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)添加导航元素. |
|||
|
|||
从v3.0开始,我们改变了添加和修改导航项的方式,以前的方法不再可用(破坏性更改). 详情请查看[文档](Modifying-the-Menu.md). |
|||
|
|||
#### 如何迁移? |
|||
|
|||
- 用 `NavItemsService` 的 `addItems` 方法替换所有 `dispatchAddNavigationElement` 调用. |
|||
|
|||
### ngx-datatable |
|||
|
|||
在v3之前,我们一直使用自定义组件 `abp-table` 作为默认表. 但是数据表是复杂的组件,要实现功能齐全的数据表需要大量的精力,我们计划将其引入其他功能. |
|||
|
|||
从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用. |
|||
|
|||
为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容: |
|||
|
|||
```json |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/index.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-index" |
|||
}, |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-icons" |
|||
}, |
|||
{ |
|||
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css", |
|||
"inject": true, |
|||
"bundleName": "ngx-datatable-material" |
|||
} |
|||
``` |
|||
|
|||
由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective` 和 `NgxDatatableDefaultDirective`. |
|||
|
|||
这些指令的用法很简单: |
|||
|
|||
```js |
|||
@Component({ |
|||
providers: [ListService], |
|||
}) |
|||
export class SomeComponent { |
|||
data$ = this.list.hookToQuery( |
|||
query => this.dataService.get(query) |
|||
); |
|||
|
|||
constructor( |
|||
public readonly list: ListService, |
|||
public readonly dataService: SomeDataService, |
|||
) {} |
|||
} |
|||
``` |
|||
|
|||
...在组件模板... |
|||
|
|||
```html |
|||
<ngx-datatable |
|||
[rows]="(data$ | async)?.items || []" |
|||
[count]="(data$ | async)?.totalCount || 0" |
|||
[list]="list" |
|||
default |
|||
> |
|||
<!-- column templates here --> |
|||
</ngx-datatable> |
|||
``` |
|||
|
|||
通过 `NgxDatatableListDirective` 绑定注入的 `ListService` 实例后,你不再需要担心分页或排序. 同样 `NgxDatatableDefaultDirective` 去除了几个属性绑定,以使ngx-datatable适合我们的样式. |
|||
|
|||
#### 一个更好的例子 |
|||
|
|||
```html |
|||
<ngx-datatable |
|||
[rows]="items" |
|||
[count]="count" |
|||
[list]="list" |
|||
default |
|||
> |
|||
<!-- the grid actions column --> |
|||
<ngx-datatable-column |
|||
name="" |
|||
[maxWidth]="150" |
|||
[width]="150" |
|||
[sortable]="false" |
|||
> |
|||
<ng-template |
|||
ngx-datatable-cell-template |
|||
let-row="row" |
|||
let-i="rowIndex" |
|||
> |
|||
<abp-grid-actions |
|||
[index]="i" |
|||
[record]="row" |
|||
text="AbpUi::Actions" |
|||
></abp-grid-actions> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
|
|||
<!-- a basic column --> |
|||
<ngx-datatable-column |
|||
prop="someProp" |
|||
[name]="'::SomeProp' | abpLocalization" |
|||
[width]="200" |
|||
></ngx-datatable-column> |
|||
|
|||
<!-- a column with a custom template --> |
|||
<ngx-datatable-column |
|||
prop="someOtherProp" |
|||
[name]="'::SomeOtherProp' | abpLocalization" |
|||
[width]="250" |
|||
> |
|||
<ng-template |
|||
ngx-datatable-cell-template |
|||
let-row="row" |
|||
let-i="index" |
|||
> |
|||
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
</ngx-datatable> |
|||
``` |
|||
|
|||
#### 如何迁移? |
|||
|
|||
- 安装 `@swimlane/ngx-datatable` 包. |
|||
- 添加ngx-datatable样式到angular.json文件. |
|||
- 如果可以的话,根据上面的例子更新你的模. |
|||
- 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页. |
|||
|
|||
**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。 |
|||
|
|||
### 过时的接口 |
|||
|
|||
某些接口早已被标记为已弃用,现在已将其删除. |
|||
|
|||
#### 如何迁移? |
|||
|
|||
请检查你是否仍在使用[Issue中列出的任何内容](https://github.com/abpframework/abp/issues/4281). |
|||
|
|||
## 下一步是什么? |
|||
|
|||
* [服务代理](Service-Proxies.md) |
|||
|
After Width: | Height: | Size: 152 KiB |
@ -1,3 +1,277 @@ |
|||
## Dynamic Forms |
|||
# 动态表单 |
|||
|
|||
目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). |
|||
`提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档. |
|||
|
|||
## 介绍 |
|||
|
|||
`abp-dynamic-form` 为给定c#模型创建bootstrap表单. |
|||
|
|||
基本用法: |
|||
|
|||
````xml |
|||
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
|||
```` |
|||
|
|||
Model: |
|||
|
|||
````csharp |
|||
public class DynamicFormsModel : PageModel |
|||
{ |
|||
[BindProperty] |
|||
public DetailedModel MyDetailedModel { get; set; } |
|||
|
|||
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
|||
{ |
|||
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<string>() { "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<string> NeighborCountries { get; set; } |
|||
} |
|||
|
|||
public enum CarType |
|||
{ |
|||
Sedan, |
|||
Hatchback, |
|||
StationWagon, |
|||
Coupe |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Demo |
|||
|
|||
参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. |
|||
|
|||
## Attributes |
|||
|
|||
### abp-model |
|||
|
|||
为动态表单设置c#模型,模型的属性以表单形式转化为输入. |
|||
|
|||
### submit-button |
|||
|
|||
可以为 `True` 或 `False`. |
|||
|
|||
如果为 `True`,则会在表单底部生成一个提交按钮. |
|||
|
|||
默认值是 `False`. |
|||
|
|||
### required-symbols |
|||
|
|||
可以为 `True` 或 `False`. |
|||
|
|||
如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的. |
|||
|
|||
默认值是 `True`. |
|||
|
|||
## 表单内容布局 |
|||
|
|||
默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用`<abp-form-content />`标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变. |
|||
|
|||
用法: |
|||
|
|||
````xml |
|||
<abp-dynamic-form abp-model="@Model.MyExampleModel"> |
|||
<div> |
|||
Some content.... |
|||
</div> |
|||
<div class="input-area"> |
|||
<abp-form-content /> |
|||
</div> |
|||
<div> |
|||
Some more content.... |
|||
</div> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
## 输入排序 |
|||
|
|||
`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 |
|||
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
|||
```` |
|||
|
|||
Model: |
|||
|
|||
````csharp |
|||
public class DynamicFormsModel : PageModel |
|||
{ |
|||
[BindProperty] |
|||
public DetailedModel MyDetailedModel { get; set; } |
|||
|
|||
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
|||
{ |
|||
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; } |
|||
```` |
|||
@ -1,3 +1,355 @@ |
|||
## Unit of Work |
|||
# 工作单元 |
|||
|
|||
待添加 |
|||
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<AbpUnitOfWorkDefaultOptions>(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<T>`的方法)被拦截. 因此同步方法无法启动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<T>`的方法)被拦截. 因此同步方法无法启动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<Category, int> _categoryRepository; |
|||
|
|||
public CategoryAppService(IRepository<Category, int> categoryRepository) |
|||
{ |
|||
_categoryRepository = categoryRepository; |
|||
} |
|||
|
|||
public async Task<int> 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<int> 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(); |
|||
```` |
|||