diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json index f461fe8bbd..d4e51eeadf 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json @@ -1,7 +1,7 @@ { "culture": "en", "texts": { - "Account": "Account", + "Account": "ABP Account - Login & Register | ABP.IO", "Welcome": "Welcome", "UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue", "FrameworkHomePage": "Framework home page", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json index 127f276819..61a3fa4a8c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json @@ -10,10 +10,10 @@ "Volo.AbpIo.Domain:010010": "Največje število mac naslovov je preseženo!", "Volo.AbpIo.Domain:010011": "Osebne licence ne sme imeti več kot 1 razvijalec!", "Volo.AbpIo.Domain:010012": "Licence ni mogoče podaljšati en mesec po poteku licence!", - "Volo.AbpIo.Domain:020001": "Tega paketa NPM ni mogoče izbrisati, ker so \ "{NugetPackages} \" Nuget paketi odvisni od tega paketa.", - "Volo.AbpIo.Domain:020002": "Tega paketa NPM ni mogoče izbrisati, ker \ "{Modules} \" moduli uporabljajo ta paket.", - "Volo.AbpIo.Domain:020003": "Tega paketa NPM ni mogoče izbrisati, ker \ "{Modules} \" moduli uporabljajo ta paket in \ "{NugetPackages} \" Nuget paketi so odvisni od tega paketa.", - "Volo.AbpIo.Domain:020004": "Nuget paketa ni mogoče izbrisati, ker \ "{Modules} \" moduli uporabljajo ta paket.", + "Volo.AbpIo.Domain:020001": "Tega paketa NPM ni mogoče izbrisati, ker so \"{NugetPackages}\" Nuget paketi odvisni od tega paketa.", + "Volo.AbpIo.Domain:020002": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", + "Volo.AbpIo.Domain:020003": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket in \"{NugetPackages}\" Nuget paketi so odvisni od tega paketa.", + "Volo.AbpIo.Domain:020004": "Nuget paketa ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", "WantToLearn?": "Se želite naučiti?", "ReadyToGetStarted?": "Pripravljeni, da bi začeli?", "JoinOurCommunity": "Pridružite se naši skupnosti", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 2502dce76a..accf9a91b3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -1,7 +1,7 @@ { "culture": "en", "texts": { - "GetStarted": "Get Started", + "GetStarted": "Get Started - Startup Templates", "Create": "Create", "NewProject": "New Project", "DirectDownload": "Direct Download", @@ -86,7 +86,7 @@ "BasedOnFamiliarToolsExplanation": "Built on and integrated to popular tools you already know. Low learning curve, easy adaptation, comfortable development.", "ORMIndependent": "ORM Independent", "ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.", - "Features": "Features", + "Features": "Explore the ABP Framework Features", "ABPCLI": "ABP CLI", "Modularity": "Modularity", "BootstrapTagHelpers": "Bootstrap Tag Helpers", diff --git a/docs/en/API/Auto-API-Controllers.md b/docs/en/API/Auto-API-Controllers.md new file mode 100644 index 0000000000..77819c48c9 --- /dev/null +++ b/docs/en/API/Auto-API-Controllers.md @@ -0,0 +1,140 @@ +# Auto API Controllers + +Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. + +ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. + +## Configuration + +Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: + +````csharp +[DependsOn(BookStoreApplicationModule)] +public class BookStoreWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly); + }); + } +} +```` + +This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). + +![bookstore-apis](../images/bookstore-apis.png) + +### Examples + +Some example method names and the corresponding routes calculated by convention: + +| Service Method Name | HTTP Method | Route | +| ----------------------------------------------------- | ----------- | -------------------------- | +| GetAsync(Guid id) | GET | /api/app/book/{id} | +| GetListAsync() | GET | /api/app/book | +| CreateAsync(CreateBookDto input) | POST | /api/app/book | +| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | +| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | +| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | +| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | + +### HTTP Method + +ABP uses a naming convention while determining the HTTP method for a service method (action): + +- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. +- **Put**: Used if the method name starts with 'Put' or 'Update'. +- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. +- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. +- **Patch**: Used if the method name starts with 'Patch'. +- Otherwise, **Post** is used **by default**. + +If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. + +### Route + +Route is calculated based on some conventions: + +* It always starts with '**/api**'. +* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: + +````csharp +Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.RootPath = "volosoft/book-store"; + }); +}); +```` + +Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. + +* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. + * If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. +* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. +* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; + * Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. + * Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. + * Converting the result to **camelCase**. + * If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. + * Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. +* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). + +## Service Selection + +Creating conventional HTTP API controllers are not unique to application services actually. + +### IRemoteService Interface + +If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. + +### RemoteService Attribute + +`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: + +````csharp +[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +### TypePredicate Option + +You can further filter classes to become an API controller by providing the `TypePredicate` option: + +````csharp +services.Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.TypePredicate = type => { return true; }; + }); +}); +```` + +Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. + +## API Explorer + +API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. + +API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: + +````csharp +[RemoteService(IsMetadataEnabled = false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md new file mode 100644 index 0000000000..3c3c2ec26c --- /dev/null +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -0,0 +1,165 @@ +# Dynamic C# API Clients + +ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. + +## Service Interface + +Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: + +````csharp +public interface IBookAppService : IApplicationService +{ + Task> GetListAsync(); +} +```` + +Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. + +Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. + +## Client Proxy Generation + +First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: + +```` +Install-Package Volo.Abp.Http.Client +```` + +Then add `AbpHttpClientModule` dependency to your module: + +````csharp +[DependsOn(typeof(AbpHttpClientModule))] //add the dependency +public class MyClientAppModule : AbpModule +{ +} +```` + +Now, it's ready to create the client proxies. Example: + +````csharp +[DependsOn( + typeof(AbpHttpClientModule), //used to create client proxies + typeof(BookStoreApplicationModule) //contains the application service interfaces + )] +public class MyClientAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Create dynamic client proxies + context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly + ); + } +} +```` + +`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. + +### Endpoint Configuration + +`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: + +```` +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + } + } +} +```` + +See the "RemoteServiceOptions" section below for more detailed configuration. + +## Usage + +It's straightforward to use. Just inject the service interface in the client application code: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IBookAppService _bookService; + + public MyService(IBookAppService bookService) + { + _bookService = bookService; + } + + public async Task DoIt() + { + var books = await _bookService.GetListAsync(); + foreach (var book in books) + { + Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); + } + } +} +```` + +This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. + +### IHttpClientProxy Interface + +While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy` interface. + +## Configuration + +### RemoteServiceOptions + +`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.Configure(options => + { + options.RemoteServices.Default = + new RemoteServiceConfiguration("http://localhost:53929/"); + }); + + //... +} +```` + +### Multiple Remote Service Endpoints + +The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: + +````json +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + }, + "BookStore": { + "BaseUrl": "http://localhost:48392/" + } + } +} +```` + +`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + remoteServiceName: "BookStore" +); +```` + +`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. + +### As Default Services + +When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + asDefaultServices: false +); +```` + +Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. + +> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies (see the related section above). \ No newline at end of file diff --git a/docs/en/API/JavaScript-API/Auth.md b/docs/en/API/JavaScript-API/Auth.md new file mode 100644 index 0000000000..60c9eb8866 --- /dev/null +++ b/docs/en/API/JavaScript-API/Auth.md @@ -0,0 +1,3 @@ +# abp.auth JavaScript API + +TODO \ No newline at end of file diff --git a/docs/en/API/JavaScript-API/Index.md b/docs/en/API/JavaScript-API/Index.md new file mode 100644 index 0000000000..ca9afca97f --- /dev/null +++ b/docs/en/API/JavaScript-API/Index.md @@ -0,0 +1,24 @@ +# JavaScript API + +ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. + +## APIs + +* abp.ajax +* [abp.auth](Auth.md) +* abp.currentUser +* abp.dom +* abp.event +* abp.features +* abp.localization +* abp.log +* abp.ModalManager +* abp.notify +* abp.security +* abp.setting +* abp.ui +* abp.utils +* abp.ResourceLoader +* abp.WidgetManager +* Other APIs + diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index c860f0dd82..3fb52377b3 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -201,7 +201,7 @@ See the [authorization document](Authorization.md) for more. ## CRUD Application Services -If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from `CrudAppService`. +If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from the `CrudAppService`. ### Example @@ -219,7 +219,9 @@ public interface IBookAppService : } ```` -* `ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface). +`ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface). + +> Creating interface for an application service is a good practice, but not required by the ABP Framework. You can skip the interface part. `ICrudAppService` declares the following methods: @@ -292,6 +294,52 @@ public class BookAppService : `CrudAppService` implements all methods declared in the `ICrudAppService` interface. You can then add your own custom methods or override and customize base methods. +> `CrudAppService` has different versions gets different number of generic arguments. Use the one suitable for you. + +### AbstractKeyCrudAppService + +`CrudAppService` requires to have an Id property as the primary key of your entity. If you are using composite keys then you can not utilize it. + +`AbstractKeyCrudAppService` implements the same `ICrudAppService` interface, but this time without making assumption about your primary key. + +#### Example + +Assume that you have a `District` entity with `CityId` and `Name` as a composite primary key. Using `AbstractKeyCrudAppService` requires to implement `DeleteByIdAsync` and `GetEntityByIdAsync` methods yourself: + +````csharp +public class DistrictAppService + : AbstractKeyCrudAppService +{ + public DistrictAppService(IRepository repository) + : base(repository) + { + } + + protected override async Task DeleteByIdAsync(DistrictKey id) + { + await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); + } + + protected override async Task GetEntityByIdAsync(DistrictKey id) + { + return await AsyncQueryableExecuter.FirstOrDefaultAsync( + Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) + ); + } +} +```` + +This implementation requires you to create a class represents your composite key: + +````csharp +public class DistrictKey +{ + public Guid CityId { get; set; } + + public string Name { get; set; } +} +```` + ## Lifetime Lifetime of application services are [transient](Dependency-Injection.md) and they are automatically registered to the dependency injection system. diff --git a/docs/en/AspNetCore/Auto-API-Controllers.md b/docs/en/AspNetCore/Auto-API-Controllers.md index 77819c48c9..52db2bde06 100644 --- a/docs/en/AspNetCore/Auto-API-Controllers.md +++ b/docs/en/AspNetCore/Auto-API-Controllers.md @@ -1,140 +1,3 @@ -# Auto API Controllers +This document has moved. -Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. - -ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. - -## Configuration - -Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). - -![bookstore-apis](../images/bookstore-apis.png) - -### Examples - -Some example method names and the corresponding routes calculated by convention: - -| Service Method Name | HTTP Method | Route | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP uses a naming convention while determining the HTTP method for a service method (action): - -- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. -- **Put**: Used if the method name starts with 'Put' or 'Update'. -- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. -- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. -- **Patch**: Used if the method name starts with 'Patch'. -- Otherwise, **Post** is used **by default**. - -If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. - -### Route - -Route is calculated based on some conventions: - -* It always starts with '**/api**'. -* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. - -* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. - * If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. -* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. -* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; - * Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. - * Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. - * Converting the result to **camelCase**. - * If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. - * Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. -* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). - -## Service Selection - -Creating conventional HTTP API controllers are not unique to application services actually. - -### IRemoteService Interface - -If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. - -### RemoteService Attribute - -`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate Option - -You can further filter classes to become an API controller by providing the `TypePredicate` option: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. - -## API Explorer - -API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. - -API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. +[Click to navigate to Auto API Controllers document](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Bundling-Minification.md b/docs/en/AspNetCore/Bundling-Minification.md index 8868d441e1..a67fbe429d 100644 --- a/docs/en/AspNetCore/Bundling-Minification.md +++ b/docs/en/AspNetCore/Bundling-Minification.md @@ -1,352 +1,4 @@ -# ASP.NET Core MVC Bundling & Minification +This document has moved. -There are many ways of bundling & minification of client side resources (JavaScript and CSS files). Most common ways are: - -* Using the [Bundler & Minifier](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier) Visual Studio extension or the [NuGet package](https://www.nuget.org/packages/BuildBundlerMinifier/). -* Using [Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/) task managers and their plugins. - -ABP offers a simple, dynamic, powerful, modular and built-in way. - -## Volo.Abp.AspNetCore.Mvc.UI.Bundling Package - -> This package is already installed by default with the startup templates. So, most of the time, you don't need to install it manually. - -Install the `Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget package to your project: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -Then you can add the `AbpAspNetCoreMvcUiBundlingModule` dependency to your module: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -## Razor Bundling Tag Helpers - -The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-style-bundle` tag helpers. Example: - -````html - - - - - - -```` - -This bundle defines a style bundle with a **unique name**: `MyGlobalBundle`. It's very easy to understand how to use it. Let's see how it *works*: - -* ABP creates the bundle as **lazy** from the provided files when it's **first requested**. For the subsequent calls, it's returned from the **cache**. That means if you conditionally add the files to the bundle, it's executed only once and any changes of the condition will not effect the bundle for the next requests. -* ABP adds bundle files **individually** to the page for the `development` environment. It automatically bundles & minifies for other environments (`staging`, `production`...). -* The bundle files may be **physical** files or [**virtual/embedded** files](../Virtual-File-System.md). -* ABP automatically adds **version query string** to the bundle file URL to prevent browsers from caching when the bundle is being updated. (like ?_v=67872834243042 - generated from last change date of the related files). The versioning works even if the bundle files are individually added to the page (on the development environment). - -### Importing The Bundling Tag Helpers - -> This is already imported by default with the startup templates. So, most of the time, you don't need to add it manually. - -In order to use bundle tag helpers, you need to add it into your `_ViewImports.cshtml` file or into your page: - -```` -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -### Unnamed Bundles - -The `name` is **optional** for the razor bundle tag helpers. If you don't define a name, it's automatically **calculated** based on the used bundle file names (they are **concatenated** and **hashed**). Example: - -````html - - - - - @if (ViewBag.IncludeCustomStyles != false) - { - - } - -```` - -This will potentially create **two different bundles** (one incudes the `my-global-style.css` and other does not). - -Advantages of **unnamed** bundles: - -* Can **conditionally add items** to the bundle. But this may lead to multiple variations of the bundle based on the conditions. - -Advantages of **named** bundles: - -* Other **modules can contribute** to the bundle by its name (see the sections below). - -### Single File - -If you need to just add a single file to the page, you can use the `abp-script` or `abp-style` tag without a wrapping in the `abp-script-bundle` or `abp-style-bundle` tag. Example: - -````xml - -```` - -The bundle name will be *scripts.my-scripts* for the example above ("/" is replaced by "."). All bundling features are work as expected for single file bundles too. - -## Bundling Options - -If you need to use same bundle in **multiple pages** or want to use some more **powerful features**, you can configure bundles **by code** in your [module](../Module-Development-Basics.md) class. - -### Creating A New Bundle - -Example usage: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] -public class MyWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Add("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/libs/jquery/jquery.js", - "/libs/bootstrap/js/bootstrap.js", - "/libs/toastr/toastr.min.js", - "/scripts/my-global-scripts.js" - ); - }); - }); - } -} -```` - -> You can use the same name (*MyGlobalBundle* here) for a script & style bundle since they are added to different collections (`ScriptBundles` and `StyleBundles`). - -After defining such a bundle, it can be included into a page using the same tag helpers defined above. Example: - -````html - -```` - -This time, no file defined in the tag helper definition because the bundle files are defined by the code. - -### Configuring An Existing Bundle - -ABP supports [modularity](../Module-Development-Basics.md) for bundling as well. A module can modify an existing bundle that is created by a dependant module. Example: - -````C# -[DependsOn(typeof(MyWebModule))] -public class MyWebExtensionModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/scripts/my-extension-script.js" - ); - }); - }); - } -} -```` - -> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. - -## Bundle Contributors - -Adding files to an existing bundle seems useful. What if you need to **replace** a file in the bundle or you want to **conditionally** add files? Defining a bundle contributor provides extra power for such cases. - -An example bundle contributor that replaces bootstrap.css with a customized version: - -````C# -public class MyExtensionGlobalStyleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.ReplaceOne( - "/libs/bootstrap/css/bootstrap.css", - "/styles/extensions/bootstrap-customized.css" - ); - } -} -```` - -Then you can use this contributor as like below: - -````C# -services.Configure(options => -{ - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); - }); -}); -```` - -> You can also add contributors while creating a new bundle. - -Contributors can also be used in the bundle tag helpers. Example: - -````xml - - - - - -```` - -`abp-style` and `abp-script` tags can get `type` attributes (instead of `src` attributes) as shown in this sample. When you add a bundle contributor, its dependencies are also automatically added to the bundle. - -### Contributor Dependencies - -A bundle contributor can have one or more dependencies to other contributors. -Example: - -````C# -[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -When a bundle contributor is added, its dependencies are **automatically and recursively** added. Dependencies added by the **dependency order** by preventing **duplicates**. Duplicates are prevented even if they are in separated bundles. ABP organizes all bundles in a page and eliminates duplications. - -Creating contributors and defining dependencies is a way of organizing bundle creation across different modules. - -### Contributor Extensions - -In some advanced scenarios, you may want to do some additional configuration whenever a bundle contributor is used. Contributor extensions works seamlessly when the extended contributor is used. - -The example below adds some styles for prism.js library: - -````csharp -public class MyPrismjsStyleExtension : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); - } -} -```` - -Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. - -````csharp -Configure(options => -{ - options - .Extensions() - .Add(); -}); -```` - -Whenever `PrismjsStyleBundleContributor` is added into a bundle, `MyPrismjsStyleExtension` will also be automatically added. - -### Accessing to the IServiceProvider - -While it is rarely needed, `BundleConfigurationContext` has a `ServiceProvider` property that you can resolve service dependencies inside the `ConfigureBundle` method. - -### Standard Package Contributors - -Adding a specific NPM package resource (js, css files) into a bundle is pretty straight forward for that package. For example you always add the `bootstrap.css` file for the bootstrap NPM package. - -There are built-in contributors for all [standard NPM packages](Client-Side-Package-Management.md). For example, if your contributor depends on the bootstrap, you can just declare it, instead of adding the bootstrap.css yourself. - -````C# -[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -Using the built-in contributors for standard packages; - -* Prevents you typing **the invalid resource paths**. -* Prevents changing your contributor if the resource **path changes** (the dependant contributor will handle it). -* Prevents multiple modules adding the **duplicate files**. -* Manages **dependencies recursively** (adds dependencies of dependencies, if necessary). - -#### Volo.Abp.AspNetCore.Mvc.UI.Packages Package - -> This package is already installed by default in the startup templates. So, most of the time, you don't need to install it manually. - -Standard package contributors are defined in the `Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet package. -To install it to your project: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Packages -```` - -Then add the `AbpAspNetCoreMvcUiPackagesModule` module dependency to your own module; - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -### Bundle Inheritance - -In some specific cases, it may be needed to create a **new** bundle **inherited** from other bundle(s). Inheriting from a bundle (recursively) inherits all files/contributors of that bundle. Then the derived bundle can add or modify files/contributors **without modifying** the original bundle. -Example: - -````c# -services.Configure(options => -{ - options - .StyleBundles - .Add("MyTheme.MyGlobalBundle", bundle => { - bundle - .AddBaseBundles("MyGlobalBundle") //Can add multiple - .AddFiles( - "/styles/mytheme-global-styles.css" - ); - }); -}); -```` - -## Themes - -Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. - -## Best Practices & Suggestions - -It's suggested to define multiple bundles for an application, each one is used for different purposes. - -* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. -* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. -* **Module bundles**: For shared resources among the pages of an individual module. -* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. - -Establish a balance between performance, network bandwidth usage and count of many bundles. - -## See Also - -* [Client Side Package Management](Client-Side-Package-Management.md) -* [Theming](Theming.md) +[Click to navigate to ASP.NET Core MVC Bundling & Minification document](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Client-Side-Package-Management.md b/docs/en/AspNetCore/Client-Side-Package-Management.md index 63c5cec225..780633de50 100644 --- a/docs/en/AspNetCore/Client-Side-Package-Management.md +++ b/docs/en/AspNetCore/Client-Side-Package-Management.md @@ -1,116 +1,4 @@ -## ASP.NET Core MVC Client Side Package Management +This document has moved. -ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. - -However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. - -Finally, we suggest the [**Yarn**](https://classic.yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. - -### @ABP NPM Packages - -ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. - -One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. - -To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [Github repository](https://github.com/volosoft/abp/tree/master/npm/packs). - -The benefit of a **standard package** is: - -* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. -* It contains the gulp task to copy library resources (js, css, img... files) from the **node_modules** folder to **wwwroot/libs** folder. See the *Mapping The Library Resources* section for more. - -Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: - -```` -{ - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } -} -```` - -It's suggested to depend on a standard package instead of directly depending on a third-party package. - -#### Package Installation - -After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: - -```` -yarn -```` - -Alternatively, you can use `npm install` but [Yarn](https://classic.yarnpkg.com/) is suggested as mentioned before. - -#### Package Contribution - -If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: - -* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). -* It should be the **latest stable** version of the package. -* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. -* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. -* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. - -See current standard packages for examples. - -### Mapping The Library Resources - -Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. - -Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. - -ABP defines a [Gulp](https://gulpjs.com/) based task to **copy resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. - -The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. - -#### Resource Mapping Definition File - -A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: - -````js -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs" - ], - mappings: { - - } -} -```` - -* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. -* **clean** section is a list of folders to clean before copying the files. -* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. - -An example mapping configuration is shown below: - -````js -mappings: { - "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", - "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" -} -```` - -#### Using The Gulp - -Once you properly configure the `abp.resourcemapping.js` file, you can run the gulp command from the command line: - -```` -gulp -```` - -When you run the `gulp`, all packages will copy their own resources into the **wwwroot/libs** folder. Running `yarn & gulp` is only necessary if you make a change in your dependencies in the **package.json** file. - -> When you run the Gulp command, dependencies of the application are resolved using the package.json file. The Gulp task automatically discovers and maps all resources from all dependencies (recursively). - -#### See Also - -* [Bundling & Minification](Bundling-Minification.md) -* [Theming](Theming.md) +[Click to navigate to ASP.NET Core MVC Client Side Package Management document](../UI/AspNetCore/Client-Side-Package-Management.md) diff --git a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md index 3c3c2ec26c..4ca26e8220 100644 --- a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md +++ b/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md @@ -1,165 +1,3 @@ -# Dynamic C# API Clients +This document has moved. -ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. - -## Service Interface - -Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. - -Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. - -## Client Proxy Generation - -First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: - -```` -Install-Package Volo.Abp.Http.Client -```` - -Then add `AbpHttpClientModule` dependency to your module: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //add the dependency -public class MyClientAppModule : AbpModule -{ -} -```` - -Now, it's ready to create the client proxies. Example: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //used to create client proxies - typeof(BookStoreApplicationModule) //contains the application service interfaces - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Create dynamic client proxies - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly - ); - } -} -```` - -`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. - -### Endpoint Configuration - -`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: - -```` -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -```` - -See the "RemoteServiceOptions" section below for more detailed configuration. - -## Usage - -It's straightforward to use. Just inject the service interface in the client application code: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. - -### IHttpClientProxy Interface - -While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy` interface. - -## Configuration - -### RemoteServiceOptions - -`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### Multiple Remote Service Endpoints - -The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" -); -```` - -`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. - -### As Default Services - -When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - asDefaultServices: false -); -```` - -Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. - -> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies (see the related section above). \ No newline at end of file +[Click to navigate to Dynamic C# API Clients document](../API/Dynamic-CSharp-API-Clients.md) diff --git a/docs/en/AspNetCore/JavaScript-API/Auth.md b/docs/en/AspNetCore/JavaScript-API/Auth.md index 60c9eb8866..fb83bcfbff 100644 --- a/docs/en/AspNetCore/JavaScript-API/Auth.md +++ b/docs/en/AspNetCore/JavaScript-API/Auth.md @@ -1,3 +1,3 @@ -# abp.auth JavaScript API +This document has moved. -TODO \ No newline at end of file +[Click to navigate to JavaScript Auth document](../../API/JavaScript-API/Auth.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/JavaScript-API/Index.md b/docs/en/AspNetCore/JavaScript-API/Index.md index ca9afca97f..1ed7f0285c 100644 --- a/docs/en/AspNetCore/JavaScript-API/Index.md +++ b/docs/en/AspNetCore/JavaScript-API/Index.md @@ -1,24 +1,3 @@ -# JavaScript API - -ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. - -## APIs - -* abp.ajax -* [abp.auth](Auth.md) -* abp.currentUser -* abp.dom -* abp.event -* abp.features -* abp.localization -* abp.log -* abp.ModalManager -* abp.notify -* abp.security -* abp.setting -* abp.ui -* abp.utils -* abp.ResourceLoader -* abp.WidgetManager -* Other APIs +This document has moved. +[Click to navigate to JavaScript API document](../../API/JavaScript-API/Index.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/Buttons.md b/docs/en/AspNetCore/Tag-Helpers/Buttons.md deleted file mode 100644 index ba1bfb4032..0000000000 --- a/docs/en/AspNetCore/Tag-Helpers/Buttons.md +++ /dev/null @@ -1,94 +0,0 @@ -# Buttons - -ABP framework has a special Tag Helper to create bootstrap button easily. - -`` - -## Attributes - -`` has 7 different attribute. - -* [`button-type`](#button-type) -* [`size`](#size) -* [`busy-text`](#busy-text) -* [`text`](#text) -* [`icon`](#icon) -* [`disabled`](#disabled) -* [`icon-type`](#icon-type) - -### `button-type` - -`button-type` is a selectable parameter. It's default value is `Default`. - -`Button` - -You can choose one of the button type listed below. - -* `Default` -* `Primary` -* `Secondary` -* `Success` -* `Danger` -* `Warning` -* `Info` -* `Light` -* `Dark` -* `Outline_Primary` -* `Outline_Secondary` -* `Outline_Success` -* `Outline_Danger` -* `Outline_Warning` -* `Outline_Info` -* `Outline_Light` -* `Outline_Dark` -* `Link` - -### `size` - -`size` is a selectable parameter. It's default value is `Default`. - -`Button` - -You can choose one of the size type listed below. - -* `Default` -* `Small` -* `Medium` -* `Large` -* `Block` -* `Block_Small` -* `Block_Medium` -* `Block_Large` - -### `busy-text` - -`busy-text` is a string parameter. It shows the text while the button is busy. - -### `text` - -`text` is a string parameter that displaying on button. - -### `icon` - -`icon` is a string parameter. It is depending to [`icon-type`](#`icon-type`). For default, we use [Font Awesome](https://fontawesome.com/) for icons. To use it, you need to set `icon` parameter as a icon name. - -##### Example - -[fa-address-card](https://fontawesome.com/icons/address-card): ![fa-address-card](fa-address-card.png "Address Card") - -`` - -> Don't forget: You dont need to write prefix! It will add automatically "fa" prefix for [Font Awesome](https://fontawesome.com/) icons while you did not change `icon-type`. - -### `disabled` - -`disabled` is a boolean parameter. If you set it `true`, your button will be disabled. - -### `icon-type` - -`icon-type` is a selectable parameter. It's default value is `FontAwesome`. You can create your own icon type provider and change it. - -You can choose one of the icon type listed below. - -* `FontAwesome` -* `Other` diff --git a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md index 6d700f030d..35382845ce 100644 --- a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,3 @@ -## Dynamic Forms +This document has moved. -This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. \ No newline at end of file +[Click to navigate to Dynamic Forms document](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/Index.md b/docs/en/AspNetCore/Tag-Helpers/Index.md index bdeb7668b7..c15c645feb 100644 --- a/docs/en/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/AspNetCore/Tag-Helpers/Index.md @@ -1,3 +1,3 @@ -## ABP Tag Helpers +This document has moved. -"ABP tag helpers" documentation is creating now. You can see a [demo of components](http://bootstrap-taghelpers.abp.io/) for now. \ No newline at end of file +[Click to navigate to ABP Tag Helpers document](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/en/AspNetCore/Theming.md b/docs/en/AspNetCore/Theming.md index 470ef1a458..716cf5fa9e 100644 --- a/docs/en/AspNetCore/Theming.md +++ b/docs/en/AspNetCore/Theming.md @@ -1,3 +1,4 @@ -# Theming -TODO \ No newline at end of file +This document has moved. + +[Click to navigate to Theming document](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Widgets.md b/docs/en/AspNetCore/Widgets.md index a45056ba68..562678c1d8 100644 --- a/docs/en/AspNetCore/Widgets.md +++ b/docs/en/AspNetCore/Widgets.md @@ -1,505 +1,4 @@ -# Widgets -ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; +This document has moved. -* Have **scripts & styles** dependencies for your widget. -* Create **dashboards** with widgets used inside. -* Define widgets in reusable **[modules](../Module-Development-Basics.md)**. -* Co-operate widgets with **[authorization](../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. - -## Basic Widget Definition - -### Create a View Component - -As the first step, create a new regular ASP.NET Core View Component: - -![widget-basic-files](../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. - -You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. - -**Default.cshtml**: - -```xml -
-

My Simple Widget

-

This is a simple widget!

-
-``` - -### Define the Widget - -Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## Rendering a Widget - -Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -First approach uses the widget name while second approach uses the view component type. - -### Widgets with Arguments - -ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget -{ - [Widget] - public class CountersWidgetViewComponent : AbpViewComponent - { - private readonly IDashboardAppService _dashboardAppService; - - public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) - { - _dashboardAppService = dashboardAppService; - } - - public async Task InvokeAsync( - DateTime startDate, DateTime endDate) - { - var result = await _dashboardAppService.GetCountersWidgetAsync( - new CountersWidgetInputDto - { - StartDate = startDate, - EndDate = endDate - } - ); - - return View(result); - } - } -} -```` - -Now, you need to pass an anonymous object to pass arguments as shown below: - -````xml -@await Component.InvokeAsync("CountersWidget", new -{ - startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), - endDate = DateTime.Now -}) -```` - -## Widget Name - -Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. - -To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP will respect to the custom name by handling the widget. - -> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. - -### Display Name - -You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", //Localization key - DisplayNameResource = typeof(DashboardDemoResource) //localization resource - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -See [the localization document](../Localization.md) to learn about localization resources and keys. - -## Style & Script Dependencies - -There are some challenges when your widget has script and style files; - -* Any page uses the widget should also include the **its script & styles** files into the page. -* The page should also care about **depended libraries/files** of the widget. - -ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. - -### Defining as Simple File Paths - -The example widget below adds a style and a script file: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../Virtual-File-System.md). - -### Defining Bundle Contributors - -All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. - -The sample code below does the same with the code above, but defines and uses bundle contributors: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. - -See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. - -## RefreshUrl - -A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). - -````csharp -[Widget(RefreshUrl = "Widgets/Counters")] -public class CountersWidgetViewComponent : AbpViewComponent -{ - -} -```` - -Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: - -````csharp -[Route("Widgets")] -public class CountersWidgetController : AbpController -{ - [HttpGet] - [Route("Counters")] - public IActionResult Counters(DateTime startDate, DateTime endDate) - { - return ViewComponent("CountersWidget", new {startDate, endDate}); - } -} -```` - -`Widgets/Counters` route matches to the `RefreshUrl` declared before. - -> A widget supposed to be refreshed in two ways: In the first way, when you use a `RefreshUrl`, it re-rendered on the server and replaced by the HTML returned from server. In the second way the widget gets data (generally a JSON object) from server and refreshes itself in the client side (see the refresh method in the Widget JavaScript API section). - -## JavaScript API - -A widget may need to be rendered and refreshed in the client side. In such cases, you can use ABP's `WidgetManager` and define APIs for your widgets. - -### WidgetManager - -`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: - -````js -$(function() { - var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); -}) -```` - -`MyDashboardWidgetsArea` may contain one or more widgets inside. - -> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. - -#### WidgetManager.init() - -`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) - -```js -myWidgetManager.init(); -``` - -#### WidgetManager.refresh() - -`refresh` method refreshes all widgets related to this `WidgetManager`: - -```` -myWidgetManager.refresh(); -```` - -#### WidgetManager Options - -WidgetManager has some additional options. - -##### Filter Form - -If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: - -````xml -
- ...form elements -
- -
- ...widgets -
-```` - -`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. - -Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterForm: '#MyDashboardFilterForm' -}); -```` - -##### Filter Callback - -You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterCallback: function() { - return $('#MyDashboardFilterForm').serializeFormToObject(); - } -}); -```` - -This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: - -````js -filterCallback: function() { - return { - 'startDate': $('#StartDateInput').val(), - 'endDate': $('#EndDateInput').val() - }; -} -```` - -The returning filters are passed to all widgets on `init` and `refresh`. - -### Widget JavaScript API - -A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. - -````js -(function () { - abp.widgets.NewUserStatisticWidget = function ($wrapper) { - - var getFilters = function () { - return { - ... - }; - } - - var refresh = function (filters) { - ... - }; - - var init = function (filters) { - ... - }; - - return { - getFilters: getFilters, - init: init, - refresh: refresh - }; - }; -})(); -```` - -`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. - -#### getFilters - -If the widget has internal custom filters, this function should return the filter object. Example: - -````js -var getFilters = function() { - return { - frequency: $wrapper.find('.frequency-filter option:selected').val() - }; -} -```` - -This method is used by the `WidgetManager` while building filters. - -#### init - -Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. - -#### refresh - -Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. - -## Authorization - -Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: - -* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). -* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../Authorization.md) for more info about policies. - -Example: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## WidgetOptions - -As alternative to the `Widget` attribute, you can use the `AbpWidgetOptions` to configure widgets: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -Write this into the `ConfigureServices` method of your [module](../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `AbpWidgetOptions`. Example configuration that adds a style for the widget: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> Tip: `AbpWidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. - -## See Also - -* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). \ No newline at end of file +[Click to navigate to Widgets document](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 6cf6917ba6..1bceda951f 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -1,6 +1,6 @@ # Authorization -Authorization is used to check if a user is allowed to perform some specific operations in the application. +Authorization is used to check if a user is allowed to perform some specific operations in the application. ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](Application-Services.md)** too. @@ -12,7 +12,7 @@ ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet Example: -````csharp +```csharp using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -43,11 +43,11 @@ namespace Acme.BookStore } } -```` +``` -* `Authorize` attribute forces the user to login into the application in order to use the `AuthorAppService` methods. So, `GetListAsync` method is only available to the authenticated users. -* `AllowAnonymous` suppresses the authentication. So, `GetAsync` method is available to everyone including unauthorized users. -* `[Authorize("BookStore_Author_Create")]` defines a policy (see [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies)) that is checked to authorize the current user. +- `Authorize` attribute forces the user to login into the application in order to use the `AuthorAppService` methods. So, `GetListAsync` method is only available to the authenticated users. +- `AllowAnonymous` suppresses the authentication. So, `GetAsync` method is available to everyone including unauthorized users. +- `[Authorize("BookStore_Author_Create")]` defines a policy (see [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies)) that is checked to authorize the current user. "BookStore_Author_Create" is an arbitrary policy name. If you declare an attribute like that, ASP.NET Core authorization system expects a policy to be defined before. @@ -61,7 +61,7 @@ A permission is a simple policy that is granted or prohibited for a particular u To define permissions, create a class inheriting from the `PermissionDefinitionProvider` as shown below: -````csharp +```csharp using Volo.Abp.Authorization.Permissions; namespace Acme.BookStore.Permissions @@ -76,7 +76,7 @@ namespace Acme.BookStore.Permissions } } } -```` +``` > ABP automatically discovers this class. No additional configuration required! @@ -86,8 +86,8 @@ When you define a permission, it becomes usable in the ASP.NET Core authorizatio ![authorization-new-permission-ui](images/authorization-new-permission-ui.png) -* The "BookStore" group is shown as a new tab on the left side. -* "BookStore_Author_Create" on the right side is the permission name. You can grant or prohibit it for the role. +- The "BookStore" group is shown as a new tab on the left side. +- "BookStore_Author_Create" on the right side is the permission name. You can grant or prohibit it for the role. When you save the dialog, it is saved to the database and used in the authorization system. @@ -97,7 +97,7 @@ When you save the dialog, it is saved to the database and used in the authorizat "BookStore_Author_Create" is not a good permission name for the UI. Fortunately, `AddPermission` and `AddGroup` methods can take `LocalizableString` as second parameters: -````csharp +```csharp var myGroup = context.AddGroup( "BookStore", LocalizableString.Create("BookStore") @@ -107,14 +107,14 @@ myGroup.AddPermission( "BookStore_Author_Create", LocalizableString.Create("Permission:BookStore_Author_Create") ); -```` +``` Then you can define texts for "BookStore" and "Permission:BookStore_Author_Create" keys in the localization file: -````json +```json "BookStore": "Book Store", "Permission:BookStore_Author_Create": "Creating a new author" -```` +``` > For more information, see the [localization document](Localization.md) on the localization system. @@ -126,21 +126,21 @@ The localized UI will be as seen below: ABP supports [multi-tenancy](Multi-Tenancy.md) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below: -* **Host**: The permission is available only for the host side. -* **Tenant**: The permission is available only for the tenant side. -* **Both** (default): The permission is available both for tenant and host sides. +- **Host**: The permission is available only for the host side. +- **Tenant**: The permission is available only for the tenant side. +- **Both** (default): The permission is available both for tenant and host sides. > If your application is not multi-tenant, you can ignore this option. To set the multi-tenancy side option, pass to the third parameter of the `AddPermission` method: -````csharp +```csharp myGroup.AddPermission( "BookStore_Author_Create", LocalizableString.Create("Permission:BookStore_Author_Create"), multiTenancySide: MultiTenancySides.Tenant //set multi-tenancy side! ); -```` +``` #### Child Permissions @@ -148,12 +148,12 @@ A permission may have child permissions. It is especially useful when you want t Example definition: -````csharp +```csharp var authorManagement = myGroup.AddPermission("Author_Management"); authorManagement.AddChild("Author_Management_Create_Books"); authorManagement.AddChild("Author_Management_Edit_Books"); authorManagement.AddChild("Author_Management_Delete_Books"); -```` +``` The result on the UI is shown below (you probably want to localize permissions for your application): @@ -161,7 +161,7 @@ The result on the UI is shown below (you probably want to localize permissions f For the example code, it is assumed that a role/user with "Author_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below: -````csharp +```csharp [Authorize("Author_Management")] public class AuthorAppService : ApplicationService, IAuthorAppService { @@ -193,10 +193,10 @@ public class AuthorAppService : ApplicationService, IAuthorAppService ... } } -```` +``` -* `GetListAsync` and `GetAsync` will be available to users if they have `Author_Management` permission is granted. -* Other methods require additional permissions. +- `GetListAsync` and `GetAsync` will be available to users if they have `Author_Management` permission is granted. +- Other methods require additional permissions. ### Overriding a Permission by a Custom Policy @@ -204,13 +204,17 @@ If you define and register a policy to the ASP.NET Core authorization system wit See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) document to learn how to define a custom policy. +### Changing Permission Definitions of a Depended Module + +A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. + ## IAuthorizationService ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization. Example: -````csharp +```csharp public async Task CreateAsync(CreateAuthorDto input) { var result = await AuthorizationService @@ -223,7 +227,7 @@ public async Task CreateAsync(CreateAuthorDto input) //continue to the normal flow... } -```` +``` > `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](Dependency-Injection.md) it into your class. @@ -231,14 +235,14 @@ Since this is a typical code block, ABP provides extension methods to simplify i Example: -````csharp +```csharp public async Task CreateAsync(CreateAuthorDto input) { await AuthorizationService.CheckAsync("Author_Management_Create_Books"); //continue to the normal flow... } -```` +``` `CheckAsync` extension method throws `AbpAuthorizationException` if the current user/client is not granted for the given permission. There is also `IsGrantedAsync` extension method that returns `true` or `false`. @@ -250,11 +254,11 @@ public async Task CreateAsync(CreateAuthorDto input) You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example: -````js +```js abp.auth.isGranted('MyPermissionName'); -```` +``` -See [abp.auth](AspNetCore/JavaScript-API/Auth.md) API documentation for details. +See [abp.auth](API/JavaScript-API/Auth.md) API documentation for details. ## Permission Management @@ -264,7 +268,7 @@ Permission management is normally done by an admin user using the permission man If you need to manage permissions by code, inject the `IPermissionManager` and use as shown below: -````csharp +```csharp public class MyService : ITransientDependency { private readonly IPermissionManager _permissionManager; @@ -284,7 +288,7 @@ public class MyService : ITransientDependency await _permissionManager.SetForUserAsync(userId, permissionName, false); } } -```` +``` `SetForUserAsync` sets the value (true/false) for a permission of a user. There are more extension methods like `SetForRoleAsync` and `SetForClientAsync`. @@ -296,15 +300,15 @@ public class MyService : ITransientDependency Permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers: -* `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. -* `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. -* `ClientPermissionValueProvider` checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the `AbpClaimTypes.ClientId` static property. +- `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. +- `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. +- `ClientPermissionValueProvider` checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the `AbpClaimTypes.ClientId` static property. You can extend the permission checking system by defining your own permission value provider. Example: -````csharp +```csharp public class SystemAdminPermissionValueProvider : PermissionValueProvider { public SystemAdminPermissionValueProvider(IPermissionStore permissionStore) @@ -314,7 +318,7 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider public override string Name => "SystemAdmin"; - public override async Task + public override async Task CheckAsync(PermissionValueCheckContext context) { if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") @@ -325,24 +329,24 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider return PermissionGrantResult.Undefined; } } -```` +``` This provider allows for all permissions to a user with a `User_Type` claim that has `SystemAdmin` value. It is common to use current claims and `IPermissionStore` in a permission value provider. A permission value provider should return one of the following values from the `CheckAsync` method: -* `PermissionGrantResult.Granted` is returned to grant the user for the permission. If any of the providers return `Granted`, the result will be `Granted`, if no other provider returns `Prohibited`. -* `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. -* `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. +- `PermissionGrantResult.Granted` is returned to grant the user for the permission. If any of the providers return `Granted`, the result will be `Granted`, if no other provider returns `Prohibited`. +- `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. +- `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. Once a provider is defined, it should be added to the `PermissionOptions` as shown below: -````csharp +```csharp Configure(options => { options.ValueProviders.Add(); }); -```` +``` ### Permission Store @@ -354,16 +358,17 @@ Configure(options => Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](Dependency-Injection.md) system: -````csharp +```csharp public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAlwaysAllowAuthorization(); } -```` +``` This is already done for the startup template integration tests. ## See Also * [Permission Management Module](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](AspNetCore/JavaScript-API/Auth.md) +* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](API/JavaScript-API/Auth.md) +* [Permission Management in Angular UI](UI/Angular/Permission-Management.md) diff --git a/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md b/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md index 437ee87387..dc6f5fa3e8 100644 --- a/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md +++ b/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md @@ -120,7 +120,7 @@ Dynamic bundling & minification system works on the virtual file system and allo This code creates a new style bundle on the fly by including bootstrap (and its dependencies if there are) and two more css files. These files are bundled & minified on production environment, but will be added individually on the development environment. -See [the documentation](https://github.com/abpframework/abp/blob/master/docs/AspNetCore/Bundling-Minification.md) for more. +See [the documentation](https://github.com/abpframework/abp/blob/master/docs/UI/AspNetCore/Bundling-Minification.md) for more. #### Distributed Event Bus In current ABP, there is an IEventBus service to trigger and handle events inside the application. In addition to this local event bus, we are creating a distributed event bus abstraction (and RabbitMQ integration) to implement distributed messaging patterns. diff --git a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md index 4077633015..feeb3c2bb6 100644 --- a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -27,7 +27,7 @@ Angular was the first SPA UI option, but it is not the last. After v1.0 release, ### Widget System -[Widget system](https://docs.abp.io/en/abp/latest/AspNetCore/Widgets) allows to **define and reuse** widgets for ASP.NET Core MVC applications. Widgets may have their own script and style resources and dependencies to 3rd-party libraries which are managed by the ABP framework. +[Widget system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets) allows to **define and reuse** widgets for ASP.NET Core MVC applications. Widgets may have their own script and style resources and dependencies to 3rd-party libraries which are managed by the ABP framework. ### Others diff --git a/docs/en/CLI.md b/docs/en/CLI.md index b4f97b6d8f..dcab1b672e 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -48,7 +48,10 @@ abp new Acme.BookStore * `--separate-identity-server`: Separates the identity server application from the API host application. If not specified, you will have a single endpoint in the server side. * `none`: Without UI. There are some additional options for this template: * `--separate-identity-server`: Separates the identity server application from the API host application. If not specified, you will have a single endpoint in the server side. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + * `--mobile` or `-m`: Specifies the mobile application framework. Default framework is `react-native`. Available frameworks: + * `none`: no mobile application. + * `react-native`: React Native. + * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: * `ef`: Entity Framework Core. * `mongodb`: MongoDB. * `module`: [Module template](Startup-Templates/Module.md). Additional options: @@ -141,7 +144,7 @@ abp switch-to-preview [options] ```` #### Options -`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory. +`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. ### switch-to-stable @@ -154,7 +157,7 @@ abp switch-to-stable [options] ```` #### Options -`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory. +`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. ### login diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md new file mode 100644 index 0000000000..def121910e --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -0,0 +1,149 @@ +# Customizing the Application Modules: Extending Entities + +In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. + +## Extra Properties + +[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities. + +Example: + +````csharp +//SET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +user.SetProperty("Title", "My custom title value!"); +await _identityUserRepository.UpdateAsync(user); + +//GET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +return user.GetProperty("Title"); +```` + +This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). + +Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. + +See the [entities document](Entities.md) for more about the extra properties system. + +> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below. + +## Creating a New Entity Maps to the Same Database Table/Collection + +While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). + +Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). + +`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. + +## Creating a New Entity with Its Own Database Table/Collection + +Mapping your entity to an **existing table** of a depended module has a few disadvantages; + +* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. +* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. + +If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. + +In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; + +* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. +* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. + +Once you handle the event, you can update your own entity in your own database. + +### Subscribing to Local Events + +[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. + +Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler>` interface. + +````csharp +public class MyLocalIdentityUserChangeEventHandler : + ILocalEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityChangedEventData eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + + //... + } +} +```` + +* `EntityChangedEventData` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). +* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. + +> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). + +### Subscribing to Distributed Events + +[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. + +Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below: + +````csharp +public class MyDistributedIdentityUserChangeEventHandler : + IDistributedEventHandler>, + IDistributedEventHandler>, + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "created" event + } + } + + public async Task HandleEventAsync(EntityUpdatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "updated" event + } + } + + public async Task HandleEventAsync(EntityDeletedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "deleted" event + } + } +} +```` + +* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus. +* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity). + +Pre-built application modules do not define specialized event types yet (like `IdentityUserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example: + +````csharp +public class MyDistributedIdentityUserCreatedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + //...handle the "created" event + } + + //... +} +```` + +* This handler is executed only when a new user has been created. + +> The only pre-defined specialized event class is the `UserEto`. For example, you can subscribe to the `EntityCreatedEto` to get notified when a user has created. This event also works for the Identity module. + +## See Also + +* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Guide.md b/docs/en/Customizing-Application-Modules-Guide.md index 7191a41fc5..7f913f366a 100644 --- a/docs/en/Customizing-Application-Modules-Guide.md +++ b/docs/en/Customizing-Application-Modules-Guide.md @@ -47,148 +47,16 @@ One alternative scenario could be re-packaging the module source code (as NuGet/ ## Module Customization / Extending Approaches -This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. +This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways: -### Extending Entities +* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) +* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) +* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) -In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. +### See Also -#### Extra Properties +Also, see the following documents: -[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities. - -Example: - -````csharp -//SET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -user.SetProperty("Title", "My custom title value!"); -await _identityUserRepository.UpdateAsync(user); - -//GET AN EXTRA PROPERTY -var user = await _identityUserRepository.GetAsync(userId); -return user.GetProperty("Title"); -```` - -This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). - -Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. - -See the [entities document](Entities.md) for more about the extra properties system. - -> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below. - -#### Creating a New Entity Maps to the Same Database Table/Collection - -While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). - -Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). - -`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. - -#### Creating a New Entity with Its Own Database Table/Collection - -Mapping your entity to an **existing table** of a depended module has a few disadvantages; - -* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. -* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. - -If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. - -In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; - -* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. -* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. - -Once you handle the event, you can update your own entity in your own database. - -##### Subscribing to Local Events - -[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. - -Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler>` interface. - -````csharp -public class MyLocalIdentityUserChangeEventHandler : - ILocalEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityChangedEventData eventData) - { - var userId = eventData.Entity.Id; - var userName = eventData.Entity.UserName; - - //... - } -} -```` - -* `EntityChangedEventData` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). -* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. - -> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). - -##### Subscribing to Distributed Events - -[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. - -Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below: - -````csharp -public class MyDistributedIdentityUserChangeEventHandler : - IDistributedEventHandler>, - IDistributedEventHandler>, - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "created" event - } - } - - public async Task HandleEventAsync(EntityUpdatedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "updated" event - } - } - - public async Task HandleEventAsync(EntityDeletedEto eventData) - { - if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") - { - var userId = Guid.Parse(eventData.Entity.KeysAsString); - //...handle the "deleted" event - } - } -} -```` - -* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus. -* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity). - -Currently, pre-built application modules do not define specialized event types yet (like `IdentityUserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example: - -````csharp -public class MyDistributedIdentityUserChangeEventHandler : - IDistributedEventHandler>, - ITransientDependency -{ - public async Task HandleEventAsync(EntityCreatedEto eventData) - { - var userId = eventData.Entity.Id; - var userName = eventData.Entity.UserName; - //...handle the "created" event - } - - //... -} -```` - -* This handler is executed only when a new user has been created. \ No newline at end of file +* See [the localization document](Localization.md) to learn how to extend existing localization resources. +* See [the settings document](Settings.md) to learn how to change setting definitions of a depended module. +* See [the authorization document](Authorization.md) to learn how to change permission definitions of a depended module. \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md new file mode 100644 index 0000000000..16fbc9d2a4 --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -0,0 +1,166 @@ +# Customizing the Application Modules: Overriding Services + +You may need to **change behavior (business logic)** of a depended module for your application. In this case, you can use the power of the [dependency injection system](Dependency-Injection.md) to replace a service, controller or even a page model of the depended module by your own implementation. + +**Replacing a service** is possible for any type of class registered to the dependency injection, including services of the ABP Framework. + +You have different options can be used based on your requirement those will be explained in the next sections. + +> Notice that some service methods may not be virtual, so you may not be able to override. We make all virtual by design. If you find any method that is not overridable, please [create an issue](https://github.com/abpframework/abp/issues/new) or do it yourself and send a **pull request** on GitHub. + +## Replacing an Interface + +If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityAppService`, you can re-implement the same interface and replace the current implementation by your class. Example: + +````csharp +public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +`MyIdentityUserAppService` replaces the `IIdentityUserAppService` by naming convention (since both ends with `IdentityUserAppService`). If your class name doesn't match, you need to manually expose the service interface: + +````csharp +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. + +Example: + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +In this way, there will be a single implementation of the `IIdentityUserAppService` interface, while it doesn't change the result for this case. Replacing a service is also possible by code: + +````csharp +context.Services.Replace( + ServiceDescriptor.Transient() +); +```` + +You can write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). + +## Overriding a Service Class + +In most cases, you will want to change one or a few methods of the current implementation for a service. Re-implementing the complete interface would not be efficient in this case. As a better approach, inherit from the original class and override the desired method. + +### Example: Overriding an Application Service + +````csharp +[Dependency(ReplaceServices = true)] +public class MyIdentityUserAppService : IdentityUserAppService +{ + //... + public MyIdentityUserAppService( + IdentityUserManager userManager, + IIdentityUserRepository userRepository, + IGuidGenerator guidGenerator + ) : base( + userManager, + userRepository, + guidGenerator) + { + } + + public override async Task CreateAsync(IdentityUserCreateDto input) + { + if (input.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); } + + return await base.CreateAsync(input); + } +} +```` + +This class **overrides** the `CreateAsync` method of the `IdentityUserAppService` [application service](Application-Services.md) to check the phone number. Then calls the base method to continue to the **underlying business logic**. In this way, you can perform additional business logic **before** and **after** the base logic. + +You could completely **re-write** the entire business logic for a user creation without calling the base method. + +### Example: Overriding a Domain Service + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IdentityUserManager))] +public class MyIdentityUserManager : IdentityUserManager +{ + public MyIdentityUserManager( + IdentityUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider + ) : base( + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger, + cancellationTokenProvider) + { + } + + public override async Task CreateAsync(IdentityUser user) + { + if (user.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); + } + + return await base.CreateAsync(user); + } +} +```` + +This example class inherits from the `IdentityUserManager` [domain service](Domain-Services.md) and overrides the `CreateAsync` method to perform the same phone number check implemented above. The result is same, but this time we've implemented it inside the domain service assuming that this is a **core domain logic** for our system. + +> `[ExposeServices(typeof(IdentityUserManager))]` attribute is **required** here since `IdentityUserManager` does not define an interface (like `IIdentityUserManager`) and dependency injection system doesn't expose services for inherited classes (like it does for the implemented interfaces) by convention. + +Check the [localization system](Localization.md) to learn how to localize the error messages. + +### Overriding Other Classes + +Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. + +## How to Find the Services? + +[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md new file mode 100644 index 0000000000..afb701eacd --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md @@ -0,0 +1,9 @@ +# Overriding the User Interface + +You may want to override a page, a component, a JavaScript, CSS or an image file of your depended module. Overriding the UI completely depends on the UI framework you're using. Select the UI framework to continue: + +* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) +* [Angular](UI/Angular/Customization-User-Interface.md) + + + diff --git a/docs/en/IdentityServer-Integration.md b/docs/en/IdentityServer-Integration.md new file mode 100644 index 0000000000..da82d4bc6a --- /dev/null +++ b/docs/en/IdentityServer-Integration.md @@ -0,0 +1,3 @@ +# IdentityServer Integration + +TODO \ No newline at end of file diff --git a/docs/en/Localization.md b/docs/en/Localization.md index 8229ca7ec8..11b838cf7f 100644 --- a/docs/en/Localization.md +++ b/docs/en/Localization.md @@ -190,4 +190,8 @@ Localize a string: ````js var str = testResource('HelloWorld'); -```` \ No newline at end of file +```` + +## See Also + +* [Localization in Angular UI](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/en/Microservice-Architecture.md b/docs/en/Microservice-Architecture.md index ff85873ee7..64a47784bb 100644 --- a/docs/en/Microservice-Architecture.md +++ b/docs/en/Microservice-Architecture.md @@ -12,8 +12,8 @@ One of the major goals of the ABP framework is to provide a convenient infrastru * Offers an [architectural model](Best-Practices/Module-Architecture.md) to develop your modules to be compatible to microservice development and deployment. * Provides [best practices guide](Best-Practices/Index.md) to develop your module standards-compliance. * Provides base infrastructure to implement [Domain Driven Design](Domain-Driven-Design.md) in your microservices. -* Provide services to [automatically create REST-style APIs](AspNetCore/Auto-API-Controllers.md) from your application services. -* Provide services to [automatically create C# API clients](AspNetCore/Dynamic-CSharp-API-Clients.md) that makes easy to consume your services from another service/application. +* Provide services to [automatically create REST-style APIs](API/Auto-API-Controllers.md) from your application services. +* Provide services to [automatically create C# API clients](API/Dynamic-CSharp-API-Clients.md) that makes easy to consume your services from another service/application. * Provides a [distributed event bus](Event-Bus.md) to communicate your services. * Provides many other services to make your daily development easier. diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md new file mode 100644 index 0000000000..6901b38f59 --- /dev/null +++ b/docs/en/Modules/Account.md @@ -0,0 +1,3 @@ +# Account Module + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Blogging.md b/docs/en/Modules/Blogging.md index 0d2ab35c21..32d2e023ea 100644 --- a/docs/en/Modules/Blogging.md +++ b/docs/en/Modules/Blogging.md @@ -1,3 +1,3 @@ -# IdentityServer Module +# Blogging Module TODO \ No newline at end of file diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index afcc1a9dd7..db752ceb74 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -20,23 +20,25 @@ When you use GitHub to store your docs, Docs Module supports versioning. If you > Docs module follows the [module architecture best practices](../Best-Practices/Module-Architecture.md) guide. +## Installation +### 1- Download -## Installation +If you do not have an existing ABP project, this step shows you how to create a new project from [abp.io](https://abp.io) to add the Docs Module. If you already have an ABP project, you can skip this step. -### 1- Download +It is recommended to use ABP CLI to create new projects. Use the following command: -If you do not have an existing ABP project, this step shows you how to create a new project from [abp.io](https://abp.io) to add the Docs Module. If you already have an ABP project, you can skip this step. +`abp new Acme.MyProject` -Navigate to https://abp.io/Templates. Enter your project name as `Acme.MyProject`, select `ASP.NET Core Mvc Application` and select `Entity Framework Core` for the database provider. +You can also navigate to https://abp.io/get-started. Enter your project name as `Acme.MyProject`, other use default options. -Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. +Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. ![Create new project](../images/docs-module_download-new-abp-project.png) ### 2- Running The Empty Application -After you download the project, extract the ZIP file and open `Acme.MyProject.sln`. You will see that the solution consists of `Application`, `Domain `, `EntityFrameworkCore` and `Web` projects. Right click on `Acme.MyProject.Web` project and **Set as StartUp Project**. +After you download the project, extract the ZIP file and open `Acme.MyProject.sln`. You will see that the solution consists of `Application`, `Application.Contracts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `EntityFrameworkCore.DbMigations`, `HttpApi`, `HttpApi.Client` and `Web` projects. Right click on `Acme.MyProject.Web` project and **Set as StartUp Project**. ![Create a new project](../images/docs-module_solution-explorer.png) @@ -45,16 +47,14 @@ The database connection string is located in `appsettings.json` of your `Acme.My ```json { "ConnectionStrings": { - "Default": "Server=localhost;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" } } ``` +Run `Acme.MyProject.DbMigrator` project, it will be responsible for applying database migration and seed data. The database `MyProject` will be created in your database server. - -Open `Package Manager Console` in the Visual Studio and choose `src\Acme.MyProject.EntityFrameworkCore` as the default project. Run `Update-Database` command to create your new database. The database `MyProject` will be created in your database server. - -Now an empty ABP project has been created! You can now run your project and see the empty website. +Now an empty ABP project has been created! You can now run your project and see the empty website. To login your website enter `admin` as the username and `1q2w3E*` as the password. @@ -62,36 +62,27 @@ To login your website enter `admin` as the username and `1q2w3E*` as the passwor Docs module packages are hosted on NuGet. There are 4 packages that needs be to installed to your application. Each package has to be installed to the relevant project. -* [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) needs to be referenced to `Acme.MyProject.Domain` project. +It is recommended to use the ABP CLI to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command: - * Edit `Acme.MyProject.Domain.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +`abp add-module Volo.Docs` - ```csharp - - ``` -* [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) needs to be referenced to `Acme.MyProject.EntityFrameworkCore` project. +Or you can also manually install nuget package to each project: - - Edit `Acme.MyProject.EntityFrameworkCore.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Domain` project. - ```csharp - - ``` -* [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) needs to be referenced to `Acme.MyProject.Application` project. + `Install-Package Volo.Docs.Domain` - * Edit `Acme.MyProject.Application.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget package to `Acme.MyProject.EntityFrameworkCore` project. - ```csharp - - ``` -* [Volo.Docs.Web ](https://www.nuget.org/packages/Volo.Docs.Web/)needs to be referenced to `Acme.MyProject.Web` project. + `Install-Package Volo.Docs.EntityFrameworkCore` - - Edit `Acme.MyProject.Web.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget package to `Acme.MyProject.Application` project. - ```csharp - - ``` + `Install-Package Volo.Docs.Application` +* Install [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Web` project. + `Install-Package Volo.Docs.Web` ### 3- Adding Module Dependencies @@ -132,7 +123,6 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - * Open `MyProjectApplicationModule.cs`and add `typeof(DocsApplicationModule)` as shown below; ```csharp @@ -157,7 +147,6 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - * Open `MyProjectWebModule.cs`and add `typeof(DocsWebModule)` as shown below; ```csharp @@ -176,43 +165,62 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - - ### 4- Database Integration #### 4.1- Entity Framework Integration -If you choose Entity Framework as your database provider, you need to configure the Docs Module in your DbContext. To do this; +If you choose Entity Framework as your database provider, you need to configure the Docs Module. To do this; -- Open `MyProjectDbContext.cs` and add `modelBuilder.ConfigureDocs()` to the `OnModelCreating()` +- Open `MyProjectMigrationsDbContext.cs` and add `builder.ConfigureDocs()` to the `OnModelCreating()`. ```csharp - [ConnectionStringName("Default")] - public class MyProjectDbContext : AbpDbContext - { - public MyProjectDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - //... - modelBuilder.ConfigureDocs(); - } - } + public class MyProjectMigrationsDbContext : AbpDbContext + { + public MyProjectMigrationsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + builder.ConfigureDocs(); //Add this line to configure the Docs Module + + /* Configure customizations for entities from the modules included */ + + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* Configure your own tables/entities inside the ConfigureQaDoc method */ + + builder.ConfigureMyProject(); + } + } ``` -* Open `Package Manager Console` in `Visual Studio` and choose `Acme.MyProject.EntityFrameworkCore` as default project. Then write the below command to add the migration for Docs Module. +* Open `Package Manager Console` in `Visual Studio` and choose `Acme.MyProject.EntityFrameworkCore.DbMigrations` as default project. Then write the below command to add the migration for Docs Module. ```csharp add-migration Added_Docs_Module ``` - When the command successfully executes , you will see a new migration file named as `20181221111621_Added_Docs_Module` in the folder `Acme.MyProject.EntityFrameworkCore\Migrations`. + When the command successfully executes , you will see a new migration file named as `20181221111621_Added_Docs_Module` in the folder `Acme.MyProject.EntityFrameworkCore.DbMigrations\Migrations`. - Now, update the database for Docs module database changes. To do this run the below code on `Package Manager Console` in `Visual Studio`. Be sure `Acme.MyProject.EntityFrameworkCore` is still default project. + Now, update the database for Docs module database changes. To do this run the below code on `Package Manager Console` in `Visual Studio`. Be sure `Acme.MyProject.EntityFrameworkCore.DbMigrations` is still default project. ```csharp update-database @@ -220,7 +228,6 @@ If you choose Entity Framework as your database provider, you need to configure Finally, you can check your database to see the newly created tables. For example you can see `DocsProjects` table must be added to your database. - ### 5- Linking Docs Module The default route for Docs module is; @@ -250,7 +257,7 @@ To add Docs module link to your application menu; } ``` -The `Menu:Docs` keyword is a localization key. To localize the menu text, open `Localization\MyProject\en.json` in the project `Acme.MyProject.Domain`. And add the below line +The `Menu:Docs` keyword is a localization key. To localize the menu text, open `Localization\MyProject\en.json` in the project `Acme.MyProject.Domain`. And add the below line ```json "Menu:Docs": "Documents" @@ -270,7 +277,7 @@ Final look of **en.json** } ``` -The new menu item for Docs Module is added to the menu. Run your web application and browse to `http://localhost:YOUR_PORT_NUMBER/documents` URL. +The new menu item for Docs Module is added to the menu. Run your web application and browse to `http://localhost:YOUR_PORT_NUMBER/documents` URL. You will see a warning says; @@ -316,23 +323,25 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen - ExtraProperties: ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"} + {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""} ``` Note that `GitHubAccessToken` is masked with `***`. It's a private token that you must get it from GitHub. See https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ -- MainWebsiteUrl: `/` +- MainWebsiteUrl: `/` - LatestVersionBranchName: `master` For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table: ```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}', N'/', N'master', N'') +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` Be aware that `GitHubAccessToken` is masked. It's a private token and you must get your own token and replace the `***` string. +Now you can run the application and navigate to `/Documents`. + #### Sample Project Record for "FileSystem" You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documents to configure your GitHub document store. @@ -561,11 +570,34 @@ The upper sample `JSON` file renders the below navigation menu as `HTML`. ![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) +Finally a new Docs Module is added to your project which is feeded with GitHub. -Finally a new Docs Module is added to your project which is feeded with GitHub. +## Full-Text Search(Elastic Search) -## Next +The Docs module supports full-text search using Elastic Search. It is not enabled by default. You can configure `DocsElasticSearchOptions` to enable it. + +``` +Configure(options => +{ + options.Enable = true; + options.IndexName = "your_index_name"; //default IndexName is abp_documents +}); +``` + +The `Index` is automatically created after the application starts if the `Index` does not exist. -Docs Module is also available as a standalone application. Check out [VoloDocs](../Apps/VoloDocs). +`DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`. +If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](Dependency-Injection.md) system. + +``` +{ + "ElasticSearch": { + "Url": "http://localhost:9200" + } +} +``` + +## Next +Docs Module is also available as a standalone application. Check out [VoloDocs](../Apps/VoloDocs). \ No newline at end of file diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 4ee77d065c..1e1e4e9f81 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -302,7 +302,7 @@ TODO:... Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box: -* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be stay as the first contributor for security**. +* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for security**. * **QueryStringTenantResolver**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default. * **RouteTenantResolver**: Tries to find current tenant id from route (URL path). Variable name is "__tenant" by default. So, if you defined a route with this variable, then it can determine the current tenant from the route. * **HeaderTenantResolver**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default. @@ -343,8 +343,10 @@ namespace MyCompany.MyProject { Configure(options => { - //Subdomain format: {0}.mydomain.com (adding as the highest priority resolver) - options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com")); + //Subdomain format: {0}.mydomain.com + //Adding as the second highest priority resolver after 'CurrentUserTenantResolveContributor' to + //ensure the user cannot impersonate a different tenant. + options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com")); }); //... @@ -355,7 +357,7 @@ namespace MyCompany.MyProject {0} is the the placeholder to determine current tenant's unique name. -Instead of ``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut: +Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut: ````C# options.AddDomainTenantResolver("{0}.mydomain.com"); diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 74d61d6b13..567d043ef9 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -276,7 +276,7 @@ Backend admin application uses the Identity and Product microservices for all op ##### HTTP Clients -ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. +ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. For that purpose, `BackendAdminAppHostModule` class declares dependencies for `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule`. @@ -1038,7 +1038,7 @@ Product Management is a module that consists of several layers and packages/proj * `ProductManagement.Application` contains the implementation of application services. * `ProductManagement.EntityFrameworkCore` contains DbConext and other EF Core related classes and configuration. * `ProductManagement.HttpApi` contains API Controllers. -* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. +* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. * `ProductManagement.Web` contains the UI elements (pages, scripts, styles... etc). By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers. diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index d25feb490a..3a5c6c3bb2 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -145,7 +145,7 @@ While creating database & applying migrations seems only necessary for relationa This project is used to define your API Controllers. -Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../AspNetCore/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. +Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../API/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. * Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. @@ -153,7 +153,7 @@ Most of time you don't need to manually define API Controllers since ABP's [Auto This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other type of applications, they can still use your APIs, either manually or using a tool in their own platform) -Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature. +Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature. `.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. @@ -270,4 +270,4 @@ The files under the `angular/src/environments` folder has the essential configur ## What's Next? - See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. -- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template. +- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md) to learn how to develop applications using this template. diff --git a/docs/en/Themes/Basic.md b/docs/en/Themes/Basic.md new file mode 100644 index 0000000000..a0165c803a --- /dev/null +++ b/docs/en/Themes/Basic.md @@ -0,0 +1,3 @@ +## Basic Theme + +TODO \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index a6ceb26c83..20b64f3189 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -493,7 +493,7 @@ namespace Acme.BookStore ### Auto API Controllers -We normally create **Controllers** to expose application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them via AJAX. ABP can [**automagically**](https://docs.abp.io/en/abp/latest/AspNetCore/Auto-API-Controllers) configures your application services as MVC API Controllers by convention. +We normally create **Controllers** to expose application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them via AJAX. ABP can [**automagically**](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) configures your application services as MVC API Controllers by convention. #### Swagger UI @@ -698,8 +698,8 @@ Change the `Pages/Books/Index.cshtml` as following: ```` -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning**. See the [bundling & minification document](https://docs.abp.io/en/abp/latest/AspNetCore/Bundling-Minification) for details. -* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are other useful tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help the of IntelliSense and compile time type checking. Further information, see the [tag helpers](https://docs.abp.io/en/abp/latest/AspNetCore/Tag-Helpers/Index) document. +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning**. See the [bundling & minification document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification) for details. +* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are other useful tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help the of IntelliSense and compile time type checking. Further information, see the [tag helpers](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Index) document. * You can **localize** the column names in the localization file as you did for the menu items above. ##### Add a Script File @@ -727,7 +727,7 @@ $(function () { * `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to [Datatable](https://datatables.net/)'s format. * `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the [Datatables](https://datatables.net/) configuration by providing conventional values for missing options. -* `acme.bookStore.book.getList` is the function to get list of books (as described in [dynamic JavaScript proxies](#Dynamic JavaScript proxies)). +* `acme.bookStore.book.getList` is the function to get list of books (as described in [dynamic JavaScript proxies](#dynamic-javascript-proxies)). * See [Datatables documentation](https://datatables.net/manual/) for all configuration options. It's end of this part. The final UI of this work is shown as below: diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 8408cf6bfb..08e17e0e3f 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -641,7 +641,6 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
= { + name: "Home", + path: "home", + children: [ + { + name: "Dashboard", + path: "dashboard" + } + ] +}; + +this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig); +// returns a state stream which emits after dispatch action is complete +``` + +### How to Add a New Route Configuration + +The `dispatchAddRoute` adds a new route to the configuration state in the `Store`. For this, the route config should be passed as the parameter of the method. + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +The `newRoute` will be placed as at root level, i.e. without any parent routes and its url will be stored as `'/path'`. + +If you want **to add a child route, you can do this:** + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + parentName: "AbpAccount::Login", + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +The `newRoute` will then be placed as a child of the parent route named `'AbpAccount::Login'` and its url will be set as `'/account/login/page'`. + +#### Route Configuration Properties + +Please refer to `ABP.Route` type for all the properties you can pass to `dispatchSetEnvironment` in its parameter. It can be found in the [common.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27). + +### How to Set the Environment + +The `dispatchSetEnvironment` places environment variables passed to it in the `Store` under the configuration state. Here is how it is used: + +```js +// this.config is instance of ConfigStateService + +this.config.dispatchSetEnvironment({ + /* environment properties here */ +}); +// returns a state stream which emits after dispatch action is complete +``` + +Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. + +#### Environment Properties + +Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). diff --git a/docs/en/UI/Angular/Custom-Setting-Page.md b/docs/en/UI/Angular/Custom-Setting-Page.md new file mode 100644 index 0000000000..7c51cf2cca --- /dev/null +++ b/docs/en/UI/Angular/Custom-Setting-Page.md @@ -0,0 +1,42 @@ +# Custom Setting Page + +There are several settings tabs from different modules. You can add custom settings page to your project in 3 steps. + +1. Create a Component + +```js +import { Select } from '@ngxs/store'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-your-custom-settings', + template: ` + custom-settings works! + `, +}) +export class YourCustomSettingsComponent { + // Your component logic +} +``` + +2. Add the `YourCustomSettingsComponent` to `declarations` and the `entryComponents` arrays in the `AppModule`. + +3. Open the `app.component.ts` and add the below content to the `ngOnInit` + +```js +import { addSettingTab } from '@abp/ng.theme.shared'; +// ... + +ngOnInit() { + addSettingTab({ + component: YourCustomSettingsComponent, + name: 'Type here the setting tab title (you can type a localization key, e.g: AbpAccount::Login', + order: 4, + requiredPolicy: 'type here a policy key' + }); +} +``` + +Navigate to `/setting-management` route to see the changes: + +![Custom Settings Tab](./images/custom-settings.png) diff --git a/docs/en/UI/Angular/Customization-User-Interface.md b/docs/en/UI/Angular/Customization-User-Interface.md new file mode 100644 index 0000000000..20809a1d87 --- /dev/null +++ b/docs/en/UI/Angular/Customization-User-Interface.md @@ -0,0 +1,3 @@ +# Angular User Interface Customization Guide + +* [Replacing a component](Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md new file mode 100644 index 0000000000..01394a3fea --- /dev/null +++ b/docs/en/UI/Angular/Localization.md @@ -0,0 +1,136 @@ +# Localization + +Before you read about _the Localization Pipe_ and _the Localization Service_, you should know about localization keys. + +The Localization key format consists of 2 sections which are **Resource Name** and **Key**. +`ResourceName::Key` + +> If you do not specify the resource name, it will be `defaultResourceName` which is declared in `environment.ts` + +```js +const environment = { + //... + localization: { + defaultResourceName: 'MyProjectName', + }, +}; +``` + +So these two are the same: + +```html +

{%{{{ '::Key' | abpLocalization }}}%}

+ +

{%{{{ 'MyProjectName::Key' | abpLocalization }}}%}

+``` + +## Using the Localization Pipe + +You can use the `abpLocalization` pipe to get localized text as in this example: + +```html +

{%{{{ 'Resource::Key' | abpLocalization }}}%}

+``` + +The pipe will replace the key with the localized text. + +You can also specify a default value as shown below: + +```html +

{%{{{ { key: 'Resource::Key', defaultValue: 'Default Value' } | abpLocalization }}}%}

+``` + +To use interpolation, you must give the values for interpolation as pipe parameters, for example: + +Localization data is stored in key-value pairs: + +```js +{ + //... + AbpAccount: { // AbpAccount is the resource name + Key: "Value", + PagerInfo: "Showing {0} to {1} of {2} entries" + } +} +``` + +So we can use this key like this: + +```html +

{%{{{ 'AbpAccount::PagerInfo' | abpLocalization:'20':'30':'50' }}}%}

+ + +``` + +### Using the Localization Service + +First of all you should import the `LocalizationService` from **@abp/ng.core** + +```js +import { LocalizationService } from '@abp/ng.core'; + +class MyClass { + constructor(private localizationService: LocalizationService) {} +} +``` + +After that, you are able to use localization service. + +> You can add interpolation parameters as arguments to `instant()` and `get()` methods. + +```js +this.localizationService.instant('AbpIdentity::UserDeletionConfirmation', 'John'); + +// with fallback value +this.localizationService.instant( + { key: 'AbpIdentity::UserDeletionConfirmation', defaultValue: 'Default Value' }, + 'John', +); + +// Output +// User 'John' will be deleted. Do you confirm that? +``` + +To get a localized text as [_Observable_](https://rxjs.dev/guide/observable) use `get` method instead of `instant`: + +```js +this.localizationService.get('Resource::Key'); + +// with fallback value +this.localizationService.get({ key: 'Resource::Key', defaultValue: 'Default Value' }); +``` + +### Using the Config State + +In order to you `getLocalization` method you should import ConfigState. + +```js +import { ConfigState } from '@abp/ng.core'; +``` + +Then you can use it as followed: + +```js +this.store.selectSnapshot(ConfigState.getLocalization('ResourceName::Key')); +``` + +`getLocalization` method can be used with both `localization key` and [`LocalizationWithDefault`](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L34) interface. + +```js +this.store.selectSnapshot( + ConfigState.getLocalization( + { + key: 'AbpIdentity::UserDeletionConfirmation', + defaultValue: 'Default Value', + }, + 'John', + ), +); +``` + +Localization resources are stored in the `localization` property of `ConfigState`. + + +## See Also + +* [Localization in ASP.NET Core](../../Localization.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md new file mode 100644 index 0000000000..6e5c5d7155 --- /dev/null +++ b/docs/en/UI/Angular/Permission-Management.md @@ -0,0 +1,79 @@ +# Permission Management + +A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../Authorization.md) document. + +You can get permission of authenticated user using `getGrantedPolicy` selector of `ConfigState`. + +You can get permission as boolean value from store: + +```js +import { Store } from '@ngxs/store'; +import { ConfigState } from '../states'; + +export class YourComponent { + constructor(private store: Store) {} + + ngOnInit(): void { + const canCreate = this.store.selectSnapshot(ConfigState.getGrantedPolicy('AbpIdentity.Roles.Create')); + } + + // ... +} +``` + +Or you can get it via `ConfigStateService`: + +```js +import { ConfigStateService } from '../services/config-state.service'; + +export class YourComponent { + constructor(private configStateService: ConfigStateService) {} + + ngOnInit(): void { + const canCreate = this.configStateService.getGrantedPolicy('AbpIdentity.Roles.Create'); + } + + // ... +} +``` + +## Permission Directive + +You can use the `PermissionDirective` to manage visibility of a DOM Element accordingly to user's permission. + +```html +
+ This content is only visible if the user has 'AbpIdentity.Roles' permission. +
+``` + +As shown above you can remove elements from DOM with `abpPermission` structural directive. + +The directive can also be used as an attribute directive but we recommend to you to use it as a structural directive. + +## Permission Guard + +You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation. + +Add `requiredPolicy` to the `routes` property in your routing module. + +```js +const routes: Routes = [ + { + path: 'path', + component: YourComponent, + canActivate: [PermissionGuard], + data: { + routes: { + requiredPolicy: 'AbpIdentity.Roles.Create', + }, + }, + }, +]; +``` + +Granted Policies are stored in the `auth` property of `ConfigState`. + +## What's Next? + +* [Component Replacement](./Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/images/component-replacement.gif b/docs/en/UI/Angular/images/component-replacement.gif new file mode 100644 index 0000000000..3a88500e53 Binary files /dev/null and b/docs/en/UI/Angular/images/component-replacement.gif differ diff --git a/docs/en/UI/Angular/images/custom-settings.png b/docs/en/UI/Angular/images/custom-settings.png new file mode 100644 index 0000000000..05dd3f695a Binary files /dev/null and b/docs/en/UI/Angular/images/custom-settings.png differ diff --git a/docs/en/UI/AspNetCore/Bundling-Minification.md b/docs/en/UI/AspNetCore/Bundling-Minification.md new file mode 100644 index 0000000000..5af3f177d5 --- /dev/null +++ b/docs/en/UI/AspNetCore/Bundling-Minification.md @@ -0,0 +1,352 @@ + +# ASP.NET Core MVC Bundling & Minification + +There are many ways of bundling & minification of client side resources (JavaScript and CSS files). Most common ways are: + +* Using the [Bundler & Minifier](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier) Visual Studio extension or the [NuGet package](https://www.nuget.org/packages/BuildBundlerMinifier/). +* Using [Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/) task managers and their plugins. + +ABP offers a simple, dynamic, powerful, modular and built-in way. + +## Volo.Abp.AspNetCore.Mvc.UI.Bundling Package + +> This package is already installed by default with the startup templates. So, most of the time, you don't need to install it manually. + +Install the `Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget package to your project: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +Then you can add the `AbpAspNetCoreMvcUiBundlingModule` dependency to your module: + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +## Razor Bundling Tag Helpers + +The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-style-bundle` tag helpers. Example: + +````html + + + + + + +```` + +This bundle defines a style bundle with a **unique name**: `MyGlobalBundle`. It's very easy to understand how to use it. Let's see how it *works*: + +* ABP creates the bundle as **lazy** from the provided files when it's **first requested**. For the subsequent calls, it's returned from the **cache**. That means if you conditionally add the files to the bundle, it's executed only once and any changes of the condition will not effect the bundle for the next requests. +* ABP adds bundle files **individually** to the page for the `development` environment. It automatically bundles & minifies for other environments (`staging`, `production`...). +* The bundle files may be **physical** files or [**virtual/embedded** files](../../Virtual-File-System.md). +* ABP automatically adds **version query string** to the bundle file URL to prevent browsers from caching when the bundle is being updated. (like ?_v=67872834243042 - generated from last change date of the related files). The versioning works even if the bundle files are individually added to the page (on the development environment). + +### Importing The Bundling Tag Helpers + +> This is already imported by default with the startup templates. So, most of the time, you don't need to add it manually. + +In order to use bundle tag helpers, you need to add it into your `_ViewImports.cshtml` file or into your page: + +```` +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +### Unnamed Bundles + +The `name` is **optional** for the razor bundle tag helpers. If you don't define a name, it's automatically **calculated** based on the used bundle file names (they are **concatenated** and **hashed**). Example: + +````html + + + + + @if (ViewBag.IncludeCustomStyles != false) + { + + } + +```` + +This will potentially create **two different bundles** (one incudes the `my-global-style.css` and other does not). + +Advantages of **unnamed** bundles: + +* Can **conditionally add items** to the bundle. But this may lead to multiple variations of the bundle based on the conditions. + +Advantages of **named** bundles: + +* Other **modules can contribute** to the bundle by its name (see the sections below). + +### Single File + +If you need to just add a single file to the page, you can use the `abp-script` or `abp-style` tag without a wrapping in the `abp-script-bundle` or `abp-style-bundle` tag. Example: + +````xml + +```` + +The bundle name will be *scripts.my-scripts* for the example above ("/" is replaced by "."). All bundling features are work as expected for single file bundles too. + +## Bundling Options + +If you need to use same bundle in **multiple pages** or want to use some more **powerful features**, you can configure bundles **by code** in your [module](../../Module-Development-Basics.md) class. + +### Creating A New Bundle + +Example usage: + +````C# +[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] +public class MyWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Add("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/libs/jquery/jquery.js", + "/libs/bootstrap/js/bootstrap.js", + "/libs/toastr/toastr.min.js", + "/scripts/my-global-scripts.js" + ); + }); + }); + } +} +```` + +> You can use the same name (*MyGlobalBundle* here) for a script & style bundle since they are added to different collections (`ScriptBundles` and `StyleBundles`). + +After defining such a bundle, it can be included into a page using the same tag helpers defined above. Example: + +````html + +```` + +This time, no file defined in the tag helper definition because the bundle files are defined by the code. + +### Configuring An Existing Bundle + +ABP supports [modularity](../../Module-Development-Basics.md) for bundling as well. A module can modify an existing bundle that is created by a depended module. Example: + +````C# +[DependsOn(typeof(MyWebModule))] +public class MyWebExtensionModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/scripts/my-extension-script.js" + ); + }); + }); + } +} +```` + +> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. + +## Bundle Contributors + +Adding files to an existing bundle seems useful. What if you need to **replace** a file in the bundle or you want to **conditionally** add files? Defining a bundle contributor provides extra power for such cases. + +An example bundle contributor that replaces bootstrap.css with a customized version: + +````C# +public class MyExtensionGlobalStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.ReplaceOne( + "/libs/bootstrap/css/bootstrap.css", + "/styles/extensions/bootstrap-customized.css" + ); + } +} +```` + +Then you can use this contributor as like below: + +````C# +services.Configure(options => +{ + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); + }); +}); +```` + +> You can also add contributors while creating a new bundle. + +Contributors can also be used in the bundle tag helpers. Example: + +````xml + + + + + +```` + +`abp-style` and `abp-script` tags can get `type` attributes (instead of `src` attributes) as shown in this sample. When you add a bundle contributor, its dependencies are also automatically added to the bundle. + +### Contributor Dependencies + +A bundle contributor can have one or more dependencies to other contributors. +Example: + +````C# +[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +When a bundle contributor is added, its dependencies are **automatically and recursively** added. Dependencies added by the **dependency order** by preventing **duplicates**. Duplicates are prevented even if they are in separated bundles. ABP organizes all bundles in a page and eliminates duplications. + +Creating contributors and defining dependencies is a way of organizing bundle creation across different modules. + +### Contributor Extensions + +In some advanced scenarios, you may want to do some additional configuration whenever a bundle contributor is used. Contributor extensions works seamlessly when the extended contributor is used. + +The example below adds some styles for prism.js library: + +````csharp +public class MyPrismjsStyleExtension : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); + } +} +```` + +Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. + +````csharp +Configure(options => +{ + options + .Extensions() + .Add(); +}); +```` + +Whenever `PrismjsStyleBundleContributor` is added into a bundle, `MyPrismjsStyleExtension` will also be automatically added. + +### Accessing to the IServiceProvider + +While it is rarely needed, `BundleConfigurationContext` has a `ServiceProvider` property that you can resolve service dependencies inside the `ConfigureBundle` method. + +### Standard Package Contributors + +Adding a specific NPM package resource (js, css files) into a bundle is pretty straight forward for that package. For example you always add the `bootstrap.css` file for the bootstrap NPM package. + +There are built-in contributors for all [standard NPM packages](Client-Side-Package-Management.md). For example, if your contributor depends on the bootstrap, you can just declare it, instead of adding the bootstrap.css yourself. + +````C# +[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +Using the built-in contributors for standard packages; + +* Prevents you typing **the invalid resource paths**. +* Prevents changing your contributor if the resource **path changes** (the dependant contributor will handle it). +* Prevents multiple modules adding the **duplicate files**. +* Manages **dependencies recursively** (adds dependencies of dependencies, if necessary). + +#### Volo.Abp.AspNetCore.Mvc.UI.Packages Package + +> This package is already installed by default in the startup templates. So, most of the time, you don't need to install it manually. + +Standard package contributors are defined in the `Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet package. +To install it to your project: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Packages +```` + +Then add the `AbpAspNetCoreMvcUiPackagesModule` module dependency to your own module; + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +### Bundle Inheritance + +In some specific cases, it may be needed to create a **new** bundle **inherited** from other bundle(s). Inheriting from a bundle (recursively) inherits all files/contributors of that bundle. Then the derived bundle can add or modify files/contributors **without modifying** the original bundle. +Example: + +````c# +services.Configure(options => +{ + options + .StyleBundles + .Add("MyTheme.MyGlobalBundle", bundle => { + bundle + .AddBaseBundles("MyGlobalBundle") //Can add multiple + .AddFiles( + "/styles/mytheme-global-styles.css" + ); + }); +}); +```` + +## Themes + +Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. + +## Best Practices & Suggestions + +It's suggested to define multiple bundles for an application, each one is used for different purposes. + +* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. +* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. +* **Module bundles**: For shared resources among the pages of an individual module. +* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. + +Establish a balance between performance, network bandwidth usage and count of many bundles. + +## See Also + +* [Client Side Package Management](Client-Side-Package-Management.md) +* [Theming](Theming.md) diff --git a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md new file mode 100644 index 0000000000..63c5cec225 --- /dev/null +++ b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md @@ -0,0 +1,116 @@ + +## ASP.NET Core MVC Client Side Package Management + +ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. + +However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. + +Finally, we suggest the [**Yarn**](https://classic.yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. + +### @ABP NPM Packages + +ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. + +One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. + +To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [Github repository](https://github.com/volosoft/abp/tree/master/npm/packs). + +The benefit of a **standard package** is: + +* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. +* It contains the gulp task to copy library resources (js, css, img... files) from the **node_modules** folder to **wwwroot/libs** folder. See the *Mapping The Library Resources* section for more. + +Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: + +```` +{ + ... + "dependencies": { + "@abp/bootstrap": "^1.0.0" + } +} +```` + +It's suggested to depend on a standard package instead of directly depending on a third-party package. + +#### Package Installation + +After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: + +```` +yarn +```` + +Alternatively, you can use `npm install` but [Yarn](https://classic.yarnpkg.com/) is suggested as mentioned before. + +#### Package Contribution + +If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: + +* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). +* It should be the **latest stable** version of the package. +* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. +* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. +* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. + +See current standard packages for examples. + +### Mapping The Library Resources + +Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. + +Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. + +ABP defines a [Gulp](https://gulpjs.com/) based task to **copy resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. + +The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. + +#### Resource Mapping Definition File + +A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: + +````js +module.exports = { + aliases: { + "@node_modules": "./node_modules", + "@libs": "./wwwroot/libs" + }, + clean: [ + "@libs" + ], + mappings: { + + } +} +```` + +* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. +* **clean** section is a list of folders to clean before copying the files. +* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. + +An example mapping configuration is shown below: + +````js +mappings: { + "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", + "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", + "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" +} +```` + +#### Using The Gulp + +Once you properly configure the `abp.resourcemapping.js` file, you can run the gulp command from the command line: + +```` +gulp +```` + +When you run the `gulp`, all packages will copy their own resources into the **wwwroot/libs** folder. Running `yarn & gulp` is only necessary if you make a change in your dependencies in the **package.json** file. + +> When you run the Gulp command, dependencies of the application are resolved using the package.json file. The Gulp task automatically discovers and maps all resources from all dependencies (recursively). + +#### See Also + +* [Bundling & Minification](Bundling-Minification.md) +* [Theming](Theming.md) diff --git a/docs/en/UI/AspNetCore/Customization-User-Interface.md b/docs/en/UI/AspNetCore/Customization-User-Interface.md new file mode 100644 index 0000000000..c5c974e5eb --- /dev/null +++ b/docs/en/UI/AspNetCore/Customization-User-Interface.md @@ -0,0 +1,470 @@ +# ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide + +This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) for ASP.NET Core MVC / Razor Page applications. + +## Overriding a Page + +This section covers the [Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/) development, which is the recommended approach to create server rendered user interface for ASP.NET Core. Pre-built modules typically uses the Razor Pages approach instead of the classic MVC pattern (next sections will cover the MVC pattern too). + +You typically have three kind of override requirement for a page: + +* Overriding **only the Page Model** (C#) side to perform additional logic without changing the page UI. +* Overriding **only the Razor Page** (.chtml file) to change the UI without changing the c# behind the page. +* **Completely overriding** the page. + +### Overriding a Page Model (C#) + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.Web.Pages.Identity.Users; + +namespace Acme.BookStore.Web.Pages.Identity.Users +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(EditModalModel))] + public class MyEditModalModel : EditModalModel + { + public MyEditModalModel( + IIdentityUserAppService identityUserAppService, + IIdentityRoleAppService identityRoleAppService + ) : base( + identityUserAppService, + identityRoleAppService) + { + } + + public override async Task OnPostAsync() + { + //TODO: Additional logic + await base.OnPostAsync(); + //TODO: Additional logic + } + } +} +```` + +* This class inherits from and replaces the `EditModalModel` for the users and overrides the `OnPostAsync` method to perform additional logic before and after the underlying code. +* It uses `ExposeServices` and `Dependency` attributes to replace the class. + +### Overriding a Razor Page (.CSHTML) + +Overriding a `.cshtml` file (razor page, razor view, view component... etc.) is possible through the [Virtual File System](../../Virtual-File-System.md). + +Virtual File system allows us to **embed resources into assemblies**. In this way, pre-built modules define the razor pages inside their NuGet packages. When you depend a module, you can override any file added to the virtual file system by that module, including pages/views. + +#### Example + +This example overrides the **login page** UI defined by the [Account Module](../../Modules/Account.md). + +Physical files override the embedded files defined in the same location. The account module defines a `Login.cshtml` file under the `Pages/Account` folder. So, you can override it by creating a file in the same path: + +![overriding-login-cshtml](../../images/overriding-login-cshtml.png) + +You typically want to copy the original `.cshtml` file of the module, then make the necessary changes. You can find the original file [here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml). Do not copy the `Login.cshtml.cs` file which is the code behind file for the razor page and we don't want to override it yet (see the next section). + +That's all, you can change the file content however you like. + +### Completely Overriding a Razor Page + +You may want to completely override a page; the razor and the c# file related to the page. + +In such a case; + +1. Override the C# page model class just like described above, but don't replace the existing page model class. +2. Override the Razor Page just described above, but also change the @model directive to point your new page model. + +#### Example + +This example overrides the **login page** defined by the [Account Module](../../Modules/Account.md). + +Create a page model class deriving from the ` LoginModel ` (defined in the ` Volo.Abp.Account.Web.Pages.Account ` namespace): + +````csharp +public class MyLoginModel : LoginModel +{ + public MyLoginModel( + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions + ) : base( + schemeProvider, + accountOptions) + { + + } + + public override Task OnPostAsync(string action) + { + //TODO: Add logic + return base.OnPostAsync(action); + } + + //TODO: Add new methods and properties... +} +```` + +You can override any method or add new properties/methods if needed. + +> Notice that we didn't use `[Dependency(ReplaceServices = true)]` or `[ExposeServices(typeof(LoginModel))]` since we don't want to replace the existing class in the dependency injection, we define a new one. + +Copy `Login.cshtml` file into your solution as just described above. Change the **@model** directive to point to the `MyLoginModel`: + +````xml +@page +... +@model Acme.BookStore.Web.Pages.Account.MyLoginModel +... +```` + +That's all! Make any change in the view and run your application. + +#### Replacing Page Model Without Inheritance + +You don't have to inherit from the original page model class (like done in the previous example). Instead, you can completely **re-implement** the page yourself. In this case, just derive from `PageModel`, `AbpPageModel` or any suitable base class you need. + +## Overriding a View Component + +The ABP Framework, pre-built themes and modules define some **re-usable view components**. These view components can be replaced just like a page described above. + +### Example + +The screenshot below was taken from the **basic theme** comes with the application startup template. + +![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) + +[The basic theme](../../Themes/Basic.md) defines some view components for the layout. For example, the highlighted area with the red rectangle above is called **Brand component**. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. + +First, create your logo and place under a folder in your web application. We used `wwwroot/logos/bookstore-logo.png` path. Then copy the Brand component's view ([from here](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml)) from the basic theme files under the `Themes/Basic/Components/Brand` folder. The result should be similar the picture below: + +![bookstore-added-brand-files](../../images/bookstore-added-brand-files.png) + +Then change the `Default.cshtml` as you like. Example content can be like that: + +````xml + + + +```` + +Now, you can run the application to see the result: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) + +If you need, you can also replace [the code behind c# class](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) of the component just using the dependency injection system. + +### Overriding the Theme + +Just as explained above, you can replace any component, layout or c# class of the used theme. See the [theming document](Theming.md) for more information on the theming system. + +## Overriding Static Resources + +Overriding a static embedded resource (like JavaScript, Css or image files) of a module is pretty easy. Just place a file in the same path in your solution and let the Virtual File System to handle it. + +## Manipulating the Bundles + +The [Bundling & Minification](Bundling-Minification.md) system provides an **extensible and dynamic** system to create **script** and **style** bundles. It allows you to extend and manipulate the existing bundles. + +### Example: Add a Global CSS File + +For example, ABP Framework defines a **global style bundle** which is added to every page (actually, added to the layout by the themes). Let's add a **custom style file** to the end of the bundle files, so we can override any global style. + +First, create a CSS file and locate it in a folder inside the `wwwroot`: + +![bookstore-global-css-file](../../images/bookstore-global-css-file.png) + +Define some custom CSS rules inside the file. Example: + +````css +.card-title { + color: orange; + font-size: 2em; + text-decoration: underline; +} + +.btn-primary { + background-color: red; +} +```` + +Then add this file to the standard global style bundle in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, //The bundle name! + bundleConfiguration => + { + bundleConfiguration.AddFiles("/styles/my-global-styles.css"); + } + ); +}); +```` + +#### The Global Script Bundle + +Just like the `StandardBundles.Styles.Global`, there is a `StandardBundles.Scripts.Global` that you can add files or manipulate the existing ones. + +### Example: Manipulate the Bundle Files + +The example above adds a new file to the bundle. You can do more if you create a **bundle contributor** class. Example: + +````csharp +public class MyGlobalStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Clear(); + context.Files.Add("/styles/my-global-styles.css"); + } +} +```` + +Then you can add the contributor to an existing bundle: + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundleConfiguration => + { + bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); + } + ); +}); +```` + +It is not a good idea to clear all CSS files. In a real world scenario, you can find and replace a specific file with your own file. + +### Example: Add a JavaScript File for a Specific Page + +The examples above works with the global bundle added to the layout. What if you want to add a CSS/JavaScript file (or replace a file) for a specific page defines inside a depended module? + +Assume that you want to run a **JavaScript code** once the user enters to the **Role Management** page of the Identity Module. + +First, create a standard JavaScript file under the `wwwroot`, `Pages` or `Views` folder (ABP support to add static resources inside these folders by default). We prefer the `Pages/Identity/Roles` folder to follow the conventions: + +![bookstore-added-role-js-file](../../images/bookstore-added-role-js-file.png) + +Content of the file is simple: + +````js +$(function() { + abp.log.info('My custom role script file has been loaded!'); +}); +```` + +Then add this file to the bundle of the role management page: + +````csharp +Configure(options => +{ + options.ScriptBundles + .Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName, + bundleConfig => + { + bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js"); + }); +}); +```` + +`typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName` is the safe way to get the bundle name for the role management page. + +> Notice that not every page defines such page bundles. They define only if needed. + +In addition to adding new CSS/JavaScript file to a page, you also can replace the existing one (by defining a bundle contributor). + +## Layout Customization + +Layouts are defined by the theme ([see the theming](Theming.md)) by design. They are not included in a downloaded application solution. In this way you can easily **upgrade** the theme and get new features. You can not **directly change** the layout code in your application unless you replace it by your own layout (will be explained in the next sections). + +There are some common ways to **customize the layout** described in the next sections. + +### Menu Contributors + +There are two **standard menus** defined by the ABP Framework: + +![bookstore-menus-highlighted](../../images/bookstore-menus-highlighted.png) + +* `StandardMenus.Main`: The main menu of the application. +* `StandardMenus.User`: The user menu (generally at the top right of the screen). + +Rendering the menus is a responsibility of the theme, but **menu items** are determined by the modules and your application code. Just implement the `IMenuContributor` interface and **manipulate the menu items** in the `ConfigureMenuAsync` method. + +Menu contributors are executed whenever need to render the menu. There is already a menu contributor defined in the **application startup template**, so you can take it as an example and improve if necessary. See the [navigation menu](Navigation-Menu.md) document for more. + +### Toolbar Contributors + +[Toolbar system](Toolbars.md) is used to define **toolbars** on the user interface. Modules (or your application) can add **items** to a toolbar, then the theme renders the toolbar on the **layout**. + +There is only one **standard toolbar** (named "Main" - defined as a constant: `StandardToolbars.Main`). For the basic theme, it is rendered as shown below:![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) + +In the screenshot above, there are two items added to the main toolbar: Language switch component & user menu. You can add your own items here. + +#### Example: Add a Notification Icon + +In this example, we will add a **notification (bell) icon** to the left of the language switch item. A item in the toolbar should be a **view component**. So, first, create a new view component in your project: + +![bookstore-notification-view-component](../../images/bookstore-notification-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class NotificationViewComponent : AbpViewComponent +{ + public async Task InvokeAsync() + { + return View("/Pages/Shared/Components/Notification/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````xml +
+ +
+```` + +Now, we can create a class implementing the `IToolbarContributor` interface: + +````csharp +public class MyToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items + .Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); + } + + return Task.CompletedTask; + } +} +```` + +This class adds the `NotificationViewComponent` as the first item in the `Main` toolbar. + +Finally, you need to add this contributor to the `AbpToolbarOptions`, in the `ConfigureServices` of your module: + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyToolbarContributor()); +}); +```` + +That's all, you will see the notification icon on the toolbar when you run the application: + +![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) + +`NotificationViewComponent` in this sample simply returns a view without any data. In real life, you probably want to **query database** (or call an HTTP API) to get notifications and pass to the view. If you need, you can add a `JavaScript` or `CSS` file to the global bundle (as described before) for your toolbar item. + +See the [toolbars document](Toolbars.md) for more about the toolbar system. + +### Layout Hooks + +[Layout Hooks](Layout-Hooks.md) system allows you to **add code** at some specific parts of the layout. All layouts of all themes should implement these hooks. Then you can then add a **view component** into a hook point. + +#### Example: Add Google Analytics Script + +Assume that you need to add the Google Analytics script to the layout (that will be available for all the pages). First, **create a view component** in your project: + +![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class GoogleAnalyticsViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````html + +```` + +Change `UA-xxxxxx-1` with your own code. + +You can then add this component to any of the hook points in the `ConfigureServices` of your module: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, //The hook name + typeof(GoogleAnalyticsViewComponent) //The component to add + ); +}); +```` + +Now, the GA code will be inserted in the `head` of the page as the last item. You (or the modules you are using) can add multiple items to the same hook. All of them will be added to the layout. + +The configuration above adds the `GoogleAnalyticsViewComponent` to all layouts. You may want to only add to a specific layout: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, + typeof(GoogleAnalyticsViewComponent), + layout: StandardLayouts.Application //Set the layout to add + ); +}); +```` + +See the layouts section below to learn more about the layout system. + +### Layouts + +Layout system allows themes to define standard, named layouts and allows any page to select a proper layout for its purpose. There are three pre-defined layouts: + +* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. +* "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. +* "**Empty**": Empty and minimal layout. + +These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are standard layout names and implemented by all the themes out of the box. + +#### Layout Location + +You can find the layout files [here](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts) for the basic theme. You can take them as references to build your own layouts or you can override them if necessary. + +#### ITheme + +ABP Framework uses the `ITheme` service to get the layout location by the layout name. You can replace this service to dynamically select the layout location. + +#### IThemeManager + +`IThemeManager` is used to obtain the current theme and get the layout path. Any page can determine the layout of its own. Example: + +````html +@using Volo.Abp.AspNetCore.Mvc.UI.Theming +@inject IThemeManager ThemeManager +@{ + Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); +} +```` + +This page will use the empty layout. You use `ThemeManager.CurrentTheme.GetEmptyLayout();` extension method as a shortcut. + +If you want to set the layout for all the pages under a specific folder, then write the code above in a `_ViewStart.cshtml` file under that folder. diff --git a/docs/en/UI/AspNetCore/Layout-Hooks.md b/docs/en/UI/AspNetCore/Layout-Hooks.md new file mode 100644 index 0000000000..49b334c6c0 --- /dev/null +++ b/docs/en/UI/AspNetCore/Layout-Hooks.md @@ -0,0 +1,3 @@ +# Layout Hooks + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md new file mode 100644 index 0000000000..4e0164ec72 --- /dev/null +++ b/docs/en/UI/AspNetCore/Navigation-Menu.md @@ -0,0 +1,3 @@ +# Navigation Menu + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md b/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..ae62c7f2b7 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md @@ -0,0 +1,86 @@ +# Buttons + +## Introduction + +`abp-button` is the main element to create buttons. + +Basic usage: + +````xml +Click Me +```` + +## Demo + +See the [buttons demo page](https://bootstrap-taghelpers.abp.io/Components/Buttons) to see it in action. + +## Attributes + +### button-type + +A value indicates the main style/type of the button. Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Outline_Primary` +* `Outline_Secondary` +* `Outline_Success` +* `Outline_Danger` +* `Outline_Warning` +* `Outline_Info` +* `Outline_Light` +* `Outline_Dark` +* `Link` + +### size + +A value indicates the size of the button. Should be one of the following values: + +* `Default` (default value) +* `Small` +* `Medium` +* `Large` +* `Block` +* `Block_Small` +* `Block_Medium` +* `Block_Large` + +### busy-text + +A text that is shown when the button is busy. + +### text + +The text of the button. This is a shortcut if you simply want to set a text to the button. Example: + +````xml + +```` + +In this case, you can use a self-closing tag to make it shorter. + +### icon + +Used to set an icon for the button. It works with the [Font Awesome](https://fontawesome.com/) icon classes by default. Example: + +````xml + +```` + +##### icon-type + +If you don't want to use font-awesome, you have two options: + +1. Set `icon-type` to `Other` and write the CSS class of the font icon you're using. +2. If you don't use a font icon use the opening and closing tags manually and write any code inside the tags. + +### disabled + +Set `true` to make the button initially disabled. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md new file mode 100644 index 0000000000..6d700f030d --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -0,0 +1,3 @@ +## Dynamic Forms + +This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..d7e393933e --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,26 @@ +# ABP Tag Helpers + +ABP Framework defines a set of **tag helper components** to simply the user interface development for ASP.NET Core (MVC / Razor Pages) applications. + +## Bootstrap Component Wrappers + +Most of the tag helpers are [Bootstrap](https://getbootstrap.com/) (v4+) wrappers. Coding bootstrap is not so easy, not so type-safe and contains too much repetitive HTML tags. ABP Tag Helpers makes it **easier** and **type safe**. + +We don't aim to wrap bootstrap components 100%. Writing **native bootstrap style code** is still possible (actually, tag helpers generates native bootstrap code in the end), but we suggest to use the tag helpers wherever possible. + +ABP Framework also adds some **useful features** to the standard bootstrap components. + +Here, the list of components those are wrapped by the ABP Framework: + +* [Buttons](Buttons.md) +* ... + +> Until all the tag helpers are documented, you can visit https://bootstrap-taghelpers.abp.io/ to see them with live samples. + +## Form Elements + +See [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). + +## Dynamic Inputs + +See [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Theming.md b/docs/en/UI/AspNetCore/Theming.md new file mode 100644 index 0000000000..e41545635c --- /dev/null +++ b/docs/en/UI/AspNetCore/Theming.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages Theming + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Toolbars.md b/docs/en/UI/AspNetCore/Toolbars.md new file mode 100644 index 0000000000..cc3bcd95be --- /dev/null +++ b/docs/en/UI/AspNetCore/Toolbars.md @@ -0,0 +1,3 @@ +# Toolbars + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Widgets.md b/docs/en/UI/AspNetCore/Widgets.md new file mode 100644 index 0000000000..062fefb078 --- /dev/null +++ b/docs/en/UI/AspNetCore/Widgets.md @@ -0,0 +1,505 @@ +# Widgets + +ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; + +* Have **scripts & styles** dependencies for your widget. +* Create **dashboards** with widgets used inside. +* Define widgets in reusable **[modules](../../Module-Development-Basics.md)**. +* Co-operate widgets with **[authorization](../../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. + +## Basic Widget Definition + +### Create a View Component + +As the first step, create a new regular ASP.NET Core View Component: + +![widget-basic-files](../../images/widget-basic-files.png) + +**MySimpleWidgetViewComponent.cs**: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. + +You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. + +**Default.cshtml**: + +```xml +
+

My Simple Widget

+

This is a simple widget!

+
+``` + +### Define the Widget + +Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## Rendering a Widget + +Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: + +````xml +@await Component.InvokeAsync("MySimpleWidget") +@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) +```` + +First approach uses the widget name while second approach uses the view component type. + +### Widgets with Arguments + +ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget +{ + [Widget] + public class CountersWidgetViewComponent : AbpViewComponent + { + private readonly IDashboardAppService _dashboardAppService; + + public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) + { + _dashboardAppService = dashboardAppService; + } + + public async Task InvokeAsync( + DateTime startDate, DateTime endDate) + { + var result = await _dashboardAppService.GetCountersWidgetAsync( + new CountersWidgetInputDto + { + StartDate = startDate, + EndDate = endDate + } + ); + + return View(result); + } + } +} +```` + +Now, you need to pass an anonymous object to pass arguments as shown below: + +````xml +@await Component.InvokeAsync("CountersWidget", new +{ + startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), + endDate = DateTime.Now +}) +```` + +## Widget Name + +Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. + +To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + [ViewComponent(Name = "MyCustomNamedWidget")] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); + } + } +} +``` + +ABP will respect to the custom name by handling the widget. + +> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. + +### Display Name + +You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: + +````csharp +using DashboardDemo.Localization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + DisplayName = "MySimpleWidgetDisplayName", //Localization key + DisplayNameResource = typeof(DashboardDemoResource) //localization resource + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +See [the localization document](../../Localization.md) to learn about localization resources and keys. + +## Style & Script Dependencies + +There are some challenges when your widget has script and style files; + +* Any page uses the widget should also include the **its script & styles** files into the page. +* The page should also care about **depended libraries/files** of the widget. + +ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. + +### Defining as Simple File Paths + +The example widget below adds a style and a script file: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, + ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../../Virtual-File-System.md). + +### Defining Bundle Contributors + +All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. + +The sample code below does the same with the code above, but defines and uses bundle contributors: + +````csharp +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } + + public class MySimpleWidgetStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); + } + } + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} + +```` + +Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. + +See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. + +## RefreshUrl + +A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). + +````csharp +[Widget(RefreshUrl = "Widgets/Counters")] +public class CountersWidgetViewComponent : AbpViewComponent +{ + +} +```` + +Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: + +````csharp +[Route("Widgets")] +public class CountersWidgetController : AbpController +{ + [HttpGet] + [Route("Counters")] + public IActionResult Counters(DateTime startDate, DateTime endDate) + { + return ViewComponent("CountersWidget", new {startDate, endDate}); + } +} +```` + +`Widgets/Counters` route matches to the `RefreshUrl` declared before. + +> A widget supposed to be refreshed in two ways: In the first way, when you use a `RefreshUrl`, it re-rendered on the server and replaced by the HTML returned from server. In the second way the widget gets data (generally a JSON object) from server and refreshes itself in the client side (see the refresh method in the Widget JavaScript API section). + +## JavaScript API + +A widget may need to be rendered and refreshed in the client side. In such cases, you can use ABP's `WidgetManager` and define APIs for your widgets. + +### WidgetManager + +`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: + +````js +$(function() { + var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); +}) +```` + +`MyDashboardWidgetsArea` may contain one or more widgets inside. + +> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. + +#### WidgetManager.init() + +`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) + +```js +myWidgetManager.init(); +``` + +#### WidgetManager.refresh() + +`refresh` method refreshes all widgets related to this `WidgetManager`: + +```` +myWidgetManager.refresh(); +```` + +#### WidgetManager Options + +WidgetManager has some additional options. + +##### Filter Form + +If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: + +````xml +
+ ...form elements +
+ +
+ ...widgets +
+```` + +`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. + +Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: + +````js +var myWidgetManager = new abp.WidgetManager({ + wrapper: '#MyDashboardWidgetsArea', + filterForm: '#MyDashboardFilterForm' +}); +```` + +##### Filter Callback + +You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: + +````js +var myWidgetManager = new abp.WidgetManager({ + wrapper: '#MyDashboardWidgetsArea', + filterCallback: function() { + return $('#MyDashboardFilterForm').serializeFormToObject(); + } +}); +```` + +This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: + +````js +filterCallback: function() { + return { + 'startDate': $('#StartDateInput').val(), + 'endDate': $('#EndDateInput').val() + }; +} +```` + +The returning filters are passed to all widgets on `init` and `refresh`. + +### Widget JavaScript API + +A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. + +````js +(function () { + abp.widgets.NewUserStatisticWidget = function ($wrapper) { + + var getFilters = function () { + return { + ... + }; + } + + var refresh = function (filters) { + ... + }; + + var init = function (filters) { + ... + }; + + return { + getFilters: getFilters, + init: init, + refresh: refresh + }; + }; +})(); +```` + +`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. + +#### getFilters + +If the widget has internal custom filters, this function should return the filter object. Example: + +````js +var getFilters = function() { + return { + frequency: $wrapper.find('.frequency-filter option:selected').val() + }; +} +```` + +This method is used by the `WidgetManager` while building filters. + +#### init + +Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. + +#### refresh + +Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. + +## Authorization + +Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: + +* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). +* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../../Authorization.md) for more info about policies. + +Example: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget(RequiredPolicies = new[] { "MyPolicyName" })] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## WidgetOptions + +As alternative to the `Widget` attribute, you can use the `AbpWidgetOptions` to configure widgets: + +```csharp +Configure(options => +{ + options.Widgets.Add(); +}); +``` + +Write this into the `ConfigureServices` method of your [module](../../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `AbpWidgetOptions`. Example configuration that adds a style for the widget: + +````csharp +Configure(options => +{ + options.Widgets + .Add() + .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); +}); +```` + +> Tip: `AbpWidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. + +## See Also + +* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 17c0ac67b4..c273e59346 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -58,7 +58,21 @@ "items": [ { "text": "Customizing the Application Modules", - "path": "Customizing-Application-Modules-Guide.md" + "path": "Customizing-Application-Modules-Guide.md", + "items": [ + { + "text": "Extending Entities", + "path": "Customizing-Application-Modules-Extending-Entities.md" + }, + { + "text": "Overriding Services", + "path": "Customizing-Application-Modules-Overriding-Services.md" + }, + { + "text": "Overriding the User Interface", + "path": "Customizing-Application-Modules-Overriding-User-Interface.md" + } + ] }, { "text": "Migrating from the ASP.NET Boilerplate", @@ -251,52 +265,68 @@ ] }, { - "text": "ASP.NET Core", + "text": "API", "items": [ { - "text": "API", + "text": "Auto API Controllers", + "path": "API/Auto-API-Controllers.md" + }, + { + "text": "Dynamic C# API Clients", + "path": "API/Dynamic-CSharp-API-Clients.md" + } + ] + }, + { + "text": "User Interface", + "items": [ + { + "text": "ASP.NET Core", "items": [ { - "text": "Auto API Controllers", - "path": "AspNetCore/Auto-API-Controllers.md" + "text": "Client Side Package Management", + "path": "UI/AspNetCore/Client-Side-Package-Management.md" }, { - "text": "Dynamic C# API Clients", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" + "text": "Bundling & Minification", + "path": "UI/AspNetCore/Bundling-Minification.md" + }, + { + "text": "Tag Helpers", + "path": "UI/AspNetCore/Tag-Helpers/Index.md" + }, + { + "text": "Widgets", + "path": "UI/AspNetCore/Widgets.md" + }, + { + "text": "Theming", + "path": "UI/AspNetCore/Theming.md" } ] }, { - "text": "User Interface", + "text": "Angular", "items": [ { - "text": "Client Side Package Management", - "path": "AspNetCore/Client-Side-Package-Management.md" + "text": "Localization", + "path": "UI/Angular/Localization.md" }, { - "text": "Bundling & Minification", - "path": "AspNetCore/Bundling-Minification.md" + "text": "Permission Management", + "path": "UI/Angular/Permission-Management.md" }, { - "text": "Tag Helpers", - "items":[ - { - "text": "Live Demo", - "path": "AspNetCore/Tag-Helpers/Index.md" - }, - { - "text": "Buttons", - "path": "AspNetCore/Tag-Helpers/Buttons.md" - } - ] + "text": "Config State", + "path": "UI/Angular/Config-State.md" }, { - "text": "Widgets", - "path": "AspNetCore/Widgets.md" + "text": "Component Replacement", + "path": "UI/Angular/Component-Replacement.md" }, { - "text": "Theming", - "path": "AspNetCore/Theming.md" + "text": "Custom Setting Page", + "path": "UI/Angular/Custom-Setting-Page.md" } ] } diff --git a/docs/en/images/bookstore-added-brand-files.png b/docs/en/images/bookstore-added-brand-files.png new file mode 100644 index 0000000000..15ef283a2a Binary files /dev/null and b/docs/en/images/bookstore-added-brand-files.png differ diff --git a/docs/en/images/bookstore-added-logo.png b/docs/en/images/bookstore-added-logo.png new file mode 100644 index 0000000000..55af77a5f4 Binary files /dev/null and b/docs/en/images/bookstore-added-logo.png differ diff --git a/docs/en/images/bookstore-added-role-js-file.png b/docs/en/images/bookstore-added-role-js-file.png new file mode 100644 index 0000000000..00881bf808 Binary files /dev/null and b/docs/en/images/bookstore-added-role-js-file.png differ diff --git a/docs/en/images/bookstore-brand-area-highlighted.png b/docs/en/images/bookstore-brand-area-highlighted.png new file mode 100644 index 0000000000..7ab1b8fb7f Binary files /dev/null and b/docs/en/images/bookstore-brand-area-highlighted.png differ diff --git a/docs/en/images/bookstore-global-css-file.png b/docs/en/images/bookstore-global-css-file.png new file mode 100644 index 0000000000..e764fef8ee Binary files /dev/null and b/docs/en/images/bookstore-global-css-file.png differ diff --git a/docs/en/images/bookstore-google-analytics-view-component.png b/docs/en/images/bookstore-google-analytics-view-component.png new file mode 100644 index 0000000000..e548a00935 Binary files /dev/null and b/docs/en/images/bookstore-google-analytics-view-component.png differ diff --git a/docs/en/images/bookstore-menus-highlighted.png b/docs/en/images/bookstore-menus-highlighted.png new file mode 100644 index 0000000000..c4f3237b00 Binary files /dev/null and b/docs/en/images/bookstore-menus-highlighted.png differ diff --git a/docs/en/images/bookstore-notification-icon-on-toolbar.png b/docs/en/images/bookstore-notification-icon-on-toolbar.png new file mode 100644 index 0000000000..817491e9eb Binary files /dev/null and b/docs/en/images/bookstore-notification-icon-on-toolbar.png differ diff --git a/docs/en/images/bookstore-notification-view-component.png b/docs/en/images/bookstore-notification-view-component.png new file mode 100644 index 0000000000..3fc300efe1 Binary files /dev/null and b/docs/en/images/bookstore-notification-view-component.png differ diff --git a/docs/en/images/bookstore-toolbar-highlighted.png b/docs/en/images/bookstore-toolbar-highlighted.png new file mode 100644 index 0000000000..f2b7c3f57e Binary files /dev/null and b/docs/en/images/bookstore-toolbar-highlighted.png differ diff --git a/docs/en/images/docs-module_download-new-abp-project.png b/docs/en/images/docs-module_download-new-abp-project.png index 0da3b7a67a..6424ad7622 100644 Binary files a/docs/en/images/docs-module_download-new-abp-project.png and b/docs/en/images/docs-module_download-new-abp-project.png differ diff --git a/docs/en/images/docs-module_solution-explorer.png b/docs/en/images/docs-module_solution-explorer.png index cafc38f0b0..2988ec4134 100644 Binary files a/docs/en/images/docs-module_solution-explorer.png and b/docs/en/images/docs-module_solution-explorer.png differ diff --git a/docs/en/images/overriding-login-cshtml.png b/docs/en/images/overriding-login-cshtml.png new file mode 100644 index 0000000000..d6daa4a141 Binary files /dev/null and b/docs/en/images/overriding-login-cshtml.png differ diff --git a/docs/zh-Hans/API/Auto-API-Controllers.md b/docs/zh-Hans/API/Auto-API-Controllers.md new file mode 100644 index 0000000000..93ac124913 --- /dev/null +++ b/docs/zh-Hans/API/Auto-API-Controllers.md @@ -0,0 +1,140 @@ +# 自动API控制器 + +创建[应用程序服务](Application-Services.md)后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做. + +ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义. + +## 配置 + +基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: + +````csharp +[DependsOn(BookStoreApplicationModule)] +public class BookStoreWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly); + }); + } +} +```` + +此示例代码配置包含类`BookStoreApplicationModule`的程序集中的所有应用程序服务.下图显示了[Swagger UI](https://swagger.io/tools/swagger-ui/)上的API内容. + +![bookstore-apis](../images/bookstore-apis.png) + +### 例子 + +一些示例方法名称和按约定生成的相应路由: + +| 服务方法名称 | HTTP Method | 路由 | +| ----------------------------------------------------- | ----------- | -------------------------- | +| GetAsync(Guid id) | GET | /api/app/book/{id} | +| GetListAsync() | GET | /api/app/book | +| CreateAsync(CreateBookDto input) | POST | /api/app/book | +| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | +| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | +| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | +| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | + +### HTTP Method + +ABP在确定服务方法的HTTP Method时使用命名约定: + +- **Get**: 如果方法名称以`GetList`,`GetAll`或`Get`开头. +- **Put**: 如果方法名称以`Put`或`Update`开头. +- **Delete**: 如果方法名称以`Delete`或`Remove`开头. +- **Post**: 如果方法名称以`Create`,`Add`,`Insert`或`Post`开头. +- **Patch**: 如果方法名称以`Patch`开头. +- 其他情况, **Post** 为 **默认方式**. + +如果需要为特定方法自定义HTTP Method, 则可以使用标准ASP.NET Core的属性([HttpPost], [HttpGet], [HttpPut]... 等等.). 这需要添加[Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core)的Nuget包. + +### 路由 + +路由根据一些惯例生成: + +* 它始终以 **/api**开头. +* 接着是**路由路径**. 默认值为"**/app**", 可以进行如下配置: + +````csharp +Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.RootPath = "volosoft/book-store"; + }); +}); +```` + +然后获得一本书的路由将是'**/api/volosoft/book-store/book/{id}**'. 此示例使用两级根路径,但通常使用单个级别的深度. + +* 接着 **标准化控制器/服务名称**. 会删除`AppService`,`ApplicationService`和`Service`的后缀并将其转换为 **camelCase**. 如果你的应用程序服务类名称为`BookAppService`.那么它将变为`/book`. + * 如果要自定义命名, 则设置`UrlControllerNameNormalizer`选项. 它是一个委托允许你自定义每个控制器/服务的名称. +* 如果该方法具有 '**id**'参数, 则会在路由中添加'**/{id}**'. +* 如有必要,它会添加操作名称. 操作名称从服务上的方法名称获取并标准化; + * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. + * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. + * 将结果转换为**camelCase**. + * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. + * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. +* 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). + +## 服务选择 + +创建的HTTP API控制器并不是应用服务所独有的功能. + +### IRemoteService 接口 + +如果一个类实现了`IRemoteService`接口, 那么它会被自动选择为API控制器. 由于应用程序服务本身实现了`IRemoteService`接口, 因此它自然就成为API控制器. + +### RemoteService Attribute + +`RemoteService`可用于将实现`IRemoteService`接口的类标记为远程服务或禁用它. 例如: + +````csharp +[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +### TypePredicate 选项 + +你可以通过提供`TypePedicate`选项进一步过滤类以成为API控制器: + +````csharp +services.Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.TypePredicate = type => { return true; }; + }); +}); +```` + +如果你不想将此类型公开为API控制器, 则可以在类型检查时返回`false`. + +## API Explorer + +API Explorer是可以由客户端获取API结构的服务. Swagger使用它为endpoint创建文档和test UI. + +默认情况下, HTTP API控制器会自动启用API Explorer, 可以使用`RemoteService`按类或方法的级别控制它. 例如: + +````csharp +[RemoteService(IsMetadataEnabled = false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +禁用`IsMetadataEnabled`从而从API Explorer中隐藏此服务, 并且无法被发现. 但是它仍然可以被知道确切API路径/路由的客户端使用. \ No newline at end of file diff --git a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md new file mode 100644 index 0000000000..331e66c18e --- /dev/null +++ b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md @@ -0,0 +1,165 @@ +# 动态 C# API 客户端 + +ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 `HttpClient` 或者其他低级的HTTP功能调用远程服务并获取数据. + +## 服务接口 + +你的service或controller需要实现一个在服务端和客户端共享的接口.因此,首先需要在一个共享的类库项目中定义一个服务接口.例如: + +````csharp +public interface IBookAppService : IApplicationService +{ + Task> GetListAsync(); +} +```` + +为了能自动被发现,你的接口需要实现`IRemoteService`接口.由于`IApplicationService`继承自`IRemoteService`接口.所以`IBookAppService`完全满足这个条件. + +在你的服务中实现这个类,你可以使用[auto API controller system](Auto-API-Controllers.md)将你的服务暴漏为一个REST API 端点. + +## 客户端代理生成 + +首先,将[Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget包添加到你的客户端项目中: + +```` +Install-Package Volo.Abp.Http.Client +```` + +然后给你的模块添加`AbpHttpClientModule`依赖: + +````csharp +[DependsOn(typeof(AbpHttpClientModule))] //添加依赖 +public class MyClientAppModule : AbpModule +{ +} +```` + +现在,已经可以创建客户端代理了.例如: + +````csharp +[DependsOn( + typeof(AbpHttpClientModule), //用来创建客户端代理 + typeof(BookStoreApplicationModule) //包含应用服务接口 + )] +public class MyClientAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //创建动态客户端代理 + context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly + ); + } +} +```` + +`AddHttpClientproxies`方法获得一个程序集,找到这个程序集中所有的服务接口,创建并注册代理类. + +### Endpoint配置 + +`appsettings.json`文件中的`RemoteServices`节点被用来设置默认的服务地址.下面是最简单的配置: + +```` +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + } + } +} +```` + +查看下面的"RemoteServiceOptions"章节获取更多详细配置. + +## 使用 + +可以很直接地使用.只需要在你的客户端程序中注入服务接口: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IBookAppService _bookService; + + public MyService(IBookAppService bookService) + { + _bookService = bookService; + } + + public async Task DoIt() + { + var books = await _bookService.GetListAsync(); + foreach (var book in books) + { + Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); + } + } +} +```` + +本例注入了上面定义的`IBookAppService`服务接口.当客户端调用服务方法的时候动态客户端代理就会创建一个HTTP调用. + +### IHttpClientProxy接口 + +你可以像上面那样注入`IBookAppService`来使用客户端代理,也可以注入`IHttpClientProxy`获取更多明确的用法.这种情况下你可以使用`IHttpClientProxy`接口的`Service`属性. + +## 配置 + +### RemoteServiceOptions + +默认情况下`AbpRemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.Configure(options => + { + options.RemoteServices.Default = + new RemoteServiceConfiguration("http://localhost:53929/"); + }); + + //... +} +```` + +### 多个远程服务端点 + +上面的例子已经配置了"Default"远程服务端点.你可能需要为不同的服务创建不同的端点.(就像在微服务方法中一样,每个微服务具有不同的端点).在这种情况下,你可以在你的配置文件中添加其他的端点: + +````json +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + }, + "BookStore": { + "BaseUrl": "http://localhost:48392/" + } + } +} +```` + +`AddHttpClientProxies`方法有一个可选的参数来定义远程服务的名字: + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + remoteServiceName: "BookStore" +); +```` + +`remoteServiceName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. + +### 作为默认服务 + +当你为`IBookAppService`创建了一个服务代理,你可以直接注入`IBookAppService`来使用代理客户端(像上面章节中将的那样).你可以传递`asDefaultService:false`到`AddHttpClientProxies`方法来禁用此功能. + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + asDefaultServices: false +); +```` + +如果你的程序中已经有一个服务的实现并且你不想用你的客户端代理重写或替换其他的实现,就需要使用`asDefaultServices:false` + +> 如果你禁用了`asDefaultService`,你只能使用`IHttpClientProxy`接口去使用客户端代理.(参见上面的相关章节). \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Auth.md b/docs/zh-Hans/API/JavaScript-API/Auth.md new file mode 100644 index 0000000000..60c9eb8866 --- /dev/null +++ b/docs/zh-Hans/API/JavaScript-API/Auth.md @@ -0,0 +1,3 @@ +# abp.auth JavaScript API + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Index.md b/docs/zh-Hans/API/JavaScript-API/Index.md new file mode 100644 index 0000000000..854ec8214b --- /dev/null +++ b/docs/zh-Hans/API/JavaScript-API/Index.md @@ -0,0 +1,23 @@ +# JavaScript API + +ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. + +## APIs + +* abp.ajax +* [abp.auth](Auth.md) +* abp.currentUser +* abp.dom +* abp.event +* abp.features +* abp.localization +* abp.log +* abp.ModalManager +* abp.notify +* abp.security +* abp.setting +* abp.ui +* abp.utils +* abp.ResourceLoader +* abp.WidgetManager +* Other APIs \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md b/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md index 93ac124913..19a26440eb 100644 --- a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md +++ b/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md @@ -1,140 +1,3 @@ -# 自动API控制器 +文档已经移动到其他位置. -创建[应用程序服务](Application-Services.md)后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做. - -ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义. - -## 配置 - -基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -此示例代码配置包含类`BookStoreApplicationModule`的程序集中的所有应用程序服务.下图显示了[Swagger UI](https://swagger.io/tools/swagger-ui/)上的API内容. - -![bookstore-apis](../images/bookstore-apis.png) - -### 例子 - -一些示例方法名称和按约定生成的相应路由: - -| 服务方法名称 | HTTP Method | 路由 | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP在确定服务方法的HTTP Method时使用命名约定: - -- **Get**: 如果方法名称以`GetList`,`GetAll`或`Get`开头. -- **Put**: 如果方法名称以`Put`或`Update`开头. -- **Delete**: 如果方法名称以`Delete`或`Remove`开头. -- **Post**: 如果方法名称以`Create`,`Add`,`Insert`或`Post`开头. -- **Patch**: 如果方法名称以`Patch`开头. -- 其他情况, **Post** 为 **默认方式**. - -如果需要为特定方法自定义HTTP Method, 则可以使用标准ASP.NET Core的属性([HttpPost], [HttpGet], [HttpPut]... 等等.). 这需要添加[Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core)的Nuget包. - -### 路由 - -路由根据一些惯例生成: - -* 它始终以 **/api**开头. -* 接着是**路由路径**. 默认值为"**/app**", 可以进行如下配置: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -然后获得一本书的路由将是'**/api/volosoft/book-store/book/{id}**'. 此示例使用两级根路径,但通常使用单个级别的深度. - -* 接着 **标准化控制器/服务名称**. 会删除`AppService`,`ApplicationService`和`Service`的后缀并将其转换为 **camelCase**. 如果你的应用程序服务类名称为`BookAppService`.那么它将变为`/book`. - * 如果要自定义命名, 则设置`UrlControllerNameNormalizer`选项. 它是一个委托允许你自定义每个控制器/服务的名称. -* 如果该方法具有 '**id**'参数, 则会在路由中添加'**/{id}**'. -* 如有必要,它会添加操作名称. 操作名称从服务上的方法名称获取并标准化; - * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. - * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. - * 将结果转换为**camelCase**. - * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. - * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. -* 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). - -## 服务选择 - -创建的HTTP API控制器并不是应用服务所独有的功能. - -### IRemoteService 接口 - -如果一个类实现了`IRemoteService`接口, 那么它会被自动选择为API控制器. 由于应用程序服务本身实现了`IRemoteService`接口, 因此它自然就成为API控制器. - -### RemoteService Attribute - -`RemoteService`可用于将实现`IRemoteService`接口的类标记为远程服务或禁用它. 例如: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate 选项 - -你可以通过提供`TypePedicate`选项进一步过滤类以成为API控制器: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -如果你不想将此类型公开为API控制器, 则可以在类型检查时返回`false`. - -## API Explorer - -API Explorer是可以由客户端获取API结构的服务. Swagger使用它为endpoint创建文档和test UI. - -默认情况下, HTTP API控制器会自动启用API Explorer, 可以使用`RemoteService`按类或方法的级别控制它. 例如: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -禁用`IsMetadataEnabled`从而从API Explorer中隐藏此服务, 并且无法被发现. 但是它仍然可以被知道确切API路径/路由的客户端使用. \ No newline at end of file +[点击链接跳转到自动API控制器文档](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/AspNetCore/Bundling-Minification.md index b7c8c0a1b1..c102c89fd2 100644 --- a/docs/zh-Hans/AspNetCore/Bundling-Minification.md +++ b/docs/zh-Hans/AspNetCore/Bundling-Minification.md @@ -1,354 +1,3 @@ +文档已经移动到其他位置. -## ASP.NET Core MVC 捆绑 & 压缩 - -有许多方法可以捆绑&压缩客户端资源(JavaScript和CSS文件). 最常见的方式是: - -* 使用Visual Studio[捆绑&压缩](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier)扩展或者其它的[NuGet相关包](https://www.nuget.org/packages/BuildBundlerMinifier/). - -* 使用[Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/)及其插件. - -ABP内置了简单,动态,强大,模块化的方式. - -### Volo.Abp.AspNetCore.Mvc.UI.Bundling 包 - -> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. - -将`Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget包安装到你的项目中: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -然后将`AbpAspNetCoreMvcUiBundlingModule`依赖项添加到你的模块上: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -### Razor Bundling Tag Helpers - -创建bundle的最简单方法是使用`abp-script-bundle`或`abp-style-bundle` tag helpers. 例如: - -````html - - - - - - -```` - -`abp-script-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: - -* 当首次请求时,ABP从提供的文件中 **(延迟)lazy** 创建. 后续将从 **缓存** 中返回内容. 这意味着如果你有条件地将文件添加到包中,它只执行一次, 并且条件的任何更改都不会影响下一个请求的包. -* 在`development`环境中ABP会将包文件**单独**添加到页面中, 其他环境(`staging`,`production`...)会自动捆绑和压缩. -* 捆绑文件可以是**物理**文件或[**虚拟/嵌入**](../Virtual-File-System.md)的文件. -* ABP自动将 **版本查询字符串(version query string)** 添加到捆绑文件的URL中,以防止浏览器缓存. 如:?_v=67872834243042(从文件的上次更改日期生成). 即使捆绑文件单独添加到页面(在`development`环境中), 版本控制仍然有效. - -#### 导入 Bundling Tag Helpers - -> 默认情况下已在启动模板导入. 大多数情况下,你不需要手动安装它. - -要使用`bundle tag helpers`, 你需要将其添加到`_ViewImports.cshtml`文件或页面中: - -```` -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -#### 未命名的 Bundles - -对于razor bundle tag helpers, `name`是**可选**. 如果没有定义一个名字,它将根据使用的捆绑文件名自动**计算生成**(they are **concatenated** and **hashed**) 例: - -````html - - - - - @if (ViewBag.IncludeCustomStyles != false) - { - - } - -```` - -这将潜在地创建**两个不同的bundles**(一个包括`my-global-style.css`而另一个则不包括). - -**未命名的** bundles优点: - -* 可以**有条件地将项目**添加到捆绑包中. 但这可能会导致基于条件的捆绑的存在多种变化. - -**命名** bundles优点: - -* 其他模块可以通过其名称为捆绑包做出贡献(参见下面的部分). - -#### 单个文件 - -如果你只需要在页面中添加一个文件, 你可以使用`abp-script`或`abp-style`而不需要包含在`abp-script-bundle`或`abp-style-bundle`中. 例: - -````xml - -```` - -对于上面的示例,包名称将是 *scripts.my-scripts*("/"替换为"."). 所有捆绑功能也可以按预期应用于单个文件. - -### Bundling 选项 - -如果你需要在 **多个页面中使用相同的包** 或想要使用更多 **强大功能**, 你可以在[模块](../Module-Development-Basics.md)类中进行**配置**. - -#### 创建一个新的捆绑包 - -用法示例: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] -public class MyWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Add("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/libs/jquery/jquery.js", - "/libs/bootstrap/js/bootstrap.js", - "/libs/toastr/toastr.min.js", - "/scripts/my-global-scripts.js" - ); - }); - }); - } -} -```` - -> 你可以在脚本和样式包中使用相同的名称(*MyGlobalBundle*), 因为它们被添加到不同的集合(`ScriptBundles`和`StyleBundles`). - -在定义bundle之后, 可以使用上面定义的相同tag helpers将其包括在页面中. 例如: - -````html - -```` - -这次tag helper定义中没有定义文件, 因为捆绑文件是由代码定义的. - -#### 配置现有的 Bundle - -ABP也支持[模块化](../Module-Development-Basics.md)捆绑. 模块可以修改由依赖模块创建的捆绑包. -例如: - -````C# -[DependsOn(typeof(MyWebModule))] -public class MyWebExtensionModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/scripts/my-extension-script.js" - ); - }); - }); - } -} -```` - -> 无法通过代码配置未命名的bundle tag helpers, 因为它们的名称在开发时是未知的. 建议始终使用bundle tag helper的名称. - -### Bundle 贡献者 - -将文件添加到现有bundle似乎很有用. 如果你需要**替换**bundle中的文件或者你想**有条件地**添加文件怎么办? 定义bundle贡献者可为此类情况提供额外的功能. - -一个bundle的贡献者使用自定义版本bootstrap.css替换示例: - -````C# -public class MyExtensionGlobalStyleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.ReplaceOne( - "/libs/bootstrap/css/bootstrap.css", - "/styles/extensions/bootstrap-customized.css" - ); - } -} -```` - -然后你可以按照下面的代码使用这个贡献者: - -````C# -services.Configure(options => -{ - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); - }); -}); -```` - -贡献者也可以在bundle tag helpers中使用. -例如: - -````xml - - - - - -```` - -`abp-style`和`abp-script`标签可以使用`type`属性(而不是`src`属性), 如本示例所示. 添加bundle贡献者时, 其依赖关系也会自动添加到bundle中. - -#### 贡献者依赖关系 - -bundle贡献者可以与其他贡献者具有一个或多个依赖关系. -例如: - -````C# -[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -添加bundle贡献者时,其依赖关系将 **自动并递归** 添加. **依赖顺序** 通过阻止 **重复** 添加的依赖关系. 即使它们处于分离的bundle中,也会阻止重复. ABP在页面中组织所有bundle并消除重复. - -创建贡献者和定义依赖关系是一种跨不同模块组织bundle创建的方法. - - -#### 贡献者扩展 - -在某些高级应用场景中, 当用到一个bundle贡献者时,你可能想做一些额外的配置. 贡献者扩展可以和被扩展的贡献者无缝衔接. - -下面的示例为 prism.js 脚本库添加一些样式: - -````csharp -public class MyPrismjsStyleExtension : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); - } -} -```` - -然后你可以配置 `BundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`. - -````csharp -Configure(options => -{ - options - .Extensions() - .Add(); -}); -```` - -任何时候当 `PrismjsStyleBundleContributor` 被添加到bundle中时, `MyPrismjsStyleExtension` 也会被自动添加. - -#### 访问 IServiceProvider - -虽然很少需要它, 但是`BundleConfigurationContext`有一个`ServiceProvider`属性, 你可以在`ConfigureBundle`方法中解析服务依赖. - -#### 标准包装贡献者 - -将特定的NPM包资源(js,css文件)添加到包中对于该包非常简单. 例如, 你总是为bootstrap NPM包添加`bootstrap.css`文件. - -所有[标准NPM包](Client-Side-Package-Management.md)都有内置的贡献者. 例如,如果你的贡献者依赖于bootstrap,你可以声明它,而不是自己添加bootstrap.css. - -````C# -[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -使用标准包的内置贡献者: - -* 防止你输入**无效的资源路径**. -* 如果资源 **路径发生变化** (依赖贡献者将处理它),则防止更改你的贡献者. -* 防止多个模块添加**重复文件**. -* 以递归方式管理依赖项(如果需要,添加依赖项的依赖项). - -##### Volo.Abp.AspNetCore.Mvc.UI.Packages 包 - -> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. - -标准包贡献者在`Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet包中定义. -将它安装到你的项目中: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Packages -```` - -然后将`AbpAspNetCoreMvcUiPackagesModule`模块依赖项添加到你的模块中; - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -#### Bundle 继承 - -在某些特定情况下, 可能需要从其他bundle创建一个 **新** bundle **继承**, 从bundle继承(递归)会继承该bundle的所有文件/贡献者. 然后派生的bundle可以添加或修改文件/贡献者**而无需修改**原始bundle. -例如: - -````c# -services.Configure(options => -{ - options - .StyleBundles - .Add("MyTheme.MyGlobalBundle", bundle => { - bundle - .AddBaseBundles("MyGlobalBundle") //Can add multiple - .AddFiles( - "/styles/mytheme-global-styles.css" - ); - }); -}); -```` - -### 主题 - -主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). - -### 最佳实践 & 建议 - -建议为应用程序定义多个包, 每个包用于不同的目的. - -* **全局包**: 应用程序中的每个页面都包含全局样式/脚本包. 主题已经定义了全局样式和脚本包. 你的模块可以为他们做出贡献. -* **布局包**: 这是针对单个布局的特定包. 仅包含在所有页面之间共享的资源使用布局. 使用bundling tag helpers创建捆绑包是一种很好的做法. -* **模块包**: 用于单个模块页面之间的共享资源. -* **页面包**: 为每个页面创建的特定包. 使用bundling tag helpers创建捆绑包作为最佳实践. - -在性能,网络带宽使用和捆绑包的数量之间建立平衡. - -### 参见 - -* [客户端包管理](Client-Side-Package-Management.md) -* [主题](Theming.md) +[点击链接跳转到ASP.NET Core MVC 捆绑 & 压缩文档](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md index 2c50064be0..f61d4bd623 100644 --- a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md +++ b/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md @@ -1,115 +1,3 @@ +文档已经移动到其他位置. -## ASP.NET Core MVC 客户端包管理 - -ABP框架可以与任何类型的客户端包管理系统一起使用. 甚至你可以决定不使用包管理系统并手动管理依赖项. - -但是, ABP框架最适用于**NPM/Yarn**. 默认情况下,内置模块配置为与NPM/Yarn一起使用. - -最后, 我们建议[**Yarn**](https://classic.yarnpkg.com/)而不是NPM,因为它更快,更稳定并且与NPM兼容. - -### @ABP NPM Packages - -ABP是一个模块化平台. 每个开发人员都可以创建模块, 模块应该在**兼容**和**稳定**状态下协同工作. - -一个挑战是依赖NPM包的**版本**. 如果两个不同的模块使用相同的JavaScript库但其不同(并且可能不兼容)的版本会怎样. - -为了解决版本问题, 我们创建了一套**标准包**, 这取决于一些常见的第三方库. 一些示例包是[@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@ abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap)和[@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). 你可以从[Github存储库](https://github.com/volosoft/abp/tree/master/npm/packs)中查看**列表**. - -**标准包**的好处是: - -* 它取决于包装的**标准版本**. 取决于此包是**安全**,因为所有模块都依赖于相同的版本. -* 它包含将库资源(js,css,img...文件)从**node_modules**文件夹复制到**wwwroot/libs**文件夹的gulp任务. 有关更多信息, 请参阅 *映射库资源* 部分. - -依赖标准包装很容易. 只需像往常一样将它添加到**package.json**文件中. 例如: - -```` - { - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } - } -```` - -建议依赖于标准软件包, 而不是直接依赖于第三方软件包. - -#### 安装包 - -依赖于NPM包后, 你应该做的就是从命令行运行**yarn**命令来安装所有包及其依赖项: - -```` -yarn -```` - -虽然你可以使用`npm install`,但如前所述,建议使用[Yarn](https://classic.yarnpkg.com/). - -#### 贡献包 - -如果你需要不在标准软件包中的第三方NPM软件包,你可以在Github[repository](https://github.com/volosoft/abp)上创建Pull请求. 接受遵循这些规则的拉取请求: - -* 对于NPM上的`package-name`, 包名称应该命名为`@abp/package-name`(例如:`bootstrap`包的`@abp/bootstrap`). -* 它应该是**最新的稳定**版本的包. -* 它应该只依赖于**单个**第三方包. 它可以依赖于多个`@abp/*`包. -* 包应包含一个`abp.resourcemapping.js`文件格式,如*映射库资源*部分中所定义. 此文件应仅映射所依赖包的资源. -* 你还需要为你创建的包创建[bundle贡献者](Bundling-Minification.md). - -有关示例, 请参阅当前标准包. - -### 映射库资源 - -使用NPM包和NPM/Yarn工具是客户端库的事实标准. NPM/Yarn工具在Web项目的根文件夹中创建一个**node_modules**文件夹. - -下一个挑战是将所需的资源(js,css,img ...文件)从`node_modules`复制到**wwwroot**文件夹内的文件夹中,以使其可供客户端/浏览器访问. - -ABP将基于[Gulp](https://gulpjs.com/)的任务定义为**将资源**从**node_modules**复制到**wwwroot/libs**文件夹. 每个**标准包**(参见*@ABP NPM Packages*部分)定义了自己文件的映射. 因此, 大多数情况你只配置依赖项. - -**启动模板**已经配置为开箱即用的所有这些. 本节将介绍配置选项. - -#### 资源映射定义文件 - -模块应该定义一个名为`abp.resourcemapping.js`的JavaScript文件,其格式如下例所示: - -````js -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs" - ], - mappings: { - - } -} -```` - -* **aliases**部分定义了可在映射路径中使用的标准别名(占位符). **@node_modules**和 **@libs**是必需的(通过标准包), 你可以定义自己的别名以减少重复. -* **clean**部分是在复制文件之前要清理的文件夹列表. -* **mappings**部分是要复制的文件/文件夹的映射列表.此示例不会复制任何资源本身,但取决于标准包. - -示例映射配置如下所示: - -````js -mappings: { - "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" -} -```` - -#### 使用 Gulp - -正确配置`abp.resourcemapping.js`文件后, 可以从命令行运行gulp命令: - -```` -gulp -```` - -当你运行`gulp`时,所有包都会将自己的资源复制到**wwwroot/libs**文件夹中. 只有在**package.json**文件中对依赖项进行更改时, 才需要运行`yarn&gulp`. - -> 运行Gulp命令时, 使用package.json文件解析应用程序的依赖关系. Gulp任务自动发现并映射来自所有依赖项的所有资源(递归). - -#### 参见 - -* [捆绑 & 压缩](Bundling-Minification.md) -* [主题](Theming.md) +[点击链接跳转到ASP.NET Core MVC 客户端包管理文档](../UI/AspNetCore/Client-Side-Package-Management.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md index 331e66c18e..6f45ba4549 100644 --- a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md +++ b/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md @@ -1,165 +1,3 @@ -# 动态 C# API 客户端 +文档已经移动到其他位置. -ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 `HttpClient` 或者其他低级的HTTP功能调用远程服务并获取数据. - -## 服务接口 - -你的service或controller需要实现一个在服务端和客户端共享的接口.因此,首先需要在一个共享的类库项目中定义一个服务接口.例如: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -为了能自动被发现,你的接口需要实现`IRemoteService`接口.由于`IApplicationService`继承自`IRemoteService`接口.所以`IBookAppService`完全满足这个条件. - -在你的服务中实现这个类,你可以使用[auto API controller system](Auto-API-Controllers.md)将你的服务暴漏为一个REST API 端点. - -## 客户端代理生成 - -首先,将[Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget包添加到你的客户端项目中: - -```` -Install-Package Volo.Abp.Http.Client -```` - -然后给你的模块添加`AbpHttpClientModule`依赖: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //添加依赖 -public class MyClientAppModule : AbpModule -{ -} -```` - -现在,已经可以创建客户端代理了.例如: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //用来创建客户端代理 - typeof(BookStoreApplicationModule) //包含应用服务接口 - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //创建动态客户端代理 - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly - ); - } -} -```` - -`AddHttpClientproxies`方法获得一个程序集,找到这个程序集中所有的服务接口,创建并注册代理类. - -### Endpoint配置 - -`appsettings.json`文件中的`RemoteServices`节点被用来设置默认的服务地址.下面是最简单的配置: - -```` -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -```` - -查看下面的"RemoteServiceOptions"章节获取更多详细配置. - -## 使用 - -可以很直接地使用.只需要在你的客户端程序中注入服务接口: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -本例注入了上面定义的`IBookAppService`服务接口.当客户端调用服务方法的时候动态客户端代理就会创建一个HTTP调用. - -### IHttpClientProxy接口 - -你可以像上面那样注入`IBookAppService`来使用客户端代理,也可以注入`IHttpClientProxy`获取更多明确的用法.这种情况下你可以使用`IHttpClientProxy`接口的`Service`属性. - -## 配置 - -### RemoteServiceOptions - -默认情况下`AbpRemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### 多个远程服务端点 - -上面的例子已经配置了"Default"远程服务端点.你可能需要为不同的服务创建不同的端点.(就像在微服务方法中一样,每个微服务具有不同的端点).在这种情况下,你可以在你的配置文件中添加其他的端点: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies`方法有一个可选的参数来定义远程服务的名字: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" -); -```` - -`remoteServiceName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. - -### 作为默认服务 - -当你为`IBookAppService`创建了一个服务代理,你可以直接注入`IBookAppService`来使用代理客户端(像上面章节中将的那样).你可以传递`asDefaultService:false`到`AddHttpClientProxies`方法来禁用此功能. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - asDefaultServices: false -); -```` - -如果你的程序中已经有一个服务的实现并且你不想用你的客户端代理重写或替换其他的实现,就需要使用`asDefaultServices:false` - -> 如果你禁用了`asDefaultService`,你只能使用`IHttpClientProxy`接口去使用客户端代理.(参见上面的相关章节). \ No newline at end of file +[点击链接跳转到动态 C# API 客户端文档](../API/Dynamic-CSharp-API-Clients.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md index 60c9eb8866..857606a0f2 100644 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md +++ b/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md @@ -1,3 +1,3 @@ -# abp.auth JavaScript API +文档已经移动到其他位置. -TODO \ No newline at end of file +[点击链接跳转到JavaScript Auth文档](../../API/JavaScript-API/Auth.md) diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md index 87d3b3021d..9f3e4d1645 100644 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md +++ b/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md @@ -1,24 +1,3 @@ -# JavaScript API - -ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. - -## APIs - -* abp.ajax -* [abp.auth](Auth.md) -* abp.currentUser -* abp.dom -* abp.event -* abp.features -* abp.localization -* abp.log -* abp.ModalManager -* abp.notify -* abp.security -* abp.setting -* abp.ui -* abp.utils -* abp.ResourceLoader -* abp.WidgetManager -* Other APIs +文档已经移动到其他位置. +[点击链接跳转到JavaScript API文档](../../API/JavaScript-API/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md index ceaa3d3d69..71e25d25ea 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,3 @@ -## Dynamic Forms +文档已经移动到其他位置. -目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file +[点击链接跳转到Dynamic Forms文档](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md index 1f19d50701..96ca033863 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md @@ -1,3 +1,3 @@ -## ABP Tag Helpers +文档已经移动到其他位置. -"ABP tag helpers" 文档还在创建中. 你现在可以参阅[组件演示](http://bootstrap-taghelpers.abp.io/). \ No newline at end of file +[点击链接跳转到ABP Tag Helpers文档](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Theming.md b/docs/zh-Hans/AspNetCore/Theming.md index 470ef1a458..4a863567f1 100644 --- a/docs/zh-Hans/AspNetCore/Theming.md +++ b/docs/zh-Hans/AspNetCore/Theming.md @@ -1,3 +1,4 @@ -# Theming -TODO \ No newline at end of file +文档已经移动到其他位置. + +[点击链接跳转到Theming文档](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Widgets.md b/docs/zh-Hans/AspNetCore/Widgets.md index 0e75a24b6f..423169b6fc 100644 --- a/docs/zh-Hans/AspNetCore/Widgets.md +++ b/docs/zh-Hans/AspNetCore/Widgets.md @@ -1,274 +1,4 @@ -# 小部件 -ABP为创建**可重用的部件**提供了模型和基础设施. 部件系统是[ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components)的扩展. 在你有以下需求时,小部件会非常有用; +文档已经移动到其他位置. -* 在可复用的 **[模块](../Module-Development-Basics.md)** 中定义部件. -* 在部件中引用 **scripts & styles** 脚本. -* 使用部件创建 **[仪表盘](Dashboards.md)**. -* 支持 **[授权](../Authorization.md)** 与 **[捆绑`bundling`](Bundling-Minification.md)** 的部件 - -## 基本部件定义 - -### 创建一个视图组件 - -第一部,创建一个新的ASP.NET Core View Component: - -![widget-basic-files](../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -继承 `AbpViewComponent` 不是必需的. 你也可以继承ASP.NET Core的 `ViewComponent`. `AbpViewComponent` 只是定义了一些基本的实用属性. - -**Default.cshtml**: - -```xml -
-

My Simple Widget

-

This is a simple widget!

-
-``` - -### 定义部件 - -添加 `Widget` attribute 到 `MySimpleWidgetViewComponent` 类,将此视图组件标记为部件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 渲染部件 - -渲染部件的用法是ASP.NET Core的标准用法. 在razor view/page中使用 `Component.InvokeAsync` 方法, 就像渲染一个View Component一样. 例如: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -第一行代码使用名称渲染了部件,第二行代码使用type渲染了View Comonent. - -## 部件名称 - -默认下名称是根据View Conponent组件的名称计算的, 比如你的视图组件名是 `MySimpleWidgetViewComponent`, 那么部件的名称就是 `MySimpleWidget` (删除`ViewComponent`后缀). 这与ASP.NET Core的默认视图组件名称的方式一样. - -想要自定义组件名称,只需要使用ASP.NET Core的 `ViewComponent` attribute: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP会通过自定义的名称去处理部件. - -> 如果视图组件名与视图组件的文件夹名称不匹配,那么需要像本例中那样去手动编写视图路径. - -### 显示名称 - -你还可以定义对于使用者友好的本地化显示名称. 需要时在UI中使用显示名称. 显示名称是可选的,在 `Widget` attribute 的`DisplayName`属性中定义: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", //Localization key - DisplayNameResource = typeof(DashboardDemoResource) //localization resource - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -参阅 [本地化文档](../Localization.md) 学习关于本地化资源的更多内容. - -## 引用 Style & Script - -当部件含有样式和scirpt文件时,会存在一些挑战; - -* 使用部件的页面应该将 **script & styles** 文件引用到页面中. -* 页面还需要解析部件的 `依赖库/文件`. - -将资源与部件正确的关联在一起时,ABP会解决这些问题. 使用正确的方法,就不用担心部件的依赖关系. - -### 定义一个简单的文件路径 - -下面的示例中部件添加了样式和scirpt文件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP会考虑到这些依赖关系, 在view/page中使用正确的方法添加部件 . 样式和script可以是物理文件也可以是虚拟文件. 它于[虚拟文件系统](../Virtual-File-System.md)完全集成]. - -### 定义 Bundle - -页面中使用的组件的所有资源都做为捆绑包添加(如果没有其他配置,会在生产中合并和压缩). 除了简单的添加文件,你还可以充分的利用捆绑功能. - -下面的示例与上面的代码相同,但是在添加文件时文件路径替换成了 `BundleContributor`: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -捆绑系统非常强大,如果你的部件使用了JavaScript库来呈现图表, 你可以将它声明为依赖项, 如果之前未添加JavaScript库. 则会自动添加到页面中. 使用这种方式让页面使用部件时不用关心依赖项. - -参阅 [捆包&压缩 文档](Bundling-Minification.md) 了解更多内容. - -## 授权 - -某些组件可能只对通过身份验证或授权的用户可用,这时可以使用 `Widget` attribute 的以下属性: - -* `RequiresAuthentication` (`bool`): 设置为true,只有通过身份验证的用户(登录用户)可用. -* `RequiredPolicies` (`List`): 授权用户的策略名称列表. 有关策略的详细信息请参阅[授权文档](../Authorization.md). - -示例: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 部件选项 - -`AbpWidgetOptions` 是 `Widget` attribute 替代, 你可以使用它去配置部件: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -将上面的代码写到[模块](../Module-Development-Basics.md)的 `ConfigureServices` 方法中. `AbpWidgetOptions` 可以完成 `Widget` attribute 的所有功能. 比如为组件添加样式: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> 提示: `AbpWidgetOptions` 还可以更改现有的部件配置. 如果要修改应用程序使用的模块内的组件配置,这会很有用. 使用 `options.Widgets.Find` 获取现有的 `WidgetDefinition`. \ No newline at end of file +[点击链接跳转到小部件文档](../UI/AspNetCore/Widgets.md) \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index 0a15f6e2ca..7ae525e1f4 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -367,4 +367,5 @@ public override void ConfigureServices(ServiceConfigurationContext context) ## 接下来 * [权限管理模块](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor 页面 JavaScript Auth API](AspNetCore/JavaScript-API/Auth.md) +* [ASP.NET Core MVC / Razor 页面 JavaScript Auth API](API/JavaScript-API/Auth.md) +* [Angular界面中的权限管理](UI/Angular/Permission-Management.md) diff --git a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md index 1011b5a10a..7d633ee9e0 100644 --- a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md +++ b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md @@ -144,7 +144,7 @@ UI组合是主要目标之一.为此,主题系统将提供菜单,工具栏和其 这段代码通过包含bootstrap(及其依赖项,如果有)和另外两个css文件来动态创建一个新的样式包.这些文件在生产环境中捆绑和压缩,但将在开发环境中单独添加. -有关更多信息,请参阅[文档](https://github.com/abpframework/abp/blob/master/docs/AspNetCore/Bundling-Minification.md) +有关更多信息,请参阅[文档](https://github.com/abpframework/abp/blob/master/docs/UI/AspNetCore/Bundling-Minification.md) ### 分布式事件总线(Distributed Event Bus) diff --git a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md index dfb5a66123..831cc791e9 100644 --- a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -27,7 +27,7 @@ Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我 ### Widget系统 -[Widget系统](https://docs.abp.io/en/abp/latest/AspNetCore/Widgets)允许为ASP.NET Core MVC应用程序**定义和重用**Widget.Widget可能有自己的脚本和样式资源以及由ABP框架管理的第三方库的依赖关系. +[Widget系统](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets)允许为ASP.NET Core MVC应用程序**定义和重用**Widget.Widget可能有自己的脚本和样式资源以及由ABP框架管理的第三方库的依赖关系. ### 其他 diff --git a/docs/zh-Hans/Localization.md b/docs/zh-Hans/Localization.md index 28203410fb..fce4b14407 100644 --- a/docs/zh-Hans/Localization.md +++ b/docs/zh-Hans/Localization.md @@ -190,3 +190,7 @@ var testResource = abp.localization.getResource('Test'); ````js var str = testResource('HelloWorld'); ```` + +## See Also + +* [Angular UI中的本地化](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/Microservice-Architecture.md b/docs/zh-Hans/Microservice-Architecture.md index 3b96cdc7f6..856cfee034 100644 --- a/docs/zh-Hans/Microservice-Architecture.md +++ b/docs/zh-Hans/Microservice-Architecture.md @@ -12,8 +12,8 @@ ABP框架的主要目标之一就是提供**便捷的基础设施来创建微服 * 提供[架构模型](Best-Practices/Module-Architecture.md)来开发模块,与微服务开发和部署兼容. * 提供[最佳实践指南](Best-Practices/Index.md)制定模块开发标准. * 提供基础设施来实现微服务中的[领域驱动设计](Domain-Driven-Design.md). -* 提供从应用程序服务[自动生成REST风格的API](AspNetCore/Auto-API-Controllers.md)的服务. -* 提供[自动创建C#API客户端](AspNetCore/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. +* 提供从应用程序服务[自动生成REST风格的API](API/Auto-API-Controllers.md)的服务. +* 提供[自动创建C#API客户端](API/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. * 提供[分布式事件总线](Event-Bus.md)用于服务通信. * 提供更多其他服务,使日常开发更加简便. diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index f2a92334fb..20382d24f2 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -26,15 +26,19 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. 如果你没有现有的ABP项目, 这个步骤向你展示如何在[abp.io](https://abp.io)创建一个新项目并添加文档模块. 如果你本地已经有了一个ABP项目, 那么你可以跳过这一步. -打开 https://abp.io/Templates. 输入项目名称为 `Acme.MyProject`, 选择 `ASP.NET Core Mvc Application` 和选择 `Entity Framework Core` 做为数据库提供者. +推荐使用ABP CLI创建新项目,使用以下命令行: -请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. +`abp new Acme.MyProject` + +你也可以在浏览器中导航到 https://abp.io/get-started. 输入项目名称为 `Acme.MyProject`, 其它保持默认选项. + +请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. ![创建新项目](../images/docs-module_download-new-abp-project.png) ### 2- 运行这个空项目 -下载项目后, 解压压缩文档并且打开 `Acme.MyProject.sln`. 你可以看到这个解决方案包含了 `Application`, `Domain`, `EntityFrameworkCore` 和 `Web` 项目. 右键选择 `Acme.MyProject.Web` 项目**设置为启动项目**. +下载项目后, 解压压缩文档并且打开 `Acme.MyProject.sln`. 你可以看到这个解决方案包含了 `Application`, `Application.Contrawcts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `EntityFrameworkCore.DbMigations`, `HttpApi`, `HttpApi.Client` 和 `Web` 项目. 右键选择 `Acme.MyProject.Web` 项目**设置为启动项目**. ![创建新项目](../images/docs-module_solution-explorer.png) @@ -43,12 +47,12 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ```json { "ConnectionStrings": { - "Default": "Server=localhost;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" } } ``` -打开Visual Studio包管理控制台选择`src\Acme.MyProject.EntityFrameworkCore` 做为默认项目. 运行 `Update-Database` 命令创建数据库. 数据库`MyProject`将在数据库服务器中创建. +运行 `Acme.MyProject.DbMigrator` 项目,它会负责应用迁移与初始化种子数据. 数据库`MyProject`将在数据库服务器中创建. 现在一个空的ABP项目已经创建完成! 现在你可以运行项目并且查看网站. @@ -56,39 +60,28 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ### 2- 引用文档模块包 -文档模块包托管在Nuget上面. 需要有四个包安装到你的应用程序中. 每个包必须安装到相关的项目. - -* [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) 需要安装到 `Acme.MyProject.Domain` 项目. +文档模块包托管在Nuget上面. 需要有四个包安装到你的应用程序中. 每个包必须安装到相关的项目. - * 修改 `Acme.MyProject.Domain.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. +建议使用ABP CLI安装模块,在解决方案文件 (`.sln`) 目录打开 `CMD` 窗口,运行以下命令: - ```csharp - - ``` +`abp add-module Volo.Docs` -* [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) 需要安装到 `Acme.MyProject.EntityFrameworkCore` 项目. +或者你也可以手动安装nuget包到每个项目: - * 修改 `Acme.MyProject.EntityFrameworkCore.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. +* 安装[Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Domain` 项目. - ```csharp - - ``` + `Install-Package Volo.Docs.Domain` -* [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) 需要安装到 `Acme.MyProject.Application` 项目. +* 安装[Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget包到 `Acme.MyProject.EntityFrameworkCore` 项目. - * 修改 `Acme.MyProject.Application.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. + `Install-Package Volo.Docs.EntityFrameworkCore` - ```csharp - - ``` +* 安装[Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget包到 `Acme.MyProject.Application` 项目. -* [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Web/) 需要安装到 `Acme.MyProject.Web` 项目. + `Install-Package Volo.Docs.Application` - * 修改 `Acme.MyProject.Web.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. - - ```csharp - - ``` +* 安装[Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Web` 项目. + `Install-Package Volo.Docs.Web` ### 3- 添加模块依赖 @@ -176,37 +169,58 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. #### 4.1- Entity Framework 集成 -如果你选择了Entity Framework 做为数据库供应者,你需要在DbContext中配置文档模块. 做以下操作; +如果你选择了Entity Framework 做为数据库供应者,你需要配置文档模块. 做以下操作; -* 打开 `MyProjectDbContext.cs` 并且添加 `modelBuilder.ConfigureDocs()` 到 `OnModelCreating()` 方法中 +* 打开 `MyProjectMigrationsDbContext.cs` 并且添加 `builder.ConfigureDocs()` 到 `OnModelCreating()` 方法中 ```csharp - [ConnectionStringName("Default")] - public class MyProjectDbContext : AbpDbContext - { - public MyProjectDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - //... - modelBuilder.ConfigureDocs(); - } - } + public class MyProjectMigrationsDbContext : AbpDbContext + { + public MyProjectMigrationsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + builder.ConfigureDocs(); //Add this line to configure the Docs Module + + /* Configure customizations for entities from the modules included */ + + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* Configure your own tables/entities inside the ConfigureQaDoc method */ + + builder.ConfigureMyProject(); + } + } ``` -* 打开 `Visual Studio` 的 `包管理控制台` 选择 `Acme.MyProject.EntityFrameworkCore` 做为默认项目. 然后编写以下命令为文档模块添加迁移. +* 打开 `Visual Studio` 的 `包管理控制台` 选择 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 做为默认项目. 然后编写以下命令为文档模块添加迁移. ```csharp add-migration Added_Docs_Module ``` - 当命令执行成功后 , 你会看到`Acme.MyProject.EntityFrameworkCore\Migrations` 目录下有名为 `20181221111621_Added_Docs_Module` 的迁移文件. + 当命令执行成功后 , 你会看到`Acme.MyProject.EntityFrameworkCore.DbMigrations\Migrations` 目录下有名为 `20181221111621_Added_Docs_Module` 的迁移文件. - 现在更新数据库. 在 `Visual Studio` 的 `包管理控制台` 中执行以下代码. 要确认已 `Acme.MyProject.EntityFrameworkCore` 项目设置为默认项目. + 现在更新数据库. 在 `Visual Studio` 的 `包管理控制台` 中执行以下代码. 要确认已 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 项目设置为默认项目. ```csharp update-database @@ -309,7 +323,7 @@ There are no projects yet! - ExtraProperties: ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***"} + {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***","GitHubUserAgent":""} ``` 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ @@ -321,11 +335,13 @@ There are no projects yet! 对于 `SQL` 数据库,你可以使用下面的 `T-SQL` 命令将指定的示例插入到 `DocsProjects` 表中: ```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}', N'/', N'master', N'') +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` 请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. +现在你可以运行应用程序并导航到 `/Documents`. + #### "FileSystem" 项目的示例记录 你可以使用 [ABP Framework](https://github.com/abpframework/abp/) GitHub文档来配置你的文件系统存储. @@ -556,6 +572,30 @@ This document assumes that you prefer to use **{{ UI_Value }}** as the UI framew 最后,为您的项目添加了一个新的Docs模块, 该模块由GitHub提供. +## 全文搜索(Elastic Search) + +文档模块支持使用Elastic Search对内容进行全文搜索. 默认没有启用, 你可以配置`DocsElasticSearchOptions`启用它. + +``` +Configure(options => +{ + options.Enable = true; + options.IndexName = "your_index_name"; //default IndexName is abp_documents +}); +``` + +应用程序启动后如果`Index`不存在则会自动创建`Index`. + +`DefaultElasticClientProvider`负责创建`IElasticClient`, 默认情况下它会从`IConfiguration`中读取Elastic Search的`Url`. +如果你的IElasticClient需要其它配置请使用重写IElasticClientProvider服务并在依赖注入系统中替换它. +``` +{ + "ElasticSearch": { + "Url": "http://localhost:9200" + } +} +``` + ## 下一步 文档模块也可以做为独立的应用程序. 查看 [VoloDocs](../Apps/VoloDocs). \ No newline at end of file diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md index 1cfa2996d2..8913c27ede 100644 --- a/docs/zh-Hans/Multi-Tenancy.md +++ b/docs/zh-Hans/Multi-Tenancy.md @@ -343,8 +343,8 @@ namespace MyCompany.MyProject { Configure(options => { - //子域名格式: {0}.mydomain.com (作为最高优先级解析器添加) - options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com")); + //子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后) + options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com")); }); //... @@ -355,7 +355,7 @@ namespace MyCompany.MyProject {0}是用来确定当前租户唯一名称的占位符. -你可以使用下面的方法,代替``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));``: +你可以使用下面的方法,代替``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));``: ````C# options.AddDomainTenantResolver("{0}.mydomain.com"); diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index aaa780188d..9b559797f7 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -24,82 +24,15 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 下图展示了该系统: -![microservice-sample-diagram](../images/microservice-sample-diagram-2.png) +![microservice-sample-diagram](../images/microservice-sample-diagram-3.png) ### 源码 你可以从[GitHub仓库](https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo)获取源码. -### 状态 - -该示例的初始版本已完成.其他改进仍在开发中. - ## 运行解决方案 -您可以从 **源代码** 或者预先配置好的 **docker-compose** 文件运行. - -### 使用Docker容器 - -#### 预先要求 - -由于所有依赖项都已预先配置, 因此作为Docker容器运行更容易. 你只需要安装[最新的docker](https://docs.docker.com/compose/install/). - -#### 运行容器 - -- 克隆或下载 [ABP仓库](https://github.com/abpframework/abp). - -- 在存储库的`samples/MicroserviceDemo`文件夹中打开命令行. - -- 从Docker Hub中拉取image: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml pull - ``` - -- 如果要在本地构建映像, 可以跳过上述步骤, 使用build命令: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml build - ``` - - 根据你的电脑配置, 构建image可能需要**很长时间**. - -- 还原 SQL Server 数据库: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database - ``` - -- 启动容器: - - ``` - docker-compose up -d - ``` - -- 将此行添加到`hosts`文件的末尾: - - ``` - 127.0.0.1 auth-server - ``` - - hosts文件位于Windows上的`C:\Windows\System32\Drivers\etc\hosts`文件夹, Linux/MacOS的`/etc/hosts`中. - -#### 运行应用程序 - -你可能想要了解容器中运行一些应用程序: - -* 后端管理应用程序 (BackendAdminApp.Host): `http://localhost:51512` - *(用于管理系统中的用户和产品)* -* 公共网站 (PublicWebsite.Host): `http://localhost:51513` - *(用于列出产品并运行/管理博客模块)* -* 认证服务器 (AuthServer.Host): `http://auth-server:51511/` - *(用作使用IdentityServer4构建的单点登录和身份验证服务器)* -* Kibana UI: `http://localhost:51510` - *(用于显示/跟踪所有服务/应用程序/网关写入的日志)* - -### 从源代码运行 - -#### 预先要求 +### 预先要求 为了能够从源代码运行解决方案, 应在你的计算机上安装并运行以下工具: @@ -110,19 +43,19 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 * [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ * [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (可选,建议显示日志) -#### 打开并构建Visual Studio解决方案 +### 打开并构建Visual Studio解决方案 * 在Visual Studio 2017 (15.9.0+)中打开`samples\MicroserviceDemo\MicroserviceDemo.sln`. * 在`samples\MicroserviceDemo`文件夹中的命令行运行`dotnet restore`命令. * 在Visual Studio中构建解决方案. -#### 创建数据库 +### 创建数据库 MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构。其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移。 这个解决方案中有两个 SQL server 数据库。 -##### MsDemo_Identity 数据库 +#### MsDemo_Identity 数据库 * 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) @@ -131,7 +64,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 ![microservice-sample-update-database-authserver](../images/microservice-sample-update-database-authserver.png) -##### MsDemo_ProductManagement +#### MsDemo_ProductManagement * 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) @@ -140,12 +73,13 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 ![microservice-sample-update-database-products](../images/microservice-sample-update-database-products.png) -#### 运行项目 +### 运行项目 按以下顺序运行项目(右键单击每个项目设置为启动项目,按Ctrl+F5运行,无需调试): * AuthServer.Host * IdentityService.Host +* TenantManagementService.Host * BloggingService.Host * ProductService.Host * InternalGateway.Host @@ -158,7 +92,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 Visual Studio解决方案由多个项目组成,每个项目在系统中具有不同的角色: -![microservice-sample-solution](../images/microservice-sample-solution.png) +![microservice-sample-solution](../images/microservice-sample-solution-2.png) ### 应用程序(Applications) @@ -182,6 +116,7 @@ Visual Studio解决方案由多个项目组成,每个项目在系统中具有不 微服务没有UI,但暴露了一些REST API. - **IdentityService.Host**: 托管用于管理用户和角色的ABP Identity模块. 它没有其他服务,仅托管Identity模块的API. +- **TenantManagementService.Host**: 托管用于管理角色的ABP租户管理模块. 它没有其他服务,仅托管租户管理模块的API. - **BloggingService.Host**: 托管ABP博客模块,该模块用于管理博客和帖子(典型的博客应用程序). 它没有其他服务,仅托管Blogging模块的API. - **ProductService.Host**: 托管用于管理产品的产品模块(位于解决方案内). 它还包含用于创建/更新产品管理数据库架构的EF Core迁移. @@ -193,7 +128,7 @@ Visual Studio解决方案由多个项目组成,每个项目在系统中具有不 此解决方案使用多个数据库: -* **MsDemo_Identity**: 一个SQL数据库. 默认使用**SQL Server**,但可以是EF Core支持的任何DBMS. 由AuthServer和IdentityService共享. 审计日志,权限和设置也存储在此数据库中(虽然它们可以轻松拥有自己的数据库,共享相同的数据库以保持简单). +* **MsDemo_Identity**: 一个SQL数据库. 默认使用**SQL Server**,但可以是EF Core支持的任何DBMS. 由AuthServer,IdentityService和TenantManagementService共享. 审计日志,权限和设置也存储在此数据库中(虽然它们可以轻松拥有自己的数据库,共享相同的数据库以保持简单). * **MsDemo_ProductManagement**: 一个SQL数据库. 同样默认使用 **SQL Server**,但可以是EF Core支持的任何DBMS. 由ProductService用作专用数据库. * **MsDemo_Blogging**: **MongoDB**数据库. 由BloggingService使用. * **Elasticsearch**: 用于在Serilog上写日志. @@ -341,7 +276,7 @@ BackendAdminApp.Host项目本身没有单个UI元素/页面. 它仅用于提供 ##### HTTP Clients -ABP应用程序模块通常提供C#客户端库以轻松地使用服务(API)(它们通常使用ABP框架的[Dynamic C# API客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)). 这意味着如果你需要使用Identity Service API, 你可以引用其客户端软件包,并通过提供的接口轻松使用API. +ABP应用程序模块通常提供C#客户端库以轻松地使用服务(API)(它们通常使用ABP框架的[Dynamic C# API客户端](../API/Dynamic-CSharp-API-Clients.md)). 这意味着如果你需要使用Identity Service API, 你可以引用其客户端软件包,并通过提供的接口轻松使用API. 为此`BackendAdminAppHostModule`类声明了`AbpIdentityHttpApiClientModule`和`ProductManagementHttpApiClientModule`的依赖关系. @@ -1104,7 +1039,7 @@ ABP提供强大的基础架构,通过提供服务和架构,使模块化应用程 * `ProductManagement.Application` 包含应用程序服务的实现. * `ProductManagement.EntityFrameworkCore` 包含DbContext和其他与EF Core相关的类和配置. * `ProductManagement.HttpApi` 包含API控制器. -* `ProductManagement.HttpApi.Client` 包含C#代理以远程直接使用HTTP API. 使用ABP的[Dynamic C#API客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)功能. +* `ProductManagement.HttpApi.Client` 包含C#代理以远程直接使用HTTP API. 使用ABP的[Dynamic C#API客户端](../API/Dynamic-CSharp-API-Clients.md)功能. * `ProductManagement.Web` 包含UI元素(页面,脚本,样式..等). @@ -1474,3 +1409,7 @@ ABP提供自动审计日志记录,详细保存每个请求(当前用户,浏览 所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,您可以从单个点查询所有应用程序的所有审核日志. 审核日志记录具有`CorrelationId`属性,可用于跟踪请求. 当服务在单个Web请求中调用另一个服务时,它们都会使用相同的`CorrelationId`保存审核日志. 请参阅数据库中的`AbpAuditLogs`表. + +### 多租户 + +该解决方案已配置提供[多租户](../Multi-Tenancy.md)系统,其中每个租户可以拥有其隔离的用户,角色,权限和其他数据. \ No newline at end of file diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 2a0263d43a..53e826ab7c 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -142,7 +142,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 用于定义API控制器. -大多数情况下,你不需要手动定义API控制器,因为ABP的[动态API](../AspNetCore/Auto-API-Controllers.md)功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方. +大多数情况下,你不需要手动定义API控制器,因为ABP的[动态API](../API/Auto-API-Controllers.md)功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方. * 它依赖 `.Application.Contracts` 项目,因为它需要注入应用服务接口. @@ -150,7 +150,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 定义C#客户端代理使用解决方案的HTTP API项目. 可以将上编辑共享给第三方客户端,使其轻松的在DotNet应用程序中使用你的HTTP API(其他类型的应用程序可以手动或使用其平台的工具来使用你的API). -ABP有[动态 C# API 客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)功能,所以大多数情况下你不需要手动的创建C#客户端代理. +ABP有[动态 C# API 客户端](../API/Dynamic-CSharp-API-Clients.md)功能,所以大多数情况下你不需要手动的创建C#客户端代理. `.HttpApi.Client.ConsoleTestApp` 项目是一个用于演示客户端代理用法的控制台应用程序. diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md index afb1afc069..176ccbc6af 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md @@ -24,16 +24,16 @@ ![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) -> 你可以查看[应用程序模板文档](../../../Startup-Templates/Application.md)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. +> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. ### 创建Book实体 启动模板中的域层分为两个项目: - - `Acme.BookStore.Domain`包含你的[实体](../../../Entities.md), [领域服务](../../../Domain-Services.md)和其他核心域对象. + - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](../../../Entities.md). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: ````C# using System; @@ -66,7 +66,7 @@ namespace Acme.BookStore } ```` -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](../../../Entities.md). +* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). * `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). * `Guid`是`Book`实体的主键类型. * 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). @@ -166,7 +166,7 @@ namespace Acme.BookStore } ```` -* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](../../../Data-Transfer-Objects.md)查看更多信息. +* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. * 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. * `BookDto`继承自 `AuditedEntityDto`.跟上面定义的`Book`类一样具有一些审计属性. @@ -217,7 +217,7 @@ namespace Acme.BookStore ```` * 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. -* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](../../../Validation.md). +* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). 就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: @@ -281,12 +281,12 @@ namespace Acme.BookStore ```` * `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. -* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](../../../Repositories.md) +* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) * `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. ### 自动生成API Controllers -你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](../../../AspNetCore/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器. +你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. #### Swagger UI @@ -392,7 +392,7 @@ context.Menu.AddItem( } ```` -* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](../../../Localization.md). +* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). * 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). 运行该应用程序,看到新菜单项已添加到顶部栏: @@ -437,8 +437,8 @@ context.Menu.AddItem( ```` -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../../../AspNetCore/Bundling-Minification.md)获取更多信息. -* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../../../AspNetCore/Tag-Helpers/Index.md). +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. +* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). * 你可以像上面本地化菜单一样 **本地化** 列名. #### 添加脚本文件 diff --git a/docs/zh-Hans/UI/Angular/AddingSettingTab.md b/docs/zh-Hans/UI/Angular/AddingSettingTab.md new file mode 100644 index 0000000000..c46a559957 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/AddingSettingTab.md @@ -0,0 +1,3 @@ +## Creating a Settings Tab + +TODO... diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md new file mode 100644 index 0000000000..d2369901dc --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -0,0 +1,3 @@ +# Component Replacement + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Customization-User-Interface.md b/docs/zh-Hans/UI/Angular/Customization-User-Interface.md new file mode 100644 index 0000000000..c86d967ec9 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Customization-User-Interface.md @@ -0,0 +1,3 @@ +# Angular用户界面自定义指南 + +* [替换组件](Component-Replacement.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Localization.md b/docs/zh-Hans/UI/Angular/Localization.md new file mode 100644 index 0000000000..c8e836a177 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Localization.md @@ -0,0 +1,3 @@ +# Localization + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Permission-Management.md b/docs/zh-Hans/UI/Angular/Permission-Management.md new file mode 100644 index 0000000000..c562f7e9a7 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Permission-Management.md @@ -0,0 +1,3 @@ +# Permission Management + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/images/component-replacement.gif b/docs/zh-Hans/UI/Angular/images/component-replacement.gif new file mode 100644 index 0000000000..3a88500e53 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/component-replacement.gif differ diff --git a/docs/zh-Hans/UI/Angular/images/custom-settings.png b/docs/zh-Hans/UI/Angular/images/custom-settings.png new file mode 100644 index 0000000000..32d7516849 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/custom-settings.png differ diff --git a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md new file mode 100644 index 0000000000..c0f999f996 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md @@ -0,0 +1,354 @@ + +## ASP.NET Core MVC 捆绑 & 压缩 + +有许多方法可以捆绑&压缩客户端资源(JavaScript和CSS文件). 最常见的方式是: + +* 使用Visual Studio[捆绑&压缩](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier)扩展或者其它的[NuGet相关包](https://www.nuget.org/packages/BuildBundlerMinifier/). + +* 使用[Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/)及其插件. + +ABP内置了简单,动态,强大,模块化的方式. + +### Volo.Abp.AspNetCore.Mvc.UI.Bundling 包 + +> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. + +将`Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget包安装到你的项目中: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +然后将`AbpAspNetCoreMvcUiBundlingModule`依赖项添加到你的模块上: + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +### Razor Bundling Tag Helpers + +创建bundle的最简单方法是使用`abp-script-bundle`或`abp-style-bundle` tag helpers. 例如: + +````html + + + + + + +```` + +`abp-script-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: + +* 当首次请求时,ABP从提供的文件中 **(延迟)lazy** 创建. 后续将从 **缓存** 中返回内容. 这意味着如果你有条件地将文件添加到包中,它只执行一次, 并且条件的任何更改都不会影响下一个请求的包. +* 在`development`环境中ABP会将包文件**单独**添加到页面中, 其他环境(`staging`,`production`...)会自动捆绑和压缩. +* 捆绑文件可以是**物理**文件或[**虚拟/嵌入**](../../Virtual-File-System.md)的文件. +* ABP自动将 **版本查询字符串(version query string)** 添加到捆绑文件的URL中,以防止浏览器缓存. 如:?_v=67872834243042(从文件的上次更改日期生成). 即使捆绑文件单独添加到页面(在`development`环境中), 版本控制仍然有效. + +#### 导入 Bundling Tag Helpers + +> 默认情况下已在启动模板导入. 大多数情况下,你不需要手动安装它. + +要使用`bundle tag helpers`, 你需要将其添加到`_ViewImports.cshtml`文件或页面中: + +```` +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +#### 未命名的 Bundles + +对于razor bundle tag helpers, `name`是**可选**. 如果没有定义一个名字,它将根据使用的捆绑文件名自动**计算生成**(they are **concatenated** and **hashed**) 例: + +````html + + + + + @if (ViewBag.IncludeCustomStyles != false) + { + + } + +```` + +这将潜在地创建**两个不同的bundles**(一个包括`my-global-style.css`而另一个则不包括). + +**未命名的** bundles优点: + +* 可以**有条件地将项目**添加到捆绑包中. 但这可能会导致基于条件的捆绑的存在多种变化. + +**命名** bundles优点: + +* 其他模块可以通过其名称为捆绑包做出贡献(参见下面的部分). + +#### 单个文件 + +如果你只需要在页面中添加一个文件, 你可以使用`abp-script`或`abp-style`而不需要包含在`abp-script-bundle`或`abp-style-bundle`中. 例: + +````xml + +```` + +对于上面的示例,包名称将是 *scripts.my-scripts*("/"替换为"."). 所有捆绑功能也可以按预期应用于单个文件. + +### Bundling 选项 + +如果你需要在 **多个页面中使用相同的包** 或想要使用更多 **强大功能**, 你可以在[模块](../../Module-Development-Basics.md)类中进行**配置**. + +#### 创建一个新的捆绑包 + +用法示例: + +````C# +[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] +public class MyWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Add("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/libs/jquery/jquery.js", + "/libs/bootstrap/js/bootstrap.js", + "/libs/toastr/toastr.min.js", + "/scripts/my-global-scripts.js" + ); + }); + }); + } +} +```` + +> 你可以在脚本和样式包中使用相同的名称(*MyGlobalBundle*), 因为它们被添加到不同的集合(`ScriptBundles`和`StyleBundles`). + +在定义bundle之后, 可以使用上面定义的相同tag helpers将其包括在页面中. 例如: + +````html + +```` + +这次tag helper定义中没有定义文件, 因为捆绑文件是由代码定义的. + +#### 配置现有的 Bundle + +ABP也支持[模块化](../../Module-Development-Basics.md)捆绑. 模块可以修改由依赖模块创建的捆绑包. +例如: + +````C# +[DependsOn(typeof(MyWebModule))] +public class MyWebExtensionModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/scripts/my-extension-script.js" + ); + }); + }); + } +} +```` + +> 无法通过代码配置未命名的bundle tag helpers, 因为它们的名称在开发时是未知的. 建议始终使用bundle tag helper的名称. + +### Bundle 贡献者 + +将文件添加到现有bundle似乎很有用. 如果你需要**替换**bundle中的文件或者你想**有条件地**添加文件怎么办? 定义bundle贡献者可为此类情况提供额外的功能. + +一个bundle的贡献者使用自定义版本bootstrap.css替换示例: + +````C# +public class MyExtensionGlobalStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.ReplaceOne( + "/libs/bootstrap/css/bootstrap.css", + "/styles/extensions/bootstrap-customized.css" + ); + } +} +```` + +然后你可以按照下面的代码使用这个贡献者: + +````C# +services.Configure(options => +{ + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); + }); +}); +```` + +贡献者也可以在bundle tag helpers中使用. +例如: + +````xml + + + + + +```` + +`abp-style`和`abp-script`标签可以使用`type`属性(而不是`src`属性), 如本示例所示. 添加bundle贡献者时, 其依赖关系也会自动添加到bundle中. + +#### 贡献者依赖关系 + +bundle贡献者可以与其他贡献者具有一个或多个依赖关系. +例如: + +````C# +[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +添加bundle贡献者时,其依赖关系将 **自动并递归** 添加. **依赖顺序** 通过阻止 **重复** 添加的依赖关系. 即使它们处于分离的bundle中,也会阻止重复. ABP在页面中组织所有bundle并消除重复. + +创建贡献者和定义依赖关系是一种跨不同模块组织bundle创建的方法. + + +#### 贡献者扩展 + +在某些高级应用场景中, 当用到一个bundle贡献者时,你可能想做一些额外的配置. 贡献者扩展可以和被扩展的贡献者无缝衔接. + +下面的示例为 prism.js 脚本库添加一些样式: + +````csharp +public class MyPrismjsStyleExtension : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); + } +} +```` + +然后你可以配置 `BundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`. + +````csharp +Configure(options => +{ + options + .Extensions() + .Add(); +}); +```` + +任何时候当 `PrismjsStyleBundleContributor` 被添加到bundle中时, `MyPrismjsStyleExtension` 也会被自动添加. + +#### 访问 IServiceProvider + +虽然很少需要它, 但是`BundleConfigurationContext`有一个`ServiceProvider`属性, 你可以在`ConfigureBundle`方法中解析服务依赖. + +#### 标准包装贡献者 + +将特定的NPM包资源(js,css文件)添加到包中对于该包非常简单. 例如, 你总是为bootstrap NPM包添加`bootstrap.css`文件. + +所有[标准NPM包](Client-Side-Package-Management.md)都有内置的贡献者. 例如,如果你的贡献者依赖于bootstrap,你可以声明它,而不是自己添加bootstrap.css. + +````C# +[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +使用标准包的内置贡献者: + +* 防止你输入**无效的资源路径**. +* 如果资源 **路径发生变化** (依赖贡献者将处理它),则防止更改你的贡献者. +* 防止多个模块添加**重复文件**. +* 以递归方式管理依赖项(如果需要,添加依赖项的依赖项). + +##### Volo.Abp.AspNetCore.Mvc.UI.Packages 包 + +> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. + +标准包贡献者在`Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet包中定义. +将它安装到你的项目中: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Packages +```` + +然后将`AbpAspNetCoreMvcUiPackagesModule`模块依赖项添加到你的模块中; + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +#### Bundle 继承 + +在某些特定情况下, 可能需要从其他bundle创建一个 **新** bundle **继承**, 从bundle继承(递归)会继承该bundle的所有文件/贡献者. 然后派生的bundle可以添加或修改文件/贡献者**而无需修改**原始bundle. +例如: + +````c# +services.Configure(options => +{ + options + .StyleBundles + .Add("MyTheme.MyGlobalBundle", bundle => { + bundle + .AddBaseBundles("MyGlobalBundle") //Can add multiple + .AddFiles( + "/styles/mytheme-global-styles.css" + ); + }); +}); +```` + +### 主题 + +主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). + +### 最佳实践 & 建议 + +建议为应用程序定义多个包, 每个包用于不同的目的. + +* **全局包**: 应用程序中的每个页面都包含全局样式/脚本包. 主题已经定义了全局样式和脚本包. 你的模块可以为他们做出贡献. +* **布局包**: 这是针对单个布局的特定包. 仅包含在所有页面之间共享的资源使用布局. 使用bundling tag helpers创建捆绑包是一种很好的做法. +* **模块包**: 用于单个模块页面之间的共享资源. +* **页面包**: 为每个页面创建的特定包. 使用bundling tag helpers创建捆绑包作为最佳实践. + +在性能,网络带宽使用和捆绑包的数量之间建立平衡. + +### 参见 + +* [客户端包管理](Client-Side-Package-Management.md) +* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md new file mode 100644 index 0000000000..2c50064be0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md @@ -0,0 +1,115 @@ + +## ASP.NET Core MVC 客户端包管理 + +ABP框架可以与任何类型的客户端包管理系统一起使用. 甚至你可以决定不使用包管理系统并手动管理依赖项. + +但是, ABP框架最适用于**NPM/Yarn**. 默认情况下,内置模块配置为与NPM/Yarn一起使用. + +最后, 我们建议[**Yarn**](https://classic.yarnpkg.com/)而不是NPM,因为它更快,更稳定并且与NPM兼容. + +### @ABP NPM Packages + +ABP是一个模块化平台. 每个开发人员都可以创建模块, 模块应该在**兼容**和**稳定**状态下协同工作. + +一个挑战是依赖NPM包的**版本**. 如果两个不同的模块使用相同的JavaScript库但其不同(并且可能不兼容)的版本会怎样. + +为了解决版本问题, 我们创建了一套**标准包**, 这取决于一些常见的第三方库. 一些示例包是[@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@ abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap)和[@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). 你可以从[Github存储库](https://github.com/volosoft/abp/tree/master/npm/packs)中查看**列表**. + +**标准包**的好处是: + +* 它取决于包装的**标准版本**. 取决于此包是**安全**,因为所有模块都依赖于相同的版本. +* 它包含将库资源(js,css,img...文件)从**node_modules**文件夹复制到**wwwroot/libs**文件夹的gulp任务. 有关更多信息, 请参阅 *映射库资源* 部分. + +依赖标准包装很容易. 只需像往常一样将它添加到**package.json**文件中. 例如: + +```` + { + ... + "dependencies": { + "@abp/bootstrap": "^1.0.0" + } + } +```` + +建议依赖于标准软件包, 而不是直接依赖于第三方软件包. + +#### 安装包 + +依赖于NPM包后, 你应该做的就是从命令行运行**yarn**命令来安装所有包及其依赖项: + +```` +yarn +```` + +虽然你可以使用`npm install`,但如前所述,建议使用[Yarn](https://classic.yarnpkg.com/). + +#### 贡献包 + +如果你需要不在标准软件包中的第三方NPM软件包,你可以在Github[repository](https://github.com/volosoft/abp)上创建Pull请求. 接受遵循这些规则的拉取请求: + +* 对于NPM上的`package-name`, 包名称应该命名为`@abp/package-name`(例如:`bootstrap`包的`@abp/bootstrap`). +* 它应该是**最新的稳定**版本的包. +* 它应该只依赖于**单个**第三方包. 它可以依赖于多个`@abp/*`包. +* 包应包含一个`abp.resourcemapping.js`文件格式,如*映射库资源*部分中所定义. 此文件应仅映射所依赖包的资源. +* 你还需要为你创建的包创建[bundle贡献者](Bundling-Minification.md). + +有关示例, 请参阅当前标准包. + +### 映射库资源 + +使用NPM包和NPM/Yarn工具是客户端库的事实标准. NPM/Yarn工具在Web项目的根文件夹中创建一个**node_modules**文件夹. + +下一个挑战是将所需的资源(js,css,img ...文件)从`node_modules`复制到**wwwroot**文件夹内的文件夹中,以使其可供客户端/浏览器访问. + +ABP将基于[Gulp](https://gulpjs.com/)的任务定义为**将资源**从**node_modules**复制到**wwwroot/libs**文件夹. 每个**标准包**(参见*@ABP NPM Packages*部分)定义了自己文件的映射. 因此, 大多数情况你只配置依赖项. + +**启动模板**已经配置为开箱即用的所有这些. 本节将介绍配置选项. + +#### 资源映射定义文件 + +模块应该定义一个名为`abp.resourcemapping.js`的JavaScript文件,其格式如下例所示: + +````js +module.exports = { + aliases: { + "@node_modules": "./node_modules", + "@libs": "./wwwroot/libs" + }, + clean: [ + "@libs" + ], + mappings: { + + } +} +```` + +* **aliases**部分定义了可在映射路径中使用的标准别名(占位符). **@node_modules**和 **@libs**是必需的(通过标准包), 你可以定义自己的别名以减少重复. +* **clean**部分是在复制文件之前要清理的文件夹列表. +* **mappings**部分是要复制的文件/文件夹的映射列表.此示例不会复制任何资源本身,但取决于标准包. + +示例映射配置如下所示: + +````js +mappings: { + "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", + "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" +} +```` + +#### 使用 Gulp + +正确配置`abp.resourcemapping.js`文件后, 可以从命令行运行gulp命令: + +```` +gulp +```` + +当你运行`gulp`时,所有包都会将自己的资源复制到**wwwroot/libs**文件夹中. 只有在**package.json**文件中对依赖项进行更改时, 才需要运行`yarn&gulp`. + +> 运行Gulp命令时, 使用package.json文件解析应用程序的依赖关系. Gulp任务自动发现并映射来自所有依赖项的所有资源(递归). + +#### 参见 + +* [捆绑 & 压缩](Bundling-Minification.md) +* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md b/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md new file mode 100644 index 0000000000..49b334c6c0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md @@ -0,0 +1,3 @@ +# Layout Hooks + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md b/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md new file mode 100644 index 0000000000..4e0164ec72 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md @@ -0,0 +1,3 @@ +# Navigation Menu + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..76307ad820 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md @@ -0,0 +1,86 @@ +# 按钮 + +## 介绍 + +`abp-button` 是创建按钮的主要元素. + +基本用法: + +````xml +Click Me +```` + +## Demo + +参阅 [按钮Demo页面](https://bootstrap-taghelpers.abp.io/Components/Buttons) 查看示例. + +## Attributes + +### `button-type` + +指定按钮的主样式/类型. 应为以下值之一: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Outline_Primary` +* `Outline_Secondary` +* `Outline_Success` +* `Outline_Danger` +* `Outline_Warning` +* `Outline_Info` +* `Outline_Light` +* `Outline_Dark` +* `Link` + +### `size` + +指定按钮的大小. 应为以下值之一: + +* `Default` +* `Small` +* `Medium` +* `Large` +* `Block` +* `Block_Small` +* `Block_Medium` +* `Block_Large` + +### `busy-text` + +当按钮busy时显示的文本. + +### `text` + +按钮的文本. 如果你只想为为按钮设置文本,这是一种快捷方式. 例: + +````xml + +```` + +在这个示例中,你可以使用 self-closing 标签将其缩短. + +### `icon` + +设置按钮的图标. 默认情况下它使用[Font Awesome](https://fontawesome.com/)图标库. 例: + +````xml + +```` + +### `icon-type` + +如果你不想使用font-awesome,你有两个选项: + +1. 设置 `icon-type` 为 `Other`,并为你的按钮编写图标样式. +2. 如果你不使用图标,请手动使用opening和closing标签,并在标签内写任何代码. + +### `disabled` + +设置为 `true` 禁用按钮. \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md new file mode 100644 index 0000000000..ceaa3d3d69 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -0,0 +1,3 @@ +## Dynamic Forms + +目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..acbb9e872b --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,26 @@ +# ABP Tag Helpers + +ABP框架定义了一组**标签助手组件**. 简化开发ASP.NET Core (MVC / Razor Pages) 应用程序界面. + +## bootstrap 组件包装 + +大多数标签助手是[Bootstrap](https://getbootstrap.com/) (v4+)的包装. 编写bootstrap代码并不是那么简单,其中包含太多的重复HTML标签并且也没有类型安全. ABP标签助手使其 **简单** 并且 **类型安全**. + +我们的目标并不是100%的包装bootstrap组件. 仍然可以编写 **原生bootstrap代码** (实际上标签助手生成的也是原生的bootstrap代码), 但我们建议尽量使用标签助手. + +ABP框架还向标准bootstrap组件添加了一些**实用的功能**. + +这里是ABP框架包装的组件列表: + +* [Buttons](Buttons.md) +* ... + +> 在为所有的标签助手完成文档之前,你可以访问 https://bootstrap-taghelpers.abp.io/ 查看在线示例. + +## 表单元素 + +参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). + +## 动态表单 + +参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/fa-address-card.png b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/fa-address-card.png similarity index 100% rename from docs/en/AspNetCore/Tag-Helpers/fa-address-card.png rename to docs/zh-Hans/UI/AspNetCore/Tag-Helpers/fa-address-card.png diff --git a/docs/zh-Hans/UI/AspNetCore/Theming.md b/docs/zh-Hans/UI/AspNetCore/Theming.md new file mode 100644 index 0000000000..10486dd6c5 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Theming.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages 主题 + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Toolbars.md b/docs/zh-Hans/UI/AspNetCore/Toolbars.md new file mode 100644 index 0000000000..cc3bcd95be --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Toolbars.md @@ -0,0 +1,3 @@ +# Toolbars + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Widgets.md b/docs/zh-Hans/UI/AspNetCore/Widgets.md new file mode 100644 index 0000000000..5f86595ba2 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Widgets.md @@ -0,0 +1,274 @@ +# 小部件 + +ABP为创建**可重用的部件**提供了模型和基础设施. 部件系统是[ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components)的扩展. 在你有以下需求时,小部件会非常有用; + +* 在可复用的 **[模块](../../Module-Development-Basics.md)** 中定义部件. +* 在部件中引用 **scripts & styles** 脚本. +* 使用部件创建 **仪表盘**. +* 支持 **[授权](../../Authorization.md)** 与 **[捆绑`bundling`](Bundling-Minification.md)** 的部件 + +## 基本部件定义 + +### 创建一个视图组件 + +第一部,创建一个新的ASP.NET Core View Component: + +![widget-basic-files](../../images/widget-basic-files.png) + +**MySimpleWidgetViewComponent.cs**: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +继承 `AbpViewComponent` 不是必需的. 你也可以继承ASP.NET Core的 `ViewComponent`. `AbpViewComponent` 只是定义了一些基本的实用属性. + +**Default.cshtml**: + +```xml +
+

My Simple Widget

+

This is a simple widget!

+
+``` + +### 定义部件 + +添加 `Widget` attribute 到 `MySimpleWidgetViewComponent` 类,将此视图组件标记为部件: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## 渲染部件 + +渲染部件的用法是ASP.NET Core的标准用法. 在razor view/page中使用 `Component.InvokeAsync` 方法, 就像渲染一个View Component一样. 例如: + +````xml +@await Component.InvokeAsync("MySimpleWidget") +@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) +```` + +第一行代码使用名称渲染了部件,第二行代码使用type渲染了View Comonent. + +## 部件名称 + +默认下名称是根据View Conponent组件的名称计算的, 比如你的视图组件名是 `MySimpleWidgetViewComponent`, 那么部件的名称就是 `MySimpleWidget` (删除`ViewComponent`后缀). 这与ASP.NET Core的默认视图组件名称的方式一样. + +想要自定义组件名称,只需要使用ASP.NET Core的 `ViewComponent` attribute: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + [ViewComponent(Name = "MyCustomNamedWidget")] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); + } + } +} +``` + +ABP会通过自定义的名称去处理部件. + +> 如果视图组件名与视图组件的文件夹名称不匹配,那么需要像本例中那样去手动编写视图路径. + +### 显示名称 + +你还可以定义对于使用者友好的本地化显示名称. 需要时在UI中使用显示名称. 显示名称是可选的,在 `Widget` attribute 的`DisplayName`属性中定义: + +````csharp +using DashboardDemo.Localization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + DisplayName = "MySimpleWidgetDisplayName", //Localization key + DisplayNameResource = typeof(DashboardDemoResource) //localization resource + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +参阅 [本地化文档](../../Localization.md) 学习关于本地化资源的更多内容. + +## 引用 Style & Script + +当部件含有样式和scirpt文件时,会存在一些挑战; + +* 使用部件的页面应该将 **script & styles** 文件引用到页面中. +* 页面还需要解析部件的 `依赖库/文件`. + +将资源与部件正确的关联在一起时,ABP会解决这些问题. 使用正确的方法,就不用担心部件的依赖关系. + +### 定义一个简单的文件路径 + +下面的示例中部件添加了样式和scirpt文件: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, + ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +ABP会考虑到这些依赖关系, 在view/page中使用正确的方法添加部件 . 样式和script可以是物理文件也可以是虚拟文件. 它于[虚拟文件系统](../../Virtual-File-System.md)完全集成]. + +### 定义 Bundle + +页面中使用的组件的所有资源都做为捆绑包添加(如果没有其他配置,会在生产中合并和压缩). 除了简单的添加文件,你还可以充分的利用捆绑功能. + +下面的示例与上面的代码相同,但是在添加文件时文件路径替换成了 `BundleContributor`: + +````csharp +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } + + public class MySimpleWidgetStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); + } + } + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} + +```` + +捆绑系统非常强大,如果你的部件使用了JavaScript库来呈现图表, 你可以将它声明为依赖项, 如果之前未添加JavaScript库. 则会自动添加到页面中. 使用这种方式让页面使用部件时不用关心依赖项. + +参阅 [捆包&压缩 文档](Bundling-Minification.md) 了解更多内容. + +## 授权 + +某些组件可能只对通过身份验证或授权的用户可用,这时可以使用 `Widget` attribute 的以下属性: + +* `RequiresAuthentication` (`bool`): 设置为true,只有通过身份验证的用户(登录用户)可用. +* `RequiredPolicies` (`List`): 授权用户的策略名称列表. 有关策略的详细信息请参阅[授权文档](../../Authorization.md). + +示例: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget(RequiredPolicies = new[] { "MyPolicyName" })] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## 部件选项 + +`AbpWidgetOptions` 是 `Widget` attribute 替代, 你可以使用它去配置部件: + +```csharp +Configure(options => +{ + options.Widgets.Add(); +}); +``` + +将上面的代码写到[模块](../../Module-Development-Basics.md)的 `ConfigureServices` 方法中. `AbpWidgetOptions` 可以完成 `Widget` attribute 的所有功能. 比如为组件添加样式: + +````csharp +Configure(options => +{ + options.Widgets + .Add() + .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); +}); +```` + +> 提示: `AbpWidgetOptions` 还可以更改现有的部件配置. 如果要修改应用程序使用的模块内的组件配置,这会很有用. 使用 `options.Widgets.Find` 获取现有的 `WidgetDefinition`. \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index ad49ffe143..6835f417cf 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -221,55 +221,72 @@ ] }, { - "text": "ASP.NET Core", + "text": "API", "items": [ { - "text": "API", - "items": [ - { - "text": "自动API控制器", - "path": "AspNetCore/Auto-API-Controllers.md" - }, - { - "text": "动态C# API客户端", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" - } - ] + "text": "自动API控制器", + "path": "API/Auto-API-Controllers.md" }, { - "text": "用户界面", + "text": "动态C# API客户端", + "path": "API/Dynamic-CSharp-API-Clients.md" + } + ] + }, + { + "text": "用户界面", + "items": [ + { + "text": "ASP.NET Core", "items": [ { "text": "客户端包管理", - "path": "AspNetCore/Client-Side-Package-Management.md" + "path": "UI/AspNetCore/Client-Side-Package-Management.md" }, { "text": "捆绑&压缩", - "path": "AspNetCore/Bundling-Minification.md" + "path": "UI/AspNetCore/Bundling-Minification.md" }, { "text": "Tag Helpers", "items":[ { "text": "在线演示", - "path": "AspNetCore/Tag-Helpers/Index.md" + "path": "UI/AspNetCore/Tag-Helpers/Index.md" }, { "text": "按钮", - "path": "AspNetCore/Tag-Helpers/Buttons.md" + "path": "UI/AspNetCore/Tag-Helpers/Buttons.md" } ] }, { "text": "仪表板和小部件(Widget)系统", - "path": "AspNetCore/Widgets.md" + "path": "UI/AspNetCore/Widgets.md" }, { "text": "主题化", - "path": "AspNetCore/Theming.md" + "path": "UI/AspNetCore/Theming.md" } ] - } + }, + { + "text": "Angular", + "items": [ + { + "text": "本地化", + "path": "UI/Angular/Localization.md" + }, + { + "text": "权限管理", + "path": "UI/Angular/Permission-Management.md" + }, + { + "text": "替换组件", + "path": "UI/Angular/Component-Replacement.md" + } + ] + } ] }, { diff --git a/docs/zh-Hans/images/bookstore-added-brand-files.png b/docs/zh-Hans/images/bookstore-added-brand-files.png new file mode 100644 index 0000000000..15ef283a2a Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-brand-files.png differ diff --git a/docs/zh-Hans/images/bookstore-added-logo.png b/docs/zh-Hans/images/bookstore-added-logo.png new file mode 100644 index 0000000000..55af77a5f4 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-logo.png differ diff --git a/docs/zh-Hans/images/bookstore-added-role-js-file.png b/docs/zh-Hans/images/bookstore-added-role-js-file.png new file mode 100644 index 0000000000..00881bf808 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-role-js-file.png differ diff --git a/docs/zh-Hans/images/bookstore-brand-area-highlighted.png b/docs/zh-Hans/images/bookstore-brand-area-highlighted.png new file mode 100644 index 0000000000..7ab1b8fb7f Binary files /dev/null and b/docs/zh-Hans/images/bookstore-brand-area-highlighted.png differ diff --git a/docs/zh-Hans/images/bookstore-global-css-file.png b/docs/zh-Hans/images/bookstore-global-css-file.png new file mode 100644 index 0000000000..e764fef8ee Binary files /dev/null and b/docs/zh-Hans/images/bookstore-global-css-file.png differ diff --git a/docs/zh-Hans/images/bookstore-google-analytics-view-component.png b/docs/zh-Hans/images/bookstore-google-analytics-view-component.png new file mode 100644 index 0000000000..e548a00935 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-google-analytics-view-component.png differ diff --git a/docs/zh-Hans/images/bookstore-menus-highlighted.png b/docs/zh-Hans/images/bookstore-menus-highlighted.png new file mode 100644 index 0000000000..c4f3237b00 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-menus-highlighted.png differ diff --git a/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png b/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png new file mode 100644 index 0000000000..817491e9eb Binary files /dev/null and b/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png differ diff --git a/docs/zh-Hans/images/bookstore-notification-view-component.png b/docs/zh-Hans/images/bookstore-notification-view-component.png new file mode 100644 index 0000000000..3fc300efe1 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-notification-view-component.png differ diff --git a/docs/zh-Hans/images/bookstore-toolbar-highlighted.png b/docs/zh-Hans/images/bookstore-toolbar-highlighted.png new file mode 100644 index 0000000000..f2b7c3f57e Binary files /dev/null and b/docs/zh-Hans/images/bookstore-toolbar-highlighted.png differ diff --git a/docs/zh-Hans/images/docs-module_download-new-abp-project.png b/docs/zh-Hans/images/docs-module_download-new-abp-project.png index 0da3b7a67a..6424ad7622 100644 Binary files a/docs/zh-Hans/images/docs-module_download-new-abp-project.png and b/docs/zh-Hans/images/docs-module_download-new-abp-project.png differ diff --git a/docs/zh-Hans/images/docs-module_solution-explorer.png b/docs/zh-Hans/images/docs-module_solution-explorer.png index cafc38f0b0..2988ec4134 100644 Binary files a/docs/zh-Hans/images/docs-module_solution-explorer.png and b/docs/zh-Hans/images/docs-module_solution-explorer.png differ diff --git a/docs/zh-Hans/images/microservice-sample-diagram-3.png b/docs/zh-Hans/images/microservice-sample-diagram-3.png new file mode 100644 index 0000000000..b8066ef6be Binary files /dev/null and b/docs/zh-Hans/images/microservice-sample-diagram-3.png differ diff --git a/docs/zh-Hans/images/microservice-sample-solution-2.png b/docs/zh-Hans/images/microservice-sample-solution-2.png new file mode 100644 index 0000000000..3da5d160f8 Binary files /dev/null and b/docs/zh-Hans/images/microservice-sample-solution-2.png differ diff --git a/docs/zh-Hans/images/overriding-login-cshtml.png b/docs/zh-Hans/images/overriding-login-cshtml.png new file mode 100644 index 0000000000..d6daa4a141 Binary files /dev/null and b/docs/zh-Hans/images/overriding-login-cshtml.png differ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs index 01d3a35db2..60d1307b38 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic { public const string Name = "Basic"; - public string GetLayout(string name, bool fallbackToDefault = true) + public virtual string GetLayout(string name, bool fallbackToDefault = true) { switch (name) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml index 13d438441d..9dd98b5fda 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml @@ -23,6 +23,7 @@ Layout = null; AbpAntiForgeryManager.SetCookie(); var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options + } @@ -37,6 +38,8 @@ @(ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title) + + @await RenderSectionAsync("styles", false) @@ -54,32 +57,32 @@ @if (MultiTenancyOptions.Value.IsEnabled && - (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true)) - { -
-
-
-
- @MultiTenancyStringLocalizer["Tenant"]
-
- @if (CurrentTenant.Id == null) - { - - @MultiTenancyStringLocalizer["NotSelected"] - - } - else - { - @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) - } -
-
-
- @MultiTenancyStringLocalizer["Switch"] + (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true)) + { +
+
+
+
+ @MultiTenancyStringLocalizer["Tenant"]
+
+ @if (CurrentTenant.Id == null) + { + + @MultiTenancyStringLocalizer["NotSelected"] + + } + else + { + @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) + } +
+
+
-
} @(await Component.InvokeAsync()) @RenderBody() diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs index c808250a3e..31768b5e6a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http { @@ -11,5 +12,13 @@ namespace Microsoft.AspNetCore.Http return stream.GetAllBytes(); } } + + public static async Task GetAllBytesAsync(this IFormFile file) + { + using (var stream = file.OpenReadStream()) + { + return await stream.GetAllBytesAsync(); + } + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 06ce49551b..21a675ddc4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -43,21 +43,6 @@ namespace Volo.Abp.AspNetCore.Mvc public override void ConfigureServices(ServiceConfigurationContext context) { - //Configure Razor - context.Services.Insert(0, - ServiceDescriptor.Singleton>( - new ConfigureOptions(options => - { - options.FileProviders.Add( - new RazorViewEngineVirtualFileProvider( - context.Services.GetSingletonInstance>() - ) - ); - } - ) - ) - ); - Configure(options => { options.IgnoredInterfaces.AddIfNotContains(typeof(IAsyncActionFilter)); @@ -101,7 +86,16 @@ namespace Volo.Abp.AspNetCore.Mvc }; }) .AddViewLocalization(); //TODO: How to configure from the application? Also, consider to move to a UI module since APIs does not care about it. - + + Configure(options => + { + options.FileProviders.Add( + new RazorViewEngineVirtualFileProvider( + context.Services.GetSingletonInstance>() + ) + ); + }); + context.Services.ExecutePreConfiguredActions(mvcBuilder); //TODO: AddViewLocalization by default..? diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index 9875ccd808..0254a6022b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -12,6 +12,7 @@ using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.Http.Modeling; +using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.Conventions @@ -81,7 +82,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions continue; } - if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType)) + if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType, includeEnums: true)) { if (CanUseFormBodyBinding(action, prm)) { @@ -94,7 +95,15 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual bool CanUseFormBodyBinding(ActionModel action, ParameterModel parameter) { - if (_options.ConventionalControllers.FormBodyBindingIgnoredTypes.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) + //We want to use "id" as path parameter, not body! + if (parameter.ParameterName == "id") + { + return false; + } + + if (_options.ConventionalControllers + .FormBodyBindingIgnoredTypes + .Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) { return false; } @@ -251,7 +260,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions if (!selector.ActionConstraints.OfType().Any()) { - selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] {httpMethod})); + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod })); } } } @@ -295,9 +304,24 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions var url = $"api/{rootPath}/{controllerNameInUrl.ToCamelCase()}"; //Add {id} path if needed - if (action.Parameters.Any(p => p.ParameterName == "id")) + var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); + if (idParameterModel != null) { - url += "/{id}"; + if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) + { + url += "/{id}"; + } + else + { + var properties = idParameterModel + .ParameterType + .GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in properties) + { + url += "/{" + property.Name + "}"; + } + } } //Add action name if needed @@ -341,7 +365,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual string NormalizeUrlControllerName(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) { - if(configuration?.UrlControllerNameNormalizer == null) + if (configuration?.UrlControllerNameNormalizer == null) { return controllerName; } @@ -364,7 +388,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual bool IsEmptySelector(SelectorModel selector) { - return selector.AttributeRouteModel == null + return selector.AttributeRouteModel == null && selector.ActionConstraints.IsNullOrEmpty() && selector.EndpointMetadata.IsNullOrEmpty(); } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs index 1ce71ca3f9..3326c4e65f 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs @@ -1,5 +1,7 @@ +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -11,14 +13,34 @@ namespace Volo.Abp.BackgroundWorkers public abstract class BackgroundWorkerBase : IBackgroundWorker { //TODO: Add UOW, Localization and other useful properties..? + public IServiceProvider ServiceProvider { get; set; } + protected readonly object ServiceProviderLock = new object(); - public ILogger Logger { protected get; set; } + protected TService LazyGetRequiredService(ref TService reference) + => LazyGetRequiredService(typeof(TService), ref reference); - protected BackgroundWorkerBase() + protected TRef LazyGetRequiredService(Type serviceType, ref TRef reference) { - Logger = NullLogger.Instance; + if (reference == null) + { + lock (ServiceProviderLock) + { + if (reference == null) + { + reference = (TRef)ServiceProvider.GetRequiredService(serviceType); + } + } + } + + return reference; } - + + public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); + private ILoggerFactory _loggerFactory; + + protected ILogger Logger => _lazyLogger.Value; + private Lazy _lazyLogger => new Lazy(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); + public virtual Task StartAsync(CancellationToken cancellationToken = default) { Logger.LogDebug("Started background worker: " + ToString()); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs index f25a4ad89f..04b9168a44 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs @@ -162,6 +162,7 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("-t|--template (default: app)"); sb.AppendLine("-u|--ui (if supported by the template)"); + sb.AppendLine("-m|--mobile (if supported by the template)"); sb.AppendLine("-d|--database-provider (if supported by the template)"); sb.AppendLine("-o|--output-folder (default: current folder)"); sb.AppendLine("-v|--version (default: latest version)"); @@ -177,6 +178,8 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp new Acme.BookStore --tiered"); sb.AppendLine(" abp new Acme.BookStore -u angular"); sb.AppendLine(" abp new Acme.BookStore -u angular -d mongodb"); + sb.AppendLine(" abp new Acme.BookStore -m none"); + sb.AppendLine(" abp new Acme.BookStore -m react-native"); sb.AppendLine(" abp new Acme.BookStore -d mongodb"); sb.AppendLine(" abp new Acme.BookStore -d mongodb -o d:\\my-project"); sb.AppendLine(" abp new Acme.BookStore -t module"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs index 9f0862258f..883ac4db2f 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs @@ -29,7 +29,7 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp switch-to-preview [options]"); sb.AppendLine(""); sb.AppendLine("Options:"); - sb.AppendLine("-sp|--solution-path"); + sb.AppendLine("-sd|--solution-directory"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs index 333583e96d..54db7bc082 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs @@ -29,7 +29,7 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp switch-to-stable [options]"); sb.AppendLine(""); sb.AppendLine("Options:"); - sb.AppendLine("-sp|--solution-path"); + sb.AppendLine("-sd|--solution-directory"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs index 3cdd658403..0b70ae6cca 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs @@ -95,21 +95,19 @@ namespace Volo.Abp.Cli.NuGet .OrResult(msg => !msg.IsSuccessStatusCode) .WaitAndRetryAsync(new[] { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(3), - TimeSpan.FromSeconds(7) + TimeSpan.FromSeconds(1) }, (responseMessage, timeSpan, retryCount, context) => { if (responseMessage.Exception != null) { - Logger.LogWarning( + Logger.LogDebug( $"{retryCount}. HTTP request attempt failed to {url} with an error: HTTP {(int)responseMessage.Result.StatusCode}-{responseMessage.Exception.Message}. " + $"Waiting {timeSpan.TotalSeconds} secs for the next try..."); } else if (responseMessage.Result != null) { - Logger.LogWarning( + Logger.LogDebug( $"{retryCount}. HTTP request attempt failed to {url} with an error: {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " + $"Waiting {timeSpan.TotalSeconds} secs for the next try..."); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 03504251f5..263b675a2a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -54,19 +54,21 @@ namespace Volo.Abp.Cli.ProjectBuilding version = latestVersion; } + var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version; + DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); if (!string.IsNullOrWhiteSpace(templateSource) && !IsNetworkSource(templateSource)) { Logger.LogInformation("Using local " + type + ": " + name + ", version: " + version); - return new TemplateFile(File.ReadAllBytes(Path.Combine(templateSource, name + "-" + version + ".zip")), version, latestVersion); + return new TemplateFile(File.ReadAllBytes(Path.Combine(templateSource, name + "-" + version + ".zip")), version, latestVersion, nugetVersion); } var localCacheFile = Path.Combine(CliPaths.TemplateCache, name + "-" + version + ".zip"); if (Options.CacheTemplates && File.Exists(localCacheFile) && templateSource.IsNullOrWhiteSpace()) { Logger.LogInformation("Using cached " + type + ": " + name + ", version: " + version); - return new TemplateFile(File.ReadAllBytes(localCacheFile), version, latestVersion); + return new TemplateFile(File.ReadAllBytes(localCacheFile), version, latestVersion, nugetVersion); } Logger.LogInformation("Downloading " + type + ": " + name + ", version: " + version); @@ -86,7 +88,7 @@ namespace Volo.Abp.Cli.ProjectBuilding File.WriteAllBytes(localCacheFile, fileContent); } - return new TemplateFile(fileContent, version, latestVersion); + return new TemplateFile(fileContent, version, latestVersion, nugetVersion); } @@ -110,7 +112,38 @@ namespace Volo.Abp.Cli.ProjectBuilding var result = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(result).Version; + return JsonSerializer.Deserialize(result).Version; + } + } + + private async Task GetTemplateNugetVersionAsync(string name, string type, string version) + { + try + { + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + var response = await client.PostAsync( + $"{CliUrls.WwwAbpIo}api/download/{type}/get-nuget-version/", + new StringContent( + JsonSerializer.Serialize( + new GetTemplateNugetVersionDto { Name = name, Version = version } + ), + Encoding.UTF8, + MimeTypes.Application.Json + ), + CancellationTokenProvider.Token + ); + + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + var result = await response.Content.ReadAsStringAsync(); + + return JsonSerializer.Deserialize(result).Version; + } + } + catch (Exception) + { + return null; } } @@ -162,7 +195,14 @@ namespace Volo.Abp.Cli.ProjectBuilding public string Name { get; set; } } - public class GetLatestSourceCodeVersionResultDto + public class GetTemplateNugetVersionDto + { + public string Name { get; set; } + + public string Version { get; set; } + } + + public class GetVersionResultDto { public string Version { get; set; } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs index 1a85471529..97132e8f79 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs @@ -17,9 +17,9 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building { case MobileApp.ReactNative: return "react-native"; - default: - return null; } + + throw new Exception("Mobile app folder name is not set!"); } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs index a7fd3f529f..5aaf4b1488 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs @@ -29,7 +29,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps } else { - var nugetPackageVersion = context.TemplateFile.Version; + var nugetPackageVersion = context.TemplateFile.RepositoryNugetVersion; if (IsBranchName(nugetPackageVersion)) { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs index c7f9e77cb2..fc514e8a74 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs @@ -6,13 +6,16 @@ public string LatestVersion { get; } + public string RepositoryNugetVersion { get; } + public byte[] FileBytes { get; } - public TemplateFile(byte[] fileBytes, string version, string latestVersion) + public TemplateFile(byte[] fileBytes, string version, string latestVersion, string repositoryNugetVersion) { FileBytes = fileBytes; Version = version; LatestVersion = latestVersion; + RepositoryNugetVersion = repositoryNugetVersion; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs index 9df2c03ee4..07ada2fe3d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs @@ -77,7 +77,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App if (context.BuildArgs.MobileApp != MobileApp.ReactNative) { - steps.Add(new RemoveFolderStep(MobileApp.ReactNative.GetFolderName()?.EnsureStartsWith('/'))); + steps.Add(new RemoveFolderStep(MobileApp.ReactNative.GetFolderName().EnsureStartsWith('/'))); } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs new file mode 100644 index 0000000000..2d1e237933 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class AngularModuleSourceCodeAdder : ITransientDependency + { + public async Task AddAsync(string solutionFilePath, string angularPath) + { + var angularProjectsPath = Path.Combine(angularPath, "projects"); + + var projects = await CopyAndGetNamesOfAngularProjectsAsync(solutionFilePath, angularProjectsPath); + + if (!projects.Any()) + { + return; + } + + await ReplaceProjectNamesAndCopySourCodeRequirementsToProjectsAsync(angularProjectsPath, projects); + + await RemoveRedundantFilesAsync(angularProjectsPath, projects); + + await AddPathsToTsConfigAsync(angularPath, angularProjectsPath, projects); + + await AddProjectToAngularJsonAsync(angularPath, projects); + } + + private async Task AddProjectToAngularJsonAsync(string angularPath, List projects) + { + var angularJsonFilePath = Path.Combine(angularPath, "angular.json"); + var fileContent = File.ReadAllText(angularJsonFilePath); + + var json = JObject.Parse(fileContent); + + var projectsJobject = (JObject)json["projects"]; + + foreach (var project in projects) + { + projectsJobject.Add(project, new JObject( + new JProperty("projectType", "library"), + new JProperty("root", $"projects/{project}"), + new JProperty("sourceRoot", $"projects/{project}/src"), + new JProperty("prefix", "lib"), + new JProperty("architect", new JObject( + new JProperty("build", new JObject( + new JProperty("builder", "@angular-devkit/build-ng-package:build"), + new JProperty("options", new JObject( + new JProperty("tsConfig", $"projects/{project}/tsconfig.lib.json"), + new JProperty("project", $"projects/{project}/ng-package.json") + )), + new JProperty("configurations", new JObject( + new JProperty("production", new JObject( + new JProperty("tsConfig", $"projects/{project}/tsconfig.lib.prod.json"))) + )))), + new JProperty("test", new JObject( + new JProperty("builder", "@angular-devkit/build-angular:karma"), + new JProperty("options", new JObject( + new JProperty("main", $"projects/{project}/src/test.ts"), + new JProperty("tsConfig", $"projects/{project}/tsconfig.spec.json"), + new JProperty("karmaConfig", $"projects/{project}/karma.conf.js") + ) + ))), + new JProperty("lint", new JObject( + new JProperty("builder", "@angular-devkit/build-angular:tslint"), + new JProperty("options", new JObject( + new JProperty("tsConfig", new JArray(new JValue($"projects/{project}/tsconfig.lib.json"), new JValue($"projects/{project}/tsconfig.spec.json"))), + new JProperty("exclude", new JArray(new JValue("**/node_modules/**"))) + )) + )) + )) + )); + } + + File.WriteAllText(angularJsonFilePath, json.ToString(Formatting.Indented)); + } + + private async Task AddPathsToTsConfigAsync(string angularPath, string angularProjectsPath, List projects) + { + var tsConfigPath = Path.Combine(angularPath, "tsconfig.json"); + var fileContent = File.ReadAllText(tsConfigPath); + + var tsConfigAsJson = JObject.Parse(fileContent); + + var compilerOptions = (JObject)tsConfigAsJson["compilerOptions"]; + + foreach (var project in projects) + { + var projectPackageName = await GetProjectPackageNameAsync(angularProjectsPath, project); + + var property = new JProperty($"{projectPackageName}", + new JArray(new object[] { $"projects/{project}/src/public-api.ts" }) + ); + var property2 = new JProperty($"{projectPackageName}/*", + new JArray(new object[] { $"projects/{project}/src/lib/*" }) + ); + + if (compilerOptions["paths"] == null) + { + + compilerOptions.Add("paths", new JObject()); + } + + ((JObject)compilerOptions["paths"]).Add(property); + ((JObject)compilerOptions["paths"]).Add(property2); + } + + File.WriteAllText(tsConfigPath, tsConfigAsJson.ToString(Formatting.Indented)); + } + + private async Task GetProjectPackageNameAsync(string angularProjectsPath, string project) + { + var packageJsonPath = Path.Combine(angularProjectsPath, project, "package.json"); + + var fileContent = File.ReadAllText(packageJsonPath); + + return (string)JObject.Parse(fileContent)["name"]; + } + + private async Task RemoveRedundantFilesAsync(string angularProjectsPath, List projects) + { + foreach (var project in projects) + { + var jestConfigPath = Path.Combine(angularProjectsPath, project, "jest.config.js"); + + File.Delete(jestConfigPath); + + var testPath = Path.Combine(angularProjectsPath, project, "src", "test.ts"); + + File.Delete(testPath); + } + } + + private async Task ReplaceProjectNamesAndCopySourCodeRequirementsToProjectsAsync(string angularProjectsPath, List projects) + { + foreach (var project in projects) + { + var sourceCodeReqFolder = Path.Combine(angularProjectsPath, project, "source-code-requirements"); + + Directory.CreateDirectory(sourceCodeReqFolder); + + var filesUnderSourceCodeReqFolder = Directory.GetFiles(sourceCodeReqFolder); + + foreach (var fileUnderSourceCodeReqFolder in filesUnderSourceCodeReqFolder) + { + var newDest = Path.Combine(angularProjectsPath, project, Path.GetFileName(fileUnderSourceCodeReqFolder)); + File.Move(fileUnderSourceCodeReqFolder, newDest); + + var fileContent = File.ReadAllText(newDest); + fileContent = fileContent.Replace("{{project-name}}", project); + fileContent = fileContent.Replace("{{library-name-kebab-case}}", project); + File.WriteAllText(newDest, fileContent); + } + } + } + + private async Task> CopyAndGetNamesOfAngularProjectsAsync(string solutionFilePath, string angularProjectsPath) + { + var projects = new List(); + + if (!Directory.Exists(angularProjectsPath)) + { + Directory.CreateDirectory(angularProjectsPath); + } + + var angularPathsInDownloadedSourceCode = Directory.GetDirectories(Path.Combine(Path.Combine(Path.GetDirectoryName(solutionFilePath), "modules"))).Select(p => Path.Combine(p, "angular")) + .Where(Directory.Exists); + + + foreach (var folder in angularPathsInDownloadedSourceCode) + { + var projectsInFolder = Directory.GetDirectories(folder); + foreach (var projectInFolder in projectsInFolder) + { + var projectName = Path.GetFileName(projectInFolder.TrimEnd('\\').TrimEnd('/')); + + var destDirName = Path.Combine(angularProjectsPath, projectName); + if (Directory.Exists(destDirName)) + { + Directory.Delete(projectInFolder, true); + continue; + } + + + projects.Add(projectName); + + Directory.Move(projectInFolder, destDirName); + } + } + + return projects; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs index e860a68ba8..f6c89be039 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs @@ -31,55 +31,74 @@ namespace Volo.Abp.Cli.ProjectModification { _packageSourceAdder.Add("ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json"); + var solutionPath = GetSolutionPath(commandLineArgs); + var solutionFolder = GetSolutionFolder(commandLineArgs); + await _nugetPackagesVersionUpdater.UpdateSolutionAsync( - GetSolutionPath(commandLineArgs), + solutionPath, true); await _npmPackagesUpdater.Update( - Path.GetFileName(GetSolutionPath(commandLineArgs)), + solutionFolder, true); } public async Task SwitchToStable(CommandLineArgs commandLineArgs) { + var solutionPath = GetSolutionPath(commandLineArgs); + var solutionFolder = GetSolutionFolder(commandLineArgs); + await _nugetPackagesVersionUpdater.UpdateSolutionAsync( - GetSolutionPath(commandLineArgs), + solutionPath, false, true); await _npmPackagesUpdater.Update( - Path.GetFileName(GetSolutionPath(commandLineArgs)), - false, + solutionFolder, + false, true); } - private string GetSolutionPath(CommandLineArgs commandLineArgs) { - var solutionPath = commandLineArgs.Options.GetOrNull(Options.SolutionPath.Short, Options.SolutionPath.Long); + var directory = commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long) + ?? Directory.GetCurrentDirectory(); + + var solutionPath = Directory.GetFiles(directory, "*.sln").FirstOrDefault(); if (solutionPath == null) { - try - { - solutionPath = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.sln").Single(); - } - catch (Exception) + var subDirectories = Directory.GetDirectories(directory); + + foreach (var subDirectory in subDirectories) { - Logger.LogError("There is no solution or more that one solution in current directory."); - throw; + var slnInSubDirectory = Directory.GetFiles(subDirectory, "*.sln").FirstOrDefault(); + + if (slnInSubDirectory != null) + { + return Path.Combine(subDirectory, slnInSubDirectory); + } } + + Logger.LogError("There is no solution or more that one solution in current directory."); + return null; } return solutionPath; } + private string GetSolutionFolder(CommandLineArgs commandLineArgs) + { + return commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long) + ?? Directory.GetCurrentDirectory(); + } + public static class Options { - public static class SolutionPath + public static class SolutionDirectory { - public const string Short = "sp"; - public const string Long = "solution-path"; + public const string Short = "sd"; + public const string Long = "solution-directory"; } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index 8ded87bf6f..e369690785 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -30,6 +30,7 @@ namespace Volo.Abp.Cli.ProjectModification public SourceCodeDownloadService SourceCodeDownloadService { get; } public SolutionFileModifier SolutionFileModifier { get; } public NugetPackageToLocalReferenceConverter NugetPackageToLocalReferenceConverter { get; } + public AngularModuleSourceCodeAdder AngularModuleSourceCodeAdder { get; } public SolutionModuleAdder( IJsonSerializer jsonSerializer, @@ -42,7 +43,8 @@ namespace Volo.Abp.Cli.ProjectModification IRemoteServiceExceptionHandler remoteServiceExceptionHandler, SourceCodeDownloadService sourceCodeDownloadService, SolutionFileModifier solutionFileModifier, - NugetPackageToLocalReferenceConverter nugetPackageToLocalReferenceConverter) + NugetPackageToLocalReferenceConverter nugetPackageToLocalReferenceConverter, + AngularModuleSourceCodeAdder angularModuleSourceCodeAdder) { JsonSerializer = jsonSerializer; ProjectNugetPackageAdder = projectNugetPackageAdder; @@ -55,6 +57,7 @@ namespace Volo.Abp.Cli.ProjectModification SourceCodeDownloadService = sourceCodeDownloadService; SolutionFileModifier = solutionFileModifier; NugetPackageToLocalReferenceConverter = nugetPackageToLocalReferenceConverter; + AngularModuleSourceCodeAdder = angularModuleSourceCodeAdder; Logger = NullLogger.Instance; } @@ -83,21 +86,39 @@ namespace Volo.Abp.Cli.ProjectModification await DownloadSourceCodesToSolutionFolder(module, modulesFolderInSolution, version); await SolutionFileModifier.AddModuleToSolutionFileAsync(module, solutionFile); await NugetPackageToLocalReferenceConverter.Convert(module, solutionFile); + + await HandleAngularProject(module, solutionFile); } ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations); } + private async Task HandleAngularProject(ModuleWithMastersInfo module, string solutionFilePath) + { + var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular"); + + if (!Directory.Exists(angularPath)) + { + return; + } + + await AngularModuleSourceCodeAdder.AddAsync(solutionFilePath, angularPath); + } + private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, string modulesFolderInSolution, string version = null) { + var targetModuleFolder = Path.Combine(modulesFolderInSolution, module.Name); + await SourceCodeDownloadService.DownloadAsync( module.Name, - Path.Combine(modulesFolderInSolution, module.Name), + targetModuleFolder, version, null, null ); + await DeleteAppFolderAsync(targetModuleFolder); + if (module.MasterModuleInfos == null) { return; @@ -109,6 +130,15 @@ namespace Volo.Abp.Cli.ProjectModification } } + private async Task DeleteAppFolderAsync(string targetModuleFolder) + { + var appFolder = Path.Combine(targetModuleFolder, "app"); + if (Directory.Exists(appFolder)) + { + Directory.Delete(appFolder, true); + } + } + private async Task AddNugetAndNpmReferences(ModuleWithMastersInfo module, string[] projectFiles) { foreach (var nugetPackage in module.NugetPackages) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs index da8ded6a20..587492804b 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs @@ -106,7 +106,7 @@ namespace Volo.Abp.Cli.ProjectModification } else { - Logger.LogDebug("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion); + Logger.LogInformation("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion); } } } diff --git a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs index 8eefc78f9b..2ccf9bb9a1 100644 --- a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs +++ b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs @@ -14,6 +14,7 @@ namespace Volo.Abp.Cli .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) #if DEBUG .MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Debug) #else diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs new file mode 100644 index 0000000000..e22f2002b0 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.Logging +{ + public static class HasLogLevelExtensions + { + public static TException WithLogLevel([NotNull] this TException exception, LogLevel logLevel) + where TException : IHasLogLevel + { + Check.NotNull(exception, nameof(exception)); + + exception.LogLevel = logLevel; + + return exception; + } + } +} diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs index 7816bd20eb..12f3b51d11 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs @@ -5,36 +5,30 @@ namespace Volo.Abp.Application.Services { public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : IApplicationService - where TGetOutputDto : IEntityDto - where TGetListOutputDto : IEntityDto { Task GetAsync(TKey id); diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs new file mode 100644 index 0000000000..45915826e4 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs @@ -0,0 +1,341 @@ +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Linq; +using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services +{ + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + + protected override TEntityDto MapToGetListOutputDto(TEntity entity) + { + return MapToGetOutputDto(entity); + } + } + + public abstract class AbstractKeyCrudAppService + : ApplicationService, + ICrudAppService + where TEntity : class, IEntity + { + public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } + + protected IRepository Repository { get; } + + protected virtual string GetPolicyName { get; set; } + + protected virtual string GetListPolicyName { get; set; } + + protected virtual string CreatePolicyName { get; set; } + + protected virtual string UpdatePolicyName { get; set; } + + protected virtual string DeletePolicyName { get; set; } + + protected AbstractKeyCrudAppService(IRepository repository) + { + Repository = repository; + AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance; + } + + public virtual async Task GetAsync(TKey id) + { + await CheckGetPolicyAsync(); + + var entity = await GetEntityByIdAsync(id); + return MapToGetOutputDto(entity); + } + + public virtual async Task> GetListAsync(TGetListInput input) + { + await CheckGetListPolicyAsync(); + + var query = CreateFilteredQuery(input); + + var totalCount = await AsyncQueryableExecuter.CountAsync(query); + + query = ApplySorting(query, input); + query = ApplyPaging(query, input); + + var entities = await AsyncQueryableExecuter.ToListAsync(query); + + return new PagedResultDto( + totalCount, + entities.Select(MapToGetListOutputDto).ToList() + ); + } + + public virtual async Task CreateAsync(TCreateInput input) + { + await CheckCreatePolicyAsync(); + + var entity = MapToEntity(input); + + TryToSetTenantId(entity); + + await Repository.InsertAsync(entity, autoSave: true); + + return MapToGetOutputDto(entity); + } + + public virtual async Task UpdateAsync(TKey id, TUpdateInput input) + { + await CheckUpdatePolicyAsync(); + + var entity = await GetEntityByIdAsync(id); + //TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise + MapToEntity(input, entity); + await Repository.UpdateAsync(entity, autoSave: true); + + return MapToGetOutputDto(entity); + } + + public virtual async Task DeleteAsync(TKey id) + { + await CheckDeletePolicyAsync(); + + await DeleteByIdAsync(id); + } + + protected abstract Task DeleteByIdAsync(TKey id); + + protected abstract Task GetEntityByIdAsync(TKey id); + + protected virtual async Task CheckGetPolicyAsync() + { + await CheckPolicyAsync(GetPolicyName); + } + + protected virtual async Task CheckGetListPolicyAsync() + { + await CheckPolicyAsync(GetListPolicyName); + } + + protected virtual async Task CheckCreatePolicyAsync() + { + await CheckPolicyAsync(CreatePolicyName); + } + + protected virtual async Task CheckUpdatePolicyAsync() + { + await CheckPolicyAsync(UpdatePolicyName); + } + + protected virtual async Task CheckDeletePolicyAsync() + { + await CheckPolicyAsync(DeletePolicyName); + } + + /// + /// Should apply sorting if needed. + /// + /// The query. + /// The input. + protected virtual IQueryable ApplySorting(IQueryable query, TGetListInput input) + { + //Try to sort query if available + if (input is ISortedResultRequest sortInput) + { + if (!sortInput.Sorting.IsNullOrWhiteSpace()) + { + return query.OrderBy(sortInput.Sorting); + } + } + + //IQueryable.Task requires sorting, so we should sort if Take will be used. + if (input is ILimitedResultRequest) + { + return ApplyDefaultSorting(query); + } + + //No sorting + return query; + } + + /// + /// Applies sorting if no sorting specified but a limited result requested. + /// + /// The query. + protected virtual IQueryable ApplyDefaultSorting(IQueryable query) + { + if (typeof(TEntity).IsAssignableTo()) + { + return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + } + + throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyCrudAppService!"); + } + + /// + /// Should apply paging if needed. + /// + /// The query. + /// The input. + protected virtual IQueryable ApplyPaging(IQueryable query, TGetListInput input) + { + //Try to use paging if available + if (input is IPagedResultRequest pagedInput) + { + return query.PageBy(pagedInput); + } + + //Try to limit query result if available + if (input is ILimitedResultRequest limitedInput) + { + return query.Take(limitedInput.MaxResultCount); + } + + //No paging + return query; + } + + /// + /// This method should create based on given input. + /// It should filter query if needed, but should not do sorting or paging. + /// Sorting should be done in and paging should be done in + /// methods. + /// + /// The input. + protected virtual IQueryable CreateFilteredQuery(TGetListInput input) + { + return Repository; + } + + /// + /// Maps to . + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity) + { + return ObjectMapper.Map(entity); + } + + /// + /// Maps to . + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity) + { + return ObjectMapper.Map(entity); + } + + /// + /// Maps to to create a new entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TEntity MapToEntity(TCreateInput createInput) + { + var entity = ObjectMapper.Map(createInput); + SetIdForGuids(entity); + return entity; + } + + /// + /// Sets Id value for the entity if is . + /// It's used while creating a new entity. + /// + protected virtual void SetIdForGuids(TEntity entity) + { + var entityWithGuidId = entity as IEntity; + + if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty) + { + return; + } + + EntityHelper.TrySetId( + entityWithGuidId, + () => GuidGenerator.Create(), + true + ); + } + + /// + /// Maps to to update the entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) + { + ObjectMapper.Map(updateInput, entity); + } + + protected virtual void TryToSetTenantId(TEntity entity) + { + if (entity is IMultiTenant && HasTenantIdProperty(entity)) + { + var tenantId = CurrentTenant.Id; + + if (!tenantId.HasValue) + { + return; + } + + var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)); + + if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) + { + return; + } + + propertyInfo.SetValue(entity, tenantId); + } + } + + protected virtual bool HasTenantIdProperty(TEntity entity) + { + return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs index 4134b59a4c..a2af0592a4 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs @@ -1,13 +1,10 @@ using System; using System.Linq; -using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; -using Volo.Abp.Linq; -using Volo.Abp.MultiTenancy; -using Volo.Abp.ObjectMapping; namespace Volo.Abp.Application.Services { @@ -65,274 +62,49 @@ namespace Volo.Abp.Application.Services } public abstract class CrudAppService - : ApplicationService, - ICrudAppService - where TEntity : class, IEntity + : AbstractKeyCrudAppService + where TEntity : class, IEntity where TGetOutputDto : IEntityDto where TGetListOutputDto : IEntityDto { - public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } - - protected IRepository Repository { get; } - - protected virtual string GetPolicyName { get; set; } - - protected virtual string GetListPolicyName { get; set; } - - protected virtual string CreatePolicyName { get; set; } - - protected virtual string UpdatePolicyName { get; set; } - - protected virtual string DeletePolicyName { get; set; } + protected new IRepository Repository { get; } protected CrudAppService(IRepository repository) + : base(repository) { Repository = repository; - AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance; } - public virtual async Task GetAsync(TKey id) + protected override async Task DeleteByIdAsync(TKey id) { - await CheckGetPolicyAsync(); - - var entity = await GetEntityByIdAsync(id); - return MapToGetOutputDto(entity); - } - - public virtual async Task> GetListAsync(TGetListInput input) - { - await CheckGetListPolicyAsync(); - - var query = CreateFilteredQuery(input); - - var totalCount = await AsyncQueryableExecuter.CountAsync(query); - - query = ApplySorting(query, input); - query = ApplyPaging(query, input); - - var entities = await AsyncQueryableExecuter.ToListAsync(query); - - return new PagedResultDto( - totalCount, - entities.Select(MapToGetListOutputDto).ToList() - ); - } - - public virtual async Task CreateAsync(TCreateInput input) - { - await CheckCreatePolicyAsync(); - - var entity = MapToEntity(input); - - TryToSetTenantId(entity); - - await Repository.InsertAsync(entity, autoSave: true); - - return MapToGetOutputDto(entity); - } - - public virtual async Task UpdateAsync(TKey id, TUpdateInput input) - { - await CheckUpdatePolicyAsync(); - - var entity = await GetEntityByIdAsync(id); - //TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise - MapToEntity(input, entity); - await Repository.UpdateAsync(entity, autoSave: true); - - return MapToGetOutputDto(entity); - } - - public virtual async Task DeleteAsync(TKey id) - { - await CheckDeletePolicyAsync(); - await Repository.DeleteAsync(id); } - protected virtual Task GetEntityByIdAsync(TKey id) - { - return Repository.GetAsync(id); - } - - protected virtual async Task CheckGetPolicyAsync() - { - await CheckPolicyAsync(GetPolicyName); - } - - protected virtual async Task CheckGetListPolicyAsync() - { - await CheckPolicyAsync(GetListPolicyName); - } - - protected virtual async Task CheckCreatePolicyAsync() - { - await CheckPolicyAsync(CreatePolicyName); - } - - protected virtual async Task CheckUpdatePolicyAsync() - { - await CheckPolicyAsync(UpdatePolicyName); - } - - protected virtual async Task CheckDeletePolicyAsync() + protected override async Task GetEntityByIdAsync(TKey id) { - await CheckPolicyAsync(DeletePolicyName); + return await Repository.GetAsync(id); } - /// - /// Should apply sorting if needed. - /// - /// The query. - /// The input. - protected virtual IQueryable ApplySorting(IQueryable query, TGetListInput input) - { - //Try to sort query if available - if (input is ISortedResultRequest sortInput) - { - if (!sortInput.Sorting.IsNullOrWhiteSpace()) - { - return query.OrderBy(sortInput.Sorting); - } - } - - //IQueryable.Task requires sorting, so we should sort if Take will be used. - if (input is ILimitedResultRequest) - { - return query.OrderByDescending(e => e.Id); - } - - //No sorting - return query; - } - - /// - /// Should apply paging if needed. - /// - /// The query. - /// The input. - protected virtual IQueryable ApplyPaging(IQueryable query, TGetListInput input) - { - //Try to use paging if available - if (input is IPagedResultRequest pagedInput) - { - return query.PageBy(pagedInput); - } - - //Try to limit query result if available - if (input is ILimitedResultRequest limitedInput) - { - return query.Take(limitedInput.MaxResultCount); - } - - //No paging - return query; - } - - /// - /// This method should create based on given input. - /// It should filter query if needed, but should not do sorting or paging. - /// Sorting should be done in and paging should be done in - /// methods. - /// - /// The input. - protected virtual IQueryable CreateFilteredQuery(TGetListInput input) - { - return Repository; - } - - /// - /// Maps to . - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity) - { - return ObjectMapper.Map(entity); - } - - /// - /// Maps to . - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity) - { - return ObjectMapper.Map(entity); - } - - /// - /// Maps to to create a new entity. - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TEntity MapToEntity(TCreateInput createInput) - { - var entity = ObjectMapper.Map(createInput); - SetIdForGuids(entity); - return entity; - } - - /// - /// Sets Id value for the entity if is . - /// It's used while creating a new entity. - /// - protected virtual void SetIdForGuids(TEntity entity) - { - var entityWithGuidId = entity as IEntity; - - if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty) - { - return; - } - - EntityHelper.TrySetId( - entityWithGuidId, - () => GuidGenerator.Create(), - true - ); - } - - /// - /// Maps to to update the entity. - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) + protected override void MapToEntity(TUpdateInput updateInput, TEntity entity) { if (updateInput is IEntityDto entityDto) { entityDto.Id = entity.Id; } - ObjectMapper.Map(updateInput, entity); + base.MapToEntity(updateInput, entity); } - protected virtual void TryToSetTenantId(TEntity entity) + protected override IQueryable ApplyDefaultSorting(IQueryable query) { - if (entity is IMultiTenant && HasTenantIdProperty(entity)) + if (typeof(TEntity).IsAssignableTo()) { - var tenantId = CurrentTenant.Id; - - if (!tenantId.HasValue) - { - return; - } - - var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)); - - if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) - { - return; - } - - propertyInfo.SetValue(entity, tenantId); + return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + } + else + { + return query.OrderByDescending(e => e.Id); } - } - - protected virtual bool HasTenantIdProperty(TEntity entity) - { - return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; } } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs index a33c5f56ec..2b38059ef8 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Reflection; namespace Volo.Abp.Domain.Entities { @@ -13,12 +11,24 @@ namespace Volo.Abp.Domain.Entities /// public static class EntityHelper { - public static bool IsEntity([NotNull] Type type) { return typeof(IEntity).IsAssignableFrom(type); } + public static bool IsEntityWithId([NotNull] Type type) + { + foreach (var interfaceType in type.GetInterfaces()) + { + if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>)) + { + return true; + } + } + + return false; + } + public static bool HasDefaultId(IEntity entity) { if (EqualityComparer.Default.Equals(entity.Id, default)) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs index 510d0384c2..85466ccf0e 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs @@ -47,7 +47,11 @@ namespace Volo.Abp.Domain.Entities /// Creates a new object. /// public EntityNotFoundException(Type entityType, object id, Exception innerException) - : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) + : base( + id == null + ? $"There is no such an entity given given id. Entity type: {entityType.FullName}" + : $"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", + innerException) { EntityType = entityType; Id = id; diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs index 2ac16ec229..3bda51bd2e 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs @@ -18,6 +18,34 @@ namespace Volo.Abp.Domain.Repositories public interface IRepository : IReadOnlyRepository, IBasicRepository where TEntity : class, IEntity { + /// + /// Get a single entity by the given . + /// It returns null if no entity with the given . + /// It throws if there are multiple entities with the given . + /// + /// A condition to find the entity + /// Set true to include all children of this entity + /// A to observe while waiting for the task to complete. + Task FindAsync( + [NotNull] Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default + ); + + /// + /// Get a single entity by the given . + /// It throws if there is no entity with the given . + /// It throws if there are multiple entities with the given . + /// + /// A condition to filter entities + /// Set true to include all children of this entity + /// A to observe while waiting for the task to complete. + Task GetAsync( + [NotNull] Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default + ); + /// /// Deletes many entities by function. /// Notice that: All entities fits to given predicate are retrieved and deleted. @@ -30,7 +58,11 @@ namespace Volo.Abp.Domain.Repositories /// This is useful for ORMs / database APIs those only save changes with an explicit method call, but you need to immediately save changes to the database. /// /// A to observe while waiting for the task to complete. - Task DeleteAsync([NotNull] Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default); + Task DeleteAsync( + [NotNull] Expression> predicate, + bool autoSave = false, + CancellationToken cancellationToken = default + ); } public interface IRepository : IRepository, IReadOnlyRepository, IBasicRepository diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs index 3ade403017..bd7e486d29 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs @@ -49,6 +49,26 @@ namespace Volo.Abp.Domain.Repositories protected abstract IQueryable GetQueryable(); + public abstract Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default); + + public async Task GetAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var entity = await FindAsync(predicate, includeDetails, cancellationToken); + + if (entity == null) + { + throw new EntityNotFoundException(typeof(TEntity)); + } + + return entity; + } + public abstract Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default); protected virtual TQueryable ApplyDataFilters(TQueryable query) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 106e00f77a..a48e5d7da5 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -93,6 +93,20 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore return DbSet.AsQueryable(); } + public override async Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return includeDetails + ? await WithDetails() + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)) + : await DbSet + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + public override async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { var entities = await GetQueryable() @@ -173,18 +187,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore } - public virtual TEntity Get(TKey id, bool includeDetails = true) - { - var entity = Find(id, includeDetails); - - if (entity == null) - { - throw new EntityNotFoundException(typeof(TEntity), id); - } - - return entity; - } - public virtual async Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { var entity = await FindAsync(id, includeDetails, GetCancellationToken(cancellationToken)); @@ -197,13 +199,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore return entity; } - public virtual TEntity Find(TKey id, bool includeDetails = true) - { - return includeDetails - ? WithDetails().FirstOrDefault(e => e.Id.Equals(id)) - : DbSet.Find(id); - } - public virtual async Task FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { return includeDetails diff --git a/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj b/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj index 8aa654922a..d282ce5269 100644 --- a/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj +++ b/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs index b81aa7ae87..2457c3c453 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs @@ -31,6 +31,14 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb return ApplyDataFilters(Collection.AsQueryable()); } + public override Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return Task.FromResult(Collection.AsQueryable().Where(predicate).SingleOrDefault()); + } + public override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { var entities = Collection.AsQueryable().Where(predicate).ToList(); diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs index 3333f1d505..5533cb9f74 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs @@ -169,6 +169,16 @@ namespace Volo.Abp.Domain.Repositories.MongoDB return GetMongoQueryable(); } + public override async Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + public virtual IMongoQueryable GetMongoQueryable() { return ApplyDataFilters( diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json new file mode 100644 index 0000000000..3fb22139b7 --- /dev/null +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -0,0 +1,62 @@ +{ + "culture": "ar", + "texts": { + "InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!", + "ValidationErrorMessage": "طلبك غير صحيح!", + "ValidationNarrativeErrorMessageTitle": "تم الكشف عن الأخطاء التالية أثناء التحقق .", + "DefaultErrorMessage": "حدث خطأ!", + "DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ بواسطة الخادم.", + "DefaultErrorMessage401": "أنت غير مصدق!", + "DefaultErrorMessage401Detail": "يجب عليك تسجيل الدخول لأداء هذه العملية.", + "DefaultErrorMessage403": "أنك غير مخول!", + "DefaultErrorMessage403Detail": "لا يسمح لك بإجراء هذه العملية!", + "DefaultErrorMessage404": "المورد غير موجود!", + "DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!", + "EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!", + "Languages": "اللغات", + "Error": "خطأ", + "AreYouSure": "هل أنت متأكد؟", + "Cancel": "إلغاء", + "Yes": "نعم", + "No": "لا", + "Ok": "حسنا", + "Close": "أغلق", + "Save": "حفظ", + "SavingWithThreeDot": "حفظ...", + "Actions": "أجراءات", + "Delete": "حذف", + "Edit": "تعديل", + "Refresh": "تحديث", + "Language": "لغة", + "LoadMore": "تحميل المزيد", + "ProcessingWithThreeDot": "معالجة...", + "LoadingWithThreeDot": "جار التحميل...", + "Welcome": "أهلا بك", + "Login": "تسجيل الدخول", + "Register": "تسجيل", + "Logout": "تسجيل خروج", + "Submit": "إرسال", + "Back": "عودة", + "PagerSearch": "بحث", + "PagerNext": "التالى", + "PagerPrevious": "السابق", + "PagerFirst": "الأول", + "PagerLast": "الاخير", + "PagerInfo": "عرض _START_ الي _END_ من _TOTAL_ إدخالات", + "PagerInfoEmpty": "عرض 0 الي 0 من 0 إدخالات", + "PagerInfoFiltered": "(تمت تصفيته من _MAX_ مجموع الإدخالات)", + "NoDataAvailableInDatatable": "لا توجد بيانات متاحة في الجدول", + "PagerShowMenuEntries": "عرض _MENU_ إدخالات", + "DatatableActionDropdownDefaultText": "أجراءات", + "ChangePassword": "تغيير كلمة السر", + "PersonalInfo": "ملفي الشخصي", + "AreYouSureYouWantToCancelEditingWarningMessage": "لم تحفظ التغييرات.", + "UnhandledException": "استثناء غير معالج!", + "401Message": "غير مصرح", + "403Message": "ممنوع", + "404Message": "الصفحة غير موجودة", + "500Message": "خطأ في الخادم الداخلي", + "GoHomePage": "اذهب الى الصفحة الرئيسية", + "GoBack": "عد" + } +} diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs index 8ef50ce9f7..fb3be504c5 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs @@ -58,6 +58,16 @@ namespace Volo.Abp.Validation LogLevel = LogLevel.Warning; } + /// + /// Constructor. + /// + /// Validation errors + public AbpValidationException(IList validationErrors) + { + ValidationErrors = validationErrors; + LogLevel = LogLevel.Warning; + } + /// /// Constructor. /// diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs new file mode 100644 index 0000000000..8db6898276 --- /dev/null +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace Volo.Abp.Validation +{ + public static class HasValidationErrorsExtensions + { + public static TException WithValidationError([NotNull] this TException exception, [NotNull] ValidationResult validationError) + where TException : IHasValidationErrors + { + Check.NotNull(exception, nameof(exception)); + Check.NotNull(validationError, nameof(validationError)); + + exception.ValidationErrors.Add(validationError); + + return exception; + } + + public static TException WithValidationError([NotNull] this TException exception, string errorMessage, params string[] memberNames) + where TException : IHasValidationErrors + { + var validationResult = memberNames.IsNullOrEmpty() + ? new ValidationResult(errorMessage) + : new ValidationResult(errorMessage, memberNames); + + return exception.WithValidationError(validationResult); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs index 7e1dd24ae6..067f4b6a01 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs @@ -246,6 +246,11 @@ namespace Volo.Abp.Domain.Repositories throw new NotImplementedException(); } + public override Task FindAsync(Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + public override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { throw new NotImplementedException(); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictAppService.cs new file mode 100644 index 0000000000..81052d17cc --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictAppService.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Application.Dto; + +namespace Volo.Abp.TestApp.Application +{ + //This is especially used to test the AbstractKeyCrudAppService + public class DistrictAppService : AbstractKeyCrudAppService + { + public DistrictAppService(IRepository repository) + : base(repository) + { + } + + protected override async Task DeleteByIdAsync(DistrictKey id) + { + await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); + } + + protected override async Task GetEntityByIdAsync(DistrictKey id) + { + return await AsyncQueryableExecuter.FirstOrDefaultAsync( + Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) + ); + } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictKey.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictKey.cs new file mode 100644 index 0000000000..a7d5c9b965 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictKey.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.TestApp.Application +{ + public class DistrictKey + { + public Guid CityId { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DistrictDto.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DistrictDto.cs new file mode 100644 index 0000000000..3b5f3e5f83 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DistrictDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.TestApp.Application.Dto +{ + public class DistrictDto : EntityDto + { + public Guid CityId { get; set; } + + public string Name { get; set; } + + public int Population { get; set; } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index 6d1be41f31..03e4ed5465 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs @@ -41,7 +41,7 @@ namespace Volo.Abp.TestApp { var istanbul = new City(IstanbulCityId, "Istanbul"); istanbul.Districts.Add(new District(istanbul.Id, "Bakirkoy", 1283999)); - istanbul.Districts.Add(new District(istanbul.Id, "Mecidiyek�y", 2222321)); + istanbul.Districts.Add(new District(istanbul.Id, "Mecidiyekoy", 2222321)); istanbul.Districts.Add(new District(istanbul.Id, "Uskudar", 726172)); await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Tokyo")); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs index 07e6211c09..28c94b02fc 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs @@ -28,6 +28,14 @@ namespace Volo.Abp.TestApp.Testing person.Phones.Count.ShouldBe(2); } + [Fact] + public async Task GetAsync_With_Predicate() + { + var person = await PersonRepository.GetAsync(p => p.Name == "Douglas"); + person.Name.ShouldBe("Douglas"); + person.Phones.Count.ShouldBe(2); + } + [Fact] public async Task FindAsync_Should_Return_Null_For_Not_Found_Entity() { @@ -35,6 +43,14 @@ namespace Volo.Abp.TestApp.Testing person.ShouldBeNull(); } + [Fact] + public async Task FindAsync_Should_Return_Null_For_Not_Found_Entity_With_Predicate() + { + var randomName = Guid.NewGuid().ToString(); + var person = await PersonRepository.FindAsync(p => p.Name == randomName); + person.ShouldBeNull(); + } + [Fact] public async Task DeleteAsync() { diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj index 2ab190b724..a38a51a16c 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj @@ -32,4 +32,8 @@ + + + + diff --git a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj index 027e32bd09..e98baaf3ab 100644 --- a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj +++ b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj @@ -7,12 +7,24 @@ + + + + + + + + - + + + + + diff --git a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/AbpAccountApplicationTestModule.cs b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/AbpAccountApplicationTestModule.cs index 6be56c3a87..1adac8a6ef 100644 --- a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/AbpAccountApplicationTestModule.cs +++ b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/AbpAccountApplicationTestModule.cs @@ -2,8 +2,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; +using Volo.Abp.Authorization; +using Volo.Abp.Autofac; using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Identity; using Volo.Abp.Identity.AspNetCore; using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.Modularity; @@ -12,9 +13,13 @@ using Volo.Abp.PermissionManagement.EntityFrameworkCore; namespace Volo.Abp.Account { [DependsOn( + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule), + typeof(AbpAuthorizationModule), typeof(AbpIdentityAspNetCoreModule), typeof(AbpAccountApplicationModule), - typeof(AbpIdentityDomainTestModule) + typeof(AbpIdentityEntityFrameworkCoreModule), + typeof(AbpPermissionManagementEntityFrameworkCoreModule) )] public class AbpAccountApplicationTestModule : AbpModule { @@ -30,7 +35,6 @@ namespace Volo.Abp.Account }); }); } - private static SqliteConnection CreateDatabaseAndGetConnection() { var connection = new SqliteConnection("Data Source=:memory:"); diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml index 8c0b63d998..5019c8a29e 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml @@ -7,7 +7,7 @@ @inject IAuthorizationService Authorization @model Volo.Blogging.Pages.Blog.Posts.IndexModel @{ - ViewBag.PageTitle = "Blog"; + ViewBag.Title = "Blog"; } @section scripts { diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs index 0f573634a4..9381a9b2c8 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs @@ -8,6 +8,8 @@ namespace Volo.Abp.Identity { public interface IIdentityRoleAppService : IApplicationService { + Task> GetAllListAsync(); + Task> GetListAsync(PagedAndSortedResultRequestDto input); Task CreateAsync(IdentityRoleCreateDto input); diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs index ffc13fdbca..b02319bc24 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs @@ -27,6 +27,13 @@ namespace Volo.Abp.Identity await _roleManager.GetByIdAsync(id)); } + public virtual async Task> GetAllListAsync() + { + var list = await _roleRepository.GetListAsync(); + return new ListResultDto( + ObjectMapper.Map, List>(list)); + } + public virtual async Task> GetListAsync(PagedAndSortedResultRequestDto input) { var list = await _roleRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount); diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json index 025bb7fd87..d6d6ca3983 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json @@ -82,6 +82,7 @@ "DisplayName:Abp.Identity.Lockout.LockoutDuration": "Lockout duration(seconds)", "DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "Max failed access attempts", "DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "Require confirmed email", + "DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Enable phone number confirmation", "DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Require confirmed phoneNumber", "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "Is username update enabled", "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "Is email update enabled", @@ -95,6 +96,7 @@ "Description:Abp.Identity.Lockout.LockoutDuration": "The duration a user is locked out for when a lockout occurs.", "Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "The number of failed access attempts allowed before a user is locked out, assuming lock out is enabled.", "Description:Abp.Identity.SignIn.RequireConfirmedEmail": "Whether a confirmed email address is required to sign in.", + "Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Whether the phoneNumber can be confirmed by the user.", "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Whether a confirmed telephone number is required to sign in.", "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Whether the username can be updated by the user.", "Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user." diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs index babeb4fb77..9c07f6c55b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs @@ -30,6 +30,7 @@ private const string SignInPrefix = Prefix + ".SignIn"; public const string RequireConfirmedEmail = SignInPrefix + ".RequireConfirmedEmail"; + public const string EnablePhoneNumberConfirmation = SignInPrefix + ".EnablePhoneNumberConfirmation"; public const string RequireConfirmedPhoneNumber = SignInPrefix + ".RequireConfirmedPhoneNumber"; } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs index efae6e467a..92847004ea 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs @@ -72,6 +72,11 @@ namespace Volo.Abp.Identity false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail"), L("Description:Abp.Identity.SignIn.RequireConfirmedEmail"), true), + new SettingDefinition( + IdentitySettingNames.SignIn.EnablePhoneNumberConfirmation, + true.ToString(), L("DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"), + L("Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"), + true), new SettingDefinition( IdentitySettingNames.SignIn.RequireConfirmedPhoneNumber, false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber"), diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs index 09760109d0..9925d18abb 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Application.Dtos; @@ -19,6 +20,13 @@ namespace Volo.Abp.Identity _roleAppService = roleAppService; } + [HttpGet] + [Route("all")] + public virtual Task> GetAllListAsync() + { + return _roleAppService.GetAllListAsync(); + } + [HttpGet] public virtual Task> GetListAsync(PagedAndSortedResultRequestDto input) { diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs index aad90e73cc..6c1ac1ff72 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs @@ -29,9 +29,9 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users { UserInfo = new UserInfoViewModel(); - var roleDtoList = await _identityRoleAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + var roleDtoList = (await _identityRoleAppService.GetAllListAsync()).Items; - Roles = ObjectMapper.Map, AssignedRoleViewModel[]>(roleDtoList.Items); + Roles = ObjectMapper.Map, AssignedRoleViewModel[]>(roleDtoList); foreach (var role in Roles) { diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs index 6c8d40c14d..98a32fd1bd 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs @@ -31,9 +31,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users { UserInfo = ObjectMapper.Map(await _identityUserAppService.GetAsync(id)); - Roles = ObjectMapper.Map, AssignedRoleViewModel[]>( - (await _identityRoleAppService.GetListAsync(new PagedAndSortedResultRequestDto())).Items - ); + Roles = ObjectMapper.Map, AssignedRoleViewModel[]>((await _identityRoleAppService.GetAllListAsync()).Items); var userRoleNames = (await _identityUserAppService.GetRolesAsync(UserInfo.Id)).Items.Select(r => r.Name).ToList(); foreach (var role in Roles) diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs index 663cc07786..5d64bb6c78 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs @@ -34,6 +34,18 @@ namespace Volo.Abp.Identity result.Id.ShouldBe(moderator.Id); } + [Fact] + public async Task GetAllListAsync() + { + //Act + + var result = await _roleAppService.GetAllListAsync(); + + //Assert + + result.Items.Count.ShouldBeGreaterThan(0); + } + [Fact] public async Task GetListAsync() { diff --git a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo.Abp.Identity.EntityFrameworkCore.Tests.csproj b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo.Abp.Identity.EntityFrameworkCore.Tests.csproj index bb3329cb25..f4eb09517d 100644 --- a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo.Abp.Identity.EntityFrameworkCore.Tests.csproj +++ b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo.Abp.Identity.EntityFrameworkCore.Tests.csproj @@ -13,9 +13,7 @@ - - diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs index 7561835ee3..189d7d0f80 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs @@ -1,7 +1,22 @@ -namespace Volo.Abp.TenantManagement +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp.TenantManagement.Localization; + +namespace Volo.Abp.TenantManagement { public class TenantCreateDto : TenantCreateOrUpdateDtoBase { + [Required] + [EmailAddress] + [MaxLength(256)] + public string AdminEmailAddress { get; set; } + + [Required] + [MaxLength(128)] + public string AdminPassword { get; set; } } } \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs index a5a9b4af3d..b02da79b79 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs @@ -1,7 +1,11 @@ -namespace Volo.Abp.TenantManagement +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.TenantManagement { public abstract class TenantCreateOrUpdateDtoBase { + [Required] + [StringLength(TenantConsts.MaxNameLength)] public string Name { get; set; } } } \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs index 3dabc569bd..cbe9b67c76 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs @@ -15,7 +15,7 @@ namespace Volo.Abp.TenantManagement protected ITenantManager TenantManager { get; } public TenantAppService( - ITenantRepository tenantRepository, + ITenantRepository tenantRepository, ITenantManager tenantManager, IDataSeeder dataSeeder) { @@ -51,10 +51,13 @@ namespace Volo.Abp.TenantManagement { //TODO: Handle database creation? - //TODO: Set admin email & password..? - await DataSeeder.SeedAsync(tenant.Id); + await DataSeeder.SeedAsync( + new DataSeedContext(tenant.Id) + .WithProperty("AdminEmail", input.AdminEmailAddress) + .WithProperty("AdminPassword", input.AdminPassword) + ); } - + return ObjectMapper.Map(tenant); } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json index fc51f00750..d6d1d7c155 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json @@ -15,6 +15,8 @@ "Permission:Edit": "Edit", "Permission:Delete": "Delete", "Permission:ManageConnectionStrings": "Manage connection strings", - "Permission:ManageFeatures": "Manage features" + "Permission:ManageFeatures": "Manage features", + "DisplayName:AdminEmailAddress": "Admin Email Address", + "DisplayName:AdminPassword": "Admin Password" } } \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json index 1b6bdca2c2..72b570f3f9 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json @@ -15,6 +15,8 @@ "Permission:Edit": "Düzenleme", "Permission:Delete": "Silme", "Permission:ManageConnectionStrings": "Bağlantı cümlelerini yönet", - "Permission:ManageFeatures": "Özellikleri yönet" + "Permission:ManageFeatures": "Özellikleri yönet", + "DisplayName:AdminEmailAddress": "Admin Eposta Adresi", + "DisplayName:AdminPassword": "Admin Şifresi" } } \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs index 2aabf871de..df8e513003 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs @@ -35,6 +35,7 @@ namespace Volo.Abp.TenantManagement [HttpPost] public virtual Task CreateAsync(TenantCreateDto input) { + ValidateModel(); return _service.CreateAsync(input); } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml index b3a2c9d5be..8593679a7c 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml @@ -12,7 +12,11 @@ - + + + + + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs index a602b12a65..23c71bba96 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs @@ -31,8 +31,16 @@ namespace Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants { [Required] [StringLength(TenantConsts.MaxNameLength)] - [Display(Name = "DisplayName:TenantName")] public string Name { get; set; } + + [Required] + [EmailAddress] + [MaxLength(256)] + public string AdminEmailAddress { get; set; } + + [Required] + [MaxLength(128)] + public string AdminPassword { get; set; } } } } diff --git a/modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs b/modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs index 3e44e7c8ea..b27ceff6dc 100644 --- a/modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs +++ b/modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs @@ -59,7 +59,7 @@ namespace Volo.Abp.TenantManagement public async Task CreateAsync() { var tenancyName = Guid.NewGuid().ToString("N").ToLowerInvariant(); - var tenant = await _tenantAppService.CreateAsync(new TenantCreateDto { Name = tenancyName }); + var tenant = await _tenantAppService.CreateAsync(new TenantCreateDto { Name = tenancyName , AdminEmailAddress = "admin@admin.com", AdminPassword = "123456"}); tenant.Name.ShouldBe(tenancyName); tenant.Id.ShouldNotBe(default(Guid)); } @@ -69,7 +69,7 @@ namespace Volo.Abp.TenantManagement { await Assert.ThrowsAsync(async () => { - await _tenantAppService.CreateAsync(new TenantCreateDto { Name = "acme" }); + await _tenantAppService.CreateAsync(new TenantCreateDto { Name = "acme", AdminEmailAddress = "admin@admin.com", AdminPassword = "123456" }); }); } diff --git a/npm/ng-packs/apps/dev-app/src/app/shared/shared.module.ts b/npm/ng-packs/apps/dev-app/src/app/shared/shared.module.ts index 6c4c9b016c..1f268b4572 100644 --- a/npm/ng-packs/apps/dev-app/src/app/shared/shared.module.ts +++ b/npm/ng-packs/apps/dev-app/src/app/shared/shared.module.ts @@ -3,7 +3,6 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgModule } from '@angular/core'; import { ThemeBasicModule } from '@abp/ng.theme.basic'; import { ThemeSharedModule } from '@abp/ng.theme.shared'; -import { TableModule } from 'primeng/table'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @NgModule({ @@ -12,7 +11,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; CoreModule, ThemeSharedModule, ThemeBasicModule, - TableModule, NgbDropdownModule, NgxValidateCoreModule, ], @@ -20,7 +18,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; CoreModule, ThemeSharedModule, ThemeBasicModule, - TableModule, NgbDropdownModule, NgxValidateCoreModule, ], diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index b8ec511a18..93ef5c8ae7 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -21,19 +21,19 @@ "generate:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" }, "devDependencies": { - "@abp/ng.account": "^2.1.0", - "@abp/ng.account.config": "^2.1.0", - "@abp/ng.core": "^2.1.0", - "@abp/ng.feature-management": "^2.1.0", - "@abp/ng.identity": "^2.1.0", - "@abp/ng.identity.config": "^2.1.0", - "@abp/ng.permission-management": "^2.1.0", - "@abp/ng.setting-management": "^2.1.0", - "@abp/ng.setting-management.config": "^2.1.0", - "@abp/ng.tenant-management": "^2.1.0", - "@abp/ng.tenant-management.config": "^2.1.0", - "@abp/ng.theme.basic": "^2.1.0", - "@abp/ng.theme.shared": "^2.1.0", + "@abp/ng.account": "~2.2.0", + "@abp/ng.account.config": "~2.2.0", + "@abp/ng.core": "~2.2.0", + "@abp/ng.feature-management": "~2.2.0", + "@abp/ng.identity": "~2.2.0", + "@abp/ng.identity.config": "~2.2.0", + "@abp/ng.permission-management": "~2.2.0", + "@abp/ng.setting-management": "~2.2.0", + "@abp/ng.setting-management.config": "~2.2.0", + "@abp/ng.tenant-management": "~2.2.0", + "@abp/ng.tenant-management.config": "~2.2.0", + "@abp/ng.theme.basic": "~2.2.0", + "@abp/ng.theme.shared": "~2.2.0", "@angular-builders/jest": "^8.2.0", "@angular-devkit/build-angular": "~0.803.21", "@angular-devkit/build-ng-packagr": "~0.803.21", diff --git a/npm/ng-packs/packages/account/package.json b/npm/ng-packs/packages/account/package.json index d4b136a17d..84d4b6bf56 100644 --- a/npm/ng-packs/packages/account/package.json +++ b/npm/ng-packs/packages/account/package.json @@ -7,8 +7,8 @@ "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.account.config": "^2.2.0", - "@abp/ng.theme.shared": "^2.2.0" + "@abp/ng.account.config": "~2.2.0", + "@abp/ng.theme.shared": "~2.2.0" }, "publishConfig": { "access": "public" diff --git a/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html b/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html index 145935f7c5..6e732540d4 100644 --- a/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html +++ b/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html @@ -1,8 +1,10 @@
- + + +