diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/2.4ABP\345\205\254\345\205\261\347\273\223\346\236\204-\346\227\245\345\277\227\347\256\241\347\220\206.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/2.4ABP\345\205\254\345\205\261\347\273\223\346\236\204-\346\227\245\345\277\227\347\256\241\347\220\206.md" index be7998f..614fb44 100644 --- "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/2.4ABP\345\205\254\345\205\261\347\273\223\346\236\204-\346\227\245\345\277\227\347\256\241\347\220\206.md" +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/2.4ABP\345\205\254\345\205\261\347\273\223\346\236\204-\346\227\245\345\277\227\347\256\241\347\220\206.md" @@ -1,4 +1,4 @@ -## 2.4 ABP公共结构 - 日志管理 +# 2.4 ABP公共结构 - 日志管理 ### 2.4.1 服务器端 diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/About.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/About.md" new file mode 100644 index 0000000..a492c8e --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/About.md" @@ -0,0 +1,147 @@ +- [Considerations](#considerations) +- [Source codes](#sourcecodes) +- [Contributors](#contributors) +- [Contact](#contact) + +ASP.NET Boilerplate was created to help developers build applications using +the best software design practices without repeating themselves. DRY - +**Don't Repeat Yourself!** is the key idea behind ASP.NET Boilerplate. + +All applications have some common problems and need some common +structures. ASP.NET Boilerplate works for small projects to large +enterprise web applications, providing a quick start with maintainable +code bases. + +### Considerations + +Keep these concepts in mind while developing with ASP.NET Boilerplate. + +#### Modularity + +It should be easy to share [entities](/Pages/Documents/Entities), +[repositories](/Pages/Documents/Repositories), +[services](/Pages/Documents/Application-Services) and views between web +applications. They should be packaged into +[modules](/Pages/Documents/Module-System) and can be easily distributed +(preferred as public/private NuGet packages). Modules may depend on and +use other modules. We should be able to extend models in a module for +our application needs. + +Modularity provides us with "code re-usability" (DRY!). For example, we +may develop a module that contains user management, role management, +login and error pages which can be shared by different applications. + +#### Best practices + +An application should be developed using the best software design +principles. Using [dependency +injection](/Pages/Documents/Dependency-Injection) is one of the most +important subjects in this area. AOP (Aspect Oriented Programming) is +used where it's needed and possible, especially for [cross-cutting +concerns](http://en.wikipedia.org/wiki/Cross-cutting_concern). The +application should also correctly use architectural patterns such as MVC +and MVVM, and it should follow +[SOLID](http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) +principles + +Following these best practices makes our code-base more understandable +and extensible. It also prevents us from falling in to common mistakes +that have already been experienced by other developers. + +#### Scalable code base + +The architecture of an application should provide and enforce a way of +keeping a maintainable code base. +[Layering](/Pages/Documents/NLayer-Architecture) and +[modularity](/Pages/Documents/Module-System) are the main techniques to +accomplish that. Following the best practices is important, otherwise +the application gets complicated when it grows. Many applications have +been rewritten because the code became too unmaintainable. + +#### Libraries & Frameworks + +An application should use and combine useful libraries & frameworks to +accomplish well-known tasks. It should not try to re-invent the wheel if +an existing tool meets it's requirements, and it should focus on it's +own job (to it's own business logic) as much as possible. The +application may use +[EntityFramework](/Pages/Documents/EntityFramework-Integration) or +[NHibernate](/Pages/Documents/NHibernate-Integration) for +Object-Relational Mapping, and it may also use +[Angular](https://angular.io/) or +[DurandalJs](http://durandaljs.com/) as a Single-Page Application +framework. + +Like it or not, we need to learn many different tools to build an +application, even if it's more complicated on the client side. There are +many libraries (thousands of jQuery plug-ins for instance) and +frameworks, so we should carefully choose our libraries and adapt them +for our application. + +ASP.NET Boilerplates composes and combines some of the best tools out +there, but it also does not prevent you from using your own favourite +tools. + +#### Cross-cutting concerns + +Authorization, +[validation](/Pages/Documents/Validating-Data-Transfer-Objects), [error +handling](/Pages/Documents/Handling-Exceptions), +[logging](/Pages/Documents/Logging), caching are common things all +applications implement at some level. The code should be generic and +shared by different applications. It should also be separated from the +business logic code and should be automated as much as possible. This +allows us to focus more on our application specific business logic and +prevents us from re-coding the same stuff over and over again (DRY!). + +#### More automation + +If it can be automated, it should be automated (at least in most cases). +Database migrations, unit tests, and deployments are some of the tasks +that can be automated. Automation saves us time in a long term and +prevents from making mistakes of manual tasks (DRY!). + +#### Convention over configuration + +[Convention over +configuration](http://en.wikipedia.org/wiki/Convention_over_configuration) +is a very popular software design principle. An application framework +should implement defaults as much as possible. It should be easy when +following conventions but also configurable when needed. + +#### Project startup + +It should be easy and fast to start a new application. We should not +repeat some tedious steps to create an empty application (DRY!). A +Project/Solution [templates](/Templates) is a proper way of doing it. + +### Source code + +ASP.NET Boilerplate is an open source project developed on GitHub. + +- Source code: + +- Project templates: + +- Sample projects: + +- Module Zero: + +### Contributors + +ASP.NET Boilerplate is designed and developed by [Halil İbrahim +Kalkan](http://www.halilibrahimkalkan.com/). There are also many +[contributors](https://github.com/aspnetboilerplate/aspnetboilerplate/graphs/contributors) +on GitHub. Please feel free to fork our repositories and send pull +requests! + +### Contact + +For your questions and other discussions, use [official +forum](http://forum.aspnetboilerplate.com/). + +For feature requests or bug reports, use [GitHub +issues](https://github.com/aspnetboilerplate/aspnetboilerplate/issues). + +For personal contact with me, visit my [web +page](http://halilibrahimkalkan.com/contact/). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Abp-Session.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Abp-Session.md" new file mode 100644 index 0000000..69fbea1 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Abp-Session.md" @@ -0,0 +1,113 @@ +### Introduction + +ASP.NET Boilerplate provides an **IAbpSession** interface to obtain the current +user and tenant **without** using ASP.NET's Session. IAbpSession is also +fully integrated and used by other structures in ASP.NET Boilerplate such as the +[setting](Setting-Management.md) and +[authorization](Authorization.md) systems. + +### Injecting Session + +IAbpSession is generally **[property +injected](/Pages/Documents/Dependency-Injection#property-injection-pattern)** +to needed classes unless it's not possible to work without session +information. If we use property injection, we can use +**NullAbpSession.Instance** as a default value, as shown below: + + public class MyClass : ITransientDependency + { + public IAbpSession AbpSession { get; set; } + + public MyClass() + { + AbpSession = NullAbpSession.Instance; + } + + public void MyMethod() + { + var currentUserId = AbpSession.UserId; + //... + } + } + +Since authentication/authorization is an application layer task, it's +advisable to **use the IAbpSession in the application layer and upper layers**. +This is not generally done in the domain layer. **ApplicationService**, +**AbpController,** **AbpApiController** and some other base classes have +**AbpSession** already injected, so you can, for instance, directly use the +AbpSession property in an application service method. + +### Session Properties + +AbpSession defines a few key properties: + +- **UserId**: Id of the current user or null if there is no current + user. It can not be null if the calling code is authorized. +- **TenantId**: Id of the current tenant or null if there is no + current tenant (in case of user has not logged in or he is a host + user). +- **ImpersonatorUserId**: Id of the impersonator user, if the current + session is impersonated by another user. It's null if this is not an + impersonated login. +- **ImpersonatorTenantId**: Id of the impersonator user's tenant, if + the current session is impersonated by another user. It's null if this + is not an impersonated login. +- **MultiTenancySide**: It may be Host or Tenant. + +UserId and TenantId is **nullable**. There are also the non-nullable +**GetUserId()** and **GetTenantId()** methods. If you're sure there is a +current user, you can call GetUserId(). If the current user is null, this +method throws an exception. GetTenantId() also works in this way. + +Impersonator properties are not as common as other properties and are +generally used for [audit logging](/Pages/Documents/Audit-Logging) purposes. + +**ClaimsAbpSession** + +ClaimsAbpSession is the **default implementation** of the IAbpSession +interface. It gets session properties (except MultiTenancySide, it's +calculated) from the claims of the current user's principal. For a cookie-based +form authentication, it gets the values from cookies. Thus, it's fully integrated +in to ASP.NET's authentication mechanism. + +### Overriding Current Session Values + +In some specific cases, you may need to change/override session values +for a limited scope. In such cases, you can use the IAbpSession.Use method +as shown below: + + public class MyService + { + private readonly IAbpSession _session; + + public MyService(IAbpSession session) + { + _session = session; + } + + public void Test() + { + using (_session.Use(42, null)) + { + var tenantId = _session.TenantId; //42 + var userId = _session.UserId; //null + } + } + } + +The Use method returns an IDisposable and it **must be disposed**. Once the +return value is disposed, Session values are **automatically restored** +the to previous values. + +#### Warning! + +Always use the Use method in a using block as shown above. Otherwise, you may +get unexpected session values. You can have nested Use blocks and they will +work as you expect. + +### User Identifier + +You can use **.ToUserIdentifier()** extension method to create a +UserIdentifier object from IAbpSession. Since UserIdentifier is used in +a lot of APIs, this will simplify the creation of a UserIdentifier object for the +current user. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Application-Services.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Application-Services.md" new file mode 100644 index 0000000..3644881 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Application-Services.md" @@ -0,0 +1,364 @@ +Application Services are used to expose domain logic to the presentation +layer. An Application Service is called from the presentation layer using a +DTO (Data Transfer Object) as a parameter. It also uses domain objects to perform +some specific business logic and returns a DTO back to the presentation +layer. Thus, the presentation layer is completely isolated from Domain +layer. + +In an ideally layered application, the presentation layer never +directly works with domain objects. + +### IApplicationService Interface + +In ASP.NET Boilerplate, an application service **should** implement the +**IApplicationService** interface. It's good to create an **interface** +for each Application Service. + +First, let's define an interface for an application service: + + public interface IPersonAppService : IApplicationService + { + void CreatePerson(CreatePersonInput input); + } + +**IPersonAppService** has only one method. It's used by the presentation +layer to create a new person. **CreatePersonInput** is a DTO object as +shown below: + + public class CreatePersonInput + { + [Required] + public string Name { get; set; } + + public string EmailAddress { get; set; } + } + +Now we can implement the IPersonAppService: + + public class PersonAppService : IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + var person = _personRepository.FirstOrDefault(p => p.EmailAddress == input.EmailAddress); + if (person != null) + { + throw new UserFriendlyException("There is already a person with given email address"); + } + + person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + _personRepository.Insert(person); + } + } + +There are some important points to consider here: + +- PersonAppService uses + [IRepository<Person>](/Pages/Documents/Repositories) + to perform database operations. It uses a **constructor injection** + pattern, and thereby uses [dependency injection](/Pages/Documents/Dependency-Injection). +- PersonAppService implements **IApplicationService** (since + IPersonAppService extends IApplicationService). It's automatically + registered to Dependency Injection system by ASP.NET Boilerplate and + can be injected and used by other classes. The naming convention is + important here. See the [dependency + injection](Dependency-Injection.md) document for more info. +- The **CreatePerson** method gets **CreatePersonInput**. It's an **input + DTO** and automatically validated by ASP.NET Boilerplate. See the + [DTO](/Pages/Documents/Data-Transfer-Objects) and + [validation](/Pages/Documents/Validating-Data-Transfer-Objects) + documents for details. + +### ApplicationService Class + +An application service should implement the IApplicationService interface as +declared above. **Optionally**, it can be derived from the +**ApplicationService** base class. Thus, IApplicationService is +inherently implemented. + +The ApplicationService class has some basic functionality +that makes it easy to do **logging,** **localization** and so +on... It's recommend that you create a special base class for your application +services that extends the ApplicationService class. This way, you can add some +common functionality for all your application services. A sample +application service class is shown below: + + public class TaskAppService : ApplicationService, ITaskAppService + { + public TaskAppService() + { + LocalizationSourceName = "SimpleTaskSystem"; + } + + public void CreateTask(CreateTaskInput input) + { + //Write some logs (Logger is defined in ApplicationService class) + Logger.Info("Creating a new task with description: " + input.Description); + + //Get a localized text (L is a shortcut for LocalizationHelper.GetString(...), defined in ApplicationService class) + var text = L("SampleLocalizableTextKey"); + + //TODO: Add new task to database... + } + } + +You can have a base class which defines **LocalizationSourceName** in +it's constructor. This way you do not repeat it for all service classes. +See the [logging](/Pages/Documents/Logging) and +[localization](/Pages/Documents/Localization) documents for more +information on this topic. + +### CrudAppService and AsyncCrudAppService Classes + +If you need to create an application service that will have **Create, +Update, Delete, Get, GetAll** methods for a **specific entity**, you can +easily inherit from the **CrudAppService** class. You could also use +the **AsyncCrudAppService** class to create async methods. +The CrudAppService base class is **generic**, which gets the related **Entity** and +**DTO** types as generic arguments. This is also **extensible**, allowing you to override +functionality when you need to customize it. + +#### Simple CRUD Application Service Example + +Assume that we have a Task [entity](Entities.md) defined below: + + public class Task : Entity, IHasCreationTime + { + public string Title { get; set; } + + public string Description { get; set; } + + public DateTime CreationTime { get; set; } + + public TaskState State { get; set; } + + public Person AssignedPerson { get; set; } + public Guid? AssignedPersonId { get; set; } + + public Task() + { + CreationTime = Clock.Now; + State = TaskState.Open; + } + } + +And we created a [DTO](Data-Transfer-Objects.md) for this entity: + + [AutoMap(typeof(Task))] + public class TaskDto : EntityDto, IHasCreationTime + { + public string Title { get; set; } + + public string Description { get; set; } + + public DateTime CreationTime { get; set; } + + public TaskState State { get; set; } + + public Guid? AssignedPersonId { get; set; } + + public string AssignedPersonName { get; set; } + } + +The AutoMap attribute creates a mapping configuration between the entity +and dto. Now, we can create an application service as shown below: + + public class TaskAppService : AsyncCrudAppService + { + public TaskAppService(IRepository repository) + : base(repository) + { + + } + } + +We [injected](Dependency-Injection.md) the +[repository](Repositories.md) and passed it to the base class (We +could inherit from CrudAppService if we want to create sync methods +instead of async methods). + +**That's all!** TaskAppService now has simple CRUD methods! + +If you want to define an interface for the +application service, you can create your interface like this: + + public interface ITaskAppService : IAsyncCrudAppService + { + + } + +Notice that **IAsyncCrudAppService** does not get the entity (Task) as a +generic argument. This is because the entity is related to the implementation and +should not be included in a public interface. + +We can now implement the ITaskAppService interface for the TaskAppService class: + + public class TaskAppService : AsyncCrudAppService, ITaskAppService + { + public TaskAppService(IRepository repository) + : base(repository) + { + + } + } + +#### Customize CRUD Application Services + +##### Getting a List + +A Crud application service gets **PagedAndSortedResultRequestDto** as an +argument for the **GetAll** method as default, which provides optional +sorting and paging parameters. You may also want to add other +parameters for the GetAll method. For example, you may want to add some +**custom filters**. In this case, you can create a DTO for the GetAll +method. Example: + + public class GetAllTasksInput : PagedAndSortedResultRequestDto + { + public TaskState? State { get; set; } + } + +Here we inherit from **PagedAndSortedResultRequestInput**. This is **not +required**, but if you want, you can use the paging & sorting parameters from the base +class. We also added an **optional State** property to filter tasks by +state. With this, we change the TaskAppService class in order to apply the **custom filter**: + + public class TaskAppService : AsyncCrudAppService + { + public TaskAppService(IRepository repository) + : base(repository) + { + + } + + protected override IQueryable CreateFilteredQuery(GetAllTasksInput input) + { + return base.CreateFilteredQuery(input) + .WhereIf(input.State.HasValue, t => t.State == input.State.Value); + } + } + +First, we added **GetAllTasksInput** as a 4th generic parameter to the +AsyncCrudAppService class (3rd one is PK type of the entity). Then we +override the **CreateFilteredQuery** method to apply custom filters. This +method is an extension point for the AsyncCrudAppService class. Note that WhereIf is +an extension method of ABP to simplify conditional filtering. What we're +doing here is simply filtering an IQueryable. + +Note: If you created an application service interface, you need +to add the same generic arguments to that interface, too! + +##### Create and Update + +Notice that we are using same DTO (TaskDto) for getting, **creating** +and **updating** tasks which may not be good for real life applications, +so we may want to **customize the create and update DTOs**. + +Let's start by creating a **CreateTaskInput** class: + + [AutoMapTo(typeof(Task))] + public class CreateTaskInput + { + [Required] + [StringLength(Task.MaxTitleLength)] + public string Title { get; set; } + + [StringLength(Task.MaxDescriptionLength)] + public string Description { get; set; } + + public Guid? AssignedPersonId { get; set; } + } + +In addition to this, create an **UpdateTaskInput** DTO: + + [AutoMapTo(typeof(Task))] + public class UpdateTaskInput : CreateTaskInput, IEntityDto + { + public int Id { get; set; } + + public TaskState State { get; set; } + } + +Here we inherit from **CreateTaskInput** to include all properties +for the Update operation (you may want something different). Implementing +**IEntity** (or IEntity<PrimaryKey> for a different PK than int) is +**required** here, because we need to know which entity is being +updated. Lastly, we added an additional property, **State**, which is not +in CreateTaskInput. + +We can now use these DTO classes as generic arguments for the +AsyncCrudAppService class: + + public class TaskAppService : AsyncCrudAppService + { + public TaskAppService(IRepository repository) + : base(repository) + { + + } + + protected override IQueryable CreateFilteredQuery(GetAllTasksInput input) + { + return base.CreateFilteredQuery(input) + .WhereIf(input.State.HasValue, t => t.State == input.State.Value); + } + } + +No need for any additional code changes! + +##### Other Method Arguments + +AsyncCrudAppService can get more generic arguments if you want to +customize input DTOs for **Get** and **Delete** methods. All +methods of the base class are virtual, so you can override them to +customize the behaviour. + +#### CRUD Permissions + +Do you need to [authorize](Authorization.md) your CRUD methods? +If so, there are pre-defined permission properties you can set: +GetPermissionName, GetAllPermissionName, CreatePermissionName, +UpdatePermissionName and DeletePermissionName. The base CRUD class +automatically checks permissions if you set them. + +Here, you can set it in the constructor: + + public class TaskAppService : AsyncCrudAppService + { + public TaskAppService(IRepository repository) + : base(repository) + { + CreatePermissionName = "MyTaskCreationPermission"; + } + } + +Alternatively, you can override the appropriate permission checker methods +to manually check permissions: CheckGetPermission(), +CheckGetAllPermission(), CheckCreatePermission(), +CheckUpdatePermission(), CheckDeletePermission(). By default, they all +call the CheckPermission(...) method with the related permission name. +Simply, this calls the IPermissionChecker.Authorize(...) method. + +### Unit of Work + +An application service method is a **[unit of +work](/Pages/Documents/Unit-Of-Work)** by default in ASP.NET +Boilerplate. Thus, the application service methods are transactional and +automatically save all database changes when each method ends.  + +See the [unit of work](/Pages/Documents/Unit-Of-Work) documentation for +more information. + +### Lifetime of an Application Service + +All application service instances are **Transient**. This means they are +instantiated each time. + +See the [Dependency Injection](/Pages/Documents/Dependency-Injection) documentation +for more information. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles-Tutorials.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles-Tutorials.md" new file mode 100644 index 0000000..430219e --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles-Tutorials.md" @@ -0,0 +1,11 @@ +- Introduction & Step by Step + - [With ASP.NET Core & Entity Framework Core Part-1](Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html) & [Part-2](Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/index.html) + - [With ASP.NET MVC, Web API, EntityFramework & AngularJS](Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/index.html) +- Advanced + - [Developing a Multi-Tenant (SaaS) Application with ASP.NET Core, EntityFramework Core & Angular](Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/index.html) + - [Developing a Multi-Tenant (SaaS) Application with ASP.NET MVC, EntityFramework & AngularJS](Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/index.html) +- Miscellaneous + - [Unit Testing with Entity Framework, xUnit & Effort](Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/index.html) + - [Aspect Oriented Programming using Interceptors](Articles/Aspect-Oriented-Programming-using-Interceptors/index.html) + - [Running in Docker Containers and building a Web Farm / Load Balancer Scenario](Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/index.html) + - [Using Stored Procedures, User Defined Functions and Views](Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/index.html) \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Aspect-Oriented-Programming-using-Interceptors/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Aspect-Oriented-Programming-using-Interceptors/index.html" new file mode 100644 index 0000000..bc2d919 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Aspect-Oriented-Programming-using-Interceptors/index.html" @@ -0,0 +1,524 @@ + + + + +

Contents

+ + + +

Introduction

+ +

In this article, I'll show you how to create interceptors to implement AOP techniques. I'll use ASP.NET Boilerplate (ABP) as the base application framework and Castle Windsor for the interception library. +Apart from ABP framework, most of the techniques explained here are also +eligible for using Castle Windsor.

+ +

What is Aspect Oriented Programming (AOP) and Method Interception?

+ +

From Wikipedia: "In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification".

+ +

In an application, we may have some repeating/similar code for logging, authorization, validation, exception handling and so on...

+ +

Manual Way (Without AOP)

+ +

An example code that implements it all manually:

+ +
+public class TaskAppService : ApplicationService
+{
+    private readonly IRepository<Task> _taskRepository;
+    private readonly IPermissionChecker _permissionChecker;
+    private readonly ILogger _logger;
+
+    public TaskAppService(IRepository<Task> taskRepository, 
+		IPermissionChecker permissionChecker, ILogger logger)
+    {
+        _taskRepository = taskRepository;
+        _permissionChecker = permissionChecker;
+        _logger = logger;
+    }
+
+    public void CreateTask(CreateTaskInput input)
+    {
+        _logger.Debug("Running CreateTask method: " + input.ToJsonString());
+
+        try
+        {
+            if (input == null)
+            {
+                throw new ArgumentNullException("input");
+            }
+
+            if (!_permissionChecker.IsGranted("TaskCreationPermission"))
+            {
+                throw new Exception("No permission for this operation!");
+            }
+
+            _taskRepository.Insert(new Task(input.Title, input.Description, input.AssignedUserId));
+        }
+        catch (Exception ex)
+        {
+            _logger.Error(ex.Message, ex);
+            throw;
+        }
+
+        _logger.Debug("CreateTask method is successfully completed!");
+    }
+}
+ +

In CreateTask method, the essential code is _taskRepository.Insert(...) method call. All other code is repeating code and will be the same/similar for our other methods of TaskAppService. In a real application, we will have many application services need the same functionality. Also, we may have other similar code for database connection open and close, audit logging and so on...

+ +

AOP Way

+ +

If we use AOP and interception techniques, TaskAppService could be written as shown below with the same functionality:

+ +
+public class TaskAppService : ApplicationService
+{
+    private readonly IRepository<Task> _taskRepository;
+
+    public TaskAppService(IRepository<Task> taskRepository)
+    {
+        _taskRepository = taskRepository;
+    }
+
+    [AbpAuthorize("TaskCreationPermission")]
+    public void CreateTask(CreateTaskInput input)
+    {
+        _taskRepository.Insert(new Task(input.Title, input.Description, input.AssignedUserId));
+    }
+}
+ +

Now, it exactly does what is unique to CreateTask method. Exception handling, validation and logging code are completely removed since they are similar for other methods and can be centralized conventionally. Authorization code is replaced with AbpAuthorize attribute which is simpler to write and read.

+ +

Fortunately, all these and much more are automatically done by ABP framework. But, you may want to create some custom interception logic +that is specific to your own application requirements.

+ +

About the Sample Project

+ +

I created a sample project from +ABP Download +page (including login, register, user, role and tenant management pages) and +added to the GitHub +repository.

+ +

Creating Interceptors

+ +

Let's begin with a simple interceptor that measures the execution duration of a method:

+ +
+using System.Diagnostics;
+using Castle.Core.Logging;
+using Castle.DynamicProxy;
+
+namespace InterceptionDemo.Interceptors
+{
+    public class MeasureDurationInterceptor : IInterceptor
+    {
+        public ILogger Logger { get; set; }
+
+        public MeasureDurationInterceptor()
+        {
+            Logger = NullLogger.Instance;
+        }
+
+        public void Intercept(IInvocation invocation)
+        {
+            //Before method execution
+            var stopwatch = Stopwatch.StartNew();
+
+            //Executing the actual method
+            invocation.Proceed();
+
+            //After method execution
+            stopwatch.Stop();
+            Logger.InfoFormat(
+                "MeasureDurationInterceptor: {0} executed in {1} milliseconds.",
+                invocation.MethodInvocationTarget.Name,
+                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
+                );
+        }
+    }
+}
+ +

An interceptor is a class that implements IInterceptor interface (of Castle Windsor). It defines the Intercept method which gets an IInvocation argument. With this invocation argument, we can investigate the executing method, method arguments, return value, method's declared class, assembly and much more. Intercept method is called whenever a registered method is called (see registration section below). Proceed() method executes the actual intercepted method. We can write code before and after the actual method execution, as shown in this example.

+ +

An Interceptor class can also inject its dependencies like other classes. In this example, we property-injected an ILogger to write method execution duration to the log.

+ +

Registering Interceptors

+ +

After we create an interceptor, we can register it for desired classes. For example, we may want to register MeasureDurationInterceptor for all methods of all application service classes. We can easily identify application service classes since all application service classes implement IApplicationService in ABP framework.

+ +

There are some alternative ways of registering interceptors. But, it's the most proper way in ABP to handle ComponentRegistered event of Castle Windsors Kernel:

+ +
+public static class MeasureDurationInterceptorRegistrar
+{
+    public static void Initialize(IKernel kernel)
+    {
+        kernel.ComponentRegistered += Kernel_ComponentRegistered;
+    }
+
+    private static void Kernel_ComponentRegistered(string key, IHandler handler)
+    {
+        if (typeof (IApplicationService).IsAssignableFrom(handler.ComponentModel.Implementation))
+        {
+            handler.ComponentModel.Interceptors.Add
+            (new InterceptorReference(typeof(MeasureDurationInterceptor)));
+        }
+    }
+}
+ +

In this way, whenever a class is registered to dependency injection system (IOC), we can handle the event, check if this class is one of those classes we want to intercept and add interceptor if so.

+ +

After creating such a registration code, we need to call the Initialize method from somewhere else. It's best to call it in PreInitialize event of your module (since classes are registered to IOC generally in Initialize step):

+ +
+public class InterceptionDemoApplicationModule : AbpModule
+{
+    public override void PreInitialize()
+    {
+        MeasureDurationInterceptorRegistrar.Initialize(IocManager.IocContainer.Kernel);
+    }
+
+    //...
+}
+ +

After these steps, I run and login to the application. Then, I check log file and see logs:

+ +
+INFO 2016-02-23 14:59:28,611 [63 ] .Interceptors.MeasureDurationInterceptor - 
+GetCurrentLoginInformations executed in 4,939 milliseconds.
+ +

Note: GetCurrentLoginInformations is a method of SessionAppService class. You can check it in source code, but it's not important since our interceptor does not know details of intercepted methods.

+ +

Intercepting Async Methods

+ +

Intercepting an async method is different than intercepting a sync method. For example, MeasureDurationInterceptor defined above does not work properly for async methods. Because, an async method immediately returns a Task and it's executed asynchronously. So, we can not measure when it's actually completed (Actually, the example GetCurrentLoginInformations above was also an async method and 4,939 ms was a wrong value).

+ +

Let's change MeasureDurationInterceptor to support async methods, then explain how we implemented it:

+ +
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Castle.Core.Logging;
+using Castle.DynamicProxy;
+
+namespace InterceptionDemo.Interceptors
+{
+    public class MeasureDurationAsyncInterceptor : IInterceptor
+    {
+        public ILogger Logger { get; set; }
+
+        public MeasureDurationAsyncInterceptor()
+        {
+            Logger = NullLogger.Instance;
+        }
+
+        public void Intercept(IInvocation invocation)
+        {
+            if (IsAsyncMethod(invocation.Method))
+            {
+                InterceptAsync(invocation);
+            }
+            else
+            {
+                InterceptSync(invocation);
+            }
+        }
+
+        private void InterceptAsync(IInvocation invocation)
+        {
+            //Before method execution
+            var stopwatch = Stopwatch.StartNew();
+
+            //Calling the actual method, but execution has not been finished yet
+            invocation.Proceed();
+
+            //We should wait for finishing of the method execution
+            ((Task) invocation.ReturnValue)
+                .ContinueWith(task =>
+                {
+                    //After method execution
+                    stopwatch.Stop();
+                    Logger.InfoFormat(
+                        "MeasureDurationAsyncInterceptor: {0} executed in {1} milliseconds.",
+                        invocation.MethodInvocationTarget.Name,
+                        stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
+                        );
+                });
+        }
+
+        private void InterceptSync(IInvocation invocation)
+        {
+            //Before method execution
+            var stopwatch = Stopwatch.StartNew();
+
+            //Executing the actual method
+            invocation.Proceed();
+
+            //After method execution
+            stopwatch.Stop();
+            Logger.InfoFormat(
+                "MeasureDurationAsyncInterceptor: {0} executed in {1} milliseconds.",
+                invocation.MethodInvocationTarget.Name,
+                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
+                );
+        }
+        
+        public static bool IsAsyncMethod(MethodInfo method)
+        {
+            return (
+                method.ReturnType == typeof(Task) ||
+                (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
+                );
+        }
+    }
+}
+ +

Since sync and async execution logic is completely different, I checked if current method is async or sync (IsAsyncMethod does it). I moved previous code to InterceptSync method and introduced new InterceptAsync method. I used Task.ContinueWith(...) method to perform action after task complete. ContinueWith method works even if intercepted method throws exception.

+ +

Now, I'm registering MeasureDurationAsyncInterceptor as a second interceptor for application services by modifying MeasureDurationInterceptorRegistrar defined above:

+ +
+public static class MeasureDurationInterceptorRegistrar
+{
+    public static void Initialize(IKernel kernel)
+    {
+        kernel.ComponentRegistered += Kernel_ComponentRegistered;
+    }
+
+    private static void Kernel_ComponentRegistered(string key, IHandler handler)
+    {
+        if (typeof(IApplicationService).IsAssignableFrom(handler.ComponentModel.Implementation))
+        {
+            handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(MeasureDurationInterceptor)));
+            handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(MeasureDurationAsyncInterceptor)));
+        }
+    }
+}
+ +

If we run the application again, we will see that MeasureDurationAsyncInterceptor measured much more longer than MeasureDurationInterceptor, since it actually waits until method completely executed.

+ +
+INFO  2016-03-01 10:29:07,592 [10   ] .Interceptors.MeasureDurationInterceptor - MeasureDurationInterceptor: GetCurrentLoginInformations executed in 4.964 milliseconds.
+INFO  2016-03-01 10:29:07,693 [7    ] rceptors.MeasureDurationAsyncInterceptor - MeasureDurationAsyncInterceptor: GetCurrentLoginInformations executed in 104,994 milliseconds.
+
+ +

This way, we can properly intercept async methods to run code before and after. But, if our before and after code involve another async method calls, things get a bit complicated.

+ +

First of all, I could not find a way of executing async code before invocation.Proceed(). Because Castle Windsor does not support async naturally (other IOC managers also don't support as I know). So, if you need to run code before the actual method execution, do it synchronously. If you find a way of it, please share your solution as comment to this article.

+ +

We can execute async code after method execution. I changed InterceptAsync like that to support it:

+ +
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Castle.Core.Logging;
+using Castle.DynamicProxy;
+
+namespace InterceptionDemo.Interceptors
+{
+    public class MeasureDurationWithPostAsyncActionInterceptor : IInterceptor
+    {
+        public ILogger Logger { get; set; }
+
+        public MeasureDurationWithPostAsyncActionInterceptor()
+        {
+            Logger = NullLogger.Instance;
+        }
+
+        public void Intercept(IInvocation invocation)
+        {
+            if (IsAsyncMethod(invocation.Method))
+            {
+                InterceptAsync(invocation);
+            }
+            else
+            {
+                InterceptSync(invocation);
+            }
+        }
+
+        private void InterceptAsync(IInvocation invocation)
+        {
+            //Before method execution
+            var stopwatch = Stopwatch.StartNew();
+
+            //Calling the actual method, but execution has not been finished yet
+            invocation.Proceed();
+
+            //Wait task execution and modify return value
+            if (invocation.Method.ReturnType == typeof(Task))
+            {
+                invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
+                    (Task) invocation.ReturnValue,
+                    async () => await TestActionAsync(invocation),
+                    ex =>
+                    {
+                        LogExecutionTime(invocation, stopwatch);
+                    });
+            }
+            else //Task<TResult>
+            {
+                invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
+                    invocation.Method.ReturnType.GenericTypeArguments[0],
+                    invocation.ReturnValue,
+                    async () => await TestActionAsync(invocation),
+                    ex =>
+                    {
+                        LogExecutionTime(invocation, stopwatch);
+                    });
+            }
+        }
+
+        private void InterceptSync(IInvocation invocation)
+        {
+            //Before method execution
+            var stopwatch = Stopwatch.StartNew();
+
+            //Executing the actual method
+            invocation.Proceed();
+
+            //After method execution
+            LogExecutionTime(invocation, stopwatch);
+        }
+
+        public static bool IsAsyncMethod(MethodInfo method)
+        {
+            return (
+                method.ReturnType == typeof(Task) ||
+                (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
+                );
+        }
+
+        private async Task TestActionAsync(IInvocation invocation)
+        {
+            Logger.Info("Waiting after method execution for " + invocation.MethodInvocationTarget.Name);
+            await Task.Delay(200); //Here, we can await another methods. This is just for test.
+            Logger.Info("Waited after method execution for " + invocation.MethodInvocationTarget.Name);
+        }
+
+        private void LogExecutionTime(IInvocation invocation, Stopwatch stopwatch)
+        {
+            stopwatch.Stop();
+            Logger.InfoFormat(
+                "MeasureDurationWithPostAsyncActionInterceptor: {0} executed in {1} milliseconds.",
+                invocation.MethodInvocationTarget.Name,
+                stopwatch.Elapsed.TotalMilliseconds.ToString("0.000")
+                );
+        }
+    }
+}
+ +

If we want to execute an async method after method execution, we should replace the return value with the second method's return value. I created a magical InternalAsyncHelper class to accomplish it. InternalAsyncHelper is shown below:

+ +
+internal static class InternalAsyncHelper
+{
+    public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
+    {
+        Exception exception = null;
+
+        try
+        {
+            await actualReturnValue;
+            await postAction();
+        }
+        catch (Exception ex)
+        {
+            exception = ex;
+            throw;
+        }
+        finally
+        {
+            finalAction(exception);
+        }
+    }
+
+    public static async Task<T> AwaitTaskWithPostActionAndFinallyAndGetResult<T>(Task<T> actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
+    {
+        Exception exception = null;
+
+        try
+        {
+            var result = await actualReturnValue;
+            await postAction();
+            return result;
+        }
+        catch (Exception ex)
+        {
+            exception = ex;
+            throw;
+        }
+        finally
+        {
+            finalAction(exception);
+        }
+    }
+
+    public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue, Func<Task> action, Action<Exception> finalAction)
+    {
+        return typeof (InternalAsyncHelper)
+            .GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static)
+            .MakeGenericMethod(taskReturnType)
+            .Invoke(null, new object[] { actualReturnValue, action, finalAction });
+    }
+}
+ +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/InterceptionDemo

+ +

Article History

+ +
    +
  • 2018-02-22 +
      +
    • Upgraded source code to ABP v3.4.
    • +
    +
  • 2017-06-28 +
      +
    • Upgraded source code to ABP v2.1.3.
    • +
    • Updated links in the article.
    • +
    +
  • +
  • 2016-07-20 +
      +
    • Upgraded source code to ABP v0.10.
    • +
    • Updated article upon changes.
    • +
    +
  • +
  • 2016-03-01 +
      +
    • Added async method interception sample.
    • +
    +
  • +
  • 2016-02-23 +
      +
    • Initial publication.
    • +
    +
  • +
\ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular5.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular5.md" new file mode 100644 index 0000000..c9ca1a3 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular5.md" @@ -0,0 +1,978 @@ +## Introduction + +In this article, we will see a SaaS (multi-tenant) application developed using the following frameworks: + +- ASP.NET Boilerplate as application framework. +- ASP.NET Core and ASP.NET Web API as Web Frameworks. +- Entity Framework Core as ORM. +- Angular5 as SPA framework. +- Bootstrap as HTML/CSS framework. + +## Creating Application From Template + +ASP.NET Boilerplate provides templates to make a project startup easier. We create the startup template from https://aspnetboilerplate.com/Templates + +I selected **ASP.NET Core 2.x**, **Angular** and checked **"Include login, register, user, role and tenant management pages"**. It creates a ready and working solution for us including a login page, navigation and a bootstrap based layout. After download and open the solution with Visual Studio 2017+, we see a layered solution structure including a unit test project. + +### Solution structure + +First, we select **EventCloud.Host** as startup project. Solution comes with **Entity Framework Core Code-First Migrations**. So, (after restoring nuget packages) we open the Package Manager Console (PMC) and run **Update-Database** command to create the database. + +Package Manager Console's Default project should be **EventCloud.EntityFrameworkCore** (since it contains the migrations). This command creates **EventCloud** database in local SQL Server (you can change connection string in **appsettings.json** file). + +Swagger UI + +First I'm running **EventCloud.Host** project. We will see the following screen: + +Swagger UI + +We will use **Angular-CLI** to start **Angular UI**. Here is the steps to start Angular UI: + +- Open cmd on **EventCloud/angular** location +- Run `yarn` command to install packages +- Run `npm start` to run application + +Then we will see the following login page when you browse http://localhost:4200 : + +Swagger UI + +We can enter **Default** as tenancy name, **admin** as user name and **123qwe** as password to login. + +After login, we see the basic bootstrap based [Admin BSB Material Design](https://github.com/gurayyarar/AdminBSBMaterialDesign) layout. + +Swagger UI + +This is a localized UI with a dynamic menu. Angular layout, routing and basic infrastructure are properly working. I got this project as a base for the event cloud project. + +## Event Cloud Project + +In this article, I will show key parts of the project and explain it. So, please download the sample project, open in **Visual Studio 2017+** and run migrations just like above before reading rest of the article (Be sure that there is no database named EventCloud before running the migrations). I will follow some **DDD (Domain Driven Design)** techniques to create domain (business) layer and application layer. + +Event Cloud is a free SaaS (multi-tenant) application. We can create a tenant which has it's own events, users, roles... There are some simple business rules applied while creating, canceling and registering to an event. + +So, let's start to investigate the source code. + +### Entities + +Entities are parts of our domain layer and located under **EventCloud.Core** project. **ASP.NET Boilerplate** startup template comes with **Tenant**, **User**, **Role** ... entities which are common for most applications. We can customize them based on our needs. Surely, we can add our application specific entities. + +The fundamental entity of event cloud project is the `Event` entity. + +```c# +[Table("AppEvents")] +public class Event : FullAuditedEntity, IMustHaveTenant +{ + public const int MaxTitleLength = 128; + public const int MaxDescriptionLength = 2048; + + public virtual int TenantId { get; set; } + + [Required] + [StringLength(MaxTitleLength)] + public virtual string Title { get; protected set; } + + [StringLength(MaxDescriptionLength)] + public virtual string Description { get; protected set; } + + public virtual DateTime Date { get; protected set; } + + public virtual bool IsCancelled { get; protected set; } + + /// + /// Gets or sets the maximum registration count. + /// 0: Unlimited. + /// + [Range(0, int.MaxValue)] + public virtual int MaxRegistrationCount { get; protected set; } + + [ForeignKey("EventId")] + public virtual ICollection Registrations { get; protected set; } + + /// + /// We don't make constructor public and forcing to create events using method. + /// But constructor can not be private since it's used by EntityFramework. + /// Thats why we did it protected. + /// + protected Event() + { + + } + + public static Event Create(int tenantId, string title, DateTime date, string description = null, int maxRegistrationCount = 0) + { + var @event = new Event + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Title = title, + Description = description, + MaxRegistrationCount = maxRegistrationCount + }; + + @event.SetDate(date); + + @event.Registrations = new Collection(); + + return @event; + } + + public bool IsInPast() + { + return Date < Clock.Now; + } + + public bool IsAllowedCancellationTimeEnded() + { + return Date.Subtract(Clock.Now).TotalHours <= 2.0; //2 hours can be defined as Event property and determined per event + } + + public void ChangeDate(DateTime date) + { + if (date == Date) + { + return; + } + + SetDate(date); + + DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this)); + } + + internal void Cancel() + { + AssertNotInPast(); + IsCancelled = true; + } + + private void SetDate(DateTime date) + { + AssertNotCancelled(); + + if (date < Clock.Now) + { + throw new UserFriendlyException("Can not set an event's date in the past!"); + } + + if (date <= Clock.Now.AddHours(3)) //3 can be configurable per tenant + { + throw new UserFriendlyException("Should set an event's date 3 hours before at least!"); + } + + Date = date; + + DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this)); + } + + private void AssertNotInPast() + { + if (IsInPast()) + { + throw new UserFriendlyException("This event was in the past"); + } + } + + private void AssertNotCancelled() + { + if (IsCancelled) + { + throw new UserFriendlyException("This event is canceled!"); + } + } +} +``` + +**Event** entity has not just get/set properties. Actually, it has not public setters, setters are protected. It has some domain logic. All properties must be changed by the **Event** entity itself to ensure domain logic is executed. + +**Event** entity's constructor is also protected. So, the only way to create an Event is the `Event.Create` method (They can be private normally, but private setters don't work well with Entity Framework Core since Entity Framework Core can not set privates when retrieving an entity from database). + +Event implements, `IMustHaveTenant` interface. This is an interface of **ASP.NET Boilerplate (ABP)** framework and ensures that this entity is per tenant. This is needed for multi-tenancy. Thus, different tenants will have different events and can not see each other's events. **ABP** automatically filters entities of current tenant. + +Event class inherits from `FullAuditedEntity` which contains creation, modification and deletion audit columns. `FullAuditedEntity` also implements `ISoftDelete`, so events can not be deleted from database. They are marked as deleted when you delete it. **ABP** automatically filters (hides) deleted entities when you query database. + +In **DDD**, entities have domain (business) logic. We have some simple business rules those can be understood easily when you check the entity. + +Second entity of our application is `EventRegistration` + +```c# +[Table("AppEventRegistrations")] +public class EventRegistration : CreationAuditedEntity, IMustHaveTenant +{ + public int TenantId { get; set; } + + [ForeignKey("EventId")] + public virtual Event Event { get; protected set; } + public virtual Guid EventId { get; protected set; } + + [ForeignKey("UserId")] + public virtual User User { get; protected set; } + public virtual long UserId { get; protected set; } + + /// + /// We don't make constructor public and forcing to create registrations using method. + /// But constructor can not be private since it's used by EntityFramework. + /// Thats why we did it protected. + /// + protected EventRegistration() + { + + } + + public static async Task CreateAsync(Event @event, User user, IEventRegistrationPolicy registrationPolicy) + { + await registrationPolicy.CheckRegistrationAttemptAsync(@event, user); + + return new EventRegistration + { + TenantId = @event.TenantId, + EventId = @event.Id, + Event = @event, + UserId = @user.Id, + User = user + }; + } + + public async Task CancelAsync(IRepository repository) + { + if (repository == null) { throw new ArgumentNullException("repository"); } + + if (Event.IsInPast()) + { + throw new UserFriendlyException("Can not cancel event which is in the past!"); + } + + if (Event.IsAllowedCancellationTimeEnded()) + { + throw new UserFriendlyException("It's too late to cancel your registration!"); + } + + await repository.DeleteAsync(this); + } +} +``` + +As similar to `Event`, we have a static create method. The only way of creating a new EventRegistration is this `CreateAsync` method. It gets an event, user and a registration policy. It checks if given user can register to the event using `registrationPolicy.CheckRegistrationAttemptAsync` method. This method throws exception if given user can not register to given event. With such a design, we ensure that all business rules are applied while creating a registration. There is no way of creating a registration without using registration policy. + +See Entity documentation for more information on entities. + +### Event Registration Policy + +`EventRegistrationPolicy` class is defined as shown below: + +```c# +public class EventRegistrationPolicy : IEventRegistrationPolicy +{ + private readonly IRepository _eventRegistrationRepository; + private readonly ISettingManager _settingManager; + + public EventRegistrationPolicy( + IRepository eventRegistrationRepository, + ISettingManager settingManager + ) + { + _eventRegistrationRepository = eventRegistrationRepository; + _settingManager = settingManager; + } + + public async Task CheckRegistrationAttemptAsync(Event @event, User user) + { + if (@event == null) { throw new ArgumentNullException("event"); } + if (user == null) { throw new ArgumentNullException("user"); } + + CheckEventDate(@event); + await CheckEventRegistrationFrequencyAsync(user); + } + + private static void CheckEventDate(Event @event) + { + if (@event.IsInPast()) + { + throw new UserFriendlyException("Can not register event in the past!"); + } + } + + private async Task CheckEventRegistrationFrequencyAsync(User user) + { + var oneMonthAgo = Clock.Now.AddDays(-30); + var maxAllowedEventRegistrationCountInLast30DaysPerUser = await _settingManager.GetSettingValueAsync(AppSettingNames.MaxAllowedEventRegistrationCountInLast30DaysPerUser); + if (maxAllowedEventRegistrationCountInLast30DaysPerUser > 0) + { + var registrationCountInLast30Days = await _eventRegistrationRepository.CountAsync(r => r.UserId == user.Id && r.CreationTime >= oneMonthAgo); + if (registrationCountInLast30Days > maxAllowedEventRegistrationCountInLast30DaysPerUser) + { + throw new UserFriendlyException(string.Format("Can not register to more than {0}", maxAllowedEventRegistrationCountInLast30DaysPerUser)); + } + } + } +} +``` + +This is an important part of our domain. We have two rules while creating an event registration: + +- A user can not register to an event in the past. +- A user can register to a maximum count of events in 30 days. So, we have registration frequency limit. + +### Event Manager + +`EventManager` implements business (domain) logic for events. All Event operations should be executed using this class. It's defined as shown below: + +```c# +public class EventManager : IEventManager +{ + public IEventBus EventBus { get; set; } + + private readonly IEventRegistrationPolicy _registrationPolicy; + private readonly IRepository _eventRegistrationRepository; + private readonly IRepository _eventRepository; + + public EventManager( + IEventRegistrationPolicy registrationPolicy, + IRepository eventRegistrationRepository, + IRepository eventRepository) + { + _registrationPolicy = registrationPolicy; + _eventRegistrationRepository = eventRegistrationRepository; + _eventRepository = eventRepository; + + EventBus = NullEventBus.Instance; + } + + public async Task GetAsync(Guid id) + { + var @event = await _eventRepository.FirstOrDefaultAsync(id); + if (@event == null) + { + throw new UserFriendlyException("Could not found the event, maybe it's deleted!"); + } + + return @event; + } + + public async Task CreateAsync(Event @event) + { + await _eventRepository.InsertAsync(@event); + } + + public void Cancel(Event @event) + { + @event.Cancel(); + EventBus.Trigger(new EventCancelledEvent(@event)); + } + + public async Task RegisterAsync(Event @event, User user) + { + return await _eventRegistrationRepository.InsertAsync( + await EventRegistration.CreateAsync(@event, user, _registrationPolicy) + ); + } + + public async Task CancelRegistrationAsync(Event @event, User user) + { + var registration = await _eventRegistrationRepository.FirstOrDefaultAsync(r => r.EventId == @event.Id && r.UserId == user.Id); + if (registration == null) + { + //No need to cancel since there is no such a registration + return; + } + + await registration.CancelAsync(_eventRegistrationRepository); + } + + public async Task> GetRegisteredUsersAsync(Event @event) + { + return await _eventRegistrationRepository + .GetAll() + .Include(registration => registration.User) + .Where(registration => registration.EventId == @event.Id) + .Select(registration => registration.User) + .ToListAsync(); + } +} +``` + +It performs domain logic and triggers needed events. + +See domain services documentation for more information on domain services. + +### Domain Events + +We may want to define and trigger some domain specific events on some state changes in our application. I defined 2 domain specific events: + +- **EventCancelledEvent:** Triggered when an event is canceled. It's triggered in `EventManager.Cancel` method. +- **EventDateChangedEvent:** Triggered when date of an event changed. It's triggered in `Event.ChangeDate` method. + +We handle these events and notify related users about these changes. Also, I handle `EntityCreatedEventDate` (which is a pre-defined **ABP** event and triggered automatically). + +To handle an event, we should define an event handler class. I defined `EventUserEmailer` to send emails to users when needed: + +```c# +public class EventUserEmailer : + IEventHandler>, + IEventHandler, + IEventHandler, + ITransientDependency +{ + public ILogger Logger { get; set; } + + private readonly IEventManager _eventManager; + private readonly UserManager _userManager; + + public EventUserEmailer( + UserManager userManager, + IEventManager eventManager) + { + _userManager = userManager; + _eventManager = eventManager; + + Logger = NullLogger.Instance; + } + + [UnitOfWork] + public virtual void HandleEvent(EntityCreatedEventData eventData) + { + //TODO: Send email to all tenant users as a notification + + var users = _userManager + .Users + .Where(u => u.TenantId == eventData.Entity.TenantId) + .ToList(); + + foreach (var user in users) + { + var message = string.Format("Hey! There is a new event '{0}' on {1}! Want to register?", eventData.Entity.Title, eventData.Entity.Date); + Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message)); + } + } + + public void HandleEvent(EventDateChangedEvent eventData) + { + //TODO: Send email to all registered users! + + var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity)); + foreach (var user in registeredUsers) + { + var message = eventData.Entity.Title + " event's date is changed! New date is: " + eventData.Entity.Date; + Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message)); + } + } + + public void HandleEvent(EventCancelledEvent eventData) + { + //TODO: Send email to all registered users! + + var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity)); + foreach (var user in registeredUsers) + { + var message = eventData.Entity.Title + " event is canceled!"; + Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message)); + } + } +} +``` + +We can handle same events in different classes or different events in same class (as in this sample). Here, we handle these events and send email to related users as a notification (not implemented emailing actually to make the sample application simpler). An event handler should implement `IEventHandler` interface. **ABP** automatically calls the handler when related events occur. + +See **EventBus** documentation for more information on domain events. + +### Application Services + +Application services use domain layer to implement use cases of the application (generally used by presentation layer). `EventAppService` performs application logic for events. + +```c# +[AbpAuthorize] +public class EventAppService : EventCloudAppServiceBase, IEventAppService +{ + private readonly IEventManager _eventManager; + private readonly IRepository _eventRepository; + + public EventAppService( + IEventManager eventManager, + IRepository eventRepository) + { + _eventManager = eventManager; + _eventRepository = eventRepository; + } + + public async Task> GetListAsync(GetEventListInput input) + { + var events = await _eventRepository + .GetAll() + .Include(e => e.Registrations) + .WhereIf(!input.IncludeCanceledEvents, e => !e.IsCancelled) + .OrderByDescending(e => e.CreationTime) + .Take(64) + .ToListAsync(); + + return new ListResultDto(events.MapTo>()); + } + + public async Task GetDetailAsync(EntityDto input) + { + var @event = await _eventRepository + .GetAll() + .Include(e => e.Registrations) + .ThenInclude(r => r.User) + .Where(e => e.Id == input.Id) + .FirstOrDefaultAsync(); + + if (@event == null) + { + throw new UserFriendlyException("Could not found the event, maybe it's deleted."); + } + + return @event.MapTo(); + } + + public async Task CreateAsync(CreateEventInput input) + { + var @event = Event.Create(AbpSession.GetTenantId(), input.Title, input.Date, input.Description, input.MaxRegistrationCount); + await _eventManager.CreateAsync(@event); + } + + public async Task CancelAsync(EntityDto input) + { + var @event = await _eventManager.GetAsync(input.Id); + _eventManager.Cancel(@event); + } + + public async Task RegisterAsync(EntityDto input) + { + var registration = await RegisterAndSaveAsync( + await _eventManager.GetAsync(input.Id), + await GetCurrentUserAsync() + ); + + return new EventRegisterOutput + { + RegistrationId = registration.Id + }; + } + + public async Task CancelRegistrationAsync(EntityDto input) + { + await _eventManager.CancelRegistrationAsync( + await _eventManager.GetAsync(input.Id), + await GetCurrentUserAsync() + ); + } + + private async Task RegisterAndSaveAsync(Event @event, User user) + { + var registration = await _eventManager.RegisterAsync(@event, user); + await CurrentUnitOfWork.SaveChangesAsync(); + return registration; + } +} +``` + +As you see, application service does not implement domain logic itself. It just uses entities and domain services (**EventManager**) to perform the use cases. + +See application service documentation for more information on application services. + +### Presentation Layer + +Presentation layer for this application is built using **Angular** as a SPA. + +#### Event List + +When we login to the application, we first see a list of events: + +Swagger UI + +We directly use `EventAppService` to get list of events. Here is the **events.component.ts** to create this page: + +```ts +import { Component, Injector, ViewChild } from '@angular/core'; +import { appModuleAnimation } from '@shared/animations/routerTransition'; +import { EventServiceProxy, EventListDto, ListResultDtoOfEventListDto, EntityDtoOfGuid } from '@shared/service-proxies/service-proxies'; +import { PagedListingComponentBase, PagedRequestDto } from "shared/paged-listing-component-base"; +import { CreateEventComponent } from "app/events/create-event/create-event.component"; + +@Component({ + templateUrl: './events.component.html', + animations: [appModuleAnimation()] +}) +export class EventsComponent extends PagedListingComponentBase { + + @ViewChild('createEventModal') createEventModal: CreateEventComponent; + + active: boolean = false; + events: EventListDto[] = []; + includeCanceledEvents:boolean=false; + + constructor( + injector: Injector, + private _eventService: EventServiceProxy + ) { + super(injector); + } + + protected list(request: PagedRequestDto, pageNumber: number, finishedCallback: Function): void { + this.loadEvent(); + finishedCallback(); + } + + protected delete(event: EntityDtoOfGuid): void { + abp.message.confirm( + 'Are you sure you want to cancel this event?', + (result: boolean) => { + if (result) { + this._eventService.cancelAsync(event) + .subscribe(() => { + abp.notify.info('Event is deleted'); + this.refresh(); + }); + } + } + ); + } + + includeCanceledEventsCheckboxChanged() { + this.loadEvent(); + }; + + // Show Modals + createEvent(): void { + this.createEventModal.show(); + } + + loadEvent() { + this._eventService.getListAsync(this.includeCanceledEvents) + .subscribe((result: ListResultDtoOfEventListDto) => { + this.events = result.items; + }); + } +} +``` + +We inject `EventServiceProxy` into **events.component.ts** component. We used dynamic web api layer feature of **ABP**. It creates needed Web API controller and Angular service automatically and dynamically. So, we can use application service methods like calling regular typescript functions. So, to call `EventAppService.GetListAsync` C# method, we simple call `_eventService.getListAsync` typescript function. + +We also open a "new event" modal (dialog) when user clicks to "+ New event" button (which triggers `createEvent` function). I will not go details of Angular views, since they are simpler and you can check it in source code. + +#### Event Detail + +When we click "Details" button for an event, we go to event details with a URL like [http://eventcloud.aspnetboilerplate.com/#/events/e9499e3e-35c0-492c-98ce-7e410461103f](http://eventcloud.aspnetboilerplate.com/#/events/e9499e3e-35c0-492c-98ce-7e410461103f) . GUID is id of the event. + +Swagger UI + +Here, we see event details with registered users. We can register to the event or cancel registration. This view's component is defined in **event-detail.component.ts** as shown below: + +```ts +import { Component, OnInit, Injector } from '@angular/core'; +import { appModuleAnimation } from '@shared/animations/routerTransition'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { AppComponentBase } from '@shared/app-component-base'; +import { EventDetailOutput, EventServiceProxy, EntityDtoOfGuid, EventRegisterOutput } from '@shared/service-proxies/service-proxies'; + +import * as _ from 'lodash'; + +@Component({ + templateUrl: './event-detail.component.html', + animations: [appModuleAnimation()] +}) + +export class EventDetailComponent extends AppComponentBase implements OnInit { + + event: EventDetailOutput = new EventDetailOutput(); + eventId:string; + + constructor( + injector: Injector, + private _eventService: EventServiceProxy, + private _router: Router, + private _activatedRoute: ActivatedRoute + ) { + super(injector); + } + + ngOnInit(): void { + this._activatedRoute.params.subscribe((params: Params) => { + this.eventId = params['eventId']; + this.loadEvent(); + }); + } + + registerToEvent(): void { + var input = new EntityDtoOfGuid(); + input.id = this.event.id; + + this._eventService.registerAsync(input) + .subscribe((result: EventRegisterOutput) => { + abp.notify.success('Successfully registered to event. Your registration id: ' + result.registrationId + "."); + this.loadEvent(); + }); + }; + + cancelRegistrationFromEvent(): void { + var input = new EntityDtoOfGuid(); + input.id = this.event.id; + + this._eventService.cancelRegistrationAsync(input) + .subscribe(() => { + abp.notify.info('Canceled your registration.'); + this.loadEvent(); + }); + }; + + cancelEvent(): void { + var input = new EntityDtoOfGuid(); + input.id = this.event.id; + + this._eventService.cancelAsync(input) + .subscribe(() => { + abp.notify.info('Canceled the event.'); + this.backToEventsPage(); + }); + }; + + isRegistered(): boolean { + return _.some(this.event.registrations, { userId: abp.session.userId }); + }; + + isEventCreator(): boolean { + return this.event.creatorUserId === abp.session.userId; + }; + + loadEvent() { + this._eventService.getDetailAsync(this.eventId) + .subscribe((result: EventDetailOutput) => { + this.event = result; + }); + } + + backToEventsPage() { + this._router.navigate(['app/events']); + }; +} +``` + +We simply use event application service to perform actions. + +#### Main Menu + +Top menu is automatically created by **ABP template**. We define menu items in `sidebar-nav.component.ts` class: + +```ts +@Component({ + templateUrl: './sidebar-nav.component.html', + selector: 'sidebar-nav', + encapsulation: ViewEncapsulation.None +}) +export class SideBarNavComponent extends AppComponentBase { + + menuItems: MenuItem[] = [ + new MenuItem(this.l("HomePage"), "", "home", "/app/home"), + + new MenuItem(this.l("Tenants"), "Pages.Tenants", "business", "/app/tenants"), + new MenuItem(this.l("Users"), "Pages.Users", "people", "/app/users"), + new MenuItem(this.l("Roles"), "Pages.Roles", "local_offer", "/app/roles"), + new MenuItem(this.l("Events"), "Pages.Events", "event", "/app/events"), + new MenuItem(this.l("About"), "", "info", "/app/about"), + +... +``` + +#### Angular Route + +Defining the menu only shows it on the page. Angular has it's own route system. Routes are defined in app-routing-module.ts as shown below: + +```ts +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; +import { AppRouteGuard } from '@shared/auth/auth-route-guard'; +import { HomeComponent } from './home/home.component'; +import { AboutComponent } from './about/about.component'; +import { UsersComponent } from './users/users.component'; +import { TenantsComponent } from './tenants/tenants.component'; +import { RolesComponent } from "app/roles/roles.component"; +import { EventsComponent } from "app/events/events.component"; +import { EventDetailComponent } from "app/events/event-detail/event-detail.component"; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: AppComponent, + children: [ + { path: 'home', component: HomeComponent, canActivate: [AppRouteGuard] }, + { path: 'users', component: UsersComponent, data: { permission: 'Pages.Users' }, canActivate: [AppRouteGuard] }, + { path: 'roles', component: RolesComponent, data: { permission: 'Pages.Roles' }, canActivate: [AppRouteGuard] }, + { path: 'tenants', component: TenantsComponent, data: { permission: 'Pages.Tenants' }, canActivate: [AppRouteGuard] }, + { path: 'events', component: EventsComponent, data: { permission: 'Pages.Events' }, canActivate: [AppRouteGuard] }, + { path: 'events/:eventId', component: EventDetailComponent }, + { path: 'about', component: AboutComponent } + ] + } + ]) + ], + exports: [RouterModule] +}) +export class AppRoutingModule { } + +``` + +### Unit and Integration Tests + +**ASP.NET Boilerplate** provides tools to make unit and integration tests easier. You can find all test code from source code of the project. Here, I will briefly explain basic tests. Solution includes `EventAppService_Tests` class which tests the `EventAppService`. See 2 tests from this class: + +```c# +public class EventAppService_Tests : EventCloudTestBase +{ + private readonly IEventAppService _eventAppService; + + public EventAppService_Tests() + { + _eventAppService = Resolve(); + } + + [Fact] + public async Task Should_Get_Test_Events() + { + var output = await _eventAppService.GetListAsync(new GetEventListInput()); + output.Items.Count.ShouldBe(1); + } + + [Fact] + public async Task Should_Create_Event() + { + //Arrange + var eventTitle = Guid.NewGuid().ToString(); + + //Act + await _eventAppService.CreateAsync(new CreateEventInput + { + Title = eventTitle, + Description = "A description", + Date = Clock.Now.AddDays(2) + }); + + //Assert + UsingDbContext(context => + { + context.Events.FirstOrDefault(e => e.Title == eventTitle).ShouldNotBe(null); + }); + } + + [Fact] + public async Task Should_Not_Create_Events_In_The_Past() + { + //Arrange + var eventTitle = Guid.NewGuid().ToString(); + + //Act + await Assert.ThrowsAsync(async () => + { + await _eventAppService.CreateAsync(new CreateEventInput + { + Title = eventTitle, + Description = "A description", + Date = Clock.Now.AddDays(-1) + }); + }); + } + + [Fact] + public async Task Should_Cancel_Event() + { + //Act + await _eventAppService.CancelAsync(new EntityDto(GetTestEvent().Id)); + + //Assert + GetTestEvent().IsCancelled.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Register_To_Events() + { + //Arrange + var testEvent = GetTestEvent(); + + //Act + var output = await _eventAppService.RegisterAsync(new EntityDto(testEvent.Id)); + + //Assert + output.RegistrationId.ShouldBeGreaterThan(0); + + UsingDbContext(context => + { + var currentUserId = AbpSession.GetUserId(); + var registration = context.EventRegistrations.FirstOrDefault(r => r.EventId == testEvent.Id && r.UserId == currentUserId); + registration.ShouldNotBeNull(); + }); + } + + [Fact] + public async Task Should_Cancel_Registration() + { + //Arrange + var currentUserId = AbpSession.GetUserId(); + await UsingDbContext(async context => + { + var testEvent = GetTestEvent(context); + var currentUser = await context.Users.SingleAsync(u => u.Id == currentUserId); + var testRegistration = await EventRegistration.CreateAsync( + testEvent, + currentUser, + Substitute.For() + ); + + context.EventRegistrations.Add(testRegistration); + }); + + //Act + await _eventAppService.CancelRegistrationAsync(new EntityDto(GetTestEvent().Id)); + + //Assert + UsingDbContext(context => + { + var testEvent = GetTestEvent(context); + var testRegistration = context.EventRegistrations.FirstOrDefault(r => r.EventId == testEvent.Id && r.UserId == currentUserId); + testRegistration.ShouldBeNull(); + }); + } + + private Event GetTestEvent() + { + return UsingDbContext(context => GetTestEvent(context)); + } + + private static Event GetTestEvent(EventCloudDbContext context) + { + return context.Events.Single(e => e.Title == TestDataBuilder.TestEventTitle); + } +} +``` + +We use xUnit as test framework. In the first test, we simply create an event and check database if it's in there. In the second test, we intentionally trying to create an event in the past. Since our business rule don't allow it, we should get an exception here. + +With such tests, we tested everything starting from application service including all aspects of **ASP.NET Boilerplate** (like validation, unit of work and so on). + +### Token Based Authentication + +If you want to consume APIs/application services from a mobile application, you can use the token based authentication mechanism just like we do for the Angular client. The startup template includes the JwtBearer token authentication infrastructure. + +We will use Postman (a chrome extension) to demonstrate requests and responses. + +#### Authentication + +Just send a POST request to http://localhost:21021/api/TokenAuth/Authenticate with the **Context-Type="application/json"** header as shown below: + +Swagger UI + +We sent a JSON request body includes tenancyName, userNameOrEmailAddress and password. tenancyName is not required for host users. As seen above, result property of returning JSON contains the token. We can save it and use for next requests. + + +#### Use API + +After authenticate and get the **token**, we can use it to call any **authorized** action. All **application services** are available to be used remotely. For example, we can use the **User service** to get a **list of users**: + +Using API + +Just made a **GET** request to **http://localhost:21021/api/services/app/user/getAll** with **Content-Type="application/json"** and **Authorization="Bearer *your-******auth-token*** **"**. All functionality available on UI is also available as API. + +Almost all operations available on UI also available as Web API (since UI uses the same Web API) and can be consumed easily. + +### Source Code + +You can get the latest source code here [Event Cloud Source](https://github.com/aspnetboilerplate/eventcloudcore) diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-db.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-db.png" new file mode 100644 index 0000000..031de81 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-db.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-template.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-template.png" new file mode 100644 index 0000000..c599aaa Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-create-template.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-dashboard.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-dashboard.png" new file mode 100644 index 0000000..45dc7de Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-dashboard.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-event-detail.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-event-detail.png" new file mode 100644 index 0000000..e88e6a3 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-event-detail.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-events.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-events.png" new file mode 100644 index 0000000..8d01343 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-events.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-host-dashboard.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-host-dashboard.png" new file mode 100644 index 0000000..9115c47 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-host-dashboard.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-localization-resources.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-localization-resources.png" new file mode 100644 index 0000000..9f85297 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-localization-resources.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-login-page.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-login-page.png" new file mode 100644 index 0000000..5c8b40a Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-login-page.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-run-angular.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-run-angular.png" new file mode 100644 index 0000000..c4b9cd9 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-run-angular.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-solution-structure.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-solution-structure.png" new file mode 100644 index 0000000..61f6d20 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-solution-structure.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-swagger-ui.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-swagger-ui.png" new file mode 100644 index 0000000..76254e7 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/event-cloud-swagger-ui.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/index.html" new file mode 100644 index 0000000..0667b51 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/index.html" @@ -0,0 +1,988 @@ + + + + +

Introduction

+ +

In this article, we will see a SaaS (multi-tenant) application developed using the following frameworks:

+ +
    +
  • ASP.NET Boilerplate as application framework.
  • + +
  • ASP.NET Core and ASP.NET Web API as Web Frameworks.
  • + +
  • Entity Framework Core as ORM.
  • + +
  • Angular5 as SPA framework.
  • + +
  • Bootstrap as HTML/CSS framework.
  • +
+ +

Creating Application From Template

+ +

ASP.NET Boilerplate provides templates to make a project startup easier. We create the startup template from https://aspnetboilerplate.com/Templates

+ +

I selected ASP.NET Core 2.x, Angular and checked "Include login, register, user, role and tenant management pages". It creates a ready and working solution for us including a login page, navigation and a bootstrap based layout. After download and open the solution with Visual Studio 2017+, we see a layered solution structure including a unit test project.

+ +

Solution structure

+ +

First, we select EventCloud.Host as startup project. Solution comes with Entity Framework Core Code-First Migrations. So, (after restoring nuget packages) we open the Package Manager Console (PMC) and run Update-Database command to create the database.

+ +

Package Manager Console's Default project should be EventCloud.EntityFrameworkCore (since it contains the migrations). This command creates EventCloud database in local SQL Server (you can change connection string in appsettings.json file).

+ +

Swagger UI

+ +

First I'm running EventCloud.Host project. We will see the following screen:

+ +

Swagger UI

+ +

We will use Angular-CLI to start Angular UI. Here is the steps to start Angular UI:

+ +
    +
  • Open cmd on EventCloud/angular location
  • + +
  • Run yarn command to install packages
  • + +
  • Run npm start to run application
  • +
+ +

Then we will see the following login page when you browse http://localhost:4200 :

+ +

Swagger UI

+ +

We can enter Default as tenancy name, admin as user name and 123qwe as password to login.

+ +

After login, we see the basic bootstrap based Admin BSB Material Design layout.

+ +

Swagger UI

+ +

This is a localized UI with a dynamic menu. Angular layout, routing and basic infrastructure are properly working. I got this project as a base for the event cloud project.

+ +

Event Cloud Project

+ +

In this article, I will show key parts of the project and explain it. So, please download the sample project, open in Visual Studio 2017+ and run migrations just like above before reading rest of the article (Be sure that there is no database named EventCloud before running the migrations). I will follow some DDD (Domain Driven Design) techniques to create domain (business) layer and application layer.

+ +

Event Cloud is a free SaaS (multi-tenant) application. We can create a tenant which has it's own events, users, roles... There are some simple business rules applied while creating, canceling and registering to an event.

+ +

So, let's start to investigate the source code.

+ +

Entities

+ +

Entities are parts of our domain layer and located under EventCloud.Core project. ASP.NET Boilerplate startup template comes with Tenant, User, Role ... entities which are common for most applications. We can customize them based on our needs. Surely, we can add our application specific entities.

+ +

The fundamental entity of event cloud project is the Event entity.

+ +
[Table("AppEvents")]
+public class Event : FullAuditedEntity<Guid>, IMustHaveTenant
+{
+    public const int MaxTitleLength = 128;
+    public const int MaxDescriptionLength = 2048;
+
+    public virtual int TenantId { get; set; }
+
+    [Required]
+    [StringLength(MaxTitleLength)]
+    public virtual string Title { get; protected set; }
+
+    [StringLength(MaxDescriptionLength)]
+    public virtual string Description { get; protected set; }
+
+    public virtual DateTime Date { get; protected set; }
+
+    public virtual bool IsCancelled { get; protected set; }
+
+    /// <summary>
+    /// Gets or sets the maximum registration count.
+    /// 0: Unlimited.
+    /// </summary>
+    [Range(0, int.MaxValue)]
+    public virtual int MaxRegistrationCount { get; protected set; }
+
+    [ForeignKey("EventId")]
+    public virtual ICollection<EventRegistration> Registrations { get; protected set; }
+
+    /// <summary>
+    /// We don't make constructor public and forcing to create events using <see cref="Create"/> method.
+    /// But constructor can not be private since it's used by EntityFramework.
+    /// Thats why we did it protected.
+    /// </summary>
+    protected Event()
+    {
+
+    }
+
+    public static Event Create(int tenantId, string title, DateTime date, string description = null, int maxRegistrationCount = 0)
+    {
+        var @event = new Event
+        {
+            Id = Guid.NewGuid(),
+            TenantId = tenantId,
+            Title = title,
+            Description = description,
+            MaxRegistrationCount = maxRegistrationCount
+        };
+
+        @event.SetDate(date);
+
+        @event.Registrations = new Collection<EventRegistration>();
+
+        return @event;
+    }
+
+    public bool IsInPast()
+    {
+        return Date < Clock.Now;
+    }
+
+    public bool IsAllowedCancellationTimeEnded()
+    {
+        return Date.Subtract(Clock.Now).TotalHours <= 2.0; //2 hours can be defined as Event property and determined per event
+    }
+
+    public void ChangeDate(DateTime date)
+    {
+        if (date == Date)
+        {
+            return;
+        }
+
+        SetDate(date);
+
+        DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
+    }
+
+    internal void Cancel()
+    {
+        AssertNotInPast();
+        IsCancelled = true;
+    }
+
+    private void SetDate(DateTime date)
+    {
+        AssertNotCancelled();
+
+        if (date < Clock.Now)
+        {
+            throw new UserFriendlyException("Can not set an event's date in the past!");
+        }
+
+        if (date <= Clock.Now.AddHours(3)) //3 can be configurable per tenant
+        {
+            throw new UserFriendlyException("Should set an event's date 3 hours before at least!");
+        }
+
+        Date = date;
+
+        DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
+    }
+
+    private void AssertNotInPast()
+    {
+        if (IsInPast())
+        {
+            throw new UserFriendlyException("This event was in the past");
+        }
+    }
+
+    private void AssertNotCancelled()
+    {
+        if (IsCancelled)
+        {
+            throw new UserFriendlyException("This event is canceled!");
+        }
+    }
+}
+
+ +

Event entity has not just get/set properties. Actually, it has not public setters, setters are protected. It has some domain logic. All properties must be changed by the Event entity itself to ensure domain logic is executed.

+ +

Event entity's constructor is also protected. So, the only way to create an Event is the Event.Create method (They can be private normally, but private setters don't work well with Entity Framework Core since Entity Framework Core can not set privates when retrieving an entity from database).

+ +

Event implements, IMustHaveTenant interface. This is an interface of ASP.NET Boilerplate (ABP) framework and ensures that this entity is per tenant. This is needed for multi-tenancy. Thus, different tenants will have different events and can not see each other's events. ABP automatically filters entities of current tenant.

+ +

Event class inherits from FullAuditedEntity which contains creation, modification and deletion audit columns. FullAuditedEntity also implements ISoftDelete, so events can not be deleted from database. They are marked as deleted when you delete it. ABP automatically filters (hides) deleted entities when you query database.

+ +

In DDD, entities have domain (business) logic. We have some simple business rules those can be understood easily when you check the entity.

+ +

Second entity of our application is EventRegistration

+ +
[Table("AppEventRegistrations")]
+public class EventRegistration : CreationAuditedEntity, IMustHaveTenant
+{
+    public int TenantId { get; set; }
+
+    [ForeignKey("EventId")]
+    public virtual Event Event { get; protected set; }
+    public virtual Guid EventId { get; protected set; }
+
+    [ForeignKey("UserId")]
+    public virtual User User { get; protected set; }
+    public virtual long UserId { get; protected set; }
+
+    /// <summary>
+    /// We don't make constructor public and forcing to create registrations using <see cref="CreateAsync"/> method.
+    /// But constructor can not be private since it's used by EntityFramework.
+    /// Thats why we did it protected.
+    /// </summary>
+    protected EventRegistration()
+    {
+
+    }
+
+    public static async Task<EventRegistration> CreateAsync(Event @event, User user, IEventRegistrationPolicy registrationPolicy)
+    {
+        await registrationPolicy.CheckRegistrationAttemptAsync(@event, user);
+
+        return new EventRegistration
+        {
+            TenantId = @event.TenantId,
+            EventId = @event.Id,
+            Event = @event,
+            UserId = @user.Id,
+            User = user
+        };
+    }
+
+    public async Task CancelAsync(IRepository<EventRegistration> repository)
+    {
+        if (repository == null) { throw new ArgumentNullException("repository"); }
+
+        if (Event.IsInPast())
+        {
+            throw new UserFriendlyException("Can not cancel event which is in the past!");
+        }
+
+        if (Event.IsAllowedCancellationTimeEnded())
+        {
+            throw new UserFriendlyException("It's too late to cancel your registration!");
+        }
+
+        await repository.DeleteAsync(this);
+    }
+}
+
+ +

As similar to Event, we have a static create method. The only way of creating a new EventRegistration is this CreateAsync method. It gets an event, user and a registration policy. It checks if given user can register to the event using registrationPolicy.CheckRegistrationAttemptAsync method. This method throws exception if given user can not register to given event. With such a design, we ensure that all business rules are applied while creating a registration. There is no way of creating a registration without using registration policy.

+ +

See Entity documentation for more information on entities.

+ +

Event Registration Policy

+ +

EventRegistrationPolicy class is defined as shown below:

+ +
public class EventRegistrationPolicy : IEventRegistrationPolicy
+{
+    private readonly IRepository<EventRegistration> _eventRegistrationRepository;
+    private readonly ISettingManager _settingManager;
+
+    public EventRegistrationPolicy(
+        IRepository<EventRegistration> eventRegistrationRepository,
+        ISettingManager settingManager
+        )
+    {
+        _eventRegistrationRepository = eventRegistrationRepository;
+        _settingManager = settingManager;
+    }
+
+    public async Task CheckRegistrationAttemptAsync(Event @event, User user)
+    {
+        if (@event == null) { throw new ArgumentNullException("event"); }
+        if (user == null) { throw new ArgumentNullException("user"); }
+
+        CheckEventDate(@event);
+        await CheckEventRegistrationFrequencyAsync(user);
+    }
+
+    private static void CheckEventDate(Event @event)
+    {
+        if (@event.IsInPast())
+        {
+            throw new UserFriendlyException("Can not register event in the past!");
+        }
+    }
+
+    private async Task CheckEventRegistrationFrequencyAsync(User user)
+    {
+        var oneMonthAgo = Clock.Now.AddDays(-30);
+        var maxAllowedEventRegistrationCountInLast30DaysPerUser = await _settingManager.GetSettingValueAsync<int>(AppSettingNames.MaxAllowedEventRegistrationCountInLast30DaysPerUser);
+        if (maxAllowedEventRegistrationCountInLast30DaysPerUser > 0)
+        {
+            var registrationCountInLast30Days = await _eventRegistrationRepository.CountAsync(r => r.UserId == user.Id && r.CreationTime >= oneMonthAgo);
+            if (registrationCountInLast30Days > maxAllowedEventRegistrationCountInLast30DaysPerUser)
+            {
+                throw new UserFriendlyException(string.Format("Can not register to more than {0}", maxAllowedEventRegistrationCountInLast30DaysPerUser));
+            }
+        }
+    }
+}
+
+ +

This is an important part of our domain. We have two rules while creating an event registration:

+ +
    +
  • A user can not register to an event in the past.
  • + +
  • A user can register to a maximum count of events in 30 days. So, we have registration frequency limit.
  • +
+ +

Event Manager

+ +

EventManager implements business (domain) logic for events. All Event operations should be executed using this class. It's defined as shown below:

+ +
public class EventManager : IEventManager
+{
+    public IEventBus EventBus { get; set; }
+
+    private readonly IEventRegistrationPolicy _registrationPolicy;
+    private readonly IRepository<EventRegistration> _eventRegistrationRepository;
+    private readonly IRepository<Event, Guid> _eventRepository;
+
+    public EventManager(
+        IEventRegistrationPolicy registrationPolicy,
+        IRepository<EventRegistration> eventRegistrationRepository,
+        IRepository<Event, Guid> eventRepository)
+    {
+        _registrationPolicy = registrationPolicy;
+        _eventRegistrationRepository = eventRegistrationRepository;
+        _eventRepository = eventRepository;
+
+        EventBus = NullEventBus.Instance;
+    }
+
+    public async Task<Event> GetAsync(Guid id)
+    {
+        var @event = await _eventRepository.FirstOrDefaultAsync(id);
+        if (@event == null)
+        {
+            throw new UserFriendlyException("Could not found the event, maybe it's deleted!");
+        }
+
+        return @event;
+    }
+
+    public async Task CreateAsync(Event @event)
+    {
+        await _eventRepository.InsertAsync(@event);
+    }
+
+    public void Cancel(Event @event)
+    {
+        @event.Cancel();
+        EventBus.Trigger(new EventCancelledEvent(@event));
+    }
+
+    public async Task<EventRegistration> RegisterAsync(Event @event, User user)
+    {
+        return await _eventRegistrationRepository.InsertAsync(
+            await EventRegistration.CreateAsync(@event, user, _registrationPolicy)
+            );
+    }
+
+    public async Task CancelRegistrationAsync(Event @event, User user)
+    {
+        var registration = await _eventRegistrationRepository.FirstOrDefaultAsync(r => r.EventId == @event.Id && r.UserId == user.Id);
+        if (registration == null)
+        {
+            //No need to cancel since there is no such a registration
+            return;
+        }
+
+        await registration.CancelAsync(_eventRegistrationRepository);
+    }
+
+    public async Task<IReadOnlyList<User>> GetRegisteredUsersAsync(Event @event)
+    {
+        return await _eventRegistrationRepository
+            .GetAll()
+            .Include(registration => registration.User)
+            .Where(registration => registration.EventId == @event.Id)
+            .Select(registration => registration.User)
+            .ToListAsync();
+    }
+}
+
+ +

It performs domain logic and triggers needed events.

+ +

See domain services documentation for more information on domain services.

+ +

Domain Events

+ +

We may want to define and trigger some domain specific events on some state changes in our application. I defined 2 domain specific events:

+ +
    +
  • EventCancelledEvent: Triggered when an event is canceled. It's triggered in EventManager.Cancel method.
  • + +
  • EventDateChangedEvent: Triggered when date of an event changed. It's triggered in Event.ChangeDate method.
  • +
+ +

We handle these events and notify related users about these changes. Also, I handle EntityCreatedEventDate<Event> (which is a pre-defined ABP event and triggered automatically).

+ +

To handle an event, we should define an event handler class. I defined EventUserEmailer to send emails to users when needed:

+ +
public class EventUserEmailer :
+        IEventHandler<EntityCreatedEventData<Event>>,
+        IEventHandler<EventDateChangedEvent>,
+        IEventHandler<EventCancelledEvent>,
+        ITransientDependency
+{
+    public ILogger Logger { get; set; }
+
+    private readonly IEventManager _eventManager;
+    private readonly UserManager _userManager;
+
+    public EventUserEmailer(
+        UserManager userManager,
+        IEventManager eventManager)
+    {
+        _userManager = userManager;
+        _eventManager = eventManager;
+
+        Logger = NullLogger.Instance;
+    }
+
+    [UnitOfWork]
+    public virtual void HandleEvent(EntityCreatedEventData<Event> eventData)
+    {
+        //TODO: Send email to all tenant users as a notification
+
+        var users = _userManager
+            .Users
+            .Where(u => u.TenantId == eventData.Entity.TenantId)
+            .ToList();
+
+        foreach (var user in users)
+        {
+            var message = string.Format("Hey! There is a new event '{0}' on {1}! Want to register?", eventData.Entity.Title, eventData.Entity.Date);
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
+        }
+    }
+
+    public void HandleEvent(EventDateChangedEvent eventData)
+    {
+        //TODO: Send email to all registered users!
+
+        var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
+        foreach (var user in registeredUsers)
+        {
+            var message = eventData.Entity.Title + " event's date is changed! New date is: " + eventData.Entity.Date;
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
+        }
+    }
+
+    public void HandleEvent(EventCancelledEvent eventData)
+    {
+        //TODO: Send email to all registered users!
+
+        var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
+        foreach (var user in registeredUsers)
+        {
+            var message = eventData.Entity.Title + " event is canceled!";
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
+        }
+    }
+}
+
+ +

We can handle same events in different classes or different events in same class (as in this sample). Here, we handle these events and send email to related users as a notification (not implemented emailing actually to make the sample application simpler). An event handler should implement IEventHandler<event-type> interface. ABP automatically calls the handler when related events occur.

+ +

See EventBus documentation for more information on domain events.

+ +

Application Services

+ +

Application services use domain layer to implement use cases of the application (generally used by presentation layer). EventAppService performs application logic for events.

+ +
[AbpAuthorize]
+public class EventAppService : EventCloudAppServiceBase, IEventAppService
+{
+    private readonly IEventManager _eventManager;
+    private readonly IRepository<Event, Guid> _eventRepository;
+
+    public EventAppService(
+        IEventManager eventManager,
+        IRepository<Event, Guid> eventRepository)
+    {
+        _eventManager = eventManager;
+        _eventRepository = eventRepository;
+    }
+
+    public async Task<ListResultDto<EventListDto>> GetListAsync(GetEventListInput input)
+    {
+        var events = await _eventRepository
+            .GetAll()
+            .Include(e => e.Registrations)
+            .WhereIf(!input.IncludeCanceledEvents, e => !e.IsCancelled)
+            .OrderByDescending(e => e.CreationTime)
+            .Take(64)
+            .ToListAsync();
+
+        return new ListResultDto<EventListDto>(events.MapTo<List<EventListDto>>());
+    }
+
+    public async Task<EventDetailOutput> GetDetailAsync(EntityDto<Guid> input)
+    {
+        var @event = await _eventRepository
+            .GetAll()
+            .Include(e => e.Registrations)
+            .ThenInclude(r => r.User)
+            .Where(e => e.Id == input.Id)
+            .FirstOrDefaultAsync();
+
+        if (@event == null)
+        {
+            throw new UserFriendlyException("Could not found the event, maybe it's deleted.");
+        }
+
+        return @event.MapTo<EventDetailOutput>();
+    }
+
+    public async Task CreateAsync(CreateEventInput input)
+    {
+        var @event = Event.Create(AbpSession.GetTenantId(), input.Title, input.Date, input.Description, input.MaxRegistrationCount);
+        await _eventManager.CreateAsync(@event);
+    }
+
+    public async Task CancelAsync(EntityDto<Guid> input)
+    {
+        var @event = await _eventManager.GetAsync(input.Id);
+        _eventManager.Cancel(@event);
+    }
+
+    public async Task<EventRegisterOutput> RegisterAsync(EntityDto<Guid> input)
+    {
+        var registration = await RegisterAndSaveAsync(
+            await _eventManager.GetAsync(input.Id),
+            await GetCurrentUserAsync()
+            );
+
+        return new EventRegisterOutput
+        {
+            RegistrationId = registration.Id
+        };
+    }
+
+    public async Task CancelRegistrationAsync(EntityDto<Guid> input)
+    {
+        await _eventManager.CancelRegistrationAsync(
+            await _eventManager.GetAsync(input.Id),
+            await GetCurrentUserAsync()
+            );
+    }
+
+    private async Task<EventRegistration> RegisterAndSaveAsync(Event @event, User user)
+    {
+        var registration = await _eventManager.RegisterAsync(@event, user);
+        await CurrentUnitOfWork.SaveChangesAsync();
+        return registration;
+    }
+}
+
+ +

As you see, application service does not implement domain logic itself. It just uses entities and domain services (EventManager) to perform the use cases.

+ +

See application service documentation for more information on application services.

+ +

Presentation Layer

+ +

Presentation layer for this application is built using Angular as a SPA.

+ +

Event List

+ +

When we login to the application, we first see a list of events:

+ +

Swagger UI

+ +

We directly use EventAppService to get list of events. Here is the events.component.ts to create this page:

+ +
import { Component, Injector, ViewChild } from '@angular/core';
+import { appModuleAnimation } from '@shared/animations/routerTransition';
+import { EventServiceProxy, EventListDto, ListResultDtoOfEventListDto, EntityDtoOfGuid } from '@shared/service-proxies/service-proxies';
+import { PagedListingComponentBase, PagedRequestDto } from 'shared/paged-listing-component-base';
+import { CreateEventComponent } from 'app/events/create-event/create-event.component';
+
+@Component({
+    templateUrl: './events.component.html',
+    animations: [appModuleAnimation()]
+})
+export class EventsComponent extends PagedListingComponentBase<EventListDto> {
+
+    @ViewChild('createEventModal') createEventModal: CreateEventComponent;
+
+    active: boolean = false;
+    events: EventListDto[] = [];
+    includeCanceledEvents:boolean=false;
+
+    constructor(
+        injector: Injector,
+        private _eventService: EventServiceProxy
+    ) {
+        super(injector);
+    }
+
+    protected list(request: PagedRequestDto, pageNumber: number, finishedCallback: Function): void {
+        this.loadEvent();
+        finishedCallback();
+    }
+
+    protected delete(event: EntityDtoOfGuid): void {
+        abp.message.confirm(
+            'Are you sure you want to cancel this event?',
+            (result: boolean) => {
+                if (result) {
+                    this._eventService.cancelAsync(event)
+                        .subscribe(() => {
+                            abp.notify.info('Event is deleted');
+                            this.refresh();
+                        });
+                }
+            }
+        );
+    }
+
+    includeCanceledEventsCheckboxChanged() {
+        this.loadEvent();
+    };
+
+    // Show Modals
+    createEvent(): void {
+        this.createEventModal.show();
+    }
+
+    loadEvent() {
+        this._eventService.getListAsync(this.includeCanceledEvents)
+            .subscribe((result: ListResultDtoOfEventListDto) => {
+                this.events = result.items;
+            });
+    }
+}
+
+ +

We inject EventServiceProxy into events.component.ts component. We used dynamic web api layer feature of ABP. It creates needed Web API controller and Angular service automatically and dynamically. So, we can use application service methods like calling regular typescript functions. So, to call EventAppService.GetListAsync C# method, we simple call _eventService.getListAsync typescript function.

+ +

We also open a "new event" modal (dialog) when user clicks to "+ New event" button (which triggers createEvent function). I will not go details of Angular views, since they are simpler and you can check it in source code.

+ +

Event Detail

+ +

When we click "Details" button for an event, we go to event details with a URL like http://eventcloud.aspnetboilerplate.com/#/events/e9499e3e-35c0-492c-98ce-7e410461103f . GUID is id of the event.

+ +

Swagger UI

+ +

Here, we see event details with registered users. We can register to the event or cancel registration. This view's component is defined in event-detail.component.ts as shown below:

+ +
import { Component, OnInit, Injector } from '@angular/core';
+import { appModuleAnimation } from '@shared/animations/routerTransition';
+import { ActivatedRoute, Params, Router } from '@angular/router';
+import { AppComponentBase } from '@shared/app-component-base';
+import { EventDetailOutput, EventServiceProxy, EntityDtoOfGuid, EventRegisterOutput } from '@shared/service-proxies/service-proxies';
+
+import * as _ from 'lodash';
+
+@Component({
+    templateUrl: './event-detail.component.html',
+    animations: [appModuleAnimation()]
+})
+
+export class EventDetailComponent extends AppComponentBase implements OnInit {
+
+    event: EventDetailOutput = new EventDetailOutput();
+    eventId:string;
+
+    constructor(
+        injector: Injector,
+        private _eventService: EventServiceProxy,
+        private _router: Router,
+        private _activatedRoute: ActivatedRoute
+    ) {
+        super(injector);
+    }
+
+    ngOnInit(): void {
+        this._activatedRoute.params.subscribe((params: Params) => {
+            this.eventId = params['eventId'];
+            this.loadEvent();
+        });
+    }
+
+    registerToEvent(): void {
+        var input = new EntityDtoOfGuid();
+        input.id = this.event.id;
+
+        this._eventService.registerAsync(input)
+            .subscribe((result: EventRegisterOutput) => {
+                abp.notify.success('Successfully registered to event. Your registration id: ' + result.registrationId + ".");
+                this.loadEvent();
+            });
+    };
+
+    cancelRegistrationFromEvent(): void {
+        var input = new EntityDtoOfGuid();
+        input.id = this.event.id;
+
+        this._eventService.cancelRegistrationAsync(input)
+            .subscribe(() => {
+                abp.notify.info('Canceled your registration.');
+                this.loadEvent();
+            });
+    };
+
+    cancelEvent(): void {
+        var input = new EntityDtoOfGuid();
+        input.id = this.event.id;
+
+        this._eventService.cancelAsync(input)
+            .subscribe(() => {
+                abp.notify.info('Canceled the event.');
+                this.backToEventsPage();
+            });
+    };
+
+    isRegistered(): boolean {
+        return _.some(this.event.registrations, { userId: abp.session.userId });
+    };
+
+    isEventCreator(): boolean {
+        return this.event.creatorUserId === abp.session.userId;
+    };
+
+    loadEvent() {
+        this._eventService.getDetailAsync(this.eventId)
+            .subscribe((result: EventDetailOutput) => {
+                this.event = result;
+            });
+    }
+
+    backToEventsPage() {
+        this._router.navigate(['app/events']);
+    };
+}
+
+ +

We simply use event application service to perform actions.

+ + + +

Top menu is automatically created by ABP template. We define menu items in sidebar-nav.component.ts class:

+ +
@Component({
+    templateUrl: './sidebar-nav.component.html',
+    selector: 'sidebar-nav',
+    encapsulation: ViewEncapsulation.None
+})
+export class SideBarNavComponent extends AppComponentBase {
+
+    menuItems: MenuItem[] = [
+        new MenuItem(this.l("HomePage"), "", "home", "/app/home"),
+
+        new MenuItem(this.l("Tenants"), "Pages.Tenants", "business", "/app/tenants"),
+        new MenuItem(this.l("Users"), "Pages.Users", "people", "/app/users"),
+        new MenuItem(this.l("Roles"), "Pages.Roles", "local_offer", "/app/roles"),
+        new MenuItem(this.l("Events"), "Pages.Events", "event", "/app/events"),
+        new MenuItem(this.l("About"), "", "info", "/app/about"),
+
+...        
+
+ +

Angular Route

+ +

Defining the menu only shows it on the page. Angular has it's own route system. Routes are defined in app-routing-module.ts as shown below:

+ +
import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { AppComponent } from './app.component';
+import { AppRouteGuard } from '@shared/auth/auth-route-guard';
+import { HomeComponent } from './home/home.component';
+import { AboutComponent } from './about/about.component';
+import { UsersComponent } from './users/users.component';
+import { TenantsComponent } from './tenants/tenants.component';
+import { RolesComponent } from 'app/roles/roles.component';
+import { EventsComponent } from 'app/events/events.component';
+import { EventDetailComponent } from 'app/events/event-detail/event-detail.component';
+
+@NgModule({
+    imports: [
+        RouterModule.forChild([
+            {
+                path: '',
+                component: AppComponent,
+                children: [
+                    { path: 'home', component: HomeComponent, canActivate: [AppRouteGuard] },
+                    { path: 'users', component: UsersComponent, data: { permission: 'Pages.Users' }, canActivate: [AppRouteGuard] },
+                    { path: 'roles', component: RolesComponent, data: { permission: 'Pages.Roles' }, canActivate: [AppRouteGuard] },
+                    { path: 'tenants', component: TenantsComponent, data: { permission: 'Pages.Tenants' }, canActivate: [AppRouteGuard] },
+                    { path: 'events', component: EventsComponent, data: { permission: 'Pages.Events' }, canActivate: [AppRouteGuard] },
+                    { path: 'events/:eventId', component: EventDetailComponent },
+                    { path: 'about', component: AboutComponent }
+                ]
+            }
+        ])
+    ],
+    exports: [RouterModule]
+})
+export class AppRoutingModule { }
+
+ +

Unit and Integration Tests

+ +

ASP.NET Boilerplate provides tools to make unit and integration tests easier. You can find all test code from source code of the project. Here, I will briefly explain basic tests. Solution includes EventAppService_Tests class which tests the EventAppService. See 2 tests from this class:

+ +
public class EventAppService_Tests : EventCloudTestBase
+{
+    private readonly IEventAppService _eventAppService;
+
+    public EventAppService_Tests()
+    {
+        _eventAppService = Resolve<IEventAppService>();
+    }
+
+    [Fact]
+    public async Task Should_Get_Test_Events()
+    {
+        var output = await _eventAppService.GetListAsync(new GetEventListInput());
+        output.Items.Count.ShouldBe(1);
+    }
+
+    [Fact]
+    public async Task Should_Create_Event()
+    {
+        //Arrange
+        var eventTitle = Guid.NewGuid().ToString();
+
+        //Act
+        await _eventAppService.CreateAsync(new CreateEventInput
+        {
+            Title = eventTitle,
+            Description = "A description",
+            Date = Clock.Now.AddDays(2)
+        });
+
+        //Assert
+        UsingDbContext(context =>
+        {
+            context.Events.FirstOrDefault(e => e.Title == eventTitle).ShouldNotBe(null);
+        });
+    }
+
+    [Fact]
+    public async Task Should_Not_Create_Events_In_The_Past()
+    {
+        //Arrange
+        var eventTitle = Guid.NewGuid().ToString();
+
+        //Act
+        await Assert.ThrowsAsync<UserFriendlyException>(async () =>
+        {
+            await _eventAppService.CreateAsync(new CreateEventInput
+            {
+                Title = eventTitle,
+                Description = "A description",
+                Date = Clock.Now.AddDays(-1)
+            });
+        });
+    }
+
+    [Fact]
+    public async Task Should_Cancel_Event()
+    {
+        //Act
+        await _eventAppService.CancelAsync(new EntityDto<Guid>(GetTestEvent().Id));
+
+        //Assert
+        GetTestEvent().IsCancelled.ShouldBeTrue();
+    }
+
+    [Fact]
+    public async Task Should_Register_To_Events()
+    {
+        //Arrange
+        var testEvent = GetTestEvent();
+
+        //Act
+        var output = await _eventAppService.RegisterAsync(new EntityDto<Guid>(testEvent.Id));
+
+        //Assert
+        output.RegistrationId.ShouldBeGreaterThan(0);
+
+        UsingDbContext(context =>
+        {
+            var currentUserId = AbpSession.GetUserId();
+            var registration = context.EventRegistrations.FirstOrDefault(r => r.EventId == testEvent.Id && r.UserId == currentUserId);
+            registration.ShouldNotBeNull();
+        });
+    }
+
+    [Fact]
+    public async Task Should_Cancel_Registration()
+    {
+        //Arrange
+        var currentUserId = AbpSession.GetUserId();
+        await UsingDbContext(async context =>
+        {
+            var testEvent = GetTestEvent(context);
+            var currentUser = await context.Users.SingleAsync(u => u.Id == currentUserId);
+            var testRegistration = await EventRegistration.CreateAsync(
+                testEvent,
+                currentUser,
+                Substitute.For<IEventRegistrationPolicy>()
+                );
+
+            context.EventRegistrations.Add(testRegistration);
+        });
+
+        //Act
+        await _eventAppService.CancelRegistrationAsync(new EntityDto<Guid>(GetTestEvent().Id));
+
+        //Assert
+        UsingDbContext(context =>
+        {
+            var testEvent = GetTestEvent(context);
+            var testRegistration = context.EventRegistrations.FirstOrDefault(r => r.EventId == testEvent.Id && r.UserId == currentUserId);
+            testRegistration.ShouldBeNull();
+        });
+    }
+
+    private Event GetTestEvent()
+    {
+        return UsingDbContext(context => GetTestEvent(context));
+    }
+
+    private static Event GetTestEvent(EventCloudDbContext context)
+    {
+        return context.Events.Single(e => e.Title == TestDataBuilder.TestEventTitle);
+    }
+}
+
+ +

We use xUnit as test framework. In the first test, we simply create an event and check database if it's in there. In the second test, we intentionally trying to create an event in the past. Since our business rule don't allow it, we should get an exception here.

+ +

With such tests, we tested everything starting from application service including all aspects of ASP.NET Boilerplate (like validation, unit of work and so on).

+ +

Token Based Authentication

+ +

If you want to consume APIs/application services from a mobile application, you can use the token based authentication mechanism just like we do for the Angular client. The startup template includes the JwtBearer token authentication infrastructure.

+ +

We will use Postman (a chrome extension) to demonstrate requests and responses.

+ +

Authentication

+ +

Just send a POST request to http://localhost:21021/api/TokenAuth/Authenticate with the Context-Type="application/json" header as shown below:

+ +

Swagger UI

+ +

We sent a JSON request body includes tenancyName, userNameOrEmailAddress and password. tenancyName is not required for host users. As seen above, result property of returning JSON contains the token. We can save it and use for next requests.

+ +

Use API

+ +

After authenticate and get the token, we can use it to call any authorized action. All application services are available to be used remotely. For example, we can use the User service to get a list of users:

+ +

Using API

+ +

Just made a GET request to http://localhost:21021/api/services/app/user/getAll with Content-Type="application/json" and Authorization="Bearer +[your-auth-token]". All functionality available on UI is also available as API.

+ +

Almost all operations available on UI also available as Web API (since UI uses the same Web API) and can be consumed easily.

+ +

Source Code

+ +

You can get the latest source code here Event Cloud Source

diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-api-v2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-api-v2.png" new file mode 100644 index 0000000..774a4e1 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-api-v2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-auth.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-auth.png" new file mode 100644 index 0000000..2651216 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/swagger-ui-angular-auth.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-2.png" new file mode 100644 index 0000000..4a84cf5 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-3.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-3.png" new file mode 100644 index 0000000..903eb7e Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-3.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-4.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-4.png" new file mode 100644 index 0000000..e6715fa Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/create-template-4.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-detail-page.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-detail-page.png" new file mode 100644 index 0000000..ca5477b Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-detail-page.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-list-page.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-list-page.png" new file mode 100644 index 0000000..53e6af6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/event-list-page.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/index.html" new file mode 100644 index 0000000..0a6d0c9 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/index.html" @@ -0,0 +1,945 @@ + + + + +

Contents

+ + + +

Login page

+ +

Introduction

+ +

In this article, we will see a SaaS (multi-tenant) application developed using the following frameworks:

+ +
    +
  • ASP.NET Boilerplate as application framework.
  • +
  • ASP.NET MVC and ASP.NET Web API as Web Frameworks.
  • +
  • Entity Framework as ORM.
  • +
  • AngularJS as SPA framework.
  • +
  • Bootstrap as HTML/CSS framework.
  • +
+ +

You can see live demo before reading the article. 

+ +

Creating Application From Template

+ +

ASP.NET Boilerplate provides templates to make a project startup easier. We create the startup template from https://aspnetboilerplate.com/Templates:

+ +

Create template

+ +

I selected ASP.NET MVC 5.x, AngularJS and Entity Frameowork including "Options". It creates a ready and working solution for us including a login page, navigation and a bootstrap based layout. After downloading and opening the solution with Visual Studio 2017+, we see a layered solution structure including a unit test project:

+ +

Solution structure

+ +

First, we select EventCloud.Web as startup project. Solution comes with Entity Framework Code-First Migrations. So, (after restoring nuget packages) we open the Package Manager Console (PMC) and run Update-Database command to create the database:

+ +

Update database command

+ +

Package Manager Console's Default project should be EventCloud.EntityFramework (since it contains the migrations). This command creates EventCloud database in local SQL Server (you can change connection string from web.config file).

+ +

Now, we can run the application. We see the pre-built login page. +We can enter default as tenancy name, admin as user and 123qwe as password to login:

+ +

Initial Login Page

+ +

After login, we see the following pages: Events and About pages

+ +

Initial layout

+ +

This is a localized UI with a dynamic menu. Angular layout, routing and basic infrastructure are working properly. I take this project as a base for the event cloud project.

+ +

Event Cloud Project

+ +

In this article, I will show the key parts of the project and explain it. So, please download the sample project, open in Visual Studio 2017+ and run migrations just like above before reading rest of the article (Be sure that there is no database named EventCloud before running the migrations). I will follow some DDD (Domain Driven Design) techniques to create domain (business) layer and application layer.

+ +

Event Cloud is a free SaaS (multi-tenant) application. We can create a tenant which has it's own events, users, roles... There are some simple business rules applied while creating, canceling and registering to an event.

+ +

So, let's start to review the source code.

+ +

Entities

+ +

Entities are parts of our domain layer and located under EventCloud.Core project. ASP.NET Boilerplate startup template comes with Tenant, User, Role... entities which are common for most applications. We can customize them based on our needs. Surely, we can add our application specific entities.

+ +

The fundamental entity of event cloud project is the Event entity:

+ +
+[Table("AppEvents")]
+public class Event : FullAuditedEntity<Guid>, IMustHaveTenant
+{
+    public const int MaxTitleLength = 128;
+    public const int MaxDescriptionLength = 2048;
+
+    public virtual int TenantId { get; set; }
+
+    [Required]
+    [StringLength(MaxTitleLength)]
+    public virtual string Title { get; protected set; }
+
+    [StringLength(MaxDescriptionLength)]
+    public virtual string Description { get; protected set; }
+
+    public virtual DateTime Date { get; protected set; }
+
+    public virtual bool IsCancelled { get; protected set; }
+
+    /// <summary>
+    /// Gets or sets the maximum registration count.
+    /// 0: Unlimited.
+    /// </summary>
+    [Range(0, int.MaxValue)]
+    public virtual int MaxRegistrationCount { get; protected set; }
+
+    [ForeignKey("EventId")]
+    public virtual ICollection<EventRegistration> Registrations { get; protected set; }
+
+    /// <summary>
+    /// We don't make constructor public and forcing to create events using <see cref="Create"/> method.
+    /// But constructor can not be private since it's used by EntityFramework.
+    /// Thats why we did it protected.
+    /// </summary>
+    protected Event()
+    {
+
+    }
+
+    public static Event Create(int tenantId, string title, DateTime date, string description = null, int maxRegistrationCount = 0)
+    {
+        var @event = new Event
+        {
+            Id = Guid.NewGuid(),
+            TenantId = tenantId,
+            Title = title,
+            Description = description,
+            MaxRegistrationCount = maxRegistrationCount
+        };
+
+        @event.SetDate(date);
+
+        @event.Registrations = new Collection<EventRegistration>();
+
+        return @event;
+    }
+
+    public bool IsInPast()
+    {
+        return Date < Clock.Now;
+    }
+
+    public bool IsAllowedCancellationTimeEnded()
+    {
+        return Date.Subtract(Clock.Now).TotalHours <= 2.0; //2 hours can be defined as Event property and determined per event
+    }
+
+    public void ChangeDate(DateTime date)
+    {
+        if (date == Date)
+        {
+            return;
+        }
+
+        SetDate(date);
+
+        DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
+    }
+
+    internal void Cancel()
+    {
+        AssertNotInPast();
+        IsCancelled = true;
+    }
+
+    private void SetDate(DateTime date)
+    {
+        AssertNotCancelled();
+
+        if (date < Clock.Now)
+        {
+            throw new UserFriendlyException("Can not set an event's date in the past!");
+        }
+
+        if (date <= Clock.Now.AddHours(3)) //3 can be configurable per tenant
+        {
+            throw new UserFriendlyException("Should set an event's date 3 hours before at least!");
+        }
+
+        Date = date;
+
+        DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
+    }
+
+    private void AssertNotInPast()
+    {
+        if (IsInPast())
+        {
+            throw new UserFriendlyException("This event was in the past");
+        }
+    }
+
+    private void AssertNotCancelled()
+    {
+        if (IsCancelled)
+        {
+            throw new UserFriendlyException("This event is canceled!");
+        }
+    }
+}
+ +

Event entity has not just get/set properties. Actually, it has not public setters, setters are protected. It has some domain logic. All properties must be changed by the Event entity itself to ensure domain logic is being executed.

+ +

Event entity's constructor is also protected. So, the only way to create an Event is the Event.Create method (They can be private normally, but private setters don't work well with Entity Framework since Entity Framework can not set privates when retrieving an entity from database).

+ +

Event implements IMustHaveTenant interface. This is an interface of ASP.NET Boilerplate (ABP) framework and ensures that this entity is per tenant. This is needed for multi-tenancy. Thus, different tenants will have different events and can not see each other's events. ABP automatically filters entities of current tenant.

+ +

Event class inherits from FullAuditedEntity which contains creation, modification and deletion audit columns. FullAuditedEntity also implements ISoftDelete, so events can not be deleted from database. They are marked as deleted when you delete it +(soft delete). ABP automatically filters (hides) deleted entities when you query database.

+ +

In DDD, entities have domain (business) logic. We have some simple business rules those can be understood easily when you check the entity.

+ +

Second entity of our application is EventRegistration:

+ +
+[Table("AppEventRegistrations")]
+public class EventRegistration : CreationAuditedEntity, IMustHaveTenant
+{
+    public int TenantId { get; set; }
+
+    [ForeignKey("EventId")]
+    public virtual Event Event { get; protected set; }
+    public virtual Guid EventId { get; protected set; }
+
+    [ForeignKey("UserId")]
+    public virtual User User { get; protected set; }
+    public virtual long UserId { get; protected set; }
+
+    /// <summary>
+    /// We don't make constructor public and forcing to create registrations using <see cref="CreateAsync"/> method.
+    /// But constructor can not be private since it's used by EntityFramework.
+    /// Thats why we did it protected.
+    /// </summary>
+    protected EventRegistration()
+    {
+            
+    }
+
+    public async static Task<EventRegistration> CreateAsync(Event @event, User user, IEventRegistrationPolicy registrationPolicy)
+    {
+        await registrationPolicy.CheckRegistrationAttemptAsync(@event, user);
+
+        return new EventRegistration
+        {
+            TenantId = @event.TenantId,
+            EventId = @event.Id,
+            Event = @event,
+            UserId = @user.Id,
+            User = user
+        };
+    }
+
+    public async Task CancelAsync(IRepository<EventRegistration> repository)
+    {
+        if (repository == null) { throw new ArgumentNullException("repository"); }
+
+        if (Event.IsInPast())
+        {
+            throw new UserFriendlyException("Can not cancel event which is in the past!");
+        }
+
+        if (Event.IsAllowedCancellationTimeEnded())
+        {
+            throw new UserFriendlyException("It's too late to cancel your registration!");
+        }
+
+        await repository.DeleteAsync(this);
+    }
+}
+ +

As similar to Event, we have a static create method. The only way of creating a new EventRegistration is this CreateAsync method. It gets an event, user and a registration policy. It checks if given user can register to the event using registrationPolicy.CheckRegistrationAttemptAsync method. This method throws exception if given user can not register to given event. With such a design, we ensure that all business rules are applied while creating a registration. There is no way of creating a registration without using registration policy.

+ +

See Entity documentation for more information on entities.

+ +

Event Registration Policy

+ +

EventRegistrationPolicy class is defined as shown below:

+ +
+public class EventRegistrationPolicy : EventCloudServiceBase, IEventRegistrationPolicy
+{
+    private readonly IRepository<EventRegistration> _eventRegistrationRepository;
+
+    public EventRegistrationPolicy(IRepository<EventRegistration> eventRegistrationRepository)
+    {
+        _eventRegistrationRepository = eventRegistrationRepository;
+    }
+
+    public async Task CheckRegistrationAttemptAsync(Event @event, User user)
+    {
+        if (@event == null) { throw new ArgumentNullException("event"); }
+        if (user == null) { throw new ArgumentNullException("user"); }
+
+        CheckEventDate(@event);
+        await CheckEventRegistrationFrequencyAsync(user);
+    }
+
+    private static void CheckEventDate(Event @event)
+    {
+        if (@event.IsInPast())
+        {
+            throw new UserFriendlyException("Can not register event in the past!");
+        }
+    }
+
+    private async Task CheckEventRegistrationFrequencyAsync(User user)
+    {
+        var oneMonthAgo = Clock.Now.AddDays(-30);
+        var maxAllowedEventRegistrationCountInLast30DaysPerUser = await SettingManager.GetSettingValueAsync<int>(EventCloudSettingNames.MaxAllowedEventRegistrationCountInLast30DaysPerUser);
+        if (maxAllowedEventRegistrationCountInLast30DaysPerUser > 0)
+        {
+            var registrationCountInLast30Days = await _eventRegistrationRepository.CountAsync(r => r.UserId == user.Id && r.CreationTime >= oneMonthAgo);
+            if (registrationCountInLast30Days > maxAllowedEventRegistrationCountInLast30DaysPerUser)
+            {
+                throw new UserFriendlyException(string.Format("Can not register to more than {0}", maxAllowedEventRegistrationCountInLast30DaysPerUser));
+            }
+        }
+    }
+}
+ +

This is an important part of our domain. We have two rules while creating an event registration:

+ +
    +
  1. A used can not register to an event in the past.
  2. +
  3. A user can register to a maximum count of events in 30 days. So, we have registration frequency limit.
  4. +
+ +

Event Manager

+ +

EventManager implements business (domain) logic for events. All Event operations should be executed using this class. It's defined as shown below:

+ +
+public class EventManager : IEventManager
+{
+    public IEventBus EventBus { get; set; }
+
+    private readonly IEventRegistrationPolicy _registrationPolicy;
+    private readonly IRepository<EventRegistration> _eventRegistrationRepository;
+    private readonly IRepository<Event, Guid> _eventRepository;
+
+    public EventManager(
+        IEventRegistrationPolicy registrationPolicy,
+        IRepository<EventRegistration> eventRegistrationRepository,
+        IRepository<Event, Guid> eventRepository)
+    {
+        _registrationPolicy = registrationPolicy;
+        _eventRegistrationRepository = eventRegistrationRepository;
+        _eventRepository = eventRepository;
+
+        EventBus = NullEventBus.Instance;
+    }
+
+    public async Task<Event> GetAsync(Guid id)
+    {
+        var @event = await _eventRepository.FirstOrDefaultAsync(id);
+        if (@event == null)
+        {
+            throw new UserFriendlyException("Could not found the event, maybe it's deleted!");
+        }
+
+        return @event;
+    }
+
+    public async Task CreateAsync(Event @event)
+    {
+        await _eventRepository.InsertAsync(@event);
+    }
+
+    public void Cancel(Event @event)
+    {
+        @event.Cancel();
+        EventBus.Trigger(new EventCancelledEvent(@event));
+    }
+
+    public async Task<EventRegistration> RegisterAsync(Event @event, User user)
+    {
+        return await _eventRegistrationRepository.InsertAsync(
+            await EventRegistration.CreateAsync(@event, user, _registrationPolicy)
+            );
+    }
+
+    public async Task CancelRegistrationAsync(Event @event, User user)
+    {
+        var registration = await _eventRegistrationRepository.FirstOrDefaultAsync(r => r.EventId == @event.Id && r.UserId == user.Id);
+        if (registration == null)
+        {
+            //No need to cancel since there is no such a registration
+            return;
+        }
+
+        await registration.CancelAsync(_eventRegistrationRepository);
+    }
+
+    public async Task<IReadOnlyList<User>> GetRegisteredUsersAsync(Event @event)
+    {
+        return await _eventRegistrationRepository
+            .GetAll()
+            .Include(registration => registration.User)
+            .Where(registration => registration.EventId == @event.Id)
+            .Select(registration => registration.User)
+            .ToListAsync();
+    }
+}
+ +

It performs domain logic and triggers needed events.

+ +

See domain services documentation for more information on domain services.

+ +

Domain Events

+ +

We may want to define and trigger some domain specific events on some state changes in our application. I defined 2 domain specific events:

+ +
    +
  • EventCancelledEvent: Triggered when an event is canceled. It's triggered in EventManager.Cancel method.
  • +
  • EventDateChangedEvent: Triggered when date of an event changed. It's triggered in Event.ChangeDate method.
  • +
+ +

We handle these events and notify related users about these changes. Also, I handle EntityCreatedEventDate<Event> (which is a pre-defined ABP event and triggered automatically).

+ +

To handle an event, we should define an event handler class. I defined EventUserEmailer to send emails to users when needed:

+ +
+public class EventUserEmailer : 
+    IEventHandler<EntityCreatedEventData<Event>>,
+    IEventHandler<EventDateChangedEvent>, 
+    IEventHandler<EventCancelledEvent>,
+    ITransientDependency
+{
+    public ILogger Logger { get; set; }
+
+    private readonly IEventManager _eventManager;
+    private readonly UserManager _userManager;
+
+    public EventUserEmailer(
+        UserManager userManager, 
+        IEventManager eventManager)
+    {
+        _userManager = userManager;
+        _eventManager = eventManager;
+
+        Logger = NullLogger.Instance;
+    }
+
+    [UnitOfWork]
+    public virtual void HandleEvent(EntityCreatedEventData<Event> eventData)
+    {
+        //TODO: Send email to all tenant users as a notification
+
+        var users = _userManager
+            .Users
+            .Where(u => u.TenantId == eventData.Entity.TenantId)
+            .ToList();
+
+        foreach (var user in users)
+        {
+            var message = string.Format("Hey! There is a new event '{0}' on {1}! Want to register?",eventData.Entity.Title, eventData.Entity.Date);
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
+        }
+    }
+
+    public void HandleEvent(EventDateChangedEvent eventData)
+    {
+        //TODO: Send email to all registered users!
+
+        var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
+        foreach (var user in registeredUsers)
+        {
+            var message = eventData.Entity.Title + " event's date is changed! New date is: " + eventData.Entity.Date;
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}",user.EmailAddress, message));
+        }
+    }
+
+    public void HandleEvent(EventCancelledEvent eventData)
+    {
+        //TODO: Send email to all registered users!
+
+        var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
+        foreach (var user in registeredUsers)
+        {
+            var message = eventData.Entity.Title + " event is canceled!";
+            Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
+        }
+    }
+}
+ +

We can handle same events in different classes or different events in same class (as in this sample). Here, we handle these events and send email to related users as a notification (not implemented emailing actually to make the sample application simpler). An event handler should implement IEventHandler<event-type> interface. ABP automatically calls the handler when related events occur.

+ +

See EventBus documentation for more information on domain events.

+ +

Application Services

+ +

Application services use domain layer to implement use cases of the application (generally used by presentation layer). EventAppService performs application logic for events.

+ +
+[AbpAuthorize]
+public class EventAppService : EventCloudAppServiceBase, IEventAppService
+{
+    private readonly IEventManager _eventManager;
+    private readonly IRepository<Event, Guid> _eventRepository;
+
+    public EventAppService(
+        IEventManager eventManager, 
+        IRepository<Event, Guid> eventRepository)
+    {
+        _eventManager = eventManager;
+        _eventRepository = eventRepository;
+    }
+
+    public async Task<ListResultOutput<EventListDto>> GetList(GetEventListInput input)
+    {
+        var events = await _eventRepository
+            .GetAll()
+            .Include(e => e.Registrations)
+            .WhereIf(!input.IncludeCanceledEvents, e => !e.IsCancelled)
+            .OrderByDescending(e => e.CreationTime)
+            .ToListAsync();
+
+        return new ListResultOutput<EventListDto>(events.MapTo<List<EventListDto>>());
+    }
+
+    public async Task<EventDetailOutput> GetDetail(EntityRequestInput<Guid> input)
+    {
+        var @event = await _eventRepository
+            .GetAll()
+            .Include(e => e.Registrations)
+            .Where(e => e.Id == input.Id)
+            .FirstOrDefaultAsync();
+
+        if (@event == null)
+        {
+            throw new UserFriendlyException("Could not found the event, maybe it's deleted.");
+        }
+
+        return @event.MapTo<EventDetailOutput>();
+    }
+
+    public async Task Create(CreateEventInput input)
+    {
+        var @event = Event.Create(AbpSession.GetTenantId(), input.Title, input.Date, input.Description, input.MaxRegistrationCount);
+        await _eventManager.CreateAsync(@event);
+    }
+
+    public async Task Cancel(EntityRequestInput<Guid> input)
+    {
+        var @event = await _eventManager.GetAsync(input.Id);
+        _eventManager.Cancel(@event);
+    }
+
+    public async Task<EventRegisterOutput> Register(EntityRequestInput<Guid> input)
+    {
+        var registration = await RegisterAndSaveAsync(
+            await _eventManager.GetAsync(input.Id),
+            await GetCurrentUserAsync()
+            );
+
+        return new EventRegisterOutput
+        {
+            RegistrationId = registration.Id
+        };
+    }
+
+    public async Task CancelRegistration(EntityRequestInput<Guid> input)
+    {
+        await _eventManager.CancelRegistrationAsync(
+            await _eventManager.GetAsync(input.Id),
+            await GetCurrentUserAsync()
+            );
+    }
+
+    private async Task<EventRegistration> RegisterAndSaveAsync(Event @event, User user)
+    {
+        var registration = await _eventManager.RegisterAsync(@event, user);
+        await CurrentUnitOfWork.SaveChangesAsync();
+        return registration;
+    }
+}
+ +

As you see, application service does not implement domain logic itself. It just uses entities and domain services (EventManager) to perform the use cases.

+ +

See application service documentation for more information on application services.

+ +

Presentation Layer

+ +

Presentation layer for this application is built using AngularJS as a SPA.

+ +

Event List

+ +

When we login to the application, we first see a list of events:

+ +

Event list page

+ +

We directly use EventAppService to get list of events. Here, the Angular controller to create this page:

+ +
+(function() {
+    var controllerId = 'app.views.events.index';
+    angular.module('app').controller(controllerId, [
+        '$scope', '$modal', 'abp.services.app.event',
+        function ($scope, $modal, eventService) {
+            var vm = this;
+
+            vm.events = [];
+            vm.filters = {
+                includeCanceledEvents: false
+            };
+
+            function loadEvents() {
+                eventService.getList(vm.filters).success(function (result) {
+                    vm.events = result.items;
+                });
+            };
+
+            vm.openNewEventDialog = function() {
+                var modalInstance = $modal.open({
+                    templateUrl: abp.appPath + 'App/Main/views/events/createDialog.cshtml',
+                    controller: 'app.views.events.createDialog as vm',
+                    size: 'md'
+                });
+
+                modalInstance.result.then(function () {
+                    loadEvents();
+                });
+            };
+
+            $scope.$watch('vm.filters.includeCanceledEvents', function (newValue, oldValue) {
+                if (newValue != oldValue) {
+                    loadEvents();
+                }
+            });
+
+            loadEvents();
+        }
+    ]);
+})();
+ +

We inject EventAppService as 'abp.services.app.event' into Angular controller. We used dynamic web api layer feature of ABP. It creates needed Web API controller and AngularJS service automatically and dynamically. So, we can use application service methods like calling regular JavaScript functions. So, to call EventAppService.GetList C# method, we simply call eventService.getList JavaScript function which returns a promise ($q for angular).

+ +

We also open a "new event" modal (dialog) when user clicks to "+ New event" button (which triggers vm.openNewEventDialog function). I will not go in details of Angular views, since they are simple, you can check it out in the source code.

+ +

Event Detail

+ +

When we click "Details" button for an event, we go to event details with a URL like "http://eventcloud-mvc5x.aspnetboilerplate.com/#/events/b680ad0a-d751-4d85-a7ad-34df5c8a86c2". GUID is id of the event.

+ +

Event details

+ +

Here, we see event details with registered users. We can register to the event or cancel registration. This view's controller is defined in detail.js as shown below:

+ +
+(function () {
+    var controllerId = 'app.views.events.detail';
+    angular.module('app').controller(controllerId, [
+        '$scope', '$state','$stateParams', 'abp.services.app.event',
+        function ($scope, $state, $stateParams, eventService) {
+            var vm = this;
+
+            function loadEvent() {
+                eventService.getDetail({
+                    id: $stateParams.id
+                }).then(function (result) {
+                    vm.event = result.data;
+                });
+            }
+
+            vm.isRegistered = function () {
+                if (!vm.event) {
+                    return false;
+                }
+
+                return _.find(vm.event.registrations, function(registration) {
+                    return registration.userId == abp.session.userId;
+                });
+            };
+
+            vm.isEventCreator = function() {
+                return vm.event && vm.event.creatorUserId == abp.session.userId;
+            };
+
+            vm.getUserThumbnail = function(registration) {
+                return registration.userName.substr(0, 1).toLocaleUpperCase();
+            };
+
+            vm.register = function() {
+                eventService.register({
+                    id: vm.event.id
+                }).then(function (result) {
+                    abp.notify.success('Successfully registered to event. Your registration id: ' + result.data.registrationId + ".");
+                    loadEvent();
+                });
+            };
+
+            vm.cancelRegistertration = function() {
+                eventService.cancelRegistration({
+                    id: vm.event.id
+                }).then(function () {
+                    abp.notify.info('Canceled your registration.');
+                    loadEvent();
+                });
+            };
+
+            vm.cancelEvent = function() {
+                eventService.cancel({
+                    id: vm.event.id
+                }).then(function () {
+                    abp.notify.info('Canceled the event.');
+                    vm.backToEventsPage();
+                });
+            };
+
+            vm.backToEventsPage = function() {
+                $state.go('events');
+            };
+
+            loadEvent();
+        }
+    ]);
+})();
+ +

We simply use event application service to perform actions.

+ +

Main Menu

+ +

Top menu is automatically created by ABP template. We define menu items in EventCloudNavigationProvider class:

+ +
+public class EventCloudNavigationProvider : NavigationProvider
+{
+    public override void SetNavigation(INavigationProviderContext context)
+    {
+        context.Manager.MainMenu
+            .AddItem(
+                new MenuItemDefinition(
+                    AppPageNames.Events,
+                    new LocalizableString("Events", EventCloudConsts.LocalizationSourceName),
+                    url: "#/",
+                    icon: "fa fa-calendar-check-o"
+                    )
+            ).AddItem(
+                new MenuItemDefinition(
+                    AppPageNames.About,
+                    new LocalizableString("About", EventCloudConsts.LocalizationSourceName),
+                    url: "#/about",
+                    icon: "fa fa-info"
+                    )
+            );
+    }
+}
+ +

We can add new menu items here. See navigation documentation for more information.

+ +

Angular Route

+ +

Defining the menu only shows it on the page. Angular has it's own route system. This application uses Angular ui-router. Routes are defined in app.js as shown below:

+ +
+//Configuration for Angular UI routing.
+app.config([
+    '$stateProvider', '$urlRouterProvider',
+    function($stateProvider, $urlRouterProvider) {
+        $urlRouterProvider.otherwise('/events');
+        $stateProvider
+            .state('events', {
+                url: '/events',
+                templateUrl: '/App/Main/views/events/index.cshtml',
+                menu: 'Events' //Matches to name of 'Events' menu in EventCloudNavigationProvider
+            })
+            .state('eventDetail', {
+                url: '/events/:id',
+                templateUrl: '/App/Main/views/events/detail.cshtml',
+                menu: 'Events' //Matches to name of 'Events' menu in EventCloudNavigationProvider
+            })
+            .state('about', {
+                url: '/about',
+                templateUrl: '/App/Main/views/about/about.cshtml',
+                menu: 'About' //Matches to name of 'About' menu in EventCloudNavigationProvider
+            });
+    }
+]);
+ +

Unit and Integration Tests

+ +

ASP.NET Boilerplate provides tools to make unit and integration tests easier. You can find all test code from source code of the project. Here, I will briefly explain basic tests. Solution includes EventAppService_Tests class which tests the EventAppService. See 2 tests from this class:

+ +
+public class EventAppService_Tests : EventCloudTestBase
+{
+    private readonly IEventAppService _eventAppService;
+
+    public EventAppService_Tests()
+    {
+        _eventAppService = Resolve<IEventAppService>();
+    }
+
+    [Fact]
+    public async Task Should_Create_Event()
+    {
+        //Arrange
+        var eventTitle = Guid.NewGuid().ToString();
+
+        //Act
+        await _eventAppService.Create(new CreateEventInput
+        {
+            Title = eventTitle,
+            Description = "A description",
+            Date = Clock.Now.AddDays(2)
+        });
+
+        //Assert
+        UsingDbContext(context =>
+        {
+            context.Events.FirstOrDefault(e => e.Title == eventTitle).ShouldNotBe(null);
+        });
+    }
+
+    [Fact]
+    public async Task Should_Not_Create_Events_In_The_Past()
+    {
+        //Arrange
+        var eventTitle = Guid.NewGuid().ToString();
+
+        //Act
+        await Assert.ThrowsAsync<UserFriendlyException>(async () =>
+        {
+            await _eventAppService.Create(new CreateEventInput
+            {
+                Title = eventTitle,
+                Description = "A description",
+                Date = Clock.Now.AddDays(-1)
+            });
+        });
+    }
+
+    private Event GetTestEvent()
+    {
+        return UsingDbContext(context => GetTestEvent(context));
+    }
+
+    private static Event GetTestEvent(EventCloudDbContext context)
+    {
+        return context.Events.Single(e => e.Title == TestDataBuilder.TestEventTitle);
+    }
+}
+ +

We use xUnit as test framework. In the first test, we simply create an event and check database if it's in there. In the second test, we intentionally trying to create an event in the past. Since our business rule don't allow it, we should get an exception here.

+ +

With such tests, we tested everything starting from application service including all aspects of ASP.NET Boilerplate (like validation, unit of work and so on). See my Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate article for details on unit testing.

+ +

Token Based Authentication

+ +

Startup template uses cookie based authentication for browsers. However, if you want to consume Web APIs or application services (those are exposed via dynamic web api) from a mobile application, you probably want a token based authentication mechanism. Startup template includes bearer token authentication infrastructure. AccountController in *.WebApi project contains Authenticate action to get the token. Then we can use the token for next requests.

+ +

We will use +Postman (chrome extension) to demonstrate requests and responses.

+ +

Authentication

+ +

Just send a POST request to http://localhost:6334/api/Account/Authenticate with Context-Type="application/json" header as shown below:

+ +

Token based auth

+ +

We sent a JSON request body includes tenancyName, userNameOrEmailAddress and password. tenancyName is not required for host users. As seen above, result property of returning JSON contains the token. We can save it and use for next requests.

+ +

Use API

+ +

After authenticate and get the token, we can use it to call any authorized action. All application services are available to be used remotely. For example, we can use the EventAppService to get a list of events:

+ +

Use application service via token

+ +

Just made a POST request to http://localhost:6334/api/services/app/event/GetList with Content-Type="application/json" and Authorization="Bearer your-auth-token". Request body was just empty {}. Surely, request and response body will be different for different APIs.

+ +

Almost all operations available on UI also available as Web API (since UI uses the same Web API) and can be consumed easily.

+ +

Source Code

+ +

You can get the latest source code from https://github.com/aspnetboilerplate/eventcloud/tree/master/mvc-angularjs/src

+ +

Summary

+ +

In this article, I introduced a Multi Tenant (SaaS) application built on ASP.NET Boilerplate (ABP) framework. Use the following links for more information on ASP.NET Boilerplate:

+ + + +

Article History

+ +
    +
  • 2018-02-18
      +
    • Upgraded to ABP v3.4.0.
    • +
    • Updated some screenshorts and text in the article.
    • +
    +
  • +
  • 2017-06-28 +
      +
    • Upgraded to ABP v2.1.3.
    • +
    • Updated project creation section.
    • +
    +
  • +
  • 2016-07-19 +
      +
    • Renewed images and revised content.
    • +
    • Added statistics to about page.
    • +
    • Upgraded Abp.* nuget packages to v0.10.
    • +
    +
  • +
  • 2016-01-08 +
      +
    • Added 'unit and integration tests' section.
    • +
    • Upgraded Abp.* nuget packages to v0.7.7.1.
    • +
    +
  • +
  • 2015-12-04 +
      +
    • Added 'social media login' and 'token based authentication' sections.
    • +
    • Localized UI.
    • +
    • Upgraded to .NET framework 4.5.2.
    • +
    • Updated Abp.* nuget packages to v0.7.5.
    • +
    +
  • +
  • 2015-10-26 +
      +
    • First publish of the article.
    • +
    +
  • +
diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-layout.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-layout.png" new file mode 100644 index 0000000..6abbeac Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-layout.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-login-page.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-login-page.png" new file mode 100644 index 0000000..e9caaef Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/initial-login-page.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page-v2.jpg" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page-v2.jpg" new file mode 100644 index 0000000..5087161 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page-v2.jpg" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page.jpg" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page.jpg" new file mode 100644 index 0000000..47742c7 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/login-page.jpg" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/pmc-update-database.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/pmc-update-database.png" new file mode 100644 index 0000000..d285248 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/pmc-update-database.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/solution-structure.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/solution-structure.png" new file mode 100644 index 0000000..9490b43 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/solution-structure.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-api-call.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-api-call.png" new file mode 100644 index 0000000..5950783 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-api-call.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-auth.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-auth.png" new file mode 100644 index 0000000..55bdcf4 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/token-auth.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Created-Database.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Created-Database.png" new file mode 100644 index 0000000..747c7c8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Created-Database.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update-2.png" new file mode 100644 index 0000000..f914427 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update.png" new file mode 100644 index 0000000..78b3237 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Database-Update.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration-2.png" new file mode 100644 index 0000000..3d399f9 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration.png" new file mode 100644 index 0000000..a3a297c Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EF-Core-Initial-Migration.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations-2.png" new file mode 100644 index 0000000..270a37b Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations.png" new file mode 100644 index 0000000..9fb4a58 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/EntityFrameworkCore-Project-Migrations.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-2.png" new file mode 100644 index 0000000..81ebe3c Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-3.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-3.png" new file mode 100644 index 0000000..6811a97 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation-3.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation.png" new file mode 100644 index 0000000..1029aaf Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Creation.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Projects.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Projects.png" new file mode 100644 index 0000000..90deaf0 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/Template-Projects.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/apptasks-table.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/apptasks-table.png" new file mode 100644 index 0000000..8c539cd Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/apptasks-table.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html" new file mode 100644 index 0000000..5abadd0 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html" @@ -0,0 +1,778 @@ + + + + + + +

Introduction

+ +

This is first part of the "Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application" article series. See other parts:

+ + + +

In this article, I'll show to create a simple cross platform layered web application using the following tools:

+ + + +

I will also use Log4Net and AutoMapper which are included in ABP startup template by default. We will use the following techniques:

+ + + +

The project will be developed here is a simple task management application where tasks can be assigned to people. Instead of developing the application layer by layer, I will go to vertical and change the layers as the application grows. While the application grows, I will introduce some features of ABP and other frameworks as needed.

+ +

Prerequirements

+ +

Following tools should be installed in your machine to be able to run/develop the sample application:

+ + + +

Create the Application

+ +

I used ABP's startup template (http://www.aspnetboilerplate.com/Templates) to create a new web application named "Acme.SimpleTaskApp". Company name ("Acme" here) is optional while creating templates. I also selected Multi Page Web Application since I don't want to use SPA in this article and I disabled Authentication since I want the most basic startup template:

+ +

Template creation aspnetboilerplate

+ +

It creates a layered solution as shown below: 

+ +

Startup template projects

+ +

It includes 6 projects starting with the name that I entered as the project name:

+ +
    +
  • .Core project is for domain/business layer (entities, domain services...)
  • +
  • .Application project is for application layer (DTOs, application services...)
  • +
  • .EntityFramework project is for EF Core integration (abstracts EF Core from other layers).
  • +
  • .Web project is for ASP.NET MVC layer.
  • +
  • .Tests project is for unit and integration tests (up to application layer, excluding web layer)
  • +
  • .Web.Tests project is for ASP.NET Core integrated tests (complete integration test including the web layer).
  • +
+ +

When you run the application, you can see the user interface of the template:

+ +

Template Home Page

+ +

It contains a top menu, empty Home and About pages and a language switch dropdown.

+ +

Developing the Application

+ +

Creating a Task Entity

+ +

I want to start with a simple Task entity. Since an entity is part of the domain layer, I added it into the .Core project:

+ +
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Abp.Domain.Entities;
+using Abp.Domain.Entities.Auditing;
+using Abp.Timing;
+
+namespace Acme.SimpleTaskApp.Tasks
+{
+    [Table("AppTasks")]
+    public class Task : Entity, IHasCreationTime
+    {
+        public const int MaxTitleLength = 256;
+        public const int MaxDescriptionLength = 64 * 1024; //64KB
+
+        [Required]
+        [StringLength(MaxTitleLength)]
+        public string Title { get; set; }
+
+        [StringLength(MaxDescriptionLength)]
+        public string Description { get; set; }
+
+        public DateTime CreationTime { get; set; }
+
+        public TaskState State { get; set; }
+
+        public Task()
+        {
+            CreationTime = Clock.Now;
+            State = TaskState.Open;
+        }
+
+        public Task(string title, string description = null)
+            : this()
+        {
+            Title = title;
+            Description = description;
+        }
+    }
+
+    public enum TaskState : byte
+    {
+        Open = 0,
+        Completed = 1
+    }
+}
+ +
    +
  • I derived from ABP's base Entity class, which includes Id property as int by default. We can use the generic version, Entity<TPrimaryKey>, to choice a different PK type.
  • +
  • IHasCreationTime is a simple interface just defines CreationTime property (it's good to use a standard name for CreationTime).
  • +
  • Task entity defines a required Title and an optional Description.
  • +
  • TaskState is a simple enum to define states of a Task.
  • +
  • Clock.Now returns DateTime.Now by default. But it provides an abstraction, so we can easily switch to DateTime.UtcNow in the feature if it's needed. Always use Clock.Now instead of DateTime.Now while working with ABP framework.
  • +
  • I wanted to store Task entities into AppTasks table in the database.
  • +
+ +

Adding Task to DbContext

+ +

.EntityFrameworkCore project includes a pre-defined DbContext. I should add a DbSet for the Task entity into the DbContext:

+ +
+public class SimpleTaskAppDbContext : AbpDbContext
+{
+    public DbSet<Task> Tasks { get; set; }
+
+    public SimpleTaskAppDbContext(DbContextOptions<SimpleTaskAppDbContext> options) 
+        : base(options)
+    {
+
+    }
+}
+ +

Now, EF Core knows that we have a Task entity. 

+ +

Creating the First Database Migration 

+ +

We will create an initial database migration to create database and the AppTasks table. I open the Package Manager Console from Visual Studio and run the Add-Migration command (Default project must be the .EntityFrameworkCore project):

+ +

Entity Framework Core Add Migration

+ +

This command creates a Migrations folder in the .EntityFrameworkCore project which includes a migration class and a snapshot of our database model:

+ +

EF Core initial migration

+ +

Automatically generated "Initial" migration class is shown below:

+ +
+public partial class Initial : Migration
+{
+    protected override void Up(MigrationBuilder migrationBuilder)
+    {
+        migrationBuilder.CreateTable(
+            name: "AppTasks",
+            columns: table => new
+            {
+                Id = table.Column<int>(nullable: false)
+                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
+                CreationTime = table.Column<DateTime>(nullable: false),
+                Description = table.Column<string>(maxLength: 65536, nullable: true),
+                State = table.Column<byte>(nullable: false),
+                Title = table.Column<string>(maxLength: 256, nullable: false)
+            },
+            constraints: table =>
+            {
+                table.PrimaryKey("PK_AppTasks", x => x.Id);
+            });
+    }
+
+    protected override void Down(MigrationBuilder migrationBuilder)
+    {
+        migrationBuilder.DropTable(
+            name: "AppTasks");
+    }
+}
+ +

This code is used to create AppTasks table when we execute the migrations to the database (see entity framework documentation for more information on migrations).

+ +

Creating the Database

+ +

To create the database, I run Update-Database command from Package Manager Console:

+ +

EF Update-Database command

+ +

This command created a database named SimpleTaskAppDb in the local SQL Server and executed migrations (currently, there is a single, "Initial", migration):

+ +

Created Database

+ +

Now, I have a Task entity and corresponding table in the database.  I entered a few sample Tasks to the table:

+ +

AppTasks table

+ +

Note that, the database connection string is defined in appsettings.json in the .Web application.

+ +

Task Application Service

+ +

Application Services are used to expose domain logic to the presentation layer. An Application Service is called from presentation layer with a Data Transfer Object (DTO) as parameter (if needed), uses domain objects to perform some specific business logic and returns a DTO back to the presentation layer (if needed).

+ +

I'm creating the first application service, TaskAppService, into the .Application project to perform task related application logic. First, I wanted to define an interface for the app service:

+ +
+public interface ITaskAppService : IApplicationService
+{
+    Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input);
+}
+ +

Defining an interface is not required, but suggested. By convention, all app services should implement IApplicationService interface in ABP (it's just an empty marker interface). I created a GetAll method to query tasks. To do that, I also defined the following DTOs:

+ +
+public class GetAllTasksInput
+{
+    public TaskState? State { get; set; }
+}
+
+[AutoMapFrom(typeof(Task))]
+public class TaskListDto : EntityDto, IHasCreationTime
+{
+    public string Title { get; set; }
+
+    public string Description { get; set; }
+
+    public DateTime CreationTime { get; set; }
+
+    public TaskState State { get; set; }
+}
+ +
    +
  • GetAllTasksInput DTO defines input parameters of the GetAll app service method. Instead of directly defining the state as method parameter, I added it into a DTO object. Thus, I can add other parameters into this DTO later without breaking my existing clients (we could directly add a state parameter to the method).
  • +
  • TaskListDto is used to return a Task data. It's derived from EntityDto, which just defines an Id property (we could add Id to our Dto and not derive from EntityDto). We defined [AutoMapFrom] attribute to create AutoMapper mapping from Task entity to TaskListDto. This attribute is defined in Abp.AutoMapper nuget package.
  • +
  • Lastly, ListResultDto is a simple class contains a list of items (we could directly return a List<TaskListDto>).
  • +
+ +

Now, we can implement the ITaskAppService as shown below:

+ +
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Application.Services.Dto;
+using Abp.Domain.Repositories;
+using Abp.Linq.Extensions;
+using Acme.SimpleTaskApp.Tasks.Dtos;
+using Microsoft.EntityFrameworkCore;
+
+namespace Acme.SimpleTaskApp.Tasks
+{
+    public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
+    {
+        private readonly IRepository<Task> _taskRepository;
+
+        public TaskAppService(IRepository<Task> taskRepository)
+        {
+            _taskRepository = taskRepository;
+        }
+
+        public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input)
+        {
+            var tasks = await _taskRepository
+                .GetAll()
+                .WhereIf(input.State.HasValue, t => t.State == input.State.Value)
+                .OrderByDescending(t => t.CreationTime)
+                .ToListAsync();
+
+            return new ListResultDto<TaskListDto>(
+                ObjectMapper.Map<List<TaskListDto>>(tasks)
+            );
+        }
+    }
+}
+ +
    +
  • TaskAppService is derived from SimpleTaskAppAppServiceBase included in the startup template (which is derived from ABP's ApplicationService class). This is not required, app services can be plain classes. But ApplicationService base class has some pre-injected services (like ObjectMapper used here).
  • +
  • I used dependency injection to get a repository.
  • +
  • Repositories are used to abstract database operations for entities. ABP creates a pre-defined repository (like IRepository<Task> here) for each entity to perform common tasks. IRepository.GetAll() used here returns an IQueryable to query entities.
  • +
  • WhereIf is an extension method of ABP to simplify conditional usage of IQueryable.Where method.
  • +
  • ObjectMapper (which somes from the ApplicationService base class and implemented via AutoMapper by default) is used to map list of Task objects to list of TaskListDtos objects.
  • +
+ +

Testing the TaskAppService

+ +

Before going further to create user interface, I want to test TaskAppService. You can skip this section if you don't interest in automated testing.

+ +

Startup template contains .Tests project to test our code. It uses EF Core's InMemory database provider instead of SQL Server. Thus, our unit tests can work without a real database. It creates a separated database for each test. Thus, tests are isolated from each other. We can use TestDataBuilder class to add some initial test data to InMemory database before running tests. I changed TestDataBuilder as shown below:

+ +
+public class TestDataBuilder
+{
+    private readonly SimpleTaskAppDbContext _context;
+
+    public TestDataBuilder(SimpleTaskAppDbContext context)
+    {
+        _context = context;
+    }
+
+    public void Build()
+    {
+        _context.Tasks.AddRange(
+            new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality."),
+            new Task("Clean your room") { State = TaskState.Completed }
+            );
+    }
+}
+ +

You can see the sample project's source code to understand where and how TestDataBuilder is used. I added two tasks (one of them is completed) to the dbcontext. So, I can write my tests assuming that there are two Tasks in the database. My first integration test tests the TaskAppService.GetAll method we created above.

+ +
+public class TaskAppService_Tests : SimpleTaskAppTestBase
+{
+    private readonly ITaskAppService _taskAppService;
+
+    public TaskAppService_Tests()
+    {
+        _taskAppService = Resolve<ITaskAppService>();
+    }
+
+    [Fact]
+    public async System.Threading.Tasks.Task Should_Get_All_Tasks()
+    {
+        //Act
+        var output = await _taskAppService.GetAll(new GetAllTasksInput());
+
+        //Assert
+        output.Items.Count.ShouldBe(2);
+    }
+
+    [Fact]
+    public async System.Threading.Tasks.Task Should_Get_Filtered_Tasks()
+    {
+        //Act
+        var output = await _taskAppService.GetAll(new GetAllTasksInput { State = TaskState.Open });
+
+        //Assert
+        output.Items.ShouldAllBe(t => t.State == TaskState.Open);
+    }
+}
+ +

I created two different tests to test GetAll method as shown above. Now, I can open Test Explorer (from Test\Windows\Test Explorer in the main menu of VS) and run the unit tests:

+ +

Test explorer

+ +

All of them succeed. The last one was a pre-built test in the startup template, which we can ignore for now.

+ +

Notice that; ABP startup template comes with xUnit and Shouldly installed by default. So, we used them to write our tests.

+ +

Task List View

+ +

Now, I know that TaskAppService is properly working. I can start to create a page to list all tasks.

+ +

Adding a New Menu Item

+ +

 First, I'm adding a new item to the top menu:

+ +
+public class SimpleTaskAppNavigationProvider : NavigationProvider
+{
+    public override void SetNavigation(INavigationProviderContext context)
+    {
+        context.Manager.MainMenu
+            .AddItem(
+                new MenuItemDefinition(
+                    "Home",
+                    L("HomePage"),
+                    url: "",
+                    icon: "fa fa-home"
+                    )
+            ).AddItem(
+                new MenuItemDefinition(
+                    "About",
+                    L("About"),
+                    url: "Home/About",
+                    icon: "fa fa-info"
+                    )
+            ).AddItem(
+                new MenuItemDefinition(
+                    "TaskList",
+                    L("TaskList"),
+                    url: "Tasks",
+                    icon: "fa fa-tasks"
+                    )
+            );
+    }
+
+    private static ILocalizableString L(string name)
+    {
+        return new LocalizableString(name, SimpleTaskAppConsts.LocalizationSourceName);
+    }
+}
+ +

Startup template comes with two pages: Home and About, as shown above. We can change them or create new pages. I preferred to leave them for now and create a new menu item.

+ +

Creating the TaskController and ViewModel

+ +

I'm creating a new controller class, TasksController, in the .Web project as shown below:

+ +
+public class TasksController : SimpleTaskAppControllerBase
+{
+    private readonly ITaskAppService _taskAppService;
+
+    public TasksController(ITaskAppService taskAppService)
+    {
+        _taskAppService = taskAppService;
+    }
+
+    public async Task<ActionResult> Index(GetAllTasksInput input)
+    {
+        var output = await _taskAppService.GetAll(input);
+        var model = new IndexViewModel(output.Items);
+        return View(model);
+    }
+}
+ +
    +
  • I derived from SimpleTaskAppControllerBase (which is derived from AbpController) that contains common base code for Controllers in this application.
  • +
  • I injected ITaskAppService in order to get list of tasks.
  • +
  • Instead of directly passing result of the GetAll method to the view, I created an IndexViewModel class in the .Web project which is shown below:
  • +
+ +
+public class IndexViewModel
+{
+    public IReadOnlyList<TaskListDto> Tasks { get; }
+
+    public IndexViewModel(IReadOnlyList<TaskListDto> tasks)
+    {
+        Tasks = tasks;
+    }
+
+    public string GetTaskLabel(TaskListDto task)
+    {
+        switch (task.State)
+        {
+            case TaskState.Open:
+                return "label-success";
+            default:
+                return "label-default";
+        }
+    }
+}
+ +

This simple view model gets a list of tasks (which is provided from ITaskAppService) in it's constructor. It also has GetTaskLabel method that will be used in the view to select a Bootstrap label class for given task.

+ +

Task List Page

+ +

And finally, the Index view is shown below:

+ +
+@model Acme.SimpleTaskApp.Web.Models.Tasks.IndexViewModel
+
+@{
+    ViewBag.Title = L("TaskList");
+    ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item
+}
+
+<h2>@L("TaskList")</h2>
+
+<div class="row">
+    <div>
+        <ul class="list-group" id="TaskList">
+            @foreach (var task in Model.Tasks)
+            {
+                <li class="list-group-item">
+                    <span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span>
+                    <h4 class="list-group-item-heading">@task.Title</h4>
+                    <div class="list-group-item-text">
+                        @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
+                    </div>
+                </li>
+            }
+        </ul>
+    </div>
+</div>
+ +

We simply used given model to render the view using Bootstrap's list group component. Here, we used IndexViewModel.GetTaskLabel() method to get label types for tasks. Rendered page will be like that:

+ +

Task list

+ +

Localization

+ +

We used L method in the view which comes from ABP framework. It's used to localize strings. We have define localized strings in Localization/Source folder in the .Core project as .json files. English localization is shown below:

+ +
+{
+  "culture": "en",
+  "texts": {
+    "HelloWorld": "Hello World!",
+    "ChangeLanguage": "Change language",
+    "HomePage": "HomePage",
+    "About": "About",
+    "Home_Description": "Welcome to SimpleTaskApp...",
+    "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.",
+    "TaskList": "Task List",
+    "TaskState_Open": "Open",
+    "TaskState_Completed": "Completed"
+  }
+}
+ +

Most of the texts are coming from startup template and can be deleted. I just added the last 3 lines and used in the view above. While using ABP's localization is pretty simple, you can see localization document for more information on the localization system.

+ +

Filtering Tasks

+ +

As shown above, TaskController actually gets a GetAllTasksInput that can be used to filter tasks. So, we can add a dropdown to task list view to filter tasks. First, I added the dropdown to the view (I added inside the header):

+ +
+<h2>
+    @L("TaskList")
+    <span class="pull-right">
+        @Html.DropDownListFor(
+           model => model.SelectedTaskState,
+           Model.GetTasksStateSelectListItems(LocalizationManager),
+           new
+           {
+               @class = "form-control",
+               id = "TaskStateCombobox"
+           })
+    </span>
+</h2>
+ +

Then I changed IndexViewModel to add SelectedTaskState property and GetTasksStateSelectListItems method:

+ +
+public class IndexViewModel
+{
+    //...
+
+    public TaskState? SelectedTaskState { get; set; }
+
+    public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager)
+    {
+        var list = new List<SelectListItem>
+        {
+            new SelectListItem
+            {
+                Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, "AllTasks"),
+                Value = "",
+                Selected = SelectedTaskState == null
+            }
+        };
+
+        list.AddRange(Enum.GetValues(typeof(TaskState))
+                .Cast<TaskState>()
+                .Select(state =>
+                    new SelectListItem
+                    {
+                        Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, $"TaskState_{state}"),
+                        Value = state.ToString(),
+                        Selected = state == SelectedTaskState
+                    })
+        );
+
+        return list;
+    }
+}
+ +

We should set SelectedTaskState in the controller:

+ +
+public async Task<ActionResult> Index(GetAllTasksInput input)
+{
+    var output = await _taskAppService.GetAll(input);
+    var model = new IndexViewModel(output.Items)
+    {
+        SelectedTaskState = input.State
+    };
+    return View(model);
+}
+ +

Now, we can run the application to see the combobox at the top right of the view:

+ +

Task list

+ +

I added the combobox but it can not work yet. I'll write a simple JavaScript code to re-request/refresh task list page when combobox value changes. So, I'm creating wwwroot\js\views\tasks\index.js file in the .Web project:

+ +
+(function ($) {
+    $(function () {
+
+        var _$taskStateCombobox = $('#TaskStateCombobox');
+
+        _$taskStateCombobox.change(function() {
+            location.href = '/Tasks?state=' + _$taskStateCombobox.val();
+        });
+
+    });
+})(jQuery);
+ +

Before including this JavaScript file into my view, I used Bundler & Minifier VS extension (which is default way of minifying files in ASP.NET Core projects) to minify the script:

+ +

Minify js

+ +

This adds the following lines into bundleconfig.json file in the .Web project:

+ +
+{
+  "outputFileName": "wwwroot/js/views/tasks/index.min.js",
+  "inputFiles": [
+    "wwwroot/js/views/tasks/index.js"
+  ]
+}
+ +

And creates a minified version of the script:

+ +

Minified js file

+ +

Whenever I change the index.js, index.min.js is automatically re-generated. Now, I can include the JavaScript file into my page:

+ +
+@section scripts
+{
+    <environment names="Development">
+        <script src="~/js/views/tasks/index.js"></script>
+    </environment>
+
+    <environment names="Staging,Production">
+        <script src="~/js/views/tasks/index.min.js"></script>
+    </environment>
+}
+ +

With this code, our view will use index.js in development and index.min.js (minified version) in production. This is a common approach in ASP.NET Core MVC projects.

+ +

Automated Testing Task List Page

+ +

We can create integration tests that is also integrated to ASP.NET Core MVC infrastructure. Thus, we can completely test our server side code. You can skip this section if you don't interest in automated testing.

+ +

ABP startup template includes a .Web.Tests project to do that. I created a simple test to request to TaskController.Index and check the response:

+ +
+public class TasksController_Tests : SimpleTaskAppWebTestBase
+{
+    [Fact]
+    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()
+    {
+        //Act
+
+        var response = await GetResponseAsStringAsync(
+            GetUrl<TasksController>(nameof(TasksController.Index), new
+                {
+                    state = TaskState.Open
+                }
+            )
+        );
+
+        //Assert
+
+        response.ShouldNotBeNullOrWhiteSpace();
+    }
+}
+ +

GetResponseAsStringAsync and GetUrl methods are some helper methods provided by AbpAspNetCoreIntegratedTestBase class of ABP. We can instead directly use the Client (an instance of HttpClient) property to make requests. But using these shortcut methods makes it easier. See integration testing documentation of ASP.NET Core for more.

+ +

When I debug the test, I can see the response HTML:

+ +

Web test

+ +

That shows the Index page returned a response without any exception. But... we may want to go more and check if returned HTML is what we expect. There are some libraries can be used to parse HTML. AngleSharp is one of them and comes as pre-installed in ABP startup template's .Web.Tests project. So, I used it to check the created HTML code:

+ +
+public class TasksController_Tests : SimpleTaskAppWebTestBase
+{
+    [Fact]
+    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()
+    {
+        //Act
+
+        var response = await GetResponseAsStringAsync(
+            GetUrl<TasksController>(nameof(TasksController.Index), new
+                {
+                    state = TaskState.Open
+                }
+            )
+        );
+
+        //Assert
+
+        response.ShouldNotBeNullOrWhiteSpace();
+
+        //Get tasks from database
+        var tasksInDatabase = await UsingDbContextAsync(async dbContext =>
+        {
+            return await dbContext.Tasks
+                .Where(t => t.State == TaskState.Open)
+                .ToListAsync();
+        });
+
+        //Parse HTML response to check if tasks in the database are returned
+        var document = new HtmlParser().Parse(response);
+        var listItems = document.QuerySelectorAll("#TaskList li");
+            
+        //Check task count
+        listItems.Length.ShouldBe(tasksInDatabase.Count);
+
+        //Check if returned list items are same those in the database
+        foreach (var listItem in listItems)
+        {
+            var header = listItem.QuerySelector(".list-group-item-heading");
+            var taskTitle = header.InnerHtml.Trim();
+            tasksInDatabase.Any(t => t.Title == taskTitle).ShouldBeTrue();
+        }
+    }
+}
+ +

You can check the HTML deeper and in more detailed. But in most cases, checking the fundamental tags will be enough.

+ +

Part 2

+ +

See Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application - Part II

+ +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem-Core

+ +

Article History

+ +
    +
  • 2018-02-14: Upgraded source code to ABP v3.4. Updated screenshots and article.
  • +
  • 2017-07-30: Replaced ListResultOutput by ListResultDto in the article.
  • +
  • 2017-06-02: Changed article and solution to support .net core.
  • +
  • 2016-08-08: Added link to the second article.
  • +
  • 2016-08-01: Initial publication.
  • +
diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minified-js.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minified-js.png" new file mode 100644 index 0000000..b88239f Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minified-js.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minify-js.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minify-js.png" new file mode 100644 index 0000000..30a3f0c Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/minify-js.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-1.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-1.png" new file mode 100644 index 0000000..466b0b0 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-1.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-2.png" new file mode 100644 index 0000000..7465cd8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/task-list-view-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/template-default-home.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/template-default-home.png" new file mode 100644 index 0000000..e9ced65 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/template-default-home.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/test-explorer.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/test-explorer.png" new file mode 100644 index 0000000..9c325e5 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/test-explorer.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/web-test-1.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/web-test-1.png" new file mode 100644 index 0000000..aee1f40 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/web-test-1.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/create-task-page-view.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/create-task-page-view.png" new file mode 100644 index 0000000..c3d947d Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/create-task-page-view.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/index.html" new file mode 100644 index 0000000..cac304b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/index.html" @@ -0,0 +1,642 @@ + + + + +

Contents

+ + + +

Introduction

+ +

This is the second part of the "Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application" article series. See other parts:

+ + + +

Developing the Application

+ +

Creating the Person Entity

+ +

I'll add Person concept to the application to assign tasks to people. So, I define a simple Person entity:

+ +
+[Table("AppPersons")]
+public class Person : AuditedEntity<Guid>
+{
+    public const int MaxNameLength = 32;
+
+    [Required]
+    [StringLength(MaxNameLength)]
+    public string Name { get; set; }
+
+    public Person()
+    {
+            
+    }
+
+    public Person(string name)
+    {
+        Name = name;
+    }
+}
+ +

This time, I set Id (primary key)  type as Guid, for demonstration. I also derived from AuditedEntity (which has CreationTime, CreaterUserId, LastModificationTime and LastModifierUserId properties) instead of base Entity class.

+ +

Relating Person to the Task Entity

+ +

I'm also adding AssignedPerson property to the Task entity (only sharing the changed parts here):

+ +
+[Table("AppTasks")]
+public class Task : Entity, IHasCreationTime
+{
+    //...
+
+    [ForeignKey(nameof(AssignedPersonId))]
+    public Person AssignedPerson { get; set; }
+    public Guid? AssignedPersonId { get; set; }
+
+    public Task(string title, string description = null, Guid? assignedPersonId = null)
+        : this()
+    {
+        Title = title;
+        Description = description;
+        AssignedPersonId = assignedPersonId;
+    }
+}
+ +

AssignedPerson is optional. So, as task can be assigned to a person or can be unassigned.

+ +

Adding Person to DbContext

+ +

Finally, I'm adding new Person entity to the DbContext class:

+ +
+public class SimpleTaskAppDbContext : AbpDbContext
+{
+    public DbSet<Person> People { get; set; }
+    
+    //...
+}
+ +

Adding a New Migration for Person Entity

+ +

Now, I'm running the following command in the Package Manager Console:

+ +

Add new migration

+ +

And it creates a new migration class in the project:

+ +
+public partial class Added_Person : Migration
+{
+    protected override void Up(MigrationBuilder migrationBuilder)
+    {
+        migrationBuilder.CreateTable(
+            name: "AppPersons",
+            columns: table => new
+            {
+                Id = table.Column<Guid>(nullable: false),
+                CreationTime = table.Column<DateTime>(nullable: false),
+                CreatorUserId = table.Column<long>(nullable: true),
+                LastModificationTime = table.Column<DateTime>(nullable: true),
+                LastModifierUserId = table.Column<long>(nullable: true),
+                Name = table.Column<string>(maxLength: 32, nullable: false)
+            },
+            constraints: table =>
+            {
+                table.PrimaryKey("PK_AppPersons", x => x.Id);
+            });
+
+        migrationBuilder.AddColumn<Guid>(
+            name: "AssignedPersonId",
+            table: "AppTasks",
+            nullable: true);
+
+        migrationBuilder.CreateIndex(
+            name: "IX_AppTasks_AssignedPersonId",
+            table: "AppTasks",
+            column: "AssignedPersonId");
+
+        migrationBuilder.AddForeignKey(
+            name: "FK_AppTasks_AppPersons_AssignedPersonId",
+            table: "AppTasks",
+            column: "AssignedPersonId",
+            principalTable: "AppPersons",
+            principalColumn: "Id",
+            onDelete: ReferentialAction.SetNull);
+    }
+
+    //...
+}
+ +

I just changed ReferentialAction.Restrict to ReferentialAction.SetNull. It does that: if I delete a person, assigned tasks to that person become unassigned. This is not important in this demo. But I wanted to show that you can change the migration code if you need. Actually, you always review the generated code before applying it to the database. After that, we can apply migration to our database:

+ +

Update-Database

+ +

When we open the database, we can see the new table and columns and add some test data:

+ +

Person table

+ +

I added a person and assigned to the first task:

+ +

Tasks table

+ +

Return Assigned Person in the Task List

+ +

I'll change the TaskAppService to return assigned person information. First, I'm adding two properties to TaskListDto:

+ +
+[AutoMapFrom(typeof(Task))]
+public class TaskListDto : EntityDto, IHasCreationTime
+{
+    //...
+
+    public Guid? AssignedPersonId { get; set; }
+
+    public string AssignedPersonName { get; set; }
+}
+ +

And including the Task.AssignedPerson property to the query. Just added the Include line:

+ +
+public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
+{
+    //...
+
+    public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input)
+    {
+        var tasks = await _taskRepository
+            .GetAll()
+            .Include(t => t.AssignedPerson)
+            .WhereIf(input.State.HasValue, t => t.State == input.State.Value)
+            .OrderByDescending(t => t.CreationTime)
+            .ToListAsync();
+
+        return new ListResultDto<TaskListDto>(
+            ObjectMapper.Map<List<TaskListDto>>(tasks)
+        );
+    }
+}
+ +

Thus, GetAll method will return Assigned person information with the tasks. Since we used AutoMapper, new properties will also be copied to DTO automatically.

+ +

Change Unit Test to Test Assigned Person

+ +

At this point, we can change unit tests to see if assigned people are retrieved while getting the task list. First, I changed initial test data in the TestDataBuilder class to assign a person to a task:

+ +
+public class TestDataBuilder
+{
+    //...
+
+    public void Build()
+    {
+        var neo = new Person("Neo");
+        _context.People.Add(neo);
+        _context.SaveChanges();
+
+        _context.Tasks.AddRange(
+            new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality.", neo.Id),
+            new Task("Clean your room") { State = TaskState.Completed }
+            );
+    }
+}
+ +

Then I'm changing TaskAppService_Tests.Should_Get_All_Tasks() method to check if one of the retrieved tasks has a person assigned (see the last line added):

+ +
+[Fact]
+public async System.Threading.Tasks.Task Should_Get_All_Tasks()
+{
+    //Act
+    var output = await _taskAppService.GetAll(new GetAllTasksInput());
+
+    //Assert
+    output.Items.Count.ShouldBe(2);
+    output.Items.Count(t => t.AssignedPersonName != null).ShouldBe(1);
+}
+ +

Note: Count extension method requires using System.Linq; statement.

+ +

Show Assigned Person Name in the Task List Page

+ +

Finally, we can change Tasks\Index.cshtml to show AssignedPersonName:

+ +
+@foreach (var task in Model.Tasks)
+{
+    <li class="list-group-item">
+        <span class="pull-right label label-lg @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span>
+        <h4 class="list-group-item-heading">@task.Title</h4>
+        <div class="list-group-item-text">
+            @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") | @(task.AssignedPersonName ?? L("Unassigned"))
+        </div>
+    </li>
+}
+ +

When we run the application, we can see it in the task list:

+ +

Task list with person name

+ +

New Application Service Method for Task Creation

+ +

We can list tasks, but we don't have a task creation page yet. First, adding a Create method to the ITaskAppService interface:

+ +
+public interface ITaskAppService : IApplicationService
+{
+    //...
+
+    System.Threading.Tasks.Task Create(CreateTaskInput input);
+}
+ +

And implementing it in TaskAppService class:

+ +
+public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
+{
+    private readonly IRepository<Task> _taskRepository;
+
+    public TaskAppService(IRepository<Task> taskRepository)
+    {
+        _taskRepository = taskRepository;
+    }
+
+    //...
+
+    public async System.Threading.Tasks.Task Create(CreateTaskInput input)
+    {
+        var task = ObjectMapper.Map<Task>(input);
+        await _taskRepository.InsertAsync(task);
+    }
+}
+ +

Create method automatically maps given input to a Task entity and inserting to the database using the repository. CreateTaskInput DTO is like that:

+ +
+using System;
+using System.ComponentModel.DataAnnotations;
+using Abp.AutoMapper;
+
+namespace Acme.SimpleTaskApp.Tasks.Dtos
+{
+    [AutoMapTo(typeof(Task))]
+    public class CreateTaskInput
+    {
+        [Required]
+        [StringLength(Task.MaxTitleLength)]
+        public string Title { get; set; }
+
+        [StringLength(Task.MaxDescriptionLength)]
+        public string Description { get; set; }
+
+        public Guid? AssignedPersonId { get; set; }
+    }
+}
+ +

Configured to map it to Task entity (using AutoMapTo attribute) and added data annotations to apply validation. We used constants from Task entity to use same max lengths.

+ +

Testing Task Creation Service

+ +

I'm adding some integration tests into TaskAppService_Tests class to test the Create method:

+ +
+using Acme.SimpleTaskApp.Tasks;
+using Acme.SimpleTaskApp.Tasks.Dtos;
+using Shouldly;
+using Xunit;
+using System.Linq;
+using Abp.Runtime.Validation;
+
+namespace Acme.SimpleTaskApp.Tests.Tasks
+{
+    public class TaskAppService_Tests : SimpleTaskAppTestBase
+    {
+        private readonly ITaskAppService _taskAppService;
+
+        public TaskAppService_Tests()
+        {
+            _taskAppService = Resolve<ITaskAppService>();
+        }
+
+        //...
+
+        [Fact]
+        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title()
+        {
+            await _taskAppService.Create(new CreateTaskInput
+            {
+                Title = "Newly created task #1"
+            });
+
+            UsingDbContext(context =>
+            {
+                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
+                task1.ShouldNotBeNull();
+            });
+        }
+
+        [Fact]
+        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title_And_Assigned_Person()
+        {
+            var neo = UsingDbContext(context => context.People.Single(p => p.Name == "Neo"));
+
+            await _taskAppService.Create(new CreateTaskInput
+            {
+                Title = "Newly created task #1",
+                AssignedPersonId = neo.Id
+            });
+
+            UsingDbContext(context =>
+            {
+                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
+                task1.ShouldNotBeNull();
+                task1.AssignedPersonId.ShouldBe(neo.Id);
+            });
+        }
+
+        [Fact]
+        public async System.Threading.Tasks.Task Should_Not_Create_New_Task_Without_Title()
+        {
+            await Assert.ThrowsAsync<AbpValidationException>(async () =>
+            {
+                await _taskAppService.Create(new CreateTaskInput
+                {
+                    Title = null
+                });
+            });
+        }
+    }
+}
+ +

First test creates a task with a title, second one creates a task with a title and assigned person, the last one tries to create an invalid task to show the exception case.

+ +

Task Creation Page

+ +

We know that TaskAppService.Create is properly working. Now, we can create a page to add a new task. Final page will be like that:

+ +

Create task page

+ +

First, I added a Create action to the TaskController in order to prepare the page above:

+ +
+using System.Threading.Tasks;
+using Abp.Application.Services.Dto;
+using Acme.SimpleTaskApp.Tasks;
+using Acme.SimpleTaskApp.Tasks.Dtos;
+using Acme.SimpleTaskApp.Web.Models.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using System.Linq;
+using Acme.SimpleTaskApp.Common;
+using Acme.SimpleTaskApp.Web.Models.People;
+
+namespace Acme.SimpleTaskApp.Web.Controllers
+{
+    public class TasksController : SimpleTaskAppControllerBase
+    {
+        private readonly ITaskAppService _taskAppService;
+        private readonly ILookupAppService _lookupAppService;
+
+        public TasksController(
+            ITaskAppService taskAppService,
+            ILookupAppService lookupAppService)
+        {
+            _taskAppService = taskAppService;
+            _lookupAppService = lookupAppService;
+        }
+
+        //...
+        
+        public async Task<ActionResult> Create()
+        {
+            var peopleSelectListItems = (await _lookupAppService.GetPeopleComboboxItems()).Items
+                .Select(p => p.ToSelectListItem())
+                .ToList();
+
+            peopleSelectListItems.Insert(0, new SelectListItem { Value = string.Empty, Text = L("Unassigned"), Selected = true });
+
+            return View(new CreateTaskViewModel(peopleSelectListItems));
+        }
+    }
+}
+ +

I injected ILookupAppService that is used to get people combobox items. While I could directly inject and use IRepository<Person, Guid> here, I preferred this to make a better layering and re-usability. ILookupAppService.GetPeopleComboboxItems is defined in application layer as shown below:

+ +
+public interface ILookupAppService : IApplicationService
+{
+    Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems();
+}
+
+public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService
+{
+    private readonly IRepository<Person, Guid> _personRepository;
+
+    public LookupAppService(IRepository<Person, Guid> personRepository)
+    {
+        _personRepository = personRepository;
+    }
+
+    public async Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems()
+    {
+        var people = await _personRepository.GetAllListAsync();
+        return new ListResultDto<ComboboxItemDto>(
+            people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList()
+        );
+    }
+}
+ +

ComboboxItemDto is a simple class (defined in ABP) to transfer a combobox item data. TaskController.Create method simply uses this method and converts the returned list to a list of SelectListItem (defined in AspNet Core) and passes to the view using CreateTaskViewModel class:

+ +
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace Acme.SimpleTaskApp.Web.Models.People
+{
+    public class CreateTaskViewModel
+    {
+        public List<SelectListItem> People { get; set; }
+
+        public CreateTaskViewModel(List<SelectListItem> people)
+        {
+            People = people;
+        }
+    }
+}
+ +

Create view is shown below:

+ +
+@using Acme.SimpleTaskApp.Web.Models.People
+@model CreateTaskViewModel
+
+@section scripts
+{
+    <environment names="Development">
+        <script src="~/js/views/tasks/create.js"></script>
+    </environment>
+
+    <environment names="Staging,Production">
+        <script src="~/js/views/tasks/create.min.js"></script>
+    </environment>
+}
+
+<h2>
+    @L("NewTask")
+</h2>
+
+<form id="TaskCreationForm">
+    
+    <div class="form-group">
+        <label for="Title">@L("Title")</label>
+        <input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxTitleLength">
+    </div>
+
+    <div class="form-group">
+        <label for="Description">@L("Description")</label>
+        <input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxDescriptionLength">
+    </div>
+
+    <div class="form-group">
+        @Html.Label(L("AssignedPerson"))
+        @Html.DropDownList(
+            "AssignedPersonId",
+            Model.People,
+            new
+            {
+                @class = "form-control",
+                id = "AssignedPersonCombobox"
+            })
+    </div>
+
+    <button type="submit" class="btn btn-default">@L("Save")</button>
+
+</form>
+ +

I included create.js defined like that:

+ +
+(function($) {
+    $(function() {
+
+        var _$form = $('#TaskCreationForm');
+
+        _$form.find('input:first').focus();
+
+        _$form.validate();
+
+        _$form.find('button[type=submit]')
+            .click(function(e) {
+                e.preventDefault();
+
+                if (!_$form.valid()) {
+                    return;
+                }
+
+                var input = _$form.serializeFormToObject();
+                abp.services.app.task.create(input)
+                    .done(function() {
+                        location.href = '/Tasks';
+                    });
+            });
+    });
+})(jQuery);
+ +

Let's see what's done in this JavaScript code:

+ +
    +
  • Prepares validatation for the form (using jquery validation plugin) and validates it on Save button's click.
  • +
  • Uses serializeFormToObject jquery plugin (defined in jquery-extensions.js in the solution) to convert form data to a JSON object (I included jquery-extensions.js to the _Layout.cshtml as the last script file).
  • +
  • Uses abp.services.task.create method to call TaskAppService.Create method. This is one of the important features of ABP. We can use application services from JavaScript code just like calling a JavaScript method in our code. See details.
  • +
+ +

+Here is the content of jquery-extensions.js: +

+
+(function ($) {
+    //serializeFormToObject plugin for jQuery
+    $.fn.serializeFormToObject = function () {
+        //serialize to array
+        var data = $(this).serializeArray();
+
+        //add also disabled items
+        $(':disabled[name]', this)
+            .each(function () {
+                data.push({ name: this.name, value: $(this).val() });
+            });
+
+        //map to object
+        var obj = {};
+        data.map(function (x) { obj[x.name] = x.value; });
+
+        return obj;
+    };
+})(jQuery);
+
+ +

Finally, I added an "Add Task" button to the task list page in order to navigate to the task creation page:

+ +
+<a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")</a>
+ +

Remove Home and About Page

+ +

We can remove Home and About page from the application if we don't need. To do that, first change HomeController like that:

+ +
+using Microsoft.AspNetCore.Mvc;
+
+namespace Acme.SimpleTaskApp.Web.Controllers
+{
+    public class HomeController : SimpleTaskAppControllerBase
+    {
+        public ActionResult Index()
+        {
+            return RedirectToAction("Index", "Tasks");
+        }
+    }
+}
+ +

Then delete Views/Home folder and remove menu items from SimpleTaskAppNavigationProvider class. You can also remove unnecessary keys from localization JSON files.

+ + +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem-Core

+ + +

Article History

+ +
    +
  • 2018-02-14: Upgraded source code to ABP v3.4 and updated the download link.
  • +
  • 2017-07-30: Replaced ListResultOutput by ListResultDto in the article.
  • +
  • 2017-06-02: Changed article and solution to support .net core.
  • +
  • 2016-08-09: Revised article based on feedbacks.
  • +
  • 2016-08-08: Initial publication.
  • +
\ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person-2.png" new file mode 100644 index 0000000..edd4cdc Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person.png" new file mode 100644 index 0000000..8ea673d Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-add-person.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update-2.png" new file mode 100644 index 0000000..0865379 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update.png" new file mode 100644 index 0000000..4b30b61 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/migration-person-update.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/person-table.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/person-table.png" new file mode 100644 index 0000000..6c0de01 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/person-table.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/task-list-assigned-person.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/task-list-assigned-person.png" new file mode 100644 index 0000000..e5be2e2 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/task-list-assigned-person.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/tasks-table-with-person.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/tasks-table-with-person.png" new file mode 100644 index 0000000..218e202 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-2/tasks-table-with-person.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template-2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template-2.png" new file mode 100644 index 0000000..b348c75 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template-2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template.png" new file mode 100644 index 0000000..f94a601 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/create-abp-template.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/index.html" new file mode 100644 index 0000000..a4fcc6b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/index.html" @@ -0,0 +1,651 @@ + + + + +

+ Simple Task System Screenshot +
+A screenshot of the sample application. +

+ +

Contents

+ + + +

Introduction

+ +

In this article, I'll show you how to develop a Single-Page Web Application (SPA) from ground to up using the following tools:

+ +
    +
  • ASP.NET MVC and ASP.NET Web API as web framework.
  • +
  • AngularJS as SPA framework.
  • +
  • EntityFramework as ORM (Object-Relational Mapping) framework
  • +
  • Castle Windsor as Dependency Injection framework.
  • +
  • Twitter Bootstrap as HTML/CSS framework.
  • +
  • Log4Net for logging, AutoMapper for object-to-object mapping.
  • +
  • And ASP.NET Boilerplate as startup template and application framework.
  • +
+ +

ASP.NET Boilerplate [1] is an open source application framework that combines all of these frameworks and libraries to make you start easily to develop your application. It provides us an infrastructure to develop applications in best practices. It naturally supports Dependency Injection, Domain Driven Design and Layered Architecture. The sample application also implements validation, exception handling, localization and responsive design.

+ +

Create the application from boilerplate template

+ +

ASP.NET Boilerplate saves our time while starting a new application by providing templates that combines and configures best tools to build enterprise level web applications.

+ +

Let's go to aspnetboilerplate.com/Templates to build our application from template...

+ +

Create template by ASP.NET Boilerplate

+ +

Here, I selected ASP.NET MVC 5.x, SPA (Single Page Application) with AngularJS and EntityFramework. Also entered SimpleTaskSystem for my project name. +I didn't want to include authentication options to get the most simple project +template. It created and downloaded my solution.

+ +

There are five projects included in the solution. Core project for domain (business) layer, Application project for application layer, WebApi project to implement Web Api controllers, Web project for presentation layer and finally EntityFramework project for EntityFramework implementation.

+ +

Note: If you download sample solution for this acticle, you will see 7 projects in the solution. I improved template to support NHibernate and Durandal also for same application. If you don't interest in NHibernate or Durandal, just ignore these 2 projects.

+ +

Create entities

+ +

I'm creating a simple application to create tasks and assing these tasks to people. So, I need Task and Person entities.

+ +

Task entity simply defines a Description, CreationTime and a State for a Task. It also has an optional reference to a Person (AssignedPerson):

+ +
+public class Task : Entity<long>
+{
+    [ForeignKey("AssignedPersonId")]
+    public virtual Person AssignedPerson { get; set; }
+
+    public virtual int? AssignedPersonId { get; set; }
+
+    public virtual string Description { get; set; }
+
+    public virtual DateTime CreationTime { get; set; }
+
+    public virtual TaskState State { get; set; }
+
+    public Task()
+    {
+        CreationTime = DateTime.Now;
+        State = TaskState.Active;
+    }
+}
+ +

Person entity is simpler and just defines Name of the person:

+ +
+public class Person : Entity
+{
+    public virtual string Name { get; set; }
+}
+ +

ASP.NET Boilerplate provides Entity class that defines Id poperty. I derived entities from this Entity class. Task class has an Id of type long since I derived from Entity<long>. Person class has an Id of type int. Since int is the default primary key type, I did not specified it.

+ +

I defined entities in the Core project since Entities are parts of domain/business layer.

+ +

Create DbContext

+ +

As you know, EntityFramework works with DbContext class. We should first define it. ASP.NET Boilerplate template creates a DbContext template for us. I just added IDbSets for Task and Person. This is my DbContext class:

+ +
+public class SimpleTaskSystemDbContext : AbpDbContext
+{
+    public virtual IDbSet<Task> Tasks { get; set; }
+
+    public virtual IDbSet<Person> People { get; set; }
+
+    public SimpleTaskSystemDbContext()
+        : base("Default")
+    {
+
+    }
+
+    public SimpleTaskSystemDbContext(string nameOrConnectionString)
+        : base(nameOrConnectionString)
+    {
+            
+    }
+}
+ +

It uses Default connection string in web.config. It's defined as shown below:

+ +
+<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />
+ +

Create Database Migrations

+ +

We'll use EntityFramework's Code First Migrations to create and maintain the database schema. ASP.NET Boilerplate template has enabled migrations by default and added a Configuration class as shown below:

+ +
+internalinternal sealed class Configuration : DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext>
+{
+    public Configuration()
+    {
+        AutomaticMigrationsEnabled = false;
+    }
+
+    protected override void Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context)
+    {
+        context.People.AddOrUpdate(
+            p => p.Name,
+            new Person {Name = "Isaac Asimov"},
+            new Person {Name = "Thomas More"},
+            new Person {Name = "George Orwell"},
+            new Person {Name = "Douglas Adams"}
+            );
+    }
+}
+ +

In the Seed method, I added four people for initial data. Now, I'll create the initial migration. I opened Package Manager Console and typed the following command:

+ +

Visual studio Package manager console

+ +

Add-Migration "InitialCreate" command creates a class named InitialCreate as shown below:

+ +
+public partial class InitialCreate : DbMigration
+{
+    public override void Up()
+    {
+        CreateTable(
+            "dbo.StsPeople",
+            c => new
+                {
+                    Id = c.Int(nullable: false, identity: true),
+                    Name = c.String(),
+                })
+            .PrimaryKey(t => t.Id);
+            
+        CreateTable(
+            "dbo.StsTasks",
+            c => new
+                {
+                    Id = c.Long(nullable: false, identity: true),
+                    AssignedPersonId = c.Int(),
+                    Description = c.String(),
+                    CreationTime = c.DateTime(nullable: false),
+                    State = c.Byte(nullable: false),
+                })
+            .PrimaryKey(t => t.Id)
+            .ForeignKey("dbo.StsPeople", t => t.AssignedPersonId)
+            .Index(t => t.AssignedPersonId);            
+    }
+        
+    public override void Down()
+    {
+        DropForeignKey("dbo.StsTasks", "AssignedPersonId", "dbo.StsPeople");
+        DropIndex("dbo.StsTasks", new[] { "AssignedPersonId" });
+        DropTable("dbo.StsTasks");
+        DropTable("dbo.StsPeople");
+    }
+}
+ +

We did create needed classes to create the database, but not created the database yet. To do it, I'll run the following command:

+ +
+PM> Update-Database
+ +

This command runs migrations, creates the database and populates the initial data for us:

+ +

Database created by EntityFramework Migrations

+ +

When we change Entitiy classes, we can easily create new migration classes using Add-Migration command and update the database with Update-Database command. To learn more about database migrations, see entity framework's documentation.

+ +

Define repositories

+ +

In domain driven design, repositories used to implement database-specific codes. ASP.NET Boilerplate creates an automatic repository for each entity using generic IRepository interface. IRepository defines common methods for select, insert, update, delete and a few more:

+ +

IRepository interface

+ +

We can extend these repository upon our needs. I will extend it to create a Task repository. As I want to separate interface from implementation, I declare interfaces for repositories first. Here, is the Task repository interface:

+ +
+public interface ITaskRepository : IRepository<Task, long>
+{
+    List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
+}
+ +

It extends generic IRepository interface of ASP.NET Boilerplate. So, ITaskRepository inherently defines all these methods as default. It can also add it's own methods as I defined GetAllWithPeople(...).

+ +

No need to create a repository for Person since default methods are enough for me. ASP.NET Boilerplate provides a way of injecting generic repositories without creating a repository class. We will see it in TaskAppService class in 'Build application services' section..

+ +

I defined repository interfaces in the Core project since they are parts of domain/business layer.

+ +

Implement repositories

+ +

We should implement the ITaskRepository interface defined above. I'm implementing repositories in EntityFramework project. Thus, domain layer becomes completely independent from EntityFramework.

+ +

When we created the project template, ASP.NET Boilerplate defined a generic base class for repositories in our project: SimpleTaskSystemRepositoryBase. It's a good practice to have such a base class since we can later add some common methods for our repositories. You can see definition of this class in the code. I just derive from it for TaskRepository implementation:

+ +
+public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository
+{
+    public TaskRepository(IDbContextProvider<SimpleTaskSystemDbContext> dbContextProvider)
+            : base(dbContextProvider)
+    {
+    }
+
+    public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
+    {
+        //In repository methods, we do not deal with create/dispose DB connections, DbContexes and transactions. ABP handles it.
+            
+        var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it.
+        //var query = Context.Tasks.AsQueryable(); //Alternatively, we can directly use EF's DbContext object.
+        //var query = Table.AsQueryable(); //Another alternative: We can directly use 'Table' property instead of 'Context.Tasks', they are identical.
+            
+        //Add some Where conditions...
+
+        if (assignedPersonId.HasValue)
+        {
+            query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value);
+        }
+
+        if (state.HasValue)
+        {
+            query = query.Where(task => task.State == state);
+        }
+
+        return query
+            .OrderByDescending(task => task.CreationTime)
+            .Include(task => task.AssignedPerson) //Include assigned person in a single query
+            .ToList();
+    }
+}
+ +

TaskRepository is derived from SimpleTaskSystemRepositoryBase and implements ITaskRepository we defined above.

+ +

GetAllWithPeople is our specific method to get tasks where AssignedPerson included (pre-fetched) and optionally filtered by some conditions. We can freely use Context (EF's DBContext) object and database in repositories. ASP.NET Boilerplate manages database connection, transaction, creating and disposing the DbContext for us (See documentation for more information)

+ +

Build application services

+ +

Application services is used to separate presentation layer from domain layer by providing façade style methods. I defined application services in the Application assembly in the project. First, I define interface for task application service:

+ +
+public interface ITaskAppService : IApplicationService
+{
+    GetTasksOutput GetTasks(GetTasksInput input);
+    void UpdateTask(UpdateTaskInput input);
+    void CreateTask(CreateTaskInput input);
+}
+ +

ITaskAppService extends IApplicationService. Thus, ASP.NET Boilerplate automatically provides some features for this class (like dependency injection and validation). Now, let's implement ITaskAppService:

+ +
+public class TaskAppService : ApplicationService, ITaskAppService
+{
+    //These members set in constructor using constructor injection.
+        
+    private readonly ITaskRepository _taskRepository;
+    private readonly IRepository<Person> _personRepository;
+        
+    /// <summary>
+    ///In constructor, we can get needed classes/interfaces.
+    ///They are sent here by dependency injection system automatically.
+    /// </summary>
+    public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
+    {
+        _taskRepository = taskRepository;
+        _personRepository = personRepository;
+    }
+        
+    public GetTasksOutput GetTasks(GetTasksInput input)
+    {
+        //Called specific GetAllWithPeople method of task repository.
+        var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
+
+        //Used AutoMapper to automatically convert List<Task> to List<TaskDto>.
+        return new GetTasksOutput
+                {
+                    Tasks = Mapper.Map<List<TaskDto>>(tasks)
+                };
+    }
+        
+    public void UpdateTask(UpdateTaskInput input)
+    {
+        //We can use Logger, it's defined in ApplicationService base class.
+        Logger.Info("Updating a task for input: " + input);
+
+        //Retrieving a task entity with given id using standard Get method of repositories.
+        var task = _taskRepository.Get(input.TaskId);
+
+        //Updating changed properties of the retrieved task entity.
+
+        if (input.State.HasValue)
+        {
+            task.State = input.State.Value;
+        }
+
+        if (input.AssignedPersonId.HasValue)
+        {
+            task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
+        }
+
+        //We even do not call Update method of the repository.
+        //Because an application service method is a 'unit of work' scope as default.
+        //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception).
+    }
+
+    public void CreateTask(CreateTaskInput input)
+    {
+        //We can use Logger, it's defined in ApplicationService class.
+        Logger.Info("Creating a task for input: " + input);
+
+        //Creating a new Task entity with given input's properties
+        var task = new Task { Description = input.Description };
+
+        if (input.AssignedPersonId.HasValue)
+        {
+            task.AssignedPersonId = input.AssignedPersonId.Value;
+        }
+
+        //Saving entity with standard Insert method of repositories.
+        _taskRepository.Insert(task);
+    }
+}
+ +

TaskAppService uses repositories for database operations. It gets references in it's constructor via constructor injection pattern. ASP.NET Boilerplate naturally implements dependency injection, so we can use constructor injection or property injection freely (See more on dependency injection in ASP.NET Boilerplate documentation).

+ +

Notice that we're using PersonRepository by injecting IRepository<Person>. ASP.NET Boilerplate automatically creates repositories for our entities. If default methods of IRepository enough for us, we don't have to create repository classes.

+ +

Application service methods works with Data Transfer Objects (DTOs). It's a best practice and I definitely suggest to use this pattern. But you don't have to do it as long as you can deal with problems of exposing Entities to presentation layer.

+ +

In the GetTasks method, I used the GetAllWithPeople method that I implemented before. It returns a List<Task> but I need to return a List<TaskDto> to presentation layer. AutoMapper helps us here to automatically convert Task objects to TaskDto objects. GetTasksInput and GetTasksOutput are special DTOs defined for GetTasks method.

+ +

In the UpdateTask method, I retrieved the Task from database (using IRepository's Get method) and changed peoperties of the Task. Notice that I did not even called Update method of the repository. ASP.NET Boilerplate implements UnitOfWork pattern. So, all changes in an application service method are a unit of work (atomic) and applied to database at the end of the method automatically.

+ +

In the CreateTask method, I simply created a new Task and inserted to database using the IRepository's Insert method.

+ +

ASP.NET Boilerplate's ApplicationService class has some properties to make developing application services easier. For example, it defines a Logger property for logging. So, we derived TaskAppService from ApplicationService and used it's Logger property here. It's optional to derive from this class but required to implement IApplicationService (notice that ITaskAppService extends IApplicationService).

+ +

Validation

+ +

ASP.NET Boilerplate automatically validates inputs of application service methods. CreateTask method gets CreateTaskInput as parameter:

+ +
+public class CreateTaskInput
+{
+    public int? AssignedPersonId { get; set; }
+
+    [Required]
+    public string Description { get; set; }
+    
+    public override string ToString()
+    {
+        return string.Format("[CreateTaskInput > AssignedPersonId = {0}, Description = {1}]", AssignedPersonId, Description);
+    }
+}
+ +

Here, Description is marked as Required. You can use any Data Annotation attributes here. If you want to make some custom validation, you can implement ICustomValidate as I implemented in UpdateTaskInput:

+ +
+public class UpdateTaskInput : ICustomValidate
+{
+    [Range(1, long.MaxValue)]
+    public long TaskId { get; set; }
+
+    public int? AssignedPersonId { get; set; }
+
+    public TaskState? State { get; set; }
+
+    public void AddValidationErrors(List<ValidationResult> results)
+    {
+        if (AssignedPersonId == null && State == null)
+        {
+            results.Add(new ValidationResult("Both of AssignedPersonId and State can not be null in order to update a Task!", new[] { "AssignedPersonId", "State" }));
+        }
+    }
+
+    public override string ToString()
+    {
+        return string.Format("[UpdateTask > TaskId = {0}, AssignedPersonId = {1}, State = {2}]", TaskId, AssignedPersonId, State);
+    }
+}
+ +

AddValidationErrors method is the place you can write your custom validation code.

+ +

Handling exceptions

+ +

Note that we did not handled any exception. ASP.NET Boilerplate automatically handles exceptions, logs and returns an appropriate error message to the client. Also, in client side, handles these error messages and show to the user. Actually, this is true for ASP.NET MVC and Web API Controller actions. Since we will expose the TaskAppService using Web API, we don't need to handle exceptions. See exception handling document for details.

+ +

Build Web API services

+ +

I want to expose my application services to remote clients. Thus, my AngularJS application can easily call these service methods using AJAX.

+ +

ASP.NET Boilerplate provides an automatic way of exposing application service methods as ASP.NET Web API. I just use DynamicApiControllerBuilder as shown below:

+ +
+DynamicApiControllerBuilder
+    .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
+    .Build();
+ +

For this example, ASP.NET Boilerplate finds all interfaces inherits IApplicationService in Application layer assembly and creates a web api controller for each application service class. There are alternative syntaxes for fine control. We'll see how to call these services via AJAX.

+ +

Develop the SPA

+ +

I'll implement a Single-Page Web Application as user interface of my project. AngularJS (by Google) is one (propably the top one) of the most used SPA frameworks.

+ +

ASP.NET Boilerplate provides a template that makes easy to start with AngularJS. The template has two pages (Home and About) with smooth transition between pages. Uses Twitter Bootstrap as HTML/CSS framework (thus, it's responsive). It's also localized into English and Turkish with ASP.NET Boilerplate's localization system (You can easly add other languages or remove one of them).

+ +

We first change route of the template. ASP.NET Boilerplate template uses AngularUI-Router, the de-facto standard router of AngularJS. It provides state based routing modal. We will have two views: task list and new task. So, we will change route definition in app.js as shown below:

+ +
+app.config([
+    '$stateProvider', '$urlRouterProvider',
+    function ($stateProvider, $urlRouterProvider) {
+        $urlRouterProvider.otherwise('/');
+        $stateProvider
+            .state('tasklist', {
+                url: '/',
+                templateUrl: '/App/Main/views/task/list.cshtml',
+                menu: 'TaskList' //Matches to name of 'TaskList' menu in SimpleTaskSystemNavigationProvider
+            })
+            .state('newtask', {
+                url: '/new',
+                templateUrl: '/App/Main/views/task/new.cshtml',
+                menu: 'NewTask' //Matches to name of 'NewTask' menu in SimpleTaskSystemNavigationProvider
+            });
+    }
+]);
+ +

app.js is the main JavaScript file to configure and start our SPA. Notice that we're using cshtml files as views! Normally, html files are used as views in AngularJS. ASP.NET Boilerplate makes it possible to use cshtml files. Thus we will have the power of razor engine to generate HTML.

+ +

ASP.NET Boilerplate provides an infrastructure to create and show menus in an application. It allows to define menu in C# and use same menu both in C# and JavaScript. See SimpleTaskSystemNavigationProvider class for creating menu and see header.js/header.cshtml for showing menu in the angular way.

+ +

First, I'm creating an Angular controller for the task list view:

+ +
+(function() {
+    var app = angular.module('app');
+
+    var controllerId = 'sts.views.task.list';
+    app.controller(controllerId, [
+        '$scope', 'abp.services.tasksystem.task',
+        function($scope, taskService) {
+            var vm = this;
+
+            vm.localize = abp.localization.getSource('SimpleTaskSystem');
+
+            vm.tasks = [];
+
+            $scope.selectedTaskState = 0;
+
+            $scope.$watch('selectedTaskState', function(value) {
+                vm.refreshTasks();
+            });
+
+            vm.refreshTasks = function() {
+                abp.ui.setBusy( //Set whole page busy until getTasks complete
+                    null,
+                    taskService.getTasks({ //Call application service method directly from JavaScript
+                        state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null
+                    }).success(function(data) {
+                        vm.tasks = data.tasks;
+                    })
+                );
+            };
+
+            vm.changeTaskState = function(task) {
+                var newState;
+                if (task.state == 1) {
+                    newState = 2; //Completed
+                } else {
+                    newState = 1; //Active
+                }
+
+                taskService.updateTask({
+                    taskId: task.id,
+                    state: newState
+                }).success(function() {
+                    task.state = newState;
+                    abp.notify.info(vm.localize('TaskUpdatedMessage'));
+                });
+            };
+
+            vm.getTaskCountText = function() {
+                return abp.utils.formatString(vm.localize('Xtasks'), vm.tasks.length);
+            };
+        }
+    ]);
+})();
+ +

I defined name of the controller as 'sts.views.task.list'. This my convention (for scalable code-base) but you can simply name it as 'ListController'. AngularJS also uses dependency injection. We're injecting '$scope' and 'abp.services.tasksystem.task' here. First one is Angular's scope variable, second one is the automatically created JavaScript service proxy for ITaskAppService (we built it before in 'Build Web API services' section).

+ +

ASP.NET Boilerplate provides infrastructure to use same localization texts both in server and client (see it's documentation for details). 

+ +

vm.taks is the list of tasks that will be shown in the view. vm.refreshTasks method fills this array by getting tasks using taskService. It's called when selectedTaskState changes (observed using $scope.$watch).

+ +

As you see, calling an application service method is very easy and straightforward! This is a feature of ASP.NET Boilerplate. It generates Web API layer and JavaScript proxy layer that talks with this Web API layer. Thus, we are calling the application service method as calling a simple JavaScript method. It is completely integrated with AngularJS (uses Angular's $http service).

+ +

Let's see the view side of task list:

+ +
+<div class="panel panel-default" ng-controller="sts.views.task.list as vm">
+
+    <div class="panel-heading" style="position: relative;">
+        <div class="row">
+            
+            <!-- Title -->
+            <h3 class="panel-title col-xs-6">
+                @L("TaskList") - <span>{{vm.getTaskCountText()}}</span>
+            </h3>
+            
+            <!-- Task state combobox -->
+            <div class="col-xs-6 text-right">
+                <select ng-model="selectedTaskState">
+                    <option value="0">@L("AllTasks")</option>
+                    <option value="1">@L("ActiveTasks")</option>
+                    <option value="2">@L("CompletedTasks")</option>
+                </select>
+            </div>
+        </div>
+    </div>
+
+    <!-- Task list -->
+    <ul class="list-group" ng-repeat="task in vm.tasks">
+        <div class="list-group-item">
+            <span class="task-state-icon glyphicon" ng-click="vm.changeTaskState(task)" ng-class="{'glyphicon-minus': task.state == 1, 'glyphicon-ok': task.state == 2}"></span>
+            <span ng-class="{'task-description-active': task.state == 1, 'task-description-completed': task.state == 2 }">{{task.description}}</span>
+            <br />
+            <span ng-show="task.assignedPersonId > 0">
+                <span class="task-assignedto">{{task.assignedPersonName}}</span>
+            </span>
+            <span class="task-creationtime">{{task.creationTime}}</span>
+        </div>
+    </ul>
+
+</div>
+ +

ng-controller attribute (in the first line) binds the controller to the view. @L("TaskList") gets localized text for "task list" (works on server while rendering HTML). It's possible since this is a cshtml file.

+ +

ng-model binds combobox and the JavaScript variable. When the variable changes, the combobox updated. When the combobox changes, the valiable is updated. This is two-way binding of AngularJS.

+ +

ng-repeat is another 'directive' of Angular that is used to render same HTML for each value in an array. When the array changes (an item is added for example), it's automatically reflected to the view. This is another powerful feature of AngularJS.

+ +

Note: When you add a JavaScript file (for example, for the 'task list' controller), you should add it to your page. This can be done by adding it to Home\Index.cshtml in the template.

+ +

Localization

+ +

ASP.NET Boilerplate provides a flexible and strong localization system. You can use XML files or Resource files as localization source. You can also define custom localization sources. See documentation for more. In this sample application, I used XML files (it's under Localization folder in web application):

+ +
+<?xml version="1.0" encoding="utf-8" ?>
+<localizationDictionary culture="en">
+  <texts>
+    <text name="TaskSystem" value="Task System" />
+    <text name="TaskList" value="Task List" />
+    <text name="NewTask" value="New Task" />
+    <text name="Xtasks" value="{0} tasks" />
+    <text name="AllTasks" value="All tasks" />
+    <text name="ActiveTasks" value="Active tasks" />
+    <text name="CompletedTasks" value="Completed tasks" />
+    <text name="TaskDescription" value="Task description" />
+    <text name="EnterDescriptionHere" value="Task description" />
+    <text name="AssignTo" value="Assign to" />
+    <text name="SelectPerson" value="Select person" />
+    <text name="CreateTheTask" value="Create the task" />
+    <text name="TaskUpdatedMessage" value="Task has been successfully updated." />
+    <text name="TaskCreatedMessage" value="Task {0} has been created successfully." />
+  </texts>
+</localizationDictionary>
+
+ +

Unit testing

+ +

ASP.NET Boilerplate is designed to be testable. I authored an article to show unit and integration testing for ABP based projects. See the article: Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate.

+ +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem

+ +

Summary

+ +

In this article, I demonstrated how to develop an NLayered ASP.NET MVC web application with a SPA and responsive user interface. I used ASP.NET Boilerplate since it makes easy to develop such applications using best practices and saves our time. Use these links for moe:

+ + + +

Article history

+ +
    +
  • 2018-02-18: Upgraded sample project and article for ABP + v3.4.
  • +
  • 2016-10-26: Upgraded sample project to ABP v1.0.
  • +
  • 2016-07-19: Updated article and sample project for ABP v0.10.
  • +
  • 2015-06-08: Updated article and sample project for ABP v0.6.3.1.
  • +
  • 2015-02-20: Added link to unit test article and updated the sample project
  • +
  • 2015-01-05: Updated sample project for ABP v0.5.
  • +
  • 2014-11-03: Updated article and sample project for ABP v0.4.1.
  • +
  • 2014-09-08: Updated article and sample project for ABP v0.3.2.
  • +
  • 2014-08-17: Updated sample project to ABP v0.3.1.2.
  • +
  • 2014-07-22: Updated sample project to ABP v0.3.0.1.
  • +
  • 2014-07-11: Added screenshot of 'Enable-Migrations' command.
  • +
  • 2014-07-08: Updated sample project and article.
  • +
  • 2014-07-01: First publish of the article.
  • +
+ +

References

+ +

[1] ASP.NET Boilerplate official website: http://www.aspnetboilerplate.com

diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface-v3.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface-v3.png" new file mode 100644 index 0000000..4fdc385 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface-v3.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface2.png" new file mode 100644 index 0000000..bc24d8e Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/irepository-interface2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/pkm-console2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/pkm-console2.png" new file mode 100644 index 0000000..c2a846c Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/pkm-console2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/simple-task-system-screenshot.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/simple-task-system-screenshot.png" new file mode 100644 index 0000000..2a222ad Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/simple-task-system-screenshot.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/sql-db.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/sql-db.png" new file mode 100644 index 0000000..3b87774 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/sql-db.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/Clipboard01.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/Clipboard01.png" new file mode 100644 index 0000000..dd46a1b Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/Clipboard01.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/admin.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/admin.png" new file mode 100644 index 0000000..8b95299 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/admin.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/app-on-docker.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/app-on-docker.PNG" new file mode 100644 index 0000000..3b43980 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/app-on-docker.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-folder.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-folder.PNG" new file mode 100644 index 0000000..bc09728 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-folder.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-with-ng.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-with-ng.PNG" new file mode 100644 index 0000000..9a6d1d4 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/build-with-ng.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/docker-config-folder.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/docker-config-folder.PNG" new file mode 100644 index 0000000..08938e2 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/docker-config-folder.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/haproxy.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/haproxy.PNG" new file mode 100644 index 0000000..1c22ec6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/haproxy.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/host-page.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/host-page.PNG" new file mode 100644 index 0000000..a5b9ef8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/host-page.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/index.html" new file mode 100644 index 0000000..acf2e1a --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/index.html" @@ -0,0 +1,289 @@ + + + + + + + +

Introduction

+ +

In this article, I will show you how to run ABP module zero core template on Docker, step by step. And then, we will discuss alternative scenarios like web farm using Redis and Haproxy

+ +

As you now, Docker is the most popular software container platform. I won’t go into details of how to install/configure Docker on windows or what are the advantages of using Docker. You can find related documents here. And there is a Getting Started document as well.

+ +

What is ABP Module Zero Core Template?

+ +

Module zero core template is a starter project template that is developed with using ASP.NET Boilerplate Framework. This a .net core project as a Single Page Application with using Angular4. And also there is a multi-page MVC application as well. But in this article, I will explain angular4 version.

+ +

In module zero core template project there are two separate projects, one is Angular4 project as web UI and the host project that is used by angular UI. Let's examine it to better understand the project before running it on Docker.

+ +

Getting Started

+ +

Creating Template From Site

+ +

First, I will download module zero core template from https://www.aspnetboilerplate.com/Templates site.

+ +

+ +

Framework: .NET Core2.0 + Single Page Web Application Angular + Include module zero
+Project name: Acme.ProjectName

+ +

Before preparing project to run on Docker, let's run the project, first. I am opening the .sln file in folder ProjectName\aspnet-core

+ +

+ +

Creating Database with Using EF Migrations

+ +

Before running project, we should create database with using EF migrations on Package Manager Console. First, I am setting Acme.ProjectName.Web.Host as start up project. (right-click Host project and select Set as Startup Project). And then, open Package Manager Console, select default project to EntityFrameworkCore, run the command below

+ +

update-database

+ +

 After running this command, database will be created with name ProjectNameDb.

+ +

+ +

Running Host Project

+ +

And now, Host project is ready to run. On visual studio Ctrl+F5 . It opens swagger method index page.

+ +

+ +

All these services are served in application layer of project and used by Angular UI. 

+ +

Running Angular Project

+ +

While host is already running and we can run Angular project that uses APIs. To run Angular project, make sure you have node and npm installed on your machine.

+ +

First, run cmd on location ProjectName\angular and run the command "npm install" or just "yarn" to fetch client side packages.

+ +

+ +

Run npm start command in the same directory to start angular project.

+ +

+ +

Finally you have to see the line "webpack: Compiled successfully" in the output screen.

+ +

+ +

We started Angular project successfully. Open your browser and navigate to http://localhost:4200/

+ +

+ +

Use the credentials below to login

+ + +
Username admin
Password 123qwe
+ +

After you login, you will see the screen below.

+ +

+ +

Check HERE for more details.

+ +

To summarize what we did for running Angular project:

+ +
    +
  • Run cmd on location ProjectName\angular.
  • Run yarn or npm install command(I used yarn for above example).
  • Run npm start command.
  • Browse localhost:4200 to see angular project is running.
+ +

Everything is running properly. Ready to run on docker...

+ +

Running Project on Docker

+ +

If you have not installed Angular CLI yet, you have to install it. Run the command below to install Angular CLI.

+ +

npm install -g @angular/cli

+ +

After you ensure Angular CLI installed, let's see files and folders to configure Docker environment. There is a folder that named docker under ProjectName/aspnet-core

+ +

+ +

In docker/ng folder there is a docker-compose.yml file and two powershell script to run docker compose(up.ps1) and stop(down.ps1) it. And there is one more folder and a powershell script file.

+ +

+ +

This script file to build and publish host and agular project. And also, this script copies the files that is into docker folder to build folder. First, I will run the build-with-ng.ps1 script on location ProjectName/aspnet-core/build.

+ +

+ +

After running script, when you look at the build folder, you will see the outputs folder. 

+ +

+ +

Before running up.ps1 command,

+ +
    +
  • You have to share the drives. To share it, right click Docker system tray, go to settings, navigate to shared folders and click all the drives.
  • Database is hosted on the local machine not on the docker. Website hosted on docker will be connecting to your local database. And with a trusted connection string the connection will be unsuccessful. So set your sql database username & password.  To achieve this modify the file "...\aspnet-core\src\Acme.ProjectName.Web.Host\appsettings.Staging.json". Update Default ConnectionStrings > "Server=10.0.75.1; Database=ProjectNameDb; User=sa; Password=<write your password>;"
+ +

Run up.ps1 script to run these two project on docker under location ProjectName/aspnet-core/build/outputs.

+ +

+ +

Angular and host projects are now running. Browse http://localhost:9901/ for Host Project and http://localhost:9902/ for Angular UI.

+ +

+ +

Module Zero Core Template Web Farm on Docker with Using Redis and Haproxy

+ +

+ +

In a web farm there are more than one web servers, there is a load balancer at the front of these servers and a server to store sharing sessions/caches. 

+ +

In our example, angular application will be client, haproxy will be load balancer, host app will be web servers and redis will be shared server.

+ +

Create a configuration file for haproxy named haproxy.cfg to location ProjectName\aspnet-core\docker\ng

+ +

haproxy.cfg

+ +

(Copy paste code lines makes encoding problem just download the file =>  Download haproxy.cfg)

+ +
global
+    maxconn 4096
+    
+defaults
+    mode http
+    timeout connect 5s
+    timeout client 50s
+    timeout server 50s
+    
+listen http-in
+    bind *:8080
+    
+    server web-1 outputs_abp_host_1:80
+    server web-2 outputs_abp_host_2:80
+    
+    stats enable
+    stats uri /haproxy
+    stats refresh 1s
+ +

Important lines haproxy.cfg are server web-1 and server web-2. I repreduced host applications. This will create two host application on docker container. 

+ +

Modified docker-compose.yml

+ +

(Copy paste code lines makes encoding problem just download the file =>  Download docker-compose.yml)

+ +
version: '2'
+
+services:
+
+    abp_redis:
+        image: redis
+        ports:
+            - "6379:6379"
+
+    abp_host:
+        image: abp/host
+        environment:
+            - ASPNETCORE_ENVIRONMENT=Staging
+        volumes:
+            - "./Host-Logs:/app/App_Data/Logs"
+
+    abp_ng:
+        image: abp/ng
+        ports:
+            - "9902:80"
+            
+    load_balancer:
+        image: haproxy:1.7.1
+        volumes:
+            - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
+        ports:
+            - "9904:8080"
+ +

build-with-ng.ps1

+ +

Replace the below line

+ +
(Get-Content $ngConfigPath) -replace "21021", "9901" | Set-Content $ngConfigPath
+ +

with this line

+ +
(Get-Content $ngConfigPath) -replace "21021", "9904" | Set-Content $ngConfigPath
+ +

Now Angular UI will connect to haproxy. Haproxy distribute the requests to web servers.

+ +

Overwrite up.ps1 with the content below

+ +
docker rm $(docker ps -aq)
+docker-compose up -d abp_redis
+sleep 3
+docker-compose up -d abp_host
+docker-compose up -d abp_ng
+sleep 2
+docker-compose scale abp_host=2
+sleep 2
+docker-compose up -d load_balancer
+ +

To use redis cache install Abp.RedisCache library to ProjectName.Web.Core project. And update ProjectNameWebCoreModule.cs like following:

+ +
[DependsOn(...,
+     typeof(AbpRedisCacheModule))]
+public class ProjectNameWebCoreModule : AbpModule
+{
+ +

And adding redis cache configuration to PreInitialize method (ProjectNameWebCoreModule.cs)

+ +
public override void PreInitialize()
+{
+    ...
+
+    Configuration.Caching.UseRedis(options =>
+    {
+        var connectionString = _appConfiguration["Abp:RedisCache:ConnectionString"];
+        if (connectionString != null && connectionString != "localhost")
+        {
+            options.ConnectionString = AsyncHelper.RunSync(() => Dns.GetHostAddressesAsync(connectionString))[0].ToString();
+        }
+    })
+    
+    ...
+ +

Add redis configurations appsettings.staging.json.

+ +

appsettings.staging.json

+ +
{
+  ...,
+  "Abp": {
+    "RedisCache": {
+      "ConnectionString": "outputs_abp_redis_1"
+    }
+  }
+}
+ +

outputs_abp_redis_1 is the name of redis container and this name is defining by docker automatically. After this changing, host project will resolve the dns of machine that is deployed on. And now, when I run build-with-ng.ps1 and up.ps1 , web farm project will run.  And the result:

+ +

+ +

As you can see, all containers are working. When you browse http://localhost:9902 you can see Angular UI is working.

+ +

How Will I Know If Haproxy and Redis Work?

+ +

There are tools to track haproxy activity(haproxy web interface) and get redis stored data(redis cli).

+ +

Haproxy web interface

+ +

When you browse http://localhost:9904/haproxy you will see something like following.

+ +

+ +

When you navigate between on angular application pages or run any api on host project (http://localhost:9904), you can see that the haproxy is routing the requests to different machines. You can track which machine is running under Session rate>Cur tab that are changing for web-1 and web-2. 

+ +

Redis cli

+ +

To understand if redis is working, you can use redis-cli. run docker exec -it outputs_abp_redis_1 redis-cli command to run redis-cli interactive mode to connect redis server that is running on docker container. Then to test if redis is running, write ping command and it will return PONG if it works. Now when I write keys * command, I should get the application cache keys.

+ +

+ +

As you can see, redis is working well. Cache keys stored on redis, correctly.

+ +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/ModuleZeroCoreWebFarm

+ +

 

+ +
+ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/module-zero-core-template-ui-login2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/module-zero-core-template-ui-login2.png" new file mode 100644 index 0000000..c303223 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/module-zero-core-template-ui-login2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-final-screen.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-final-screen.PNG" new file mode 100644 index 0000000..29d05aa Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-final-screen.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-start.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-start.PNG" new file mode 100644 index 0000000..784cfe9 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/npm-start.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/outputs-folder.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/outputs-folder.PNG" new file mode 100644 index 0000000..3c10623 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/outputs-folder.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/project-folder.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/project-folder.PNG" new file mode 100644 index 0000000..961c65d Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/project-folder.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/redis-it-mode.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/redis-it-mode.png" new file mode 100644 index 0000000..56f7c63 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/redis-it-mode.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/running-yarn.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/running-yarn.PNG" new file mode 100644 index 0000000..6758a60 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/running-yarn.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/sessionwebfarm.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/sessionwebfarm.png" new file mode 100644 index 0000000..c2df199 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/sessionwebfarm.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/up.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/up.PNG" new file mode 100644 index 0000000..44ff3a2 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/up.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/update-database.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/update-database.PNG" new file mode 100644 index 0000000..889de01 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/update-database.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/webfarm.PNG" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/webfarm.PNG" new file mode 100644 index 0000000..80531c6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Running-in-Docker-Containers-and-Building-a-Web-Farm-Load-Balancer-Scenario/webfarm.PNG" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/first-test-failed.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/first-test-failed.png" new file mode 100644 index 0000000..575ca75 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/first-test-failed.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/index.html" new file mode 100644 index 0000000..b808791 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/index.html" @@ -0,0 +1,431 @@ + + + + +

Contents

+ + + +

Introduction

+ +

In this article, I'll show how to create unit tests for ASP.NET Boilerplate based projects. Instead of creating a new application to be tested, I'll use the same application developed in this article (Using AngularJS, ASP.NET MVC, Web API and EntityFramework to build NLayered Single Page Web Applications). Solution structure is like that:

+ +

Solution structrure

+ +

We will test Application Services of the project. It includes SimpleTaskSystem.Core, SimpleTaskSystem.Application and SimpleTaskSystem.EntityFramework projects. You can read this article to see how to build this application. Here, I'll focus on testing. 

+ +

Create a test project

+ +

I created a new Class Library project named SimpleTaskSystem.Test and added following nuget packages:

+ +
    +
  • Abp.TestBase: Provides some base classes to make testing easier for ABP based projects.
  • +
  • Abp.EntityFramework: We use EntityFramework 6.x as ORM.
  • +
  • Effort.EF6: Makes it possible to create a fake, in-memory database for EF that is easy to use.
  • +
  • xunit: The testing framework we'll use. Also, added xunit.runner.visualstudio package to run tests in Visual Studio. This package was pre-release when I writing this article. So, I selected 'Include Prerelease' in nuget package manager dialog.
  • +
  • Shouldly: This library makes easy to write assertions.
  • +
+ +

When we add these packages, their dependencies will also be added automatically. Lastly, we should add reference to SimpleTaskSystem.Application, SimpleTaskSystem.Core and SimpleTaskSystem.EntityFramework assemblies since we will test these projects.

+ +

Preparing a base test class

+ +

To create test classes easier, I'll create a base class that prepares a fake database connection:

+ +
+/// <summary>
+/// This is base class for all our test classes.
+/// It prepares ABP system, modules and a fake, in-memory database.
+/// Seeds database with initial data (<see cref="SimpleTaskSystemInitialDataBuilder"/>).
+/// Provides methods to easily work with DbContext.
+/// </summary>
+public abstract class SimpleTaskSystemTestBase : AbpIntegratedTestBase<SimpleTaskSystemTestModule>
+{
+    protected SimpleTaskSystemTestBase()
+    {
+        //Seed initial data
+        UsingDbContext(context => new SimpleTaskSystemInitialDataBuilder().Build(context));
+    }
+
+    protected override void PreInitialize()
+    {
+        //Fake DbConnection using Effort!
+        LocalIocManager.IocContainer.Register(
+            Component.For<DbConnection>()
+                .UsingFactoryMethod(Effort.DbConnectionFactory.CreateTransient)
+                .LifestyleSingleton()
+            );
+
+        base.PreInitialize();
+    }
+
+    public void UsingDbContext(Action<SimpleTaskSystemDbContext> action)
+    {
+        using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>())
+        {
+            context.DisableAllFilters();
+            action(context);
+            context.SaveChanges();
+        }
+    }
+
+    public T UsingDbContext<T>(Func<SimpleTaskSystemDbContext, T> func)
+    {
+        T result;
+
+        using (var context = LocalIocManager.Resolve<SimpleTaskSystemDbContext>())
+        {
+            context.DisableAllFilters();
+            result = func(context);
+            context.SaveChanges();
+        }
+
+        return result;
+    }
+}
+ +

This base class extends AbpIntegratedTestBase. It's a base class which initializes the ABP system. Defines LocalIocContainer property, that is a IIocManager object. Each test will work with it's dedicated IIocManager. Thus, tests will be isolated from each other.

+ +

We should create a module dedicated for tests. It's SimpleTaskSystemTestModule here:

+ +
+[DependsOn(
+        typeof(SimpleTaskSystemDataModule),
+        typeof(SimpleTaskSystemApplicationModule),
+        typeof(AbpTestBaseModule)
+    )]
+public class SimpleTaskSystemTestModule : AbpModule
+{
+        
+}
+ +

This module only defines depended modules, which will be tested and the AbpTestBaseModule.

+ +

In the SimpleTaskSystemTestBase's PreInitialize method, we're registering DbConnection to dependency injection system using Effort (PreInitialize method is used to run some code just befor ABP initialized). We registered it as Singleton (for LocalIocConainer). Thus, same database (and connection) will be used in a test even we create more than one DbContext in same test. SimpleTaskSystemDbContext must have a constructor getting DbConnection in order to use this in-memory database. So, I added the constructor below that accepts a DbConnection:

+ +
+public class SimpleTaskSystemDbContext : AbpDbContext
+{
+    public virtual IDbSet<Task> Tasks { get; set; }
+    public virtual IDbSet<Person> People { get; set; }
+
+    public SimpleTaskSystemDbContext()
+        : base("Default")
+    {
+
+    }
+
+    public SimpleTaskSystemDbContext(string nameOrConnectionString)
+        : base(nameOrConnectionString)
+    {
+            
+    }
+
+    //This constructor is used in tests
+    public SimpleTaskSystemDbContext(DbConnection connection)
+        : base(connection, true)
+    {
+
+    }
+}
+ +

In the constructor of SimpleTaskSystemTestBase, we're also creating an initial data in the database. This is important, since some tests require a data present in the database. SimpleTaskSystemInitialDataBuilder class fills database as shown below:

+ +
+public class SimpleTaskSystemInitialDataBuilder
+{
+    public void Build(SimpleTaskSystemDbContext context)
+    {
+        //Add some people            
+        context.People.AddOrUpdate(
+            p => p.Name,
+            new Person {Name = "Isaac Asimov"},
+            new Person {Name = "Thomas More"},
+            new Person {Name = "George Orwell"},
+            new Person {Name = "Douglas Adams"}
+            );
+        context.SaveChanges();
+
+        //Add some tasks
+        context.Tasks.AddOrUpdate(
+            t => t.Description,
+            new Task
+            {
+                Description = "my initial task 1"
+            },
+            new Task
+            {
+                Description = "my initial task 2",
+                State = TaskState.Completed
+            },
+            new Task
+            {
+                Description = "my initial task 3",
+                AssignedPerson = context.People.Single(p => p.Name == "Douglas Adams")
+            },
+            new Task
+            {
+                Description = "my initial task 4",
+                AssignedPerson = context.People.Single(p => p.Name == "Isaac Asimov"),
+                State = TaskState.Completed
+            });
+        context.SaveChanges();
+    }
+}
+ +

SimpleTaskSystemTestBase's UsingDbContext methods makes it easier to create DbContextes when we need to directly use DbContect to work with database. In constructor, we used it. Also, we will see how to use it in tests.

+ +

All our test classes will be inherited from SimpleTaskSystemTestBase. Thus, all tests will be started by initializing ABP, using a fake database with an initial data. We can also add common helper methods to this base class in order to make tests easier.

+ +

Creating first test

+ +

We will create first unit test to test CreateTask method of TaskAppService class. TaskAppService class and CreateTask method are defined as shown below:

+ +
+public class TaskAppService : ApplicationService, ITaskAppService
+{
+    private readonly ITaskRepository _taskRepository;
+    private readonly IRepository<Person> _personRepository;
+        
+    public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
+    {
+        _taskRepository = taskRepository;
+        _personRepository = personRepository;
+    }
+        
+    public void CreateTask(CreateTaskInput input)
+    {
+        Logger.Info("Creating a task for input: " + input);
+
+        var task = new Task { Description = input.Description };
+
+        if (input.AssignedPersonId.HasValue)
+        {
+            task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
+        }
+
+        _taskRepository.Insert(task);
+    }
+
+    //...other methods
+}
+ +

In unit test, generally, dependencies of testing class is mocked (by creating fake implementations using some mock frameworks like Moq and NSubstitute). This makes unit testing harder, especially when dependencies grows.

+ +

We will not do it like that since we're using dependency injection. All dependencies will be filled automatically by dependency injection with real implementations, not fakes. Only fake thing is the database. Actually, this is an integration test since it not only tests the TaskAppService, but also tests repositories. Even, we're testing it with validation, unit of work and other infrastructures of ASP.NET Boilerplate. This is very valuable since we're testing the application much more realistic.

+ +

So, let's create first test to test CreateTask method.

+ +
+public class TaskAppService_Tests : SimpleTaskSystemTestBase
+{
+    private readonly ITaskAppService _taskAppService;
+
+    public TaskAppService_Tests()
+    {
+        //Creating the class which is tested (SUT - Software Under Test)
+        _taskAppService = LocalIocManager.Resolve<ITaskAppService>();
+    }
+
+    [Fact]
+    public void Should_Create_New_Tasks()
+    {
+        //Prepare for test
+        var initialTaskCount = UsingDbContext(context => context.Tasks.Count());
+        var thomasMore = GetPerson("Thomas More");
+
+        //Run SUT
+        _taskAppService.CreateTask(
+            new CreateTaskInput
+            {
+                Description = "my test task 1"
+            });
+        _taskAppService.CreateTask(
+            new CreateTaskInput
+            {
+                Description = "my test task 2",
+                AssignedPersonId = thomasMore.Id
+            });
+
+        //Check results
+        UsingDbContext(context =>
+        {
+            context.Tasks.Count().ShouldBe(initialTaskCount + 2);
+            context.Tasks.FirstOrDefault(t => t.AssignedPersonId == null && t.Description == "my test task 1").ShouldNotBe(null);
+            var task2 = context.Tasks.FirstOrDefault(t => t.Description == "my test task 2");
+            task2.ShouldNotBe(null);
+            task2.AssignedPersonId.ShouldBe(thomasMore.Id);
+        });
+    }
+
+    private Person GetPerson(string name)
+    {
+        return UsingDbContext(context => context.People.Single(p => p.Name == name));
+    }
+}
+ +

We inherited from SimpleTaskSystemTestBase as described before. In a unit test, we should create the object this will be tested. In the constructor, I used LocalIocManager (dependency injection manager) to create an ITaskAppService (it creates TaskAppService since it implements ITaskAppService). In this way, I got rid of creating mock implementations of dependencies.

+ +

Should_Create_New_Tasks is the test method. It's decorated with the Fact attribute of xUnit. Thus, xUnit understand that this is a test method, and it runs the method.

+ +

In a test method, we generally follow AAA pattern which consists of three steps:

+ +
    +
  1. Arrange: Prepare for the test
  2. +
  3. Act: Run the SUT (software under test - the actual testing code)
  4. +
  5. Assert: Check and verify the result.
  6. +
+ +

In Should_Create_New_Tasks method, we will create two tasks, one will be assigned to Thomas More. So, our three steps are:

+ +
    +
  1. Arrange: We get the person (Thomas More) from database to obtain his Id and the current task count in database (Also, we created the TaskAppService in the constructor).
  2. +
  3. Act: We're creating two tasks using TaskAppService.CreateTask method.
  4. +
  5. Assert: We're checking if task count increased by 2. We're also trying to get created tasks from database to see if they are correctly inserted to the database.
  6. +
+ +

Here, UsingDbContext method helps us while working directly with DbContext. If this test success, we understand that CreateTask method can create Tasks if we supply valid inputs. Also, repository is working since it inserted Tasks to the database.

+ +

To run tests, we're opening Visual Studio Test Explorer by selecting TEST\Windows\Test Explorer:

+ +

Open Visual Studio Test Explorer

+ +

Then we're clicking 'Run All' link in the Test Explorer. It finds and runs all test in the solution:

+ +

Running first unit test using Visual Studio Test Explorer

+ +

As shown above, our first unit test is passed. Congratulations! A test will fail if testing or tester code is incorrect. Assume that we have forgotten to assign creating task to given person (To test it, comment out the related lines in TaskAppService.CreateTask method). When we run test, it will fail:

+ +

Failing test

+ +

Shouldly library makes fail messages clearer. It also makes it easy to write assertions. Compare xUnit's Assert.Equal with Shouldly's ShouldBe extension method:

+ +
+Assert.Equal(thomasMore.Id, task2.AssignedPersonId); //Using xunit's Assert
+task2.AssignedPersonId.ShouldBe(thomasMore.Id); //Using Shouldly
+
+ +

I think the second one is more easy and natual to write and read. Shouldly have many other extension methods to make our life easier. See it's documentation.

+ +

Testing exceptions

+ +

I want to create a second test for the CreateTask method. But, this time with an invalid input:

+ +
+[Fact]
+public void Should_Not_Create_Task_Without_Description()
+{
+    //Description is not set
+    Assert.Throws<AbpValidationException>(() => _taskAppService.CreateTask(new CreateTaskInput()));
+}
+ +

I expect that CreateTask method throws AbpValidationException if I don't set Description for creating task. Because Description property is marked as Required in CreateTaskInput DTO class (see source codes). This test success if CreateTask throws the exception, otherwise fails. Note that; validating input and throwing exception are made by ASP.NET Boilerplate infrastructure.

+ +

Using repositories in tests

+ +

I'll test to assign a task from one person to another:

+ +
+//Trying to assign a task of Isaac Asimov to Thomas More
+[Fact]
+public void Should_Change_Assigned_People()
+{
+    //We can work with repositories instead of DbContext
+    var taskRepository = LocalIocManager.Resolve<ITaskRepository>();
+
+    //Obtain test data
+    var isaacAsimov = GetPerson("Isaac Asimov");
+    var thomasMore = GetPerson("Thomas More");
+    var targetTask = taskRepository.FirstOrDefault(t => t.AssignedPersonId == isaacAsimov.Id);
+    targetTask.ShouldNotBe(null);
+
+    //Run SUT
+    _taskAppService.UpdateTask(
+        new UpdateTaskInput
+        {
+            TaskId = targetTask.Id,
+            AssignedPersonId = thomasMore.Id
+        });
+
+    //Check result
+    taskRepository.Get(targetTask.Id).AssignedPersonId.ShouldBe(thomasMore.Id);
+}
+ +

In this test, I used ITaskRepository to perform database operations, instead of directly working with DbContext. You can use one or mix of these approaches.

+ +

Testing async methods

+ +

We can also test async methods with xUnit. See the method written to test GetAllPeople method of PersonAppService. GetAllPeople method is async, so, testing method should be also async:

+ +
+[Fact]
+public async Task Should_Get_All_People()
+{
+    var output = await _personAppService.GetAllPeople();
+    output.People.Count.ShouldBe(4);
+}
+ +

Source Code

+ +

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem

+ +

Summary

+ +

In this article, I wanted to show simply testing projects developed upon ASP.NET Boilerplate application framework. ASP.NET Boilerplate provides a good infrastructure to implement test driven development, or simply creating some unit/integration tests for your applications.

+ +

Effort library provides a fake database that works well with EntityFramework. It works as long as you use EntityFramework and LINQ to perform database operations. If you have a stored procedure and want to test it, Effort does not work. For such cases, I recommend using LocalDB.

+ +

Use the following links for more information on ASP.NET Boilerplate:

+ + + +

Article History

+ +
    +
  • 2018-02-22 +
      +
    • Upgraded to ABP v3.4.
    • +
    +
  • 2017-06-28 +
      +
    • Upgraded source code to ABP v2.1.3.
    • +
    +
  • +
  • 2016-07-19 +
      +
    • Upgraded article and source code for ABP v0.10 release.
    • +
    +
  • +
  • 2016-01-07 +
      +
    • Upgraded solution to .Net Framework 4.5.2.
    • +
    • Upgraded Abp to v0.7.7.1.
    • +
    +
  • +
  • 2015-06-15 +
      +
    • Updated sample project and article based on latest ABP version.
    • +
    +
  • +
  • 2015-02-02 +
      +
    • First publish of the article.
    • +
    +
  • +
\ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/opening-vs-test-explorer.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/opening-vs-test-explorer.png" new file mode 100644 index 0000000..03e28b5 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/opening-vs-test-explorer.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/running-fist-test.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/running-fist-test.png" new file mode 100644 index 0000000..a6abca5 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/running-fist-test.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/solution-structure.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/solution-structure.png" new file mode 100644 index 0000000..b2c4bd4 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Unit-Testing-with-Entity-Framework,-xUnit-Effort/solution-structure.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/index.html" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/index.html" new file mode 100644 index 0000000..86d3d58 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/index.html" @@ -0,0 +1,321 @@ + + + + +

Introduction

+

In this article, i will explain how to create custom repositories in ASP.NET Boilerplate and +use stored procedure, view, user defined functions.

+

To start with ASP.NET Boilerplate framework, you can download a startup template from + https://aspnetboilerplate.com/Templates. +I selected ASP.NET Core and Multi Page Web Application with Acme.PhoneBook +project name. If you need help with setting up the template, see +https://aspnetboilerplate.com/Pages/Documents/Zero/Startup-Template-Core

+

After opening the downloaded solution in Visual Studio 2017, we see a +solution structure as like below:

+

Projects

+ + +

Creating A Custom Repository

+ + + +

We will create a custom repository to do some basic operations on User entity using stored procedure, view and user defined function. To implement a custom repository, just derive from your application specific base repository class.

+ +

Implement the interface in domain layer (Acme.PhoneBook.Core).

+
+    public interface IUserRepository:  IRepository<User, long> 
+    {
+      ...
+      ...
+    }
+
+

Implement the repository in infrastructure layer (Acme.PhoneBook.EntityFrameworkCore).

+
+    public class UserRepository : PhoneBookRepositoryBase<User, long>, IUserRepository 
+    {
+        private readonly IActiveTransactionProvider _transactionProvider;
+
+        public UserRepository(IDbContextProvider<PhoneBookDbContext> dbContextProvider, IActiveTransactionProvider transactionProvider)
+            : base(dbContextProvider)
+        {
+            _transactionProvider = transactionProvider;
+        }
+        
+        ...
+        ...
+    }
+    
+
+ +

Helper Methods

+ +

First of all, we are creating some helper methods those will be shared by +other methods to perform some common tasks:

+ +
+private DbCommand CreateCommand(string commandText, CommandType commandType, params SqlParameter[] parameters)
+{
+    var command = Context.Database.GetDbConnection().CreateCommand();
+
+    command.CommandText = commandText;
+    command.CommandType = commandType;
+    command.Transaction = GetActiveTransaction();
+
+    foreach (var parameter in parameters)
+    {
+        command.Parameters.Add(parameter);
+    }
+
+    return command;
+}
+
+private void EnsureConnectionOpen()
+{
+    var connection = Context.Database.GetDbConnection();
+
+    if (connection.State != ConnectionState.Open)
+    {
+        connection.Open();
+    }
+}
+
+private DbTransaction GetActiveTransaction()
+{
+    return (DbTransaction)_transactionProvider.GetActiveTransaction(new ActiveTransactionProviderArgs
+    {
+        {"ContextType", typeof(PhoneBookDbContext) },
+        {"MultiTenancySide", MultiTenancySide }
+    });
+}
+
+ + + +

Stored Procedure

+ +

Here is a stored procedure call that gets username of all users. Added this +to the repository implementation (UserRepository).

+ +
+public async Task<List<string>> GetUserNames()
+{
+    EnsureConnectionOpen();
+
+    using (var command = CreateCommand("GetUsernames", CommandType.storedProcedure))
+    {
+        using (var dataReader = await command.ExecuteReaderAsync())
+        {
+            var result = new List<string>();
+
+            while (dataReader.Read())
+            {
+                result.Add(dataReader["UserName"].ToString());
+            }
+
+            return result;
+        }
+    }
+}
+
+

And defined the GetUserNames method in the IUserRepository:

+
+public interface IUserRepository:  IRepository<User, long> 
+{
+  ...
+  Task<List<string>> GetUserNames();
+  ...
+}
+
+ +

Here is the store procedure that is called:

+ +
+USE [PhoneBookDb]
+GO
+
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE PROCEDURE [dbo].[GetUsernames] 
+AS
+BEGIN
+	SET NOCOUNT ON;
+	SELECT UserName FROM AbpUsers
+END
+GO
+
+ +

Now we implemented the functon that calls stored procedure from database. Let's use it in application service:

+ +
+public class UserAppService : AsyncCrudAppService<User, UserDto, long, PagedResultRequestDto, CreateUserDto, UserDto>, IUserAppService
+{
+    private readonly IUserRepository _userRepository;
+	
+    public UserAppService(..., IUserRepository userRepository)
+        : base(repository)
+    {
+        ...
+        _userRepository = userRepository;
+    }
+    
+    ...
+    
+     public async Task<List<string>> GetUserNames()
+    {
+        return await _userRepository.GetUserNames();
+    }
+}
+
+ +

Here is another example that sends a parameter to a stored procedure to delete a user:

+ +
+public async Task DeleteUser(EntityDto input)
+{
+await Context.Database.ExecuteSqlCommandAsync(
+    "EXEC DeleteUserById @id",
+    default(CancellationToken),
+    new SqlParameter("id", input.Id)
+);}
+
+ +

Stored procedure that is called for deletion:

+ +
+USE [PhoneBookDb]
+GO
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE PROCEDURE [dbo].[DeleteUserById] 
+	@id int  
+AS
+BEGIN
+	SET NOCOUNT ON;
+	DELETE FROM AbpUsers WHERE [Id] = @id
+END
+GO
+
+ + +

And another example that sends a parameter to update a user's email address:

+
+public async Task UpdateEmail(UpdateEmailDto input)
+{
+await Context.Database.ExecuteSqlCommandAsync(
+    "EXEC UpdateEmailById @email, @id",
+    default(CancellationToken),
+    new SqlParameter("id", input.Id),
+    new SqlParameter("email", input.EmailAddress)
+);
+}
+
+ +

Stored procedure that is called for update method:

+ +
+USE [PhoneBookDb]
+GO
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+CREATE PROCEDURE [dbo].[UpdateEmailById]
+@email nvarchar(256),
+@id int
+
+AS
+BEGIN
+	SET NOCOUNT ON;
+	UPDATE AbpUsers SET [EmailAddress] = @email WHERE [Id] = @id
+END
+
+GO
+
+ + +

View

+ +

You can call a view like that:

+
+public async Task<List<string>> GetAdminUsernames()
+{
+    EnsureConnectionOpen();
+    using (var command = CreateCommand("SELECT * FROM dbo.UserAdminView", CommandType.Text))
+    {
+        using (var dataReader = await command.ExecuteReaderAsync())
+        {
+            var result = new List<string>();
+            while (dataReader.Read())
+            {
+                result.Add(dataReader["UserName"].ToString());
+            }
+            return result;
+        }
+    }
+}
+ 
+ +

View for this method:

+ +
+SELECT        *
+FROM            dbo.AbpUsers
+WHERE        (Name = 'admin')
+
+ + +

User Defined Function

+ +

You can call a User Defined Function like that:

+ +
+public async Task<GetUserByIdOutput> GetUserById(EntityDto input)
+{
+    EnsureConnectionOpen();
+    
+    using (var command = CreateCommand("SELECT dbo.GetUsernameById(@id)", CommandType.Text, new SqlParameter("@id", input.Id)))
+    {
+        var username = (await command.ExecuteScalarAsync()).ToString();
+        return new GetUserByIdOutput() { Username = username };
+    }
+}
+
+ +

User Defined Function for this method:

+ +
+USE [PhoneBookDb]
+GO
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+CREATE FUNCTION [dbo].[GetUsernameById] 
+	@id int
+)
+RETURNS nvarchar(32)
+AS
+BEGIN
+	DECLARE @username nvarchar(32)
+	SELECT @username = [UserName] FROM AbpUsers WHERE [ID] = @id
+	RETURN @username
+END
+
+GO
+ + +

Source Code

+ +

You can get the latest source code https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/StoredProcedureDemo

\ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/solutionExplorer.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/solutionExplorer.png" new file mode 100644 index 0000000..ceb3f26 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Articles/Using-Stored-Procedures,-User-Defined-Functions-and-Views/solutionExplorer.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/AspNet-Core.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/AspNet-Core.md" new file mode 100644 index 0000000..bd98ef9 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/AspNet-Core.md" @@ -0,0 +1,252 @@ +### Introduction + +This document describes the ASP.NET Core integration for ASP.NET Boilerplate. +The ASP.NET Core integration is implemented in the +[Abp.AspNetCore](https://www.nuget.org/packages/Abp.AspNetCore) NuGet +package. + +#### Migrating to ASP.NET Core? + +If you have an existing project and are considering migrating to ASP.NET +Core, you can read our [blog +post](http://volosoft.com/migrating-from-asp-net-mvc-5x-to-asp-net-core/) +about our experience on migrating. + +### Startup Template + +You can create your project from a [startup template](/Templates), which +is a simple, empty web project. It is properly integrated and configured to +work with the ABP framework. + +### Configuration + +#### Startup Class + +To integrate ABP to ASP.NET Core, we need to make some changes in the +Startup class: + + public class Startup + { + public IServiceProvider ConfigureServices(IServiceCollection services) + { + //... + + //Configure Abp and Dependency Injection. Should be called last. + return services.AddAbp(options => + { + //Configure Log4Net logging (optional) + options.IocManager.IocContainer.AddFacility( + f => f.UseLog4Net().WithConfig("log4net.config") + ); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + //Initializes ABP framework and all modules. Should be called first. + app.UseAbp(); + + //... + } + } + +#### Module Configuration + +You can use the [startup configuration](Startup-Configuration.md) to +configure the AspNetCore Module by using +*Configuration.Modules.AbpAspNetCore()* in the PreInitialize method of your +module. + +### Controllers + +Controllers can be any type of class in ASP.NET Core and are not +restricted to classes derived from the Controller class. By default, if a class +ends with Controller (like ProductController), it's considered an MVC +Controller. You can also add MVC's \[Controller\] attribute to any class +to make it a controller. This is the way ASP.NET Core MVC handles things. +See the ASP.NET Core [documentation](https://docs.asp.net) for more info. + +If you end up using the web layer classes (like HttpContext), or return a view, +it's better to inherit from **AbpController** which is derived from +MVC's Controller class. If you are creating an API controller that just +works with objects, consider creating a POCO controller class, +or use your application services as controllers as described below. + +#### Application Services as Controllers + +ASP.NET Boilerplate provides the infrastructure to create [application +services](Application-Services.md). If you want to expose your +application services to remote clients as controllers (as previously +done using [dynamic web api](Dynamic-Web-API.md)), you can easily do +that with a simple configuration in the [PreInitialize](Module-System.md) +method of your module. Example: + + Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices(typeof(MyApplicationModule).Assembly, moduleName: 'app', useConventionalHttpVerbs: true); + +The **CreateControllersForAppServices** method gets an assembly and converts +all the application services to MVC controllers in that assembly. You can +use the **RemoteService** attribute to enable or disable it for the class or it's methods. + +When an application service is converted to an MVC Controller, it's default +route will look like this: +*/api/services/<module-name>/<service-name>/<method-name>*. +For example, if ProductAppService defines a Create method, it's URL will be +**/api/services/app/product/create** (assuming that the module name is +'app'). + +If **useConventionalHttpVerbs** is set to **true** (which is the **default +value**), then the HTTP verbs for the service methods are determined by the following **naming +conventions**: + +- **Get**: Used if the method name starts with '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 'Post', 'Create' or + 'Insert'. +- **Patch**: Used if the method name starts with 'Patch'. +- Otherwise, **Post** is used **by default** as an HTTP verb. + +You can use any ASP.NET Core attributes to change the HTTP methods or routes +of the actions. This requires you to add a reference to the +Microsoft.AspNetCore.Mvc.Core package. + +**Note**: Previously, the dynamic web api system required you to create +service **interfaces** for application services. This is not +required for the ASP.NET Core integration. The MVC attributes should be +added to the service classes, even if you have interfaces. + +### Filters + +ABP defines some **pre-built filters** for ASP.NET Core. All of them are +added to **all actions of all controllers** by default. + +#### Authorization Filter + +**AbpAuthorizationFilter** is used to integrate the [authorization system](Authorization.md) and [feature +system](Feature-Management.md). + +- Use the **AbpMvcAuthorize** attribute on actions or + controllers to check the desired permissions before the action execution. +- Use the the **RequiresFeature** attribute on actions or + controllers to check for the desired features before the action execution. +- Use the **AllowAnonymous** (or AbpAllowAnonymous in + application layer) attribute on actions or controllers to suppress + authentication/authorization. + +#### Audit Action Filter + +**AbpAuditActionFilter** is used to integrate with the [audit logging +system](Audit-Logging.md). If auditing is not disabled, it logs all requests +to all actions by default . You can control audit logging +by using the **Audited** and **DisableAuditing** attributes on actions and +controllers. + +#### Validation Action Filter + +**AbpValidationActionFilter** is used to integrate with the [validation +system](Validating-Data-Transfer-Objects.md). It automatically +validates all the inputs of all actions. In addition to ABP's built-in +validation & normalization, it also checks MVC's **Model.IsValid** +property and throws a validation exception if the action input values are invalid. + +You can control validation using the **EnableValidation** and +**DisableValidation** attributes on actions and controllers. + +#### Unit of Work Action Filter + +**AbpUowActionFilter** integrates with the [Unit of +Work](Unit-Of-Work.md) system. It automatically begins a new unit of +work before an action execution, and if no exception is thrown, completes the unit of +work after the action execution. + +You can use the **UnitOfWork** attribute to control the behaviour of the UOW for an +action. You can also use a startup configuration to change the default unit of +work attribute for all actions. + +#### Exception Filter + +**AbpExceptionFilter** is used to handle exceptions thrown from +controller actions. It **handles** and **logs** exceptions and returns a +**wrapped response** to the client. + +- **This only handles object results**, and not view results. Actions + returning any object, JsonResult or ObjectResult will be handled. + Actions are not handled if they return a view or any other result type implementing + IActionsResult. It is recommend that you use the built-in UseExceptionHandler extension + method defined in the Microsoft.AspNetCore.Diagnostics package to handle view exceptions. +- Exception handling and logging behaviour can be changed using the + **WrapResult** and **DontWrapResult** attributes for methods and + classes. + +#### Result Filter + +**AbpResultFilter** is mainly used to wrap the result action if the action is +successfully executed. + +- It only wraps results for JsonResult, ObjectResult and any object + which does not implement IActionResult (and also their async + versions). If your action is returning a view or any other type of + result, it will not be wrapped. +- The **WrapResult** and **DontWrapResult** attributes can be used for + methods and classes to enable/disable wrapping. +- You can use a startup configuration to change the default behavior for + result wrapping. + +##### Result Caching For Ajax Requests + +AbpResultFilter adds a **Cache-Control** header (no-cache, no-store...) to +the response of AJAX Requests. Thus, it prevents browser caching of +AJAX responses even for GET requests. This behavior can be disabled by +configuration or attributes. You can use the **NoClientCache** attribute +to prevent caching (default) or **AllowClientCache** attrbiute to allow the +browser to cache results. Alternatively, you can implement +IClientCacheAttribute to create a custom attribute for finer +control. + +### Model Binders + +**AbpDateTimeModelBinder** is used to normalize DateTime (and +Nullable<DateTime>) inputs using the **Clock.Normalize** method. + +### Views + +MVC Views can be inherited from **AbpRazorPage** to automatically inject the +most commonly used infrastructure (LocalizationManager, PermissionChecker, +SettingManager... etc.). It also has shortcut methods, like L(...) for +localized texts. The startup template inherits this by default. + +You can inherit your web components from **AbpViewComponent** instead of +ViewComponent to take advantage of it's base properties and methods. + +### Client Proxies + +ABP can automatically create JavaScript proxies for all MVC Controllers +(not only application services). It's created for *Application Services +as Controllers* (see the section above) by default. You can add the +\[RemoteService\] attribute to any MVC controller to create a client proxy +for it. JavaScript proxies are dynamically generated on runtime. You +need to add a given script definition to your page: + + + +Currently, only jQuery proxies are generated. We can then call an MVC +method with JavaScript as shown below: + + abp.services.app.product.create({ + name: 'My test product', + price: 99 + }).done(function(result){ + //... + }); + +### Integration Testing + +Integration testing is fairly easy for ASP.NET Core and it's [documented on +it's own web +site](https://docs.asp.net/en/latest/testing/integration-testing.html) +in detail. ABP follows these guidelines and provides a +**AbpAspNetCoreIntegratedTestBase** class in the +[Abp.AspNetCore.TestBase](https://www.nuget.org/packages/Abp.AspNetCore.TestBase) +package. It makes integration testing even easier. + +Start by investigating the integration tests in the startup template to see it in action. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Audit-Logging.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Audit-Logging.md" new file mode 100644 index 0000000..8adf347 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Audit-Logging.md" @@ -0,0 +1,129 @@ +### Introduction + +Wikipedia: "*An audit trail (also called audit log) is a +security-relevant chronological record, set of records, and/or +destination and source of records that provide documentary evidence of +the sequence of activities that have affected at any time a specific +operation, procedure, or event*". + +ASP.NET Boilerplate provides the infrastructure to automatically log all +interactions within the application. It can record intended method calls +with caller info and arguments. + +Basically, the saved fields are: Related **tenant id**, caller **user id**, +called **service name** (the class of the called method), called +**method name**, execution **parameters** (serialized into JSON), +**execution time**, execution **duration** (in milliseconds), the client's +**IP address**, the client's **computer name** and the **exception** (if +the method throws an exception). + +With this information, we not just know who did the operation, but we can also +measure the **performance** of the application and observe the +**exceptions** thrown. Furthermore, you can get **statistics** about the usage +of your application. + +The auditing system uses [**IAbpSession**](/Pages/Documents/Abp-Session) to +get the current UserId and TenantId. + +The Application Service, MVC Controller, Web API and ASP.NET Core methods +are automatically audited by default. + +#### About IAuditingStore + +The auditing system uses **IAuditingStore** to +save audit information. While you can implement it in your own way, +it's fully implemented in the **Module Zero** project. If you don't +implement it, SimpleLogAuditingStore is used and it writes audit +information to the [log](/Pages/Documents/Logging). + +### Configuration + +To configure auditing, you can use the **Configuration.Auditing** property +in your [module](/Pages/Documents/Module-System)'s PreInitialize method. +Auditing is **enabled by default**. You can disable it as shown below: + + public class MyModule : AbpModule + { + public override void PreInitialize() + { + Configuration.Auditing.IsEnabled = false; + } + + //... + } + +Here are the auditing configuration properties: + +- **IsEnabled**: Used to enable/disable the auditing system completely. + Default: **true**. +- **IsEnabledForAnonymousUsers**: If this is set to true, audit logs + are saved for users that are not logged in to the system. + Default: **false**. +- **Selectors**: Used to select other classes to save audit logs. + +**Selectors** is a list of predicates to select other types of classes that save +audit logs. A selector has a unique **name** and a **predicate**. The +only **default** selector in this list is used to select **application +service classes**. It's defined as shown below: + + Configuration.Auditing.Selectors.Add( + new NamedTypeSelector( + "Abp.ApplicationServices", + type => typeof (IApplicationService).IsAssignableFrom(type) + ) + ); + +You can add your selectors in your module's PreInitialize method. +You can also remove the selector above by name if you don't want to save +audit logs for application services. This is why it has a unique name +(Use simple LINQ to find the selector in Selectors and remove it if you +want). + +Note: In addition to the standard audit configuration, MVC and ASP.NET Core +modules define configurations to enable/disable audit logging for +actions. + +### Enable/Disable by attributes + +While you can select auditing classes by configuration, you can use the +**Audited** and **DisableAuditing** attributes for a single **class** or an +individual **method**. Example: + + [Audited] + public class MyClass + { + public void MyMethod1(int a) + { + //... + } + + [DisableAuditing] + public void MyMethod2(string b) + { + //... + } + + public void MyMethod3(int a, int b) + { + //... + } + } + +All methods of MyClass are audited except MyMethod2 since it's +explicitly disabled. The Audited attribute can be used to +save audits for the desired method. + +**DisableAuditing** can also be used for a single **property of a +DTO**. Thus, you can **hide sensitive data** in audit logs, such as +passwords for example. + +### Notes + +- A method must be **public** in order to be saved in audit logs. Private + and protected methods are ignored. +- A method must be **virtual** if it's called over class reference. + This is not needed if it's injected using its interface (like + injecting the IPersonService interface to use the PersonService class). This + is needed since ASP.NET Boilerplate uses dynamic proxying and + interception. This is not true for **MVC** Controller actions. They + may not be virtual. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Authorization.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Authorization.md" new file mode 100644 index 0000000..29c8fa2 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Authorization.md" @@ -0,0 +1,205 @@ +### Introduction + +Almost all enterprise applications use authorization at some level. +Authorization is used to check if a user is allowed to perform some +specific operation in the application. ASP.NET Boilerplate defines a +**permission based** infrastructure to implement authorization. + +#### About IPermissionChecker + +The Authorization system uses **IPermissionChecker** to check permissions. +While you can implement it in your own way, it's fully implemented in the +**Module Zero** project. If it's not implemented, NullPermissionChecker +is used which grants all permissions to everyone. + +### Defining Permissions + +A unique **permission** is defined for each operation that needs to be +authorized. We need to define a permission before it is used. ASP.NET +Boilerplate is designed to be [modular](/Pages/Documents/Module-System), +so different modules can have different permissions. A module should +create a class derived from **AuthorizationProvider** in order to define +it's permissions. An example authorization provider is shown below: + + public class MyAuthorizationProvider : AuthorizationProvider + { + public override void SetPermissions(IPermissionDefinitionContext context) + { + var administration = context.CreatePermission("Administration"); + + var userManagement = administration.CreateChildPermission("Administration.UserManagement"); + userManagement.CreateChildPermission("Administration.UserManagement.CreateUser"); + + var roleManagement = administration.CreateChildPermission("Administration.RoleManagement"); + } + } + + +**IPermissionDefinitionContext** has methods to get and create +permissions. + +A permission is defined with these properties: + +- **Name**: a system-wide **unique** name. It's a good idea to define a + const string for a permission name instead of a magic string. We + prefer to use . (dot) notation for hierarchical names but it's not + required. You can set any name you like. The only rule is that it must + be unique. +- **Display name**: A localizable string that can be used to show the + permission later in UI. +- **Description**: A localizable string that can be used to show the + definition of the permission later in UI. +- **MultiTenancySides**: For the multi-tenant application, a permission + can be used by tenants or the host. This is a **Flags** enumeration + and thus a permission can be used on both sides. +- **featureDependency**: Can be used to declare a dependency to + [features](/Pages/Documents/Feature-Management). Thus, this + permission can be granted only if the feature dependency is satisfied. + It waits for an object implementing IFeatureDependency. The default + implementation is the SimpleFeatureDependency class. Example usage: + `new SimpleFeatureDependency("MyFeatureName")` + +Permissions can have parent and child permissions. While this does +not affect permission checking, it helps to group the permissions in the UI. + +After creating an authorization provider, we should register it in the +PreInitialize method of our module: + + Configuration.Authorization.Providers.Add(); + +Authorization providers are registered to [dependency +injection](/Pages/Documents/Dependency-Injection) automatically. An +authorization provider can inject any dependency (like a repository) to +build permission definitions using some other sources. + +### Checking Permissions + +#### Using AbpAuthorize Attribute + +The **AbpAuthorize** (**AbpMvcAuthorize** for MVC Controllers and +**AbpApiAuthorize** for Web API Controllers) attribute is the easiest +and most common way of checking permissions. Consider the [application +service](/Pages/Documents/Application-Services) method shown below: + + [AbpAuthorize("Administration.UserManagement.CreateUser")] + public void CreateUser(CreateUserInput input) + { + //A user can not execute this method if he is not granted the "Administration.UserManagement.CreateUser" permission. + } + +The CreateUser method can not be called by a user who is not granted the +permission "*Administration.UserManagement.CreateUser*". + +The AbpAuthorize attribute also checks if the current user is logged in (using +[IAbpSession.UserId](/Pages/Documents/Abp-Session)). If we declare +an AbpAuthorize for a method, it only checks for the login: + + [AbpAuthorize] + public void SomeMethod(SomeMethodInput input) + { + //A user can not execute this method if he did not login. + } + +##### AbpAuthorize attribute notes + +ASP.NET Boilerplate uses the power of dynamic method interception for +authorization. There are some restrictions for the methods using the +AbpAuthorize attribute. + +- It cannot be used for private methods. +- It cannot be used for static methods. +- You can not use it for methods of a non-injected class (We must use + [dependency injection](/Pages/Documents/Dependency-Injection)). + +Also: + +- You can use it for any **public** method if the method is called over an + **interface** (like Application Services used over interface). +- A method should be **virtual** if it's called directly from a class + reference (like ASP.NET MVC or Web API Controllers). +- A method should be **virtual** if it's **protected**. + +**Note**: There are four types of authorize attributes: + +- In an application service (application layer), we use the + **Abp.Authorization.AbpAuthorize** attribute. +- In an MVC controller (web layer), we use the + **Abp.Web.Mvc.Authorization.AbpMvcAuthorize** attribute. +- In ASP.NET Web API, we use the + **Abp.WebApi.Authorization.AbpApiAuthorize** attribute. +- In ASP.NET Core, we use the + **Abp.AspNetCore.Mvc.Authorization.AbpMvcAuthorize** attribute. + +This difference comes from inheritance. In the application layer it's +completely ASP.NET Boilerplate's implementation and it does not extend any +class. For MVC and Web API, it inherits from the Authorize attributes +of those frameworks. + +##### Suppress Authorization + +You can disable authorization for a method/class by adding +**AbpAllowAnonymous** attribute to application services. Use the +**AllowAnonymous** attribute for MVC, Web API and ASP.NET Core Controllers, which +is a native attribute of these frameworks. + +#### Using IPermissionChecker + +While the AbpAuthorize attribute is good enough for most cases, there are +situations where we may want to check for a permission in a method's body. We can +inject and use **IPermissionChecker** for that as shown in the example +below: + + public void CreateUser(CreateOrUpdateUserInput input) + { + if (!PermissionChecker.IsGranted("Administration.UserManagement.CreateUser")) + { + throw new AbpAuthorizationException("You are not authorized to create user!"); + } + + //A user can not reach this point if he is not granted for "Administration.UserManagement.CreateUser" permission. + } + +You can code any logic since **IsGranted** simply returns true +or false (It has an Async version, too). If you simply check a permission +and throw an exception as shown above, you can use the **Authorize** +method: + + public void CreateUser(CreateOrUpdateUserInput input) + { + PermissionChecker.Authorize("Administration.UserManagement.CreateUser"); + + //A user can not reach this point if he is not granted for "Administration.UserManagement.CreateUser" permission. + } + +Since authorization is widely used, **ApplicationService** and some +common base classes inject and define the PermissionChecker property. Thus, +permission checker can be used without injecting application service +classes. + +#### In Razor Views + +The base view class defines the IsGranted method to check if the current user has +permission. Thus, we can conditionally render the view. Example: + + @if (IsGranted("Administration.UserManagement.CreateUser")) + { + + } + +#### Client Side (JavaScript) + +In the client side, we can use the API defined in the **abp.auth** namespace. In +most cases, we need to check if the current user has a specific permission +(with permission name). Example: + + abp.auth.isGranted('Administration.UserManagement.CreateUser'); + +You can also use **abp.auth.grantedPermissions** to get all granted +permissions or **abp.auth.allPermissions** to get all available +permission names in the application. Check **abp.auth** namespace on +runtime for others. + +### Permission Manager + +We may need the definitions of permissions. **IPermissionManager** can be +[injected](/Pages/Documents/Dependency-Injection) and used in this case. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Background-Jobs-And-Workers.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Background-Jobs-And-Workers.md" new file mode 100644 index 0000000..8a9e89f --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Background-Jobs-And-Workers.md" @@ -0,0 +1,357 @@ +### Introduction + +ASP.NET Boilerplate provides background jobs and workers that are used +to execute some tasks in the **background threads** of an application. + +### Background Jobs + +In a **queued and persistent** manner, background jobs are used to queue some tasks to be executed in the +background. You may need background jobs for several reasons. Here are some examples: + +- To perform **long-running tasks** without having the users wait. For example, a + user presses a 'report' button to start a long-running reporting + job. You add this job to the **queue** and send the report's result to + your user via email when it's completed. +- To create **re-trying** and **persistent tasks** to **guarantee** code will be **successfully + executed**. For example, you can send emails in a background job to + overcome **temporary failures** and **guarantee** that it + eventually will be sent. That way users do not wait while sending + emails. + +#### About Job Persistence + +See the *Background Job Store* section for more information on job +persistence. + +#### Create a Background Job + +We can create a background job class by either inheriting from the +**BackgroundJob<TArgs>** class or by directly implementing the +**IBackgroundJob<TArgs> interface**. + +Here is the most simple background job: + + public class TestJob : BackgroundJob, ITransientDependency + { + public override void Execute(int number) + { + Logger.Debug(number.ToString()); + } + } + +A background job defines an **Execute** method that gets an input +**argument**. The argument **type** is defined as a **generic** class +parameter as shown in the example. + +A background job must be registered via [dependency +injection](/Pages/Documents/Dependency-Injection). Implementing +**ITransientDependency** is the simplest way. + +Let's define a more realistic job which sends emails in a background +queue: + + public class SimpleSendEmailJob : BackgroundJob, ITransientDependency + { + private readonly IRepository _userRepository; + private readonly IEmailSender _emailSender; + + public SimpleSendEmailJob(IRepository userRepository, IEmailSender emailSender) + { + _userRepository = userRepository; + _emailSender = emailSender; + } + + [UnitOfWork] + public override void Execute(SimpleSendEmailJobArgs args) + { + var senderUser = _userRepository.Get(args.SenderUserId); + var targetUser = _userRepository.Get(args.TargetUserId); + + _emailSender.Send(senderUser.EmailAddress, targetUser.EmailAddress, args.Subject, args.Body); + } + } + +We +[injected](/Pages/Documents/Dependency-Injection#constructor-injection-pattern) the +user [repository](/Pages/Documents/Repositories) to get user emails, +and injected the email sender (a service to send emails) and simply sent the email. +**SimpleSendEmailJobArgs** is the job argument here and defined as shown +below: + + [Serializable] + public class SimpleSendEmailJobArgs + { + public long SenderUserId { get; set; } + + public long TargetUserId { get; set; } + + public string Subject { get; set; } + + public string Body { get; set; } + } + +A job argument should be **serializable**, because it's **serialized and +stored** in the database. While ASP.NET Boilerplate's default background +job manager uses **JSON** serialization (which does not need the +\[Serializable\] attribute), it's better to define the **\[Serializable\]** +attribute since we may switch to another job manager in the future, for which we may use +something like .NET's built-in binary serialization. + +**Keep your arguments simple** (like +[DTO](Data-Transfer-Objects.md)s), do not include +[entities](/Pages/Documents/Entities) or other non-serializable objects. +As shown in the SimpleSendEmailJob sample, we can only store the **Id** of +an entity and get the entity from the repository inside the job. + +#### Add a New Job To the Queue + +After defining a background job, we can inject and use +**IBackgroundJobManager** to add a job to the queue. See this example for +TestJob as defined above: + + public class MyService + { + private readonly IBackgroundJobManager _backgroundJobManager; + + public MyService(IBackgroundJobManager backgroundJobManager) + { + _backgroundJobManager = backgroundJobManager; + } + + public void Test() + { + _backgroundJobManager.Enqueue(42); + } + } + +We sent 42 as an argument while enqueuing. IBackgroundJobManager will +instantiate and execute the TestJob with 42 as the argument. + +Let's add a new job for SimpleSendEmailJob, as we defined before: + + [AbpAuthorize] + public class MyEmailAppService : ApplicationService, IMyEmailAppService + { + private readonly IBackgroundJobManager _backgroundJobManager; + + public MyEmailAppService(IBackgroundJobManager backgroundJobManager) + { + _backgroundJobManager = backgroundJobManager; + } + + public async Task SendEmail(SendEmailInput input) + { + await _backgroundJobManager.EnqueueAsync( + new SimpleSendEmailJobArgs + { + Subject = input.Subject, + Body = input.Body, + SenderUserId = AbpSession.GetUserId(), + TargetUserId = input.TargetUserId + }); + } + } + +Enqueue (or EnqueueAsync) method has other parameters such as +**priority** and **delay**. + +#### Default Background Job Manager + +IBackgroundJobManager is implemented by **BackgroundJobManager**, +by default. It can be replaced by another background job provider (see +[hangfire integration](Hangfire-Integration.md)). Some information on the +default BackgroundJobManager: + +- It's a simple job queue that works as **FIFO** in a **single thread**. It + uses **IBackgroundJobStore** to persist jobs (see the next section). +- It **retries** job execution until the job **successfully runs** (if it does + not throw any exceptions, but logs them) or **timeouts**. Default + timeout is 2 days for a job. +- It **deletes** a job from the store (database) when it's successfully + executed. If it's timed out, it sets it as **abandoned**, and leaves it in the + database. +- It **increasingly waits between retries** for a job. It waits 1 minute + for the first retry, 2 minutes for the second retry, 4 minutes for the third + retry and so on. +- It **polls** the store for jobs in fixed intervals. It queries jobs, + ordering by priority (asc) and then by try count (asc). + +##### Background Job Store + +The default BackgroundJobManager needs a data store to save and get jobs. If +you do not implement **IBackgroundJobStore** then it uses +**InMemoryBackgroundJobStore** which does not save jobs in a persistent +database. You can simply implement it to store jobs in a database or you +can use **[Module Zero](/Pages/Documents/Zero/Overall)** which already +implements it. + +If you are using a 3rd party job manager (like +[Hangfire](Hangfire-Integration.md)), there is no need to implement +IBackgroundJobStore. + +#### Configuration + +You can use **Configuration.BackgroundJobs** in the +[PreInitialize](/Pages/Documents/Module-System) method of your module to +configure the background job system. + +##### Disabling Job Execution + +You may want to disable background job execution for your application: + + public class MyProjectWebModule : AbpModule + { + public override void PreInitialize() + { + Configuration.BackgroundJobs.IsJobExecutionEnabled = false; + } + + //... + } + +This is rarely needed. An example of this is if you're running multiple +instances of your application working on the same database (in a web +farm). In this case, each application will query the same database for jobs +and execute them. This leads to multiple executions of the same jobs and +other problems. To prevent it, you have two options: + +- You can enable job execution for only one instance of the + application. +- You can disable job execution for all instances of the web + application and create a separated, standalone application (example: + a Windows Service) that executes background jobs. + +#### Exception Handling + +Since the default background job manager should re-try failed jobs, it +handles (and logs) all exceptions. In case you want to be informed when +an exception occurred, you can create an event handler to handle +[AbpHandledExceptionData](Handling-Exceptions.md). The background manager +triggers this event with a BackgroundJobException exception object which +wraps the real exception (get InnerException for the actual exception). + +#### Hangfire Integration + +The background job manager is designed to be **replaceable** by another +background job manager. See [hangfire integration +document](/Pages/Documents/Hangfire-Integration) to replace it with +[**Hangfire**](http://hangfire.io/). + +### Background Workers + +Background workers are different than background jobs. They are simple +**independent threads** in the application running in the background. +Generally, they run **periodically** to perform some tasks. Examples; + +- A background worker can run **periodically** to **delete old logs**. +- A background worker can run **periodically** to **determine inactive users** and send emails to get users to return to your application. + +#### Create a Background Worker + +To create a background worker, we implement the **IBackgroundWorker** +interface. Alternatively, we can inherit from the **BackgroundWorkerBase** +or **PeriodicBackgroundWorkerBase** based on our needs. + +Assume that we want to make a user passive, if he did not login to the +application in last 30 days. See the code: + + public class MakeInactiveUsersPassiveWorker : PeriodicBackgroundWorkerBase, ISingletonDependency + { + private readonly IRepository _userRepository; + + public MakeInactiveUsersPassiveWorker(AbpTimer timer, IRepository userRepository) + : base(timer) + { + _userRepository = userRepository; + Timer.Period = 5000; //5 seconds (good for tests, but normally will be more) + } + + [UnitOfWork] + protected override void DoWork() + { + using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var oneMonthAgo = Clock.Now.Subtract(TimeSpan.FromDays(30)); + + var inactiveUsers = _userRepository.GetAllList(u => + u.IsActive && + ((u.LastLoginTime < oneMonthAgo && u.LastLoginTime != null) || (u.CreationTime < oneMonthAgo && u.LastLoginTime == null)) + ); + + foreach (var inactiveUser in inactiveUsers) + { + inactiveUser.IsActive = false; + Logger.Info(inactiveUser + " made passive since he/she did not login in last 30 days."); + } + + CurrentUnitOfWork.SaveChanges(); + } + } + } + +This real code directly works in ASP.NET Boilerplate with +[Module Zero](/Pages/Documents/Zero/Overall). + +- If you derive from **PeriodicBackgroundWorkerBase** (as in this + sample), you should implement the **DoWork** method to perform your + periodic working code. +- If you derive from the **BackgroundWorkerBase** or directly implement + **IBackgroundWorker**, you will override/implement the **Start**, + **Stop** and **WaitToStop** methods. Start and Stop methods should + be **non-blocking**, the WaitToStop method should **wait** for the worker to + finish its current critical job. + +#### Register Background Workers + +After creating a background worker, add it to the +**IBackgroundWorkerManager**. The most common place is the PostInitialize +method of your module: + + public class MyProjectWebModule : AbpModule + { + //... + + public override void PostInitialize() + { + var workManager = IocManager.Resolve(); + workManager.Add(IocManager.Resolve()); + } + } + +While we generally add workers in PostInitialize, there are no +restrictions on that. You can inject IBackgroundWorkerManager anywhere +and add workers at runtime. IBackgroundWorkerManager will stop and +release all registered workers when your application is being shut down. + +#### Background Worker Lifestyles + +Background workers are generally implemented as a **singleton**, but there +are no restrictions to this. If you need multiple instances of the same worker class, +you can make it transient and add more than one instance to the +IBackgroundWorkerManager. In this case, your worker will probably be +parametric (say that you have a single LogCleaner class but two +LogCleaner worker instances and they watch and clear different log folders). + +#### Advanced Scheduling + +ASP.NET Boilerplate's background worker systems are simple. It does not have a +schedule system, except for periodic running workers as demonstrated above. +If you need more advanced scheduling features, we suggest you +check out [Quartz](Quartz-Integration.md) or another library. + +### Making Your Application Always Run + +Background jobs and workers only work if your application is running. +An ASP.NET application **shuts down** by default if no request is +performed to the web application for a long period of time. So, if you host the +background job execution in your web application (this is the default +behavior), you should ensure that your web application is configured to always +be running. Otherwise, background jobs only work while your +application is in use. + +There are some techniques to accomplish that. The most simple way is to make +periodic requests to your web application from an external application. +Thus, you can also check if your web application is up and running. The +[Hangfire +documentation](http://docs.hangfire.io/en/latest/deployment-to-production/making-aspnet-app-always-running.html) +explains some other ways to accomplish this. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Caching.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Caching.md" new file mode 100644 index 0000000..b778a9c --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Caching.md" @@ -0,0 +1,258 @@ +### Introduction + +ASP.NET Boilerplate provides an abstraction for caching. It internally +uses this cache abstraction. While the default implementation uses +[MemoryCache](https://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx?f=255&MSPPError=-2147217396), +it can be implemented and swapped out with any other caching provider. +The [Abp.RedisCache](https://www.nuget.org/packages/Abp.RedisCache) package +implements cache using Redis, for instance (see the "Redis Cache Integration" +section below). + +### ICacheManager + +The main interface for caching is **ICacheManager**. We can +[inject](/Pages/Documents/Dependency-Injection) it and use it to get a +cache. Example: + + public class TestAppService : ApplicationService + { + private readonly ICacheManager _cacheManager; + + public TestAppService(ICacheManager cacheManager) + { + _cacheManager = cacheManager; + } + + public Item GetItem(int id) + { + //Try to get from cache + return _cacheManager + .GetCache("MyCache") + .Get(id.ToString(), () => GetFromDatabase(id)) as Item; + } + + public Item GetFromDatabase(int id) + { + //... retrieve item from database + } + } + +In this example, we're injecting **ICacheManager** and getting a cache +named **MyCache**. Cache names are case sensitive, that means "MyCache" +and "MYCACHE" are two different caches. + +### ICache + +The ICacheManager.**GetCache** method returns an **ICache**. A cache is a +singleton (per cache name). It is created the first time it's requested, +and then the same cache object is always returned. This way we can share the same cache +with the same name in different classes (clients). + +In the sample code, we see a simple usage of the ICache.**Get** method. It has +two arguments: + +- **key**: A unique key (string) of an item in the cache. +- **factory**: An action which is called if there is no item with the + given key. The Factory method should create and return the actual item. + This is not called if the given key is present in the cache. + +The ICache interface also has methods like **GetOrDefault**, **Set**, +**Remove** and **Clear**. There are also **async** versions for all +methods. + +#### ITypedCache + +The **ICache** interface uses a **string** as the key and an **object** as the value. +**ITypedCache** is a wrapper to ICache to provide a **type safe**, generic +cache. We can use the generic GetCache extension method to get an +ITypedCache: + + ITypedCache myCache = _cacheManager.GetCache("MyCache"); + +We can also use the **AsTyped** extension method to convert an existing +ICache instance to ITypedCache. + +### Configuration + +The default cache expiration time is 60 minutes. It's sliding, so if you don't +use an item in the cache for 60 minutes, it's automatically removed from +the cache. You can configure it for all caches or for a specific cache. + + //Configuration for all caches + Configuration.Caching.ConfigureAll(cache => + { + cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2); + }); + + //Configuration for a specific cache + Configuration.Caching.Configure("MyCache", cache => + { + cache.DefaultSlidingExpireTime = TimeSpan.FromHours(8); + }); + +This code should be placed in the +[**PreInitialize**](/Pages/Documents/Module-System#preinitialize) +method of your module. With this code, "MyCache" will expire in 8 hours +while all other cache items will expire in 2 hours. + +Your configuration action is called once the cache is first created (on +first request). Configuration is not restricted to +DefaultSlidingExpireTime only, since the cache object is an ICache, you can +use it's properties and methods to freely configure and initialize it. + +### Entity Caching + +While ASP.NET Boilerplate's cache system is for general purposes, there is an +**EntityCache** base class that can help you if you want to cache +entities. We can use this base class if we get entities by their Ids and +we want to **cache them by Id**, so as to not query from the database repeatedly. +Assume that we have a Person entity like that: + + public class Person : Entity + { + public string Name { get; set; } + + public int Age { get; set; } + } + +Assume that we frequently want to get the **Name** of people while we +know their **Id**. First, we create a class to store **cache +items**: + + [AutoMapFrom(typeof(Person))] + public class PersonCacheItem + { + public string Name { get; set; } + } + +**Do not directly store entities in the cache**, since caching may +need to **serialize** cached objects. Entities may not be serialized, +especially if they have navigation properties. That's why we defined a +simple ([DTO](Data-Transfer-Objects.md)) class to store data in +the cache. We added the **AutoMapFrom** attribute since we want to use +AutoMapper to automatically convert the Person entities to the PersonCacheItem objects. +If we don't use AutoMapper, we should **override the +MapToCacheItem** method of the EntityCache class to manually convert/map it. + +While it's **not required**, we may want to define an interface for our +cache class: + + public interface IPersonCache : IEntityCache + { + + } + +Finally, we can create the cache class to cache Person entities: + + public class PersonCache : EntityCache, IPersonCache, ITransientDependency + { + public PersonCache(ICacheManager cacheManager, IRepository repository) + : base(cacheManager, repository) + { + + } + } + +That's it. Our person cache is ready to use! Cache class can be +transient (as in this example) or a singleton. This does not mean the +cached data is transient. It's always cached globally and accessed in a +thread-safe manner in your application. + +Whenever we need the **Name** of a person, we can get it from the cache by using +the person's **Id**. Here's an example class that uses the Person cache: + + public class MyPersonService : ITransientDependency + { + private readonly IPersonCache _personCache; + + public MyPersonService(IPersonCache personCache) + { + _personCache = personCache; + } + + public string GetPersonNameById(int id) + { + return _personCache[id].Name; //alternative: _personCache.Get(id).Name; + } + } + +We simply [injected](Dependency-Injection.md) IPersonCache, got the +cache item and then got the Name property. + +#### How EntityCache Works + +- It gets the entity from the repository (the database) in it's first call. It then + gets from the cache in subsequent calls. +- It automatically invalidates a cached entity if this entity is updated + or deleted. Thus, it will be retrieved from the database in the next + call. +- It uses IObjectMapper to map an entity to a cache item. IObjectMapper is + implemented by the AutoMapper module. You need the [AutoMapper + module](/Pages/Documents/Data-Transfer-Objects) + if you are using it. You can override the MapToCacheItem method to + manually map an entity to a cache item. +- It uses the cache class's FullName as a cache name. You can change it by + passing a cache name to the base constructor. +- It's thread-safe. + +If you need more complex caching requirements, you can extend +EntityCache or create your own solution. + +### Redis Cache Integration + +The default cache manager uses **in-memory** caches. It can turn in to a problem +if you have more than one concurrent web server running the same +application. In that case, you may want a **distributed/central +cache** server. You can easily use Redis as your cache server. + +First, you need to install the +[**Abp.RedisCache**](https://www.nuget.org/packages/Abp.RedisCache) +NuGet package to your application (you can install it to your Web +project, for example). Then you need to add a **DependsOn** attribute +for the **AbpRedisCacheModule** and call the **UseRedis** extension method in the +**PreInitialize** method of your [module](Module-System.md), as shown +below: + + //...other namespaces + using Abp.Runtime.Caching.Redis; + + namespace MyProject.AbpZeroTemplate.Web + { + [DependsOn( + //...other module dependencies + typeof(AbpRedisCacheModule))] + public class MyProjectWebModule : AbpModule + { + public override void PreInitialize() + { + //...other configurations + + Configuration.Caching.UseRedis(); + } + + //...other code + } + } + +The Abp.RedisCache package uses "**localhost**" as the **connection string** by +default. You can add a connection string to your config file to override +it. Example: + + + +Also, you can add a setting to appSettings to set the database id of Redis. +Example: + + + +Different database ids are useful to create different key spaces +(isolated caches) in same server. + +The **UseRedis** method also has an overload that takes an action to +directly set option values (this overrides values in the config file). + +See the [Redis documentation](http://redis.io/documentation) for more +information on Redis and it's configuration. + +**Note**: The Redis server should be installed and running to use the Redis +cache in ABP. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Change-Logs.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Change-Logs.md" new file mode 100644 index 0000000..a9f5bfd --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Change-Logs.md" @@ -0,0 +1,3 @@ +See the GitHub repository for all release & change logs: + + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dapper-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dapper-Integration.md" new file mode 100644 index 0000000..dad1b3e --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dapper-Integration.md" @@ -0,0 +1,93 @@ +### Introduction + +[Dapper](https://github.com/StackExchange/Dapper) is an +object-relational mapper (ORM) for .NET. +The [Abp.Dapper](https://www.nuget.org/packages/Abp.Dapper) package simply +integrates Dapper to ASP.NET Boilerplate. It works as a secondary ORM +provider along with EF 6.x, EF Core or NHibernate. + +### Installation + +Before you start, you need to install +[Abp.Dapper](https://www.nuget.org/packages/Abp.Dapper) and either EF +Core, EF 6.x or the NHibernate ORM NuGet packages in to the project you want +to use. + +#### Module Registration + +First you need to add the **DependsOn** attribute for the **AbpDapperModule** on +your module where you register it: + + [DependsOn( + typeof(AbpEntityFrameworkCoreModule), + typeof(AbpDapperModule) + )] + public class MyModule : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(typeof(SampleApplicationModule).GetAssembly()); + } + } + +**Note** that the AbpDapperModule dependency should be added later than the EF +Core dependency. + +#### Entity to Table Mapping + +You can configure mappings. For example, the **Person** class maps to the +**Persons** table in the following example: + + public class PersonMapper : ClassMapper + { + public PersonMapper() + { + Table("Persons"); + Map(x => x.Roles).Ignore(); + AutoMap(); + } + } + +You should set the assemblies that contain mapper classes. Example: + + [DependsOn( + typeof(AbpEntityFrameworkModule), + typeof(AbpDapperModule) + )] + public class MyModule : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(typeof(SampleApplicationModule).GetAssembly()); + DapperExtensions.SetMappingAssemblies(new List { typeof(MyModule).GetAssembly() }); + } + } + + +### Usage + +After registering **AbpDapperModule**, you can use the Generic +IDapperRepository interface (instead of standard IRepository) to inject +dapper repositories. + + public class SomeApplicationService : ITransientDependency + { + private readonly IDapperRepository _personDapperRepository; + private readonly IRepository _personRepository; + + public SomeApplicationService( + IRepository personRepository, + IDapperRepository personDapperRepository) + { + _personRepository = personRepository; + _personDapperRepository = personDapperRepository; + } + + public void DoSomeStuff() + { + var people = _personDapperRepository.Query("select * from Persons"); + } + } + +You can use both EF and Dapper repositories at the same +time and in the same transaction! diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Filters.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Filters.md" new file mode 100644 index 0000000..e6dbf7c --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Filters.md" @@ -0,0 +1,312 @@ +### Introduction + +It's common to use the +[**soft-delete**](/Pages/Documents/Entities#soft-delete) pattern which +is used to not actually delete an entity from database but to only mark it as +'deleted'. If an entity is soft-deleted, it should not be accidentally +retrieved into the application. To provide for that, we would have to add an SQL +**where** condition like 'IsDeleted = false' in every query where we select +entities. This is not only tedious, but is more importantly a forgettable task. To keep +things DRY, there should be an automatic way to do this. + +ASP.NET Boilerplate provides **data filters** that can be used to +automatically filter queries based on some rules. There are some +pre-defined filters, but you can also create your own. + +### Pre-Defined Filters + +#### ISoftDelete + +This soft-delete filter is used to automatically filter (extract from the +results) deleted entities while querying the database. If an +[entity](/Pages/Documents/Entities) must be soft-deleted, it must +implement the **ISoftDelete** interface which defines the **IsDeleted** +property. Example: + + public class Person : Entity, ISoftDelete + { + public virtual string Name { get; set; } + + public virtual bool IsDeleted { get; set; } + } + +A **Person** entity is not actually deleted from the database, instead, the +**IsDeleted** property is set to true. This is +done automatically by ASP.NET Boilerplate when you use the +**[IRepository.Delete](/Pages/Documents/Repositories#delete)** +method (you can manually set IsDeleted to true, but the Delete method is the +more natural and preferred way). + +When you get a list of People entities that implement ISoftDelete from the +database, deleted people are not retrieved. Here is an example class that +uses a person repository to get all people: + + public class MyService + { + private readonly IRepository _personRepository; + + public MyService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public List GetPeople() + { + return _personRepository.GetAllList(); + } + } + +The GetPeople method only returns the Person entities where IsDeleted = false +(not deleted). All repository methods and navigation properties +properly work. We could add some other Where conditions, joins.. etc. +It will automatically add IsDeleted = false condition properly to the +generated SQL query. + +#### When is ISoftDelete enabled? + +The ISoftDelete filter is always enabled unless you explicitly disable it. + +**A side note**: If you implement +[IDeletionAudited](/Pages/Documents/Entities#soft-delete) (which +extends ISoftDelete) then the deletion time and user id that deleted it are also +automatically set by ASP.NET Boilerplate. + +#### IMustHaveTenant + +If you are building multi-tenant applications and store all tenant data +in single database, you definitely do not want a tenant accidentally seeing +other tenants' data. You can implement **IMustHaveTenant** in that case. +Example: + + public class Product : Entity, IMustHaveTenant + { + public int TenantId { get; set; } + + public string Name { get; set; } + } + +**IMustHaveTenant** defines the **TenantId** property to distinguish between +different tenant entities. ASP.NET Boilerplate uses the +[IAbpSession](/Pages/Documents/Abp-Session) to get the current TenantId by +default and automatically filters the query for the current tenant. + +#### When is IMustHaveTenant enabled? + +IMustHaveTenant is enabled by default. + +If the current user is not logged in to the system or the current user is a +**host** user (Host user is an upper-level user that can manage tenants +and tenant data), ASP.NET Boilerplate automatically **disables** the +IMustHaveTenant filter. Thus, all data of all tenants can be retrieved +to the application. Notice that this is not about security, you should +always [authorize](/Pages/Documents/Authorization) sensitive data! + +#### IMayHaveTenant + +If an entity class is shared by tenants **and** the host (that means an entity +object may be owned by a tenant or the host), you can use the IMayHaveTenant +filter. The **IMayHaveTenant** interface defines **TenantId** but it's +**nullable**. + + public class Role : Entity, IMayHaveTenant + { + public int? TenantId { get; set; } + + public string RoleName { get; set; } + } + +A **null** value means this is a **host** entity, a **non-null** value +means this entity is owned by a **tenant** in which the Id is the TenantId. +ASP.NET Boilerplate uses the [IAbpSession](/Pages/Documents/Abp-Session) to +get the current TenantId by default. The IMayHaveTenant filter is not as common as +IMustHaveTenant, but you may need it for **common entitiy +types** used by both the host and tenants. + +#### When is IMayHaveTenant enabled? + +IMayHaveTenant is always enabled unless you explicitly disable it. + +### Disable Filters + +You can disable a filter per [unit of +work](/Pages/Documents/Unit-Of-Work) by calling the **DisableFilter** method +as shown below: + + var people1 = _personRepository.GetAllList(); + + using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) + { + var people2 = _personRepository.GetAllList(); + } + + var people3 = _personRepository.GetAllList(); + +The DisableFilter method gets one or more filter names as strings. +AbpDataFilters.SoftDelete is a constant string that contains the name of the +standard soft delete filter of ASP.NET Boilerplate. + +**people2** will also include deleted people while people1 and people3 +will only return non-deleted people. With the **using** statement, you can +disable a filter in a **scope**. If you don't use the using stamement, +the filter will be disabled until the end of the current unit of work, or until you +enable it again explicitly. + +You can inject **IUnitOfWorkManager** and use it as in the example. Also, +you can use the the **CurrentUnitOfWork** property as a shortcut if your class +inherits some special base classes (like ApplicationService, +AbpController, AbpApiController...). + +#### About the using Statement + +If a filter is enabled when you call the DisableFilter method with a +using statement, the filter is disabled. It is then automatically re-enabled +after the using statement. If the filter was already disabled before the +using statement, DisableFilter does nothing and the filter +remains disabled even after the using statement. + +#### About Multi-Tenancy + +You can disable tenancy filters to query all tenant data. Note +that this only works for a single-database approach. If you have +separated databases for each tenant, disabling the filter does not help to +query all the data of all tenants since they are in different databases, +or even on different servers. See the [Multi-Tenancy +document](Multi-Tenancy.md) for more information. + +#### Disable Filters Globally + +If you need to, you can disable pre-defined filters globally. For example, +to disable the soft-delete filter globally, add this code to the PreInitialize +method of your module: + + Configuration.UnitOfWork.OverrideFilter(AbpDataFilters.SoftDelete, false); + +### Enable Filters + +You can enable a filter in a unit of work using the **EnableFilter** method, +which is similar to (and opposite of) DisableFilter. EnableFilter also returns a +disposable to be used in a **using** statement to automatically +re-disable the filter if needed. + +### Setting Filter Parameters + +A filter can be **parametric**. The IMustHaveTenant filter is an example of +these types of filters since the current tenant's Id is determined at +runtime. For such filters, we can change the filter value if needed. +Example: + + CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42); + +Here's an example on how to set the tenantId value for the IMayHaveTenant +filter: + + CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42); + +The SetFilterParameter method also returns an IDisposable. So, we can use it +in a **using** statement to automatically **restore the old value** +after the using statement. + +#### SetTenantId Method + +While you can use the SetFilterParameter method to change the filter value for the +MayHaveTenant and MustHaveTenant filters, there is a better way to +change the tenant filter: **SetTenantId()**. SetTenantId changes the parameter +value for both filters, and also works for single database and database +per tenant approaches. **It is highly recommended that you use SetTenantId** +to change tenancy filter parameter values. See the [Multi-Tenancy +document](Multi-Tenancy.md) for more information. + +### ORM Integrations + +Data filtering for pre-defined filters works for +[NHibernate](NHibernate-Integration.md), [Entity +Framework](EntityFramework-Integration.md) 6.x and [Entity Framework +Core](Entity-Framework-Core.md). Currently, you can only define custom +filters for Entity Framework 6.x. + +#### Entity Framework + +For [Entity Framework integration](EntityFramework-Integration.md), +automatic data filtering is implemented using the +**[EntityFramework.DynamicFilters](https://github.com/jcachat/EntityFramework.DynamicFilters)** +library. + +To create a custom filter for Entity Framework and integrate it into ASP.NET +Boilerplate, first we need to define an interface that will be +implemented by entities which use this filter. Assume that we want to +automatically filter entities by PersonId. Example interface: + + public interface IHasPerson + { + int PersonId { get; set; } + } + +We can then implement this interface for the needed entities. Example +entity: + + public class Phone : Entity, IHasPerson + { + [ForeignKey("PersonId")] + public virtual Person Person { get; set; } + public virtual int PersonId { get; set; } + + public virtual string Number { get; set; } + } + +We use it's rules to define the filter. In our **DbContext** class, we +override **OnModelCreating** and define a filter as shown below: + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, 0); + } + +"PersonFilter" is the unique name of the filter here. The second parameter +defines the filter interface and personId filter parameter (not needed if +the filter is not parametric). The last parameter is the default value of the +personId. + +Finally, we must register this filter to ASP.NET Boilerplate's +unit of work system in the PreInitialize method of our +[module](/Pages/Documents/Module-System): + + Configuration.UnitOfWork.RegisterFilter("PersonFilter", false); + +The first parameter is the same unique name we defined before. The second parameter +indicates whether this filter is enabled or disabled by default. After +declaring such a parametric filter, we can use it by supplying it's +value at runtime. + + using (CurrentUnitOfWork.EnableFilter("PersonFilter")) + { + using(CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42)) + { + var phones = _phoneRepository.GetAllList(); + //... + } + } + +We could get the personId from some source instead of it being statically coded. +The example above was for parametric filters. A filter can have zero or +more parameters. If it has no parameter, you don't need to set the +filter parameter value. Also, if it's enabled by default, there is no need to +enable it manually (you can always disable this). + +#### Documentation for EntityFramework.DynamicFilters + +For more information on dynamic data filters, see the documentation on EF's +github page: + +Custom filters can be created for security, active/passive entities and +so on. + +#### Other ORMs + +For [Entity Framework Core](Entity-Framework-Core.md) and NHibernate, +data filtering is implemented in the [repository](Repositories.md) +level. This means it only filters when you query over repositories. + +Note: If you directly use DbContext (for EF Core) or query via custom SQL, +you have to handle the filtering yourself. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Transfer-Objects.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Transfer-Objects.md" new file mode 100644 index 0000000..5a27afb --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Data-Transfer-Objects.md" @@ -0,0 +1,226 @@ +**Data Transfer Objects** are used to transfer data between the +**Application Layer** and the **Presentation Layer**. + +The Presentation Layer calls an [Application +Service](/Pages/Documents/Application-Services) method with a Data +Transfer Object (**DTO**). The application service then uses these domain objects +to perform some specific business logic, and then finally returns a DTO back to the +presentation layer. Thus, the Presentation layer is completely isolated from the +Domain layer. In an ideally layered application, the presentation layer never +works with domain objects, +([Repositories](/Pages/Documents/Repositories), or +[Entities](/Pages/Documents/Entities)...). + +### The need for DTOs + +At first, creating a DTO class for each Application Service method can be seen as +tedious and time-consuming work. However, they can save your application if you +correctly use them. Why? + +#### Abstraction of the domain layer + +DTOs provide an efficient way of abstracting domain objects from the +presentation layer. In effect, your layers are correctly separated. If +you want to change the presentation layer completely, you can continue with +the existing application and domain layers. Alternatively, you can re-write +your domain layer, completely change the database schema, entities and O/RM +framework, all without changing the presentation layer. This, of course, is +as long as the contracts (method signatures and DTOs) of your application +services remain unchanged. + +#### Data hiding + +Say you have a User entity with the properties Id, Name, EmailAddress +and Password. If the GetAllUsers() method of UserAppService returns a +List<User>, anyone can see the passwords of all your users, even if you do not +show it on the screen. It's not just about security, it's about data +hiding. Application services should return to the presentation layer what it +needs. Not more, not less. + +#### Serialization & lazy load problems + +When you return data (an object) to the presentation layer, it's most likely +serialized. For example, in an MVC method that returns +JSON, your object can be serialized to JSON and sent to the client. +Returning an Entity to the presentation layer can be problematic in that +regard. How? + +In a real-world application, your entities will have references to each other. +The User entity can have a reference to it's Roles. If you want to +serialize User, its Roles are also serialized. The Role class may +have a List<Permission> and the Permission class can have a reference +to a PermissionGroup class and so on... Imagine all of these objects being +serialized at once. You could easily and accidentally serialize your whole database! +Also, if your objects have circular references, they can **not** be serialized. + +What's the solution? Marking properties as NonSerialized? No, you can +not know when it should be serialized and when it shouldn't be. It may +be needed in one application service method, and not needed in other. +Returning safe, serializable, and specially designed DTOs is a good +choice in this situation. + +Almost all O/RM frameworks support lazy-loading. It's a feature that loads +entities from the database when they're needed. Say a User class has a reference +to a Role class. When you get a User from the database, the Role property is not +filled. When you first read the Role property, it's loaded from the database. +So, if you return such an Entity to the presentation layer, it +will cause it to retrieve additional entities from the database. If a +serialization tool reads the entity, it reads all properties recursively +and again your whole database can be retrieved (if there are suitable, +related properties between entities). + +More problems can arise if you use Entities in the presentation +layer. It's best not to reference the domain/business layer assembly in the +presentation layer. + +### DTO conventions & validation + +ASP.NET Boilerplate strongly supports DTOs. It provides +conventional classes, interfaces, and standardizes DTO naming and usage +conventions. When you write your code as described here in the documentation, +ASP.NET Boilerplate will easily automate some common tasks.  + +#### Example + +Here's a prime example. Say that we want to develop an application +service method that is used to **search people** by name and then return a +**list of people**. In that case, we may have a **Person** +[entity](/Pages/Documents/Entities) as shown below: + + public class Person : Entity + { + public virtual string Name { get; set; } + public virtual string EmailAddress { get; set; } + public virtual string Password { get; set; } + } + +We then define an interface for our [application +service](/Pages/Documents/Application-Services): + + public interface IPersonAppService : IApplicationService + { + SearchPeopleOutput SearchPeople(SearchPeopleInput input); + } + +ASP.NET Boilerplate suggests naming input/output parameters as +MethodName**Input** and MethodName**Output** and defining a separated +input and output DTO for every application service method. Even if your +method only takes and returns **one** parameter, it's better to create a DTO +class. In turn, your code will be more extensible. You can add more +properties later without changing the signature of your method and without +breaking existing client applications. + +Your method can return **void** if there is no return value. It +will not break existing applications if you add a return value later. If +your method does not use any arguments, you do not have to define an +input DTO. Keep in mind that it maybe better to write an input DTO class if you +think that you'll add parameters in the future. This is up to you. + +Here's how the input and output DTO classes are defined in this example: + + public class SearchPeopleInput + { + [StringLength(40, MinimumLength = 1)] + public string SearchedName { get; set; } + } + + public class SearchPeopleOutput + { + public List People { get; set; } + } + + public class PersonDto : EntityDto + { + public string Name { get; set; } + public string EmailAddress { get; set; } + } + +ASP.NET Boilerplate **automatically validates** the input before the execution +of a method. It's similar to ASP.NET MVC's model validation, but note that the +application service is not a Controller, it's a plain old C\# class. ASP.NET +Boilerplate makes an interception and checks the input automatically. See the [DTO +validation](/Pages/Documents/Validating-Data-Transfer-Objects) document for more info. + +**EntityDto** is a simple class that declares an **Id property**, since they +are common for most entities. It has a generic version if your entity's primary +key is not int. You don't have to use it, but it can be better to define an +Id property. + +As you can see, **PersonDto** does not include a Password property since it's +not needed for the presentation layer. It can be dangerous to send +peoples' passwords to the presentation layer! If a JavaScript client +requested it, anyone can easily grab the passwords from the result. + +Let's implement **IPersonAppService** before go further: + + public class PersonAppService : IPersonAppService + { + private readonly IPersonRepository _personRepository; + + public PersonAppService(IPersonRepository personRepository) + { + _personRepository = personRepository; + } + + public SearchPeopleOutput SearchPeople(SearchPeopleInput input) + { + //Get entities + var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName)); + + //Convert to DTOs + var peopleDtoList = peopleEntityList + .Select(person => new PersonDto + { + Id = person.Id, + Name = person.Name, + EmailAddress = person.EmailAddress + }).ToList(); + + return new SearchPeopleOutput { People = peopleDtoList }; + } + } + +Here we get entities from the database, **convert** them to DTOs and then +return an output. Notice that we didn't validate the input? ASP.NET Boilerplate +**validate**s it. It even checks **if input parameter is null** and if so, an +exception is thrown. This saves us from writing guard clauses in every +method! + +You're probably thinking, "Won't I have to write a whole bunch of code to +move data between the Person entity and the PersonDto object?" +It's really a tedious work! The Person entity could have +many more properties. + +### Auto mapping between DTOs and entities + +Fortunately, there are some tools that make this very easy! +**[AutoMapper](http://automapper.org/)** is one of them. See the [AutoMapper +Integration](Object-To-Object-Mapping.md) document to learn how to use +it in ASP.NET Boilerplate. + +### Helper interfaces and classes + +ASP.NET Boilerplate provides some helper interfaces that can be implemented to +standardize common DTO property names. + +**ILimitedResultRequest** defines a **MaxResultCount** property. This way, +you can implement it in your input DTOs to standardize the limiting result set. + +**IPagedResultRequest** extends **ILimitedResultRequest** by adding +**SkipCount**. Let's implement this interface in SearchPeopleInput +for paging: + + public class SearchPeopleInput : IPagedResultRequest + { + [StringLength(40, MinimumLength = 1)] + public string SearchedName { get; set; } + + public int MaxResultCount { get; set; } + public int SkipCount { get; set; } + } + + +As a result of a paged request, you can return an output DTO that +implements **IHasTotalCount**. Naming standardization helps us to create +re-usable code and conventions. Check out the interfaces and classes under the +**Abp.Application.Services.Dto** namespace for more info. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Debugging.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Debugging.md" new file mode 100644 index 0000000..a53047a --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Debugging.md" @@ -0,0 +1,15 @@ +While it's not generally needed, you may want to step into ABP's source +code while you are debugging your project. + +All official ASP.NET Boilerplate NuGet packages are +**[Sourcelink](https://github.com/ctaggart/SourceLink)** enabled. That +means you can easily debug **Abp.\*** NuGet packages within your +project. To enable it, change Visual Studio (2017+) Debugging options as +follows: + +Enable Sourcelink for Visual Studio + +Once you enable it, you can step into (F11) the ASP.NET Boilerplate source +code. + +  diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dependency-Injection.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dependency-Injection.md" new file mode 100644 index 0000000..acb1bc6 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dependency-Injection.md" @@ -0,0 +1,504 @@ +### What is Dependency Injection? + +If you already know the Dependency Injection, Constructor and +Property Injection pattern concepts, you can skip to the [next +section](#abpInfrastructure). + +Wikipedia says: "*Dependency injection is a software design pattern in +which one or more dependencies (or services) are injected, or passed by +reference, into a dependent object (or client) and are made part of the +client's state. The pattern separates the creation of a client's +dependencies from its own behavior, which allows program designs to be +loosely coupled and to follow the dependency inversion and single +responsibility principles. It directly contrasts the service locator +pattern, which allows clients to know about the system they use to find +dependencies.*". + +It's very hard to manage dependencies and develop a modular and +well-structured application without using dependency injection techniques. + +#### Problems of the Traditional Way + +In an application, classes depend on each other. Assume that we have an +[application service](/Pages/Documents/Application-Services) that uses a +[repository](/Pages/Documents/Entities) to insert +[entities](/Pages/Documents/Entities) into a database. In this situation, +the application service class is dependent on the repository class. See +the following example: + + public class PersonAppService + { + private IPersonRepository _personRepository; + + public PersonAppService() + { + _personRepository = new PersonRepository(); + } + + public void CreatePerson(string name, int age) + { + var person = new Person { Name = name, Age = age }; + _personRepository.Insert(person); + } + } + + +**PersonAppService** uses **PersonRepository** to insert a **Person** into +the database. Although this looks harmless, there are some problems with this code: + +- PersonAppService uses the **IPersonRepository** reference for the + **CreatePerson** method. This method depends on the + IPersonRepository interface instead of the PersonRepository concrete class. + In the constructor, however, the PersonAppService depends on the PersonRepository + rather than the interface. Components should depend on interfaces rather than concrete + implementations. This is known as the Dependency Inversion principle. +- If the PersonAppService creates the PersonRepository itself, it + becomes dependent on a specific implementation of the IPersonRepository + interface. This can not work with other implementations. Thus, + separating the interface from the implementation becomes meaningless. + Hard-dependencies make the code base tightly-coupled, making reusability negligent. +- We may need to change the creation of PersonRepository in the future. + Say we want to make it a singleton (single shared instance rather + than creating an object for each use). Or we may want to create + more than one class that implements IPersonRepository and want to + create one of them conditionally. In this situation, we would have to + change all the classes that depend on IPersonRepository. +- With such a dependency, it's very hard (or impossible) to unit test + the PersonAppService. + +To overcome some of these problems, the factory pattern can be used. Thus, +the creation of the repository class is abstracted. See the code below: + + public class PersonAppService + { + private IPersonRepository _personRepository; + + public PersonAppService() + { + _personRepository = PersonRepositoryFactory.Create(); + } + + public void CreatePerson(string name, int age) + { + var person = new Person { Name = name, Age = age }; + _personRepository.Insert(person); + } + } + + +PersonRepositoryFactory is a static class that creates and returns an +IPersonRepository. This is known as the **Service Locator** pattern. +Creation problems are solved since PersonAppService does not know how to +create an implementation of IPersonRepository and it's independent from +the PersonRepository implementation. There are still a few problems: + +- At this time, PersonAppService depends on PersonRepositoryFactory. + This is more acceptable, but there is still a hard-dependency. +- It's tedious to write a factory class/method for each repository or + for each dependency. +- Again, it's not easy to test, since it's hard to make + PersonAppService use a mock implementation of + IPersonRepository. + +#### Solution + +There are some best practices (patterns) to help us depend on other classes. + +##### Constructor Injection Pattern + +The example above can be re-written as shown below: + + public class PersonAppService + { + private IPersonRepository _personRepository; + + public PersonAppService(IPersonRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(string name, int age) + { + var person = new Person { Name = name, Age = age }; + _personRepository.Insert(person); + } + } + + +This is known as **constructor injection**. PersonAppService does +not know which classes implement IPersonRepository or how it is created. +When an PersonAppService is needed, we first create an IPersonRepository +and pass it to the constructor of the PersonAppService: + + var repository = new PersonRepository(); + var personService = new PersonAppService(repository); + personService.CreatePerson("John Doe", 32); + +Constructor Injection is a great way of making a class independent to the +creation of dependent objects, but there are some problems with the code +above: + +- Creating a PersonAppService becomes harder. It has 4 dependencies. + We must create these 4 dependent objects and pass them + into the constructor of the PersonAppService. +- Dependent classes may have other dependencies (Here, + PersonRepository has dependencies). We have to create all the + dependencies of PersonAppService, all the dependencies of these dependencies + and so on and so forth.. We might not even be able to create a single object + because the dependency graph is too complex! + +Fortunately, there are [Dependency Injection +frameworks](#dIFrameworks), which automate the management of dependencies. + +##### Property Injection pattern + +The constructor injection pattern is a great way of providing the dependencies +of a class. In this way, you can not create an instance of the class +without supplying dependencies. It's also a strong way of explicitly +declaring what the requirements are of the class so that it can work properly. + +In some situations the class may depend on another class, but can +work without it. This is usually true for cross-cutting concerns such as +logging. A class can work without logging, but it can write logs if you +supply a logger to it. In this case, you can define dependencies as public +properties rather than getting them in constructor. Think about how we would write +to logs in PersonAppService. We can re-write the class like this: + + public class PersonAppService + { + public ILogger Logger { get; set; } + + private IPersonRepository _personRepository; + + public PersonAppService(IPersonRepository personRepository) + { + _personRepository = personRepository; + Logger = NullLogger.Instance; + } + + public void CreatePerson(string name, int age) + { + Logger.Debug("Inserting a new person to database with name = " + name); + var person = new Person { Name = name, Age = age }; + _personRepository.Insert(person); + Logger.Debug("Successfully inserted!"); + } + } + +The NullLogger.Instance is a singleton object that implements ILogger, but +it doesn't do anything. It does not write logs. It implements ILogger with +empty method bodies. PersonAppService can then write logs if you +set the Logger property after creating the PersonAppService object: + + var personService = new PersonAppService(new PersonRepository()); + personService.Logger = new Log4NetLogger(); + personService.CreatePerson("John Doe", 32); + +Assume that Log4NetLogger implements ILogger and it writes logs using the +Log4Net library so that PersonAppService can actually write logs. If we +do not set the Logger, it does not write logs. We can say that ILogger is +an **optional dependency** of PersonAppService. + +Almost all Dependency Injection frameworks support the Property Injection +pattern. + +##### Dependency Injection frameworks + +There are many dependency injection frameworks that automate resolving +dependencies. They can create objects with all the dependencies, and the +dependencies of dependencies, recursively. Simply write your classes +with the constructor & property injection patterns, and the DI framework will handle the +rest! In a good application, your classes are independent even from the DI +framework. There will only be a few lines of code or classes that explicitly +interact with the DI framework in your whole application. + +ASP.NET Boilerplate uses the [Castle +Windsor](https://github.com/castleproject/Windsor/blob/master/docs/README.md) +framework for Dependency Injection. It's one of the most mature DI +frameworks out there. There are many other frameworks, such as Unity, Ninject, +StructureMap, and Autofac. + +In a dependency injection framework, you first register your +interfaces/classes to the dependency injection framework, and then +resolve (create) an object. In Castle Windsor, it's something like that: + + var container = new WindsorContainer(); + + container.Register( + Component.For().ImplementedBy().LifestyleTransient(), + Component.For().ImplementedBy().LifestyleTransient() + ); + + var personService = container.Resolve(); + personService.CreatePerson("John Doe", 32); + +First, we created the **WindsorContainer** and **registered** +PersonRepository and PersonAppService with their interfaces. We then +used the container to create an IPersonAppService. It created the concrete class +PersonAppService with it's dependencies and then returned it. In this simple +example, it may not be clear what the advantages are of using a DI framework. +You will, however, have many classes and dependencies in a real enterprise +application. The registration of dependencies are separated from the creation and use of +objects, and is made only once during the application's startup. + +Note that we also set the **life cycle** of the objects as **transient**. +This means that whenever we resolve an object of these types, a new instance +is created. There are many different life cycles, such as the **singletion**, +for example. + +### ASP.NET Boilerplate Dependency Injection Infrastructure + +ASP.NET Boilerplate makes using the dependency injection framework almost +invisible. It also helps you write your application by following the best +practices and conventions. + +#### Registering Dependencies + +There are different ways of registering your classes to the Dependency +Injection system in ASP.NET Boilerplate. Most of time, conventional +registration will be sufficient. + +##### Conventional Registrations + +ASP.NET Boilerplate automatically registers all +[Repositories](/Pages/Documents/Repositories), [Domain +Services](/Pages/Documents/Domain-Services), [Application +Services](/Pages/Documents/Application-Services), MVC Controllers and +Web API Controllers by convention. For example, you may have a +IPersonAppService interface and a PersonAppService class that implements +it: + + public interface IPersonAppService : IApplicationService + { + //... + } + + public class PersonAppService : IPersonAppService + { + //... + } + +ASP.NET Boilerplate automatically registers it since it implements the +**IApplicationService** interface (it's just an empty interface). It is +registered as **transient**, meaning it is created each time, per use. When you +inject (using constructor injection) the IPersonAppService interface into a +class, a PersonAppService object will be created and passed into the constructor, +automatically. + +**Naming conventions** are very important here. For example, you can +change the name of PersonAppService to MyPersonAppService or another name +which contains the 'PersonAppService' postfix. This registers it to IPersonAppService +because it has the same postfix. You can not, however, name your service without the postfix, +such as 'PeopleService'. If you do so, it's not registered to the IPersonAppService automatically. Instead, it's +registered to the DI framework using self-registration (not the +interface). In this case, you can manually register it. + +ASP.NET Boilerplate can register assemblies by convention. It's pretty +easy: + + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + +Assembly.GetExecutingAssembly() gets a reference to the assembly which +contains this code. You can pass other assemblies to the +RegisterAssemblyByConvention method. This is generally done when your +module is being initialized. See ASP.NET Boilerplate's [module +system](/Pages/Documents/Module-System) for more info. + +You can write your own conventional registration class by implementing the +**IConventionalRegisterer** interface and then calling the +**IocManager.AddConventionalRegisterer** method in your class. You +should add it in pre-initialize method of your module. + +##### Helper Interfaces + +You may want to register a specific class that does not fit into the +conventional registration rules. ASP.NET Boilerplate provides the +**ITransientDependency**, the **IPerWebRequestDependency** and the **ISingletonDependency** interfaces as a +shortcut. For example: + + public interface IPersonManager + { + //... + } + + public class MyPersonManager : IPersonManager, ISingletonDependency + { + //... + } + +In this way, you can easily register MyPersonManager. When you need to +inject IPersonManager, the MyPersonManager class is used. Note that the +dependency is declared as a **Singleton**. A single instance of +MyPersonManager is created and the same object is passed to all needed +classes. It's instantiated in it's first use, and then used in the +whole life of the application. + +**NOTE:** The **IPerWebRequestDependency** can only be used in the web layer. + +##### Custom/Direct Registration + +If conventional registrations are not sufficient for your needs, you +can either use the **IocManager** or **Castle Windsor** to register your +classes and dependencies. + +###### Using IocManager + +You can use the **IocManager** to register dependencies (generally in the +PreInitialize method of your [module definition](Module-System.md) class): + + IocManager.Register(DependencyLifeStyle.Transient); + +Using the Castle Windsor API + +You can use the **IIocManager.IocContainer** property to access the +Castle Windsor Container and register dependencies. Example: + + IocManager.IocContainer.Register(Classes.FromThisAssembly().BasedOn().LifestylePerThread().WithServiceSelf()); + +For more information, read [Windsor's +documentation](https://github.com/castleproject/Home/blob/master/README.md).  + +#### Resolving Dependencies + +Registration informs the IOC (Inversion of Control) Container (a.k.a. the DI +framework) about your classes, their dependencies and lifetimes. +Somewhere in your application you need to create objects using an IOC +Container. ASP.NET Provides a few options for resolving dependencies. + +#### Constructor & Property Injection + +As a **best practice**, you should use constructor and property injection to get the +dependencies to your classes. You should do it this way whenever possible. +Example: + + public class PersonAppService + { + public ILogger Logger { get; set; } + + private IPersonRepository _personRepository; + + public PersonAppService(IPersonRepository personRepository) + { + _personRepository = personRepository; + Logger = NullLogger.Instance; + } + + public void CreatePerson(string name, int age) + { + Logger.Debug("Inserting a new person to database with name = " + name); + var person = new Person { Name = name, Age = age }; + _personRepository.Insert(person); + Logger.Debug("Successfully inserted!"); + } + } + +IPersonRepository is injected from the constructor and ILogger is injected +with a public property. In this way, your code will be unaware of the +dependency injection system at all. This is the most proper way of using +DI system. + +#### IIocResolver, IIocManager and IScopedIocResolver + +You may have to directly resolve your dependency instead of using constructor +& property injection. This should be avoided when possible, but it may +be impossible. ASP.NET Boilerplate provides some services that can be +injected and used easily. Example: + + public class MySampleClass : ITransientDependency + { + private readonly IIocResolver _iocResolver; + + public MySampleClass(IIocResolver iocResolver) + { + _iocResolver = iocResolver; + } + + public void DoIt() + { + //Resolving, using and releasing manually + var personService1 = _iocResolver.Resolve(); + personService1.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" }); + _iocResolver.Release(personService1); + + //Resolving and using in a safe way + using (var personService2 = _iocResolver.ResolveAsDisposable()) + { + personService2.Object.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" }); + } + } + } + +MySampleClass in an example class in an application. It is +constructor-injected with **IIocResolver** and uses it to resolve and release +objects. There are a few overloads of the **Resolve** method which can be used as +needed. The **Release** method is used to release a component (object). It's +**critical** to call Release if you're manually resolving an object. +Otherwise, your application may have memory leaks. To be sure of +releasing the object, use **ResolveAsDisposable** (as shown in the +example above) wherever possible. Release is automatically called at +the end of the using block. + +The IIocResolver (and IIocManager) also have the **CreateScope** extension +method (defined in the Abp.Dependency namespace) to safely release all +resolved dependencies. Example: + + using (var scope = _iocResolver.CreateScope()) + { + var simpleObj1 = scope.Resolve(); + var simpleObj2 = scope.Resolve(); + //... + } + +At the end of using block, all resolved dependencies are automatically +removed. A scope is also injectable using the **IScopedIocResolver**. You +can inject this interface and resolve dependencies. When your class is +released, all resolved dependencies will be released. Use this +carefully! If your class has a long life (say it's a singleton), and you +are resolving too many objects, then all of them will +remain in memory until your class is released. + +If you want to directly reach the IOC Container (Castle Windsor) to +resolve dependencies, you can constructor-inject **IIocManager** and use +the **IIocManager.IocContainer** property. If you are in a static context or +can not inject IIocManager, as a last resort, you can use a singleton +object **IocManager.Instance** everywhere. However, in this case your code +will not be easy to test. + +#### Extras + +##### IShouldInitialize interface + +Some classes need to be initialized before their first usage. +IShouldInitialize has an Initialize() method. If you implement it, then +your Initialize() method is automatically called just after creating +your object (before it's used). You need to inject/resolve the object +in order to work with this feature. + +#### ASP.NET MVC & ASP.NET Web API integration + +We must call the dependency injection system to resolve the root object in +the dependency graph. In an **ASP.NET MVC** application, it's generally a +**Controller** class. We can also use the contructor and property +injection patterns in controllers. When a request gets to our +application, the controller is created using an IOC container and all +dependencies are resolved recursively. What makes this happen? It's all done +automatically by ASP.NET Boilerplate by extending ASP.NET MVC's default +controller factory. This is true for the ASP.NET Web API, too. You don't +have to worry about creating and disposing objects. + +#### ASP.NET Core Integration + +ASP.NET Core already has a built-in dependency injection system with the +[Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) +package. ABP uses the +[Castle.Windsor.MsDependencyInjection](https://www.nuget.org/packages/Castle.Windsor.MsDependencyInjection) +package to integrate it's dependency injection system with ASP.NET +Core's, so you don't have to think about it. + +#### Final notes + +ASP.NET Boilerplate simplifies and automates dependency injection +as long as you follow the rules and use the structures above. Most of the +time you will not need more. If you do need more, you can directly use the +raw power of Castle Windsor to perform many tasks, like custom registrations, +injection hooks, interceptors and so on. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Document-Index.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Document-Index.md" new file mode 100644 index 0000000..35aaf6b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Document-Index.md" @@ -0,0 +1,133 @@ +### ABP Framework + +Overall + +- [Introduction](Introduction.md) +- [Tutorials & Articles](Articles-Tutorials.md) +- [NLayer Architecture](NLayer-Architecture.md) +- [Module System](Module-System.md) +- [Startup Configuration](Startup-Configuration.md) +- [Multi Tenancy](Multi-Tenancy.md) +- [OWIN Integration](OWIN.md) +- [Debugging](Debugging.md) +- [API Reference](/api-docs/index.html) + +Common Structures + +- [Dependency Injection](Dependency-Injection.md) +- [Session](Abp-Session.md) +- [Caching](Caching.md) +- [Logging](Logging.md) +- [Setting Management](Setting-Management.md) +- [Timing](Timing.md) +- [Object To Object Mapping and AutoMapper Integration](Object-To-Object-Mapping.md) +- [Email Sending and MailKit Integration](Email-Sending.md) + +Domain Layer + +- [Entities](Entities.md) +- [Value Objects](Value-Objects.md) +- [Repositories](Repositories.md) +- [Domain Services](Domain-Services.md) +- [Specifications](Specifications.md) +- [Unit Of Work](Unit-Of-Work.md) +- [Domain Events (EventBus)](EventBus-Domain-Events.md) +- [Data Filters](Data-Filters.md) + +Application Layer + +- [Application Services](Application-Services.md) +- [Data Transfer Objects](Data-Transfer-Objects.md) +- [Validating Data Transfer Objects](Validating-Data-Transfer-Objects.md) +- [Authorization](Authorization.md) +- [Feature Management](Feature-Management.md) +- [Audit Logging](Audit-Logging.md) +- [Entity History ](Entity-History.md) + +Distributed Service Layer + +ASP.NET Web API +- [Web API Controllers](Web-API-Controllers.md) +- [Dynamic Web API Layer](Dynamic-Web-API.md) +- [OData Integration](OData-Integration.md) +- [Swagger UI Integration](Swagger-UI-Integration.md) + +Presentation Layer + +ASP.NET MVC +- [MVC Controllers](MVC-Controllers.md) +- [MVC Views](MVC-Views.md) +- [Handling Exceptions](Handling-Exceptions.md) + +ASP.NET Core + +- [ASP.NET Core Integration](AspNet-Core.md) +- [ASP.NET Core OData Integration ](OData-AspNetCore-Integration.md) + + +- [Localization](Localization.md) +- [Navigation](Navigation.md) +- [Alerts](UI-Alerts.md) +- [Embedded Resources](Embedded-Resource-Files.md) +- [Javascript API](/Pages/Documents/Javascript-API) +- [CSRF/XSRF Protection](XSRF-CSRF-Protection.md) + +Background Services + +- [Background Jobs and Workers](Background-Jobs-And-Workers.md) +- [Hangfire Integration](Hangfire-Integration.md) +- [Quartz Integration](Quartz-Integration.md) + +Real Time Services + +- [Notification System](Notification-System.md) +- [SignalR Integration](SignalR-Integration.md) +- [SignalR ASP.NET Core Integration ](SignalR-AspNetCore-Integration.md) + +Object-Relational Mapping + +- [EntityFramework Integration](EntityFramework-Integration.md) +- [EntityFramework Core Integration](Entity-Framework-Core.md) +- [NHibernate Integration](NHibernate-Integration.md) +- [Dapper Integration](Dapper-Integration.md) + +Releases + +- [Nuget Packages](Nuget-Packages.md) +- [Change Logs & Releases](https://github.com/aspnetboilerplate/aspnetboilerplate/releases) + +### Module Zero + +- [Introduction](Zero/Overall.md) + +Startup Templates + + - [ASP.NET Core & Angular](Zero/Startup-Template-Angular.md) + - [ASP.NET Core MVC & jQuery](Zero/Startup-Template-Core.md) + - [ASP.NET MVC 5.x / AngularJS 1.x](Zero/Startup-Template.md) + +Features + + - [Tenant Management](/Pages/Documents/Zero/Tenant-Management) + - [Edition Management](/Pages/Documents/Zero/Edition-Management) + - [User Management](/Pages/Documents/Zero/User-Management) + - [Role Management](/Pages/Documents/Zero/Role-Management) + - [Organization Unit Management](/Pages/Documents/Zero/Organization-Units) + - [Permission Management](/Pages/Documents/Zero/Permission-Management) + - [Language Management](/Pages/Documents/Zero/Language-Management) + - [Identity Server Integration ](Zero/Identity-Server.md) + +Releases + + - [Nuget Packages](/Pages/Documents/Zero/Nuget-Packages) + - [Change Logs & Releases](https://github.com/aspnetboilerplate/module-zero/releases) + + + +### Miscellaneous + +Chinese translations of ABP's documentation (from contributors) + + - [Cnblogs.com/farb](http://www.cnblogs.com/farb/p/ABPTheory.html) + - [Cnblogs.com/mienreal](http://www.cnblogs.com/mienreal/p/4528470.html) + - [GitHub.com/ABPFrameWorkGroup/AbpDocument2Chinese](https://github.com/ABPFrameWorkGroup/AbpDocument2Chinese) \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Domain-Services.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Domain-Services.md" new file mode 100644 index 0000000..6e0519f --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Domain-Services.md" @@ -0,0 +1,218 @@ +### Introduction + +Domain Services (or just Services in DDD) is used to perform domain +operations and business rules. In his DDD book, Eric Evans describes a good +Service in three characteristics: + +1. The **operation** relates to a **domain concept** that is not a + natural part of an Entity or Value Object. +2. The **interface** is defined in terms of other elements of the **domain + model**. +3. The operation is **stateless**. + +Unlike [Application Services](/Pages/Documents/Application-Services) +which get/return [Data Transfer +Objects](/Pages/Documents/Data-Transfer-Objects), a Domain Service +gets/returns **domain objects** (like +[entities](/Pages/Documents/Entities) or value types). + +A Domain Service can be used by Application Services and other Domain +Services, but not directly by the presentation layer (application +services are for that). + +### IDomainService Interface and DomainService Class + +ASP.NET Boilerplate defines the **IDomainService interface** that is +implemented by all domain services conventionally. When it's +implemented, the domain service is **automatically registered** to the +[Dependency Injection](/Pages/Documents/Dependency-Injection) system as +**transient**. + +A domain service can optionally inherit from the **DomainService +class**. With it, you can use the power of some inherited properties for +logging, localization and so on... + +Even if you do not inherit, it can be injected if you need it. + +### Example + +Assume that we have a task management system and we have some business rules +while assigning a task to a person. + +#### Creating an Interface + +First, we define an interface for the service (not required, but good practice): + + public interface ITaskManager : IDomainService + { + void AssignTaskToPerson(Task task, Person person); + } + +As you can see, the **TaskManager** service works with domain objects: a +**Task** and a **Person**. There are some conventions when naming domain +services. It can be TaskManager, TaskService or TaskDomainService... + +#### Service Implementation + +Let's see the implementation: + + public class TaskManager : DomainService, ITaskManager + { + public const int MaxActiveTaskCountForAPerson = 3; + + private readonly ITaskRepository _taskRepository; + + public TaskManager(ITaskRepository taskRepository) + { + _taskRepository = taskRepository; + } + + public void AssignTaskToPerson(Task task, Person person) + { + if (task.AssignedPersonId == person.Id) + { + return; + } + + if (task.State != TaskState.Active) + { + throw new ApplicationException("Can not assign a task to a person when task is not active!"); + } + + if (HasPersonMaximumAssignedTask(person)) + { + throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name)); + } + + task.AssignedPersonId = person.Id; + } + + private bool HasPersonMaximumAssignedTask(Person person) + { + var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id); + return assignedTaskCount >= MaxActiveTaskCountForAPerson; + } + } + +We have two business rules here: + +- A task should be in an **Active state** in order for it to be assigned + to a new Person. +- A person can have a **maximum of 3** active tasks. + +Wondering why we throw an **ApplicationException** for the first check +and **UserFriendlyException** for the second check (see [exception +handling](/Pages/Documents/Handling-Exceptions))? This is not related to +domain services at all. This is just an example, it's completely up +to you. The user interface must check a task's state and +should not allow us to assign it to a person. This is an +application-level error and we may want to hide it from user. + +The second exception is harder to check by the UI so we will show a readable error message to the user. + +For example: + +#### Using the Domain Service from an Application Service + +Now, let's see how to use TaskManager from an application service: + + public class TaskAppService : ApplicationService, ITaskAppService + { + private readonly IRepository _taskRepository; + private readonly IRepository _personRepository; + private readonly ITaskManager _taskManager; + + public TaskAppService(IRepository taskRepository, IRepository personRepository, ITaskManager taskManager) + { + _taskRepository = taskRepository; + _personRepository = personRepository; + _taskManager = taskManager; + } + + public void AssignTaskToPerson(AssignTaskToPersonInput input) + { + var task = _taskRepository.Get(input.TaskId); + var person = _personRepository.Get(input.PersonId); + + _taskManager.AssignTaskToPerson(task, person); + } + } + +The Task **Application Service** uses a given **DTO** (input), then uses +**repositories** to retrieve that related **task** and **person**. Finally, it +passes them to the **Task Manager** (the domain service). + +### Some Discussions + +Based on the example above, you may have some questions. + +#### Why not use only the Application Services? + +You may wonder why the application service itself does not implenent the logic +contained in the domain service. + +We can simply say that it's not an application service task. Because it's +not a **use-case**, instead, it's a **business operation**, we may end up using the +same 'assign a task to a user' domain logic in a different use-case. Say +that we have **another screen** to somehow update the task. This +updating can include assigning the task to another person. We can +use the same domain logic there. We may also have **2 different UIs** (one +mobile application and one web application) that share the same domain or +we may have a web API for remote clients that includes a task-assigning +operation. + +If your domain is simple, will only have one UI, and assigning a task to +a person can be done at just a single point, then you may consider +skipping domain services and implementing the logic in your application +service. This is not the best practice for DDD, but ASP.NET +Boilerplate does not force you to use such a design. + +#### How do we force to use of the Domain Service? + +You can see that the application service could simply do the following: + + public void AssignTaskToPerson(AssignTaskToPersonInput input) + { + var task = _taskRepository.Get(input.TaskId); + task.AssignedPersonId = input.PersonId; + } + +The developer writing the application service may not know there is a +**TaskManager** and can directly set a given **PersonId** to a task's +**AssignedPersonId**. So, how do we **prevent** this? There are many +discussions in DDD based on this and there are some commonly used patterns. +We will not delve into this too deeply, but we will provide a simple way of doing it. + +We can change **Task** entity as shown below: + + public class Task : Entity + { + public virtual int? AssignedPersonId { get; protected set; } + + //...other members and codes of Task entity + + public void AssignToPerson(Person person, ITaskPolicy taskPolicy) + { + taskPolicy.CheckIfCanAssignTaskToPerson(this, person); + AssignedPersonId = person.Id; + } + } + +We changed the setter of **AssignedPersonId** as **protected**. It can +not be changed outside of this Task entity class. We added an +**AssignToPerson** method that takes a person and a task policy. The +**CheckIfCanAssignTaskToPerson** method checks if it's a valid +assignment and throws a proper exception if not (it's implementation is +not important here). The application service method will look like this: + + public void AssignTaskToPerson(AssignTaskToPersonInput input) + { + var task = _taskRepository.Get(input.TaskId); + var person = _personRepository.Get(input.PersonId); + + task.AssignToPerson(person, _taskPolicy); + } + +We injected ITaskPolicy as \_taskPolicy and passed it to the AssignToPerson +method. Now there is no second way of assigning a task to a person. We +will have to use AssignToPerson and we can therefore not skip the business rules. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dynamic-Web-API.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dynamic-Web-API.md" new file mode 100644 index 0000000..9e42c62 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Dynamic-Web-API.md" @@ -0,0 +1,319 @@ +### Building Dynamic Web API Controllers + +This document is for the ASP.NET Web API. If you're interested in ASP.NET +Core, see the [ASP.NET Core](AspNet-Core.md) documentation. + +ASP.NET Boilerplate can automatically generate an **ASP.NET Web API layer** +for your **application layer**. Say that we have an [application +service](/Pages/Documents/Application-Services) as shown below: + + public interface ITaskAppService : IApplicationService + { + GetTasksOutput GetTasks(GetTasksInput input); + void UpdateTask(UpdateTaskInput input); + void CreateTask(CreateTaskInput input); + } + +Say that we also want to expose this service as a Web API Controller for clients. +ASP.NET Boilerplate can automatically and dynamically create a Web API +Controller for this application service with a single configuration line: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder.For("tasksystem/task").Build(); + +Thats it! An api controller is created with the address +'**/api/services/tasksystem/task**' and all methods are now usable by +clients. This configuration should be made in the **Initialize** method +of your [module](Module-System.md). + +**ITaskAppService** is the application service that we want to wrap with +an api controller. It is not restricted to application services but this +is the conventional and recommended way. "**tasksystem/task**" is the name +of the api controller with an arbitrary namespace. You should define at +least a one-level namespace, but you can define more deep namespaces like +"myCompany/myApplication/myNamespace1/myNamespace2/myServiceName". +'**/api/services/**' is the prefix for all dynamic web api controllers. +The address of the api controller will look like +'/api/services/tasksystem/task' and the GetTasks method's address will be +'/api/services/tasksystem/task/getTasks'. Method names are converted to +**camelCase** since it's conventional in the world of JavaScript. + +#### ForAll Method + +We may have many application services in an application and building api +controllers one by one may be a tedious and forgettable work. +DynamicApiControllerBuilder provides a method to build web api +controllers for all application services in one call. Example: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .ForAll(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") + .Build(); + +The ForAll method is generic and accepts an interface. The first parameter is an +assembly that has classes derived from the given interfaces. The second one is +the namespace prefix of services. Say that we have an ITaskAppService and +IPersonAppService in a given assembly. For this configuration, the services +will be '/api/services/**tasksystem/task**' and +'/api/services/**tasksystem/person**'. To calculate the service name, +use the 'Service' and 'AppService' postfixes, as well as the 'I' prefix, which is removed (only for +interfaces). The service name is also converted to camel case. If you don't +like this convention, there is a '**WithServiceName**' method that you +can use to determine names. There is also a **Where** method to filter +services. This can be useful if you want to skip the builds of some services. + +#### Overriding ForAll + +We can override the configuration after the ForAll method. Example: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .ForAll(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") + .Build(); + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .For("tasksystem/task") + .ForMethod("CreateTask").DontCreateAction().Build(); + +In this code, we create dynamic web api controllers for all the application +services in a given assembly. We then overide the configuration for a single +application service, ITaskAppService, to ignore the CreateTask method. + +##### ForMethods + +We can use the **ForMethods** method to better adjust each method while +using the ForAll method. Example: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .ForAll(Assembly.GetExecutingAssembly(), "app") + .ForMethods(builder => + { + if (builder.Method.IsDefined(typeof(MyIgnoreApiAttribute))) + { + builder.DontCreate = true; + } + }) + .Build(); + +In this example, we used a custom attribute, MyIgnoreApiAttribute, to ignore a +dynamic web api controller's actions/methods when they are marked with it. + +#### Http Verbs + +By default, all methods are created as a **POST**. A client has to +send post requests in order to use the created web api actions. We can +change this behavior in different ways. + +##### WithVerb Method + +We can use WithVerb for a method like this: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .For("tasksystem/task") + .ForMethod("GetTasks").WithVerb(HttpVerb.Get) + .Build(); + +##### HTTP Attributes + +We can add the HttpGet, HttpPost, and other related attributes to methods in the service +interface: + + public interface ITaskAppService : IApplicationService + { + [HttpGet] + GetTasksOutput GetTasks(GetTasksInput input); + + [HttpPut] + void UpdateTask(UpdateTaskInput input); + + [HttpPost] + void CreateTask(CreateTaskInput input); + } + +In order to use these attributes, you need to add a reference to the +[Microsoft.AspNet.WebApi.Core](https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Core) +NuGet package to your project. + +##### Naming Convention + +Instead of declaring the HTTP attributes for every method, you can use the +**WithConventionalVerbs** method as shown below: + + Configuration.Modules.AbpWebApi().DynamicApiControllerBuilder + .ForAll(Assembly.GetAssembly(typeof(SimpleTaskSystemApplicationModule)), "tasksystem") + .WithConventionalVerbs() + .Build(); + +In this case, HTTP verbs are determined by method name prefixes: + +- **Get**: Used if the method name starts with '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 'Post', 'Create' or + 'Insert'. +- **Patch**: Used if the method name starts with 'Patch'. +- Otherwise, **Post** is used **by default** as an HTTP verb. + +We can override them for specific methods as described before. + +#### API Explorer + +All dynamic web api controllers are visible to the API explorer by default +(They are available in [Swagger](Swagger-UI-Integration.md), for +example). You can control this behaviour with the fluent +DynamicApiControllerBuilder API or using the RemoteService attribute defined +below. + +#### RemoteService Attribute + +You can also use the **RemoteService** attribute for any **interface** or +**method** definition to enable or disable (**IsEnabled**) the dynamic API or +API explorer settings (**IsMetadataEnabled**). + +### Dynamic JavaScript Proxies + +You can use the dynamically created web api controller via ajax in +JavaScript. ASP.NET Boilerplate also simplifies this by creating dynamic +JavaScript proxies for dynamic web api controllers. You can call a +dynamic web api controller's action from JavaScript like a function +call: + + abp.services.tasksystem.task.getTasks({ + state: 1 + }).done(function (result) { + //use result.tasks here... + }); + +JavaScript proxies are created dynamically. You should include the +dynamic script on your page before you use it: + + + +Service methods return a promise (See +[jQuery.Deferred](http://api.jquery.com/category/deferred-object/)). You +can register to the done, fail, then... callbacks. Inside, the Service methods use +[abp.ajax](/Pages/Documents/Javascript-API/AJAX). They handle +errors and show error messages if needed. + +#### AJAX Parameters + +You may want to pass custom ajax parameters to the proxy method. You can +pass them as a second argument as shown below: + + abp.services.tasksystem.task.createTask({ + assignedPersonId: 3, + description: 'a new task description...' + },{ //override jQuery's ajax parameters + async: false, + timeout: 30000 + }).done(function () { + abp.notify.success('successfully created a task!'); + }); + +All the parameters of [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) are +valid here. + +In addition to standard jQuery.ajax parameters, you can add +**abpHandleError: false** to AJAX options in order to disable +messages displaying when errors occur. + +#### Single Service Script + +'/api/AbpServiceProxies/GetAll' generates all service proxies in one +file. You can also generate a single service proxy using +'/api/AbpServiceProxies/Get?name=*serviceName*' and by including the script +in the page as shown below: + + + +#### AngularJS Integration + +ASP.NET Boilerplate can expose dynamic api controllers as **angularjs +services**. Consider the sample below: + + (function() { + angular.module('app').controller('TaskListController', [ + '$scope', 'abp.services.tasksystem.task', + function($scope, taskService) { + var vm = this; + vm.tasks = []; + taskService.getTasks({ + state: 0 + }).then(function(result) { + vm.tasks = result.data.tasks; + }); + } + ]); + })(); + +We can inject a **service** using it's name (with a namespace). We +can call it's **functions** as regular JavaScript functions. Notice that +we registered to the **then** handler (instead of done) since it's similar to +what is in angular's **$http** service. ASP.NET Boilerplate uses the $http +service of AngularJS. If you want to pass the $http **configuration**, you +can pass a configuration object as the last parameter of the service +method. + +To be able to use auto-generated services, you should include these needed +scripts on your page: + + + + +### Enable/Disable + +If you used the **ForAll** method as defined above, then you can use +the **RemoteService** attribute to disable it for a service or for a method. +Use this attribute in the **service interface**, and not in the service's concrete +class! + +### Wrapping Results + +ASP.NET Boilerplate **wraps** the return values of dynamic Web API actions +using an **AjaxResponse** object. See the [ajax +documentation](/Pages/Documents/Javascript-API/AJAX) for more +information on wrapping. You can enable/disable wrapping **per +method** or **per application service**. See this example application +service: + + public interface ITestAppService : IApplicationService + { + [DontWrapResult] + DoItOutput DoIt(DoItInput input); + } + +We disabled wrapping for the DoIt method. This property is declared +for **interfaces**, not implemented classes. + +Unwrapping can be useful if you want greater control on exact return +values to the client. Disabling it may be especially needed while +working with **3rd party client-side libraries** which can not work with +ASP.NET Boilerplate's standard AjaxResponse. In this case, you should +also handle exceptions yourself since [exception +handling](Handling-Exceptions.md) will be disabled (DontWrapResult +attribute has WrapOnError properties that can be used to enable the handling +and wrapping for exceptions). + +Note: Dynamic JavaScript proxies can understand if the result is unwrapped +and will run properly in either case. + +### About Parameter Binding + +ASP.NET Boilerplate creates Api Controllers on runtime. ASP.NET Web +API's [model and parameter +binding](http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api) +is used to bind models and parameters. You can read the following +[documentation](http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api) +for more information. + +#### FormUri and FormBody Attributes + +**FromUri** and **FromBody** attributes can be used in the service interface +for advanced control binding. + +#### DTOs vs Primitive Types + +We strongly advise you to use +[DTO](http://www.aspnetboilerplate.com/Pages/Documents/Data-Transfer-Objects)s +as method parameters for application services and web api controllers. +You can also use primitive types like string, int, bool... or +nullable types like int?, bool?... as service arguments. More than one +parameter can be used but only one complex-type parameter is allowed in +these parameters. This is because of restrictions in the ASP.NET Web API. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-MySql-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-MySql-Integration.md" new file mode 100644 index 0000000..afba5aa --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-MySql-Integration.md" @@ -0,0 +1,83 @@ + +### Download Starter Template + +Download the starter template with **ASP.NET Core** and **Entity Framework Core** to integrate MySQL. +[Multi-page template with **ASP.NET Core 2.x** + **.NET Core Framework** + **Authentication**](https://aspnetboilerplate.com/Templates) +will be explained in this document. + +### Getting Started + +There are two Entity Framework Core providers for MySQL that are mentioned in the Micrososft Docs. One of them is the +[Official MySQL EF Core Database Provider](https://docs.microsoft.com/en-us/ef/core/providers/mysql/) and the +other is [Pomelo EF Core Database Provider for MySQL](https://docs.microsoft.com/en-us/ef/core/providers/pomelo/). + +> **NOTE:** The official provider doesn't support EF Core 2.0 just yet, so the Pomelo EF Core Database Provider will be used in this example, instead. +> +> Related issue: https://github.com/aspnet/EntityFrameworkCore/issues/10065#issuecomment-336495475 + +### Install + +Install the [`Pomelo.EntityFrameworkCore.MySql`](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql/) NuGet package to the ***.EntityFrameworkCore** project. + +### Configuration + +#### Configure DbContext + +Replace `YourProjectNameDbContextConfigurer.cs` with the following lines + +```c# +public static class MySqlDemoDbContextConfigurer +{ + public static void Configure(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseMySql(connectionString); + } + + public static void Configure(DbContextOptionsBuilder builder, DbConnection connection) + { + builder.UseMySql(connection); + } + } + ``` + +Some configuration and workarounds are needed to use MySQL with ASP.NET Core and Entity Framework Core. + +#### Configure connection string + +Change the connection string to your MySQL connection in ***.Web.Mvc/appsettings.json**. Example: + +```js +{ + "ConnectionStrings": { + "Default": "server=127.0.0.1;uid=root;pwd=1234;database=mysqldemodb" + }, + ... +} + +``` + +#### A workaround + +To prevent EF Core from calling `Program.BuildWebHost()` rename `BuildWebHost`. For example, change it to `InitWebHost`. +To understand why it needs to be renamed, check the following issues: + +> **Reason** : [EF Core 2.0: design-time DbContext discovery changes](https://github.com/aspnet/EntityFrameworkCore/issues/9033) +> +> **Workaround** : [Design: Allow IDesignTimeDbContextFactory to short-circuit service provider creation](https://github.com/aspnet/EntityFrameworkCore/issues/9076#issuecomment-313278753) +> +> **NOTE :** If you don't rename BuildWebHost, you'll get an error running BuildWebHost method. + +### Create Database + +Remove all migration classes under **\*.EntityFrameworkCore/Migrations** folder. +Because `Pomelo.EntityFrameworkCore.MySql` will add some of its own configurations to work with Entity Framework Core. + +Now it's ready to build the database. + +- Select **\*.Web.Mvc** as the startup project. +- Open **Package Manager Console** and select the **\*.EntityFrameworkCore** project. +- Run the `add-migration Initial_Migration` command +- Run the `update-database` command + +The MySQL integration is now complete. You can now run your project with MySQL. + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Oracle-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Oracle-Integration.md" new file mode 100644 index 0000000..e32fc71 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Oracle-Integration.md" @@ -0,0 +1,6 @@ +Oracle EF Core Database Provider isn't supported officially yet. For more info see: +https://docs.microsoft.com/en-us/ef/core/providers/oracle/ + +There is a third-party provider; Devart EF Core Database Providers. This is a paid product and there are some [limitations](http://blog.devart.com/entity-framework-core-1-entity-framework-7-support.html#limitations). + +When the Oracle EF Core Database Provider is released, a relevant document will be published here. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-PostgreSql-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-PostgreSql-Integration.md" new file mode 100644 index 0000000..be68fa9 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-PostgreSql-Integration.md" @@ -0,0 +1,94 @@ +### Download Starter Template + +Download the starter template with **ASP.NET Core** and **Entity Framework Core** to integrate PostgreSQL. +[Multi-page template with **ASP.NET Core 2.x** + **.NET Core Framework** + **Authentication**](https://aspnetboilerplate.com/Templates) +will be explained in this document. + +### Install + +Install the [`Npgsql.EntityFrameworkCore.PostgreSQL`](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL/) NuGet package to the ***.EntityFrameworkCore** project. + +### Configuration + +Some configuration and workarounds are needed to use PostgreSQL with ASP.NET Core and Entity Framework Core. + +#### Configure DbContext + +Replace `YourProjectNameDbContextConfigurer.cs` with the following lines + +```c# +public static class PostgreSqlDemoDbContextConfigurer +{ + public static void Configure(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseNpgsql(connectionString); + } + + public static void Configure(DbContextOptionsBuilder builder, DbConnection connection) + { + builder.UseNpgsql(connection); + } + } +``` + +#### Configure connection string + +Change the connection string to your PostgreSQL connection in ***.Web.Mvc/appsettings.json**. For example: + +```js +{ + "ConnectionStrings": { + "Default": "User ID=postgres;Password=123;Host=localhost;Port=5432;Database=PostgreSqlDemoDb;Pooling=true;" + }, + ... +} +``` + +#### A workaround + +To prevent EF Core from calling `Program.BuildWebHost()`, rename `BuildWebHost`. For example, change it to `InitWebHost`. +To understand why it needs to be renamed, check the following issues: + +> **Reason** : [EF Core 2.0: design-time DbContext discovery changes](https://github.com/aspnet/EntityFrameworkCore/issues/9033) +> +> **Workaround** : [Design: Allow IDesignTimeDbContextFactory to short-circuit service provider creation](https://github.com/aspnet/EntityFrameworkCore/issues/9076#issuecomment-313278753) +> +> **NOTE :** If you don't rename BuildWebHost, you'll get an error running the BuildWebHost method. + +### Create Database + +Before you create the database, you should change the max length of the "Value" property of ApplicationLanguageText by adding the following lines to DbContext. +This is because the max length of char in MS SQL and PostgreSQL are different. + +```c# +public class PostgreSqlDemoDbContext : AbpZeroDbContext +{ + public PostgreSqlDemoDbContext(DbContextOptions options) + : base(options) + { + } + + // add these lines to override max length of property + // we should set max length smaller than the PostgreSQL allowed size (10485760) + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(p => p.Value) + .HasMaxLength(100); // any integer that is smaller than 10485760 + } +} +``` + +Delete the ***.EntityFrameworkCore/Migrations** folder, +because `Npgsql.EntityFrameworkCore.PostgreSQL` will add some of its own configuration to work with Entity Framework Core. + +Now it's ready to build database. + +- Select **\*.Web.Mvc** as the startup project. +- Open **Package Manager Console** and select the **\*.EntityFrameworkCore** project. +- Run the `add-migration Initial_Migration` command. +- Run the `update-database` command. + +The PostgreSQL integration is now complete. You can now run your project with PostgreSQL. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Sqlite-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Sqlite-Integration.md" new file mode 100644 index 0000000..0239a19 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-Core-Sqlite-Integration.md" @@ -0,0 +1,91 @@ +### Download Starter Template + +Download a starter template with **ASP.NET Core** and **Entity Framework Core** to integrate SQLite. +[Multi-page template with **ASP.NET Core 2.x** + **.NET Core Framework** + **Authentication**](https://aspnetboilerplate.com/Templates) +will be explained in this document. + +### Install + +Install the [`Microsoft.EntityFrameworkCore.Sqlite`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite/) NuGet package to the **\*.EntityFrameworkCore** project. + +### Configuration + +Some configuration and workarounds are needed to use SQLite with ASP.NET Core and Entity Framework Core. + +#### Configure DbContext + +Since SQLite doesn't support multithreading, transactions should be disabled in the `SQLiteDemoEntityFrameworkModule.PreInitialize()` method. + +> NOTE:Check [here](https://github.com/XdX-Software/EasyDDD/issues/1) for more info and workarounds. + +```c# +[DependsOn( + typeof(SQLiteDemoCoreModule), + typeof(AbpZeroCoreEntityFrameworkCoreModule))] +public class SQLiteDemoEntityFrameworkModule : AbpModule +{ + public override void PreInitialize() + { + ... + // add this line to disable transactions + Configuration.UnitOfWork.IsTransactional = false; + ... + } +} +``` + +Replace `YourProjectNameDbContextConfigurer.cs` with the following lines + +```c# +public static class SqliteDemoDbContextConfigurer +{ + public static void Configure(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseSqlite(connectionString); + } + + public static void Configure(DbContextOptionsBuilder builder, DbConnection connection) + { + builder.UseSqlite(connection); + } + } + ``` + +#### Configure connection string + +Change the connection string to your SQLite connection in ***.Web.Mvc/appsettings.json**. Example: + +```js +{ + "ConnectionStrings": { + "Default": "Data Source=SqliteDemoDb.db" + }, + ... +} + +``` + +#### A workaround + +To prevent EF Core from calling `Program.BuildWebHost()` rename `BuildWebHost`. For example, change it to `InitWebHost`. +To understand why it needs to be renamed check the following issues, + +> **Reason** : [EF Core 2.0: design-time DbContext discovery changes](https://github.com/aspnet/EntityFrameworkCore/issues/9033) +> +> **Workaround** : [Design: Allow IDesignTimeDbContextFactory to short-circuit service provider creation](https://github.com/aspnet/EntityFrameworkCore/issues/9076#issuecomment-313278753) +> +> **NOTE :** If you don't rename BuildWebHost, you'll get an error running the BuildWebHost method. + +### Create Database + +Remove all migration classes under **\*.EntityFrameworkCore/Migrations** folder before creating database. +`Microsoft.EntityFrameworkCore.Sqlite` will add some of its own configuration to work with Entity Framework Core. + +Now it's ready to build the database. + +- Select **\*.Web.Mvc** as the startup project. +- Open **Package Manager Console** and select the **\*.EntityFrameworkCore** project. +- Run the `add-migration Initial_Migration` command +- Run the `update-database` command + +The SQLite integration is now complete. You can now run your project with SQLite. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-MySql-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-MySql-Integration.md" new file mode 100644 index 0000000..c1f1777 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EF-MySql-Integration.md" @@ -0,0 +1,54 @@ +### Introduction + +While our default templates are designed to work with SQL Server, you can +easily modify them to work with MySql. In order to do that, you need to +follow these steps. + +#### Download Project + +Go to and download a new +project. Select ASP.NET MVC 5.x tab and don't forget to select Entity +Framework. + +#### Install MySql.Data.Entity + +You then need to install the +[MySql.Data.Entity](https://www.nuget.org/packages/MySql.Data.Entity/) +NuGet package into your **.EntityFramework** and **.Web** projects. +Installing this NuGet package into your **.Web** project should make the +necessary changes in your web.config file. + +Open your DbContext's configuration class (Configuration.cs) and place +the following code in it's constructor + + SetSqlGenerator("MySql.Data.MySqlClient", new MySql.Data.Entity.MySqlMigrationSqlGenerator()); + +#### Configure ConnectionString + +You need to change your connection string in the web.config file in +order to work with your MySql database. An example connection string +would be: + + + +#### Re-generate the migrations + +If you choose "Include login, register, user, role and tenant management pages" while downloading your startup +template, there will be some migration files included in your project. +These files are generated for Sql Server. Delete all the migration files +in your **.EntityFramework** project under the Migrations folder. Migration +files start with a timestamp. A migration file name should look like this +"201506210746108\_AbpZero\_Initial" + +After deleting all the migration files, select your **.Web** project as the +startup project, open Visual Studio's Package Manager Console and select +the **.EntityFramework** project as the default project in the Package Manager +Console. Then run the following command to add a migration for MySql. + + Add-Migration "AbpZero_Initial" + +Now you can create your database using the following command + + Update-Database + +That's it! You can now run your project with MySql. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Email-Sending.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Email-Sending.md" new file mode 100644 index 0000000..0dfa790 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Email-Sending.md" @@ -0,0 +1,162 @@ +### Introduction + +Sending emails is a very common task for most applications. +ASP.NET Boilerplate provides the basic infrastructure to send +emails in a simple way. It also separates the email server configuration from the sending +of emails. + +### IEmailSender + +**IEmailSender** is a service to send emails without knowing the details. Example usage: + + public class TaskManager : IDomainService + { + private readonly IEmailSender _emailSender; + + public TaskManager(IEmailSender emailSender) + { + _emailSender = emailSender; + } + + public void Assign(Task task, Person person) + { + //Assign task to the person + task.AssignedTo = person; + + //Send a notification email + _emailSender.Send( + to: person.EmailAddress, + subject: "You have a new task!", + body: $"A new task is assigned for you: {task.Title}", + isBodyHtml: true + ); + } + } + +We simply [injected](Dependency-Injection.md) **IEmailSender** and +used the **Send** method. The Send method has additional overloads. For example, it +can also get a MailMessage object (not available for .NET Core since .NET Core +does not include SmtpClient and MailMessage). + +#### ISmtpEmailSender + +There is also an **ISmtpEmailSender** which extends IEmailSender and adds a +**BuildClient** method to create an **SmtpClient** and then directly uses it +(not available for .NET Core since .NET Core does not include SmtpClient +and MailMessage). Using IEmailSender will be enough for most cases. + +#### NullEmailSender + +There is also a [null object +pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) +implementation of IEmailSender, aptly named **NullEmailSender**. You can use it in +unit tests or inject IEmalSender with the [property +injection](Dependency-Injection.md) pattern. + +### Configuration + +Email Sender uses a [settings management](Setting-Management.md) system +to read email-sending configurations. All the setting names are defined in the +Abp.Net.Mail.EmailSettingNames class as constant strings. + +Their values and descriptions: + +- Abp.Net.Mail.**DefaultFromAddress**: Used as the sender's email address + when you don't specify a sender when sending emails (just like in the + example above). +- Abp.Net.Mail.**DefaultFromDisplayName**: Used as the sender's display name + when you don't specify a sender when sending emails (just like in the + example above). +- Abp.Net.Mail.**Smtp.Host**: The IP/Domain of the SMTP server (default: + 127.0.0.1). +- Abp.Net.Mail.**Smtp.Port**: The Port of the SMTP server (default: 25). +- Abp.Net.Mail.**Smtp.UserName**: Username, if the SMTP server requires + authentication. +- Abp.Net.Mail.**Smtp.Password**: Password, if the SMTP server requires + authentication. +- Abp.Net.Mail.**Smtp.Domain**: Domain for the username, if the SMTP + server requires authentication. +- Abp.Net.Mail.**Smtp.EnableSsl**: A value that indicates if the SMTP server + uses SSL or not ("true" or "false". Default: "false"). +- Abp.Net.Mail.**Smtp.UseDefaultCredentials**: If true, uses default + credentials instead of the provided username and password ("true" or + "false". Default: "true"). + +### MailKit Integration + +Since .NET Core does not support the standard System.Net.Mail.SmtpClient, +we need a 3rd-party vendor to send emails. Fortunately, +[MailKit](https://github.com/jstedfast/MailKit) provides a good +replacement for the default SmtpClient. It's also +[suggested](https://www.infoq.com/news/2017/04/MailKit-MimeKit-Official) +by Microsoft. + +The Abp.MailKit package gracefully integrates in to ABP's email sending system, so you +can still use IEmailSender as described above to send emails via MailKit. + +#### Installation + +First, install the [Abp.MailKit](https://www.nuget.org/packages/Abp.MailKit) +NuGet package to your project: + + Install-Package Abp.MailKit + +#### Integration + +Add the AbpMailKitModule to the dependencies of your +[module](Module-System.md): + + [DependsOn(typeof(AbpMailKitModule))] + public class MyProjectModule : AbpModule + { + //... + } + +#### Usage + +You can use **IEmailSender** as described above since the Abp.MailKit +package [registers](Dependency-Injection.md) the MailKit implementation +for it. It also uses the same configuration. + +#### Customization + +You may need to make additional configuration or customizations while +creating MailKit's SmtpClient. In that case, you can +[replace](Startup-Configuration.md) the IMailKitSmtpBuilder interface with +your own implementation. You can derive from the DefaultMailKitSmtpBuilder +to make it easier. For instance, you may want to accept all SSL +certificates. In that case, you can override the ConfigureClient method as +shown below: + + public class MyMailKitSmtpBuilder : DefaultMailKitSmtpBuilder + { + public MyMailKitSmtpBuilder(ISmtpEmailSenderConfiguration smtpEmailSenderConfiguration) + : base(smtpEmailSenderConfiguration) + { + } + + protected override void ConfigureClient(SmtpClient client) + { + client.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true; + + base.ConfigureClient(client); + } + } + +You can then replace the IMailKitSmtpBuilder interface with your +implementation in the [PreInitialize](Module-System.md) method of your +module: + + [DependsOn(typeof(AbpMailKitModule))] + public class MyProjectModule : AbpModule + { + public override void PreInitialize() + { + Configuration.ReplaceService(); + } + + //... + } + +(Don't forget to add the "using Abp.Configuration.Startup;" statement since the +ReplaceService extension method is defined in that namespace) diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Embedded-Resource-Files.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Embedded-Resource-Files.md" new file mode 100644 index 0000000..4b6548a --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Embedded-Resource-Files.md" @@ -0,0 +1,148 @@ +### Introduction + +ASP.NET Boilerplate provides an easy way of using embedded **Razor +views** (.cshtml files) and **other resources** (css, js, img... files) +in your web application. You can use this feature to create +[plugins/modules](Module-System.md) that contain UI functionality. + +### Create the Embedded Files + +First, we create a file and mark it as an **embedded resource**. Any +assembly can contain embedded resource files. The process changes based +on your project format. + +#### xproj/project.json Format + +Assume that we have a project named EmbeddedPlugIn, as shown below: + +Embedded resource sample project + +To make **all files** under the **Views** folder embedded resources, we +add the following configuration to **project.json**: + + "buildOptions": { + "embed": { + "include": [ + "Views/**/*.*" + ] + } + } + +#### csproj Format + +Assume that we have a project named EmbeddedPlugIn, as shown below: + +Embedded resource project structure + +Select the **Index.cshtml** file, go to the properties window (shorcut is F4) +and change it's **Build Action** to **Embedded Resource**. + +Embedding a file into a c# project + +You should change the build action to **embedded resource** for **all** the +files you want to use in a web application. + +### Add To Embedded Resource Manager + +Once we embed our files into the assembly, we can use the [startup +configuration](Startup-Configuration.md) to add them to the embedded +resource manager. You can add a line like this to the PreInitialize method of +your [module](Module-System.md): + + Configuration.EmbeddedResources.Sources.Add( + new EmbeddedResourceSet( + "/Views/", + Assembly.GetExecutingAssembly(), + "EmbeddedPlugIn.Views" + ) + ); + +Let's explain the parameters: + +- The first parameter defines the **root folder** for the files (like + http://yourdomain.com**/Views/**, here). It matches to the root + namespace. +- The second parameter defines the **Assembly** containing the files. This code + should be located in the assembly containing the embedded files. + Otherwise, you should change this parameter accordingly. +- The last parameter defines the **root namespace** of the files in the + assembly. This is the default namespace (generally, the assembly + name) plus the 'folders in the assembly' joined by a dot. + +### Consume Embedded Views + +For **.cshtml** files, it's straightforward to return them from a +Controller Action. BlogController in the EmbeddedPlugIn assembly is +shown below: + + using Abp.AspNetCore.Mvc.Controllers; + using Microsoft.AspNetCore.Mvc; + + namespace EmbeddedPlugIn.Controllers + { + public class BlogController : AbpController + { + public ActionResult Index() + { + return View(); + } + } + } + +As you can see, it's the same as regular controllers and works just as expected. + +### Consume Embedded Resources + +To consume embedded resources (js, css, img...), we can use them in +our views like we normally do: + + @section Styles { + + } + + @section Scripts + { + + } + +

Blog plugin!

+ +It assumes that the main application has the Styles and Scripts sections. We +can also use other files, like images, like we normally do. + +#### ASP.NET Core Configuration + +ASP.NET MVC 5.x projects will automatically integrate to the embedded +resource manager through Owin (if your startup file contains +app.UseAbp() as expected). For ASP.NET Core projects, we have to manually +add **app.UseEmbeddedFiles()** to the Startup class, just after +app.UseStaticFiles(), as shown below: + + app.UseStaticFiles(); + app.UseEmbeddedFiles(); //Allows to expose embedded files to the web! + +#### Ignored Files + +Normally, **all files** in the embedded resource manager can be directly +consumed by clients as if they were static files. You can ignore some +file extensions for security and other purposes. **.cshtml** and +**.config** files are ignored by default (for direct requests from +clients). You can add more extensions in the PreInitialize method of your +module as shown below: + + Configuration.Modules.AbpWebCommon().EmbeddedResources.IgnoredFileExtensions.Add("exe"); + +### Override Embedded Files + +One important feature of embedded resource files is that **they can be +overridden** by higher modules. This means you can create a file with the +same name in the same folder in your web application to override an +embedded file (your file in the web application does not require it to be +an embedded resource, because static files have priority over embedded +files). Thus, you can override the css, js or view files of your +modules/plugins in the application. If module A depends on module +B and module A defines an embedded resource with the same path, it will +override the embedded resource file of module B. + +Note: For ASP.NET Core projects, you should put the overriding files +in the wwwroot folder as the root path. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entities.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entities.md" new file mode 100644 index 0000000..106aaa2 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entities.md" @@ -0,0 +1,283 @@ +Entities are one of the core concepts of DDD (Domain Driven Design). +Eric Evans describe it as "*An object that is not fundamentally defined +by its attributes, but rather by a thread of continuity and identity*". + +Essentially, entities have Id's and are stored in a database. An entity is generally +mapped to a table in a relational database. + +### Entity Class + +In ASP.NET Boilerplate, Entities are derived from the **Entity** class. See +the example below: + + public class Person : Entity + { + public virtual string Name { get; set; } + + public virtual DateTime CreationTime { get; set; } + + public Person() + { + CreationTime = DateTime.Now; + } + } + +The **Person** class is defined as an entity. It has two properties as well as the +**Id** property defined in the Entity base class. The **Id** is the **primary key** of the +Entity. The name of the primary keys for all Entities are the same, it is **Id**. + +The type of the Id (primary key) can be changed. It is **int** (Int32) by +default. If you want to define another type as Id, you should explicitly +declare it as shown below: + + public class Person : Entity + { + public virtual string Name { get; set; } + + public virtual DateTime CreationTime { get; set; } + + public Person() + { + CreationTime = DateTime.Now; + } + } + +You can set it as string, Guid or something else. + +Entity class overrides the **equality** operator (==) to easily check if two +entities are equal (their Id is equal). It also defines the +**IsTransient()** method to check if it has an Id or not. + +### AggregateRoot Class + +"*Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a +cluster of domain objects that can be treated as a single unit. An +example may be an order and its line-items, these will be separate +objects, but it's useful to treat the order (together with its line +items) as a single aggregate.*" (Martin Fowler - see the [full +description](http://martinfowler.com/bliki/DDD_Aggregate.html)) + +While ABP does not enforce you to use aggregates, you may want to create +aggregates and aggregate roots in your application. ABP defines +the **AggregateRoot** class that extends an Entity to create aggregate root +entities for an aggregate. + +#### Domain Events + +AggregateRoot defines the [**DomainEvents**](EventBus-Domain-Events.md) +collection to generate domain events by the aggregate root class. These +events are automatically triggered just before the current [unit of +work](Unit-Of-Work.md) is completed. In fact, any entity can generate +domain events by implementing the **IGeneratesDomainEvents** interface, but +it's common (a best practice) to generate domain events in aggregate +roots. That's why it's the default for the AggregateRoot but not for the Entity +class. + +### Conventional Interfaces + +In many applications, similar entity properties (and database table +fields) are used, like CreationTime, which indicates when an entity was +created. ASP.NET Boilerplate provides some useful interfaces to make +these common properties explicit and expressive. This provides a +way of coding common code for Entities which implement these interfaces. + +#### Auditing + +**IHasCreationTime** makes it possible to use a common property for the +'**creation time**' information of an entity. When this interface is implemented, +ASP.NET Boilerplate automatically sets the CreationTime to the **current time** when +an Entity is inserted into the database. + + public interface IHasCreationTime + { + DateTime CreationTime { get; set; } + } + +The Person class can be re-written as shown below by implementing the +IHasCreationTime interface: + + public class Person : Entity, IHasCreationTime + { + public virtual string Name { get; set; } + + public virtual DateTime CreationTime { get; set; } + + public Person() + { + CreationTime = DateTime.Now; + } + } + +**ICreationAudited** extends IHasCreationTime by adding  +**CreatorUserId**: + + public interface ICreationAudited : IHasCreationTime + { + long? CreatorUserId { get; set; } + } + +ASP.NET Boilerplate automatically sets the CreatorUserId property +to the **current user's id** when saving a new entity. You can also +easily implement ICreationAudited by deriving your entity from the +**CreationAuditedEntity** class. It also has a generic version for +different types of Id properties. + +There are also similar interfaces for modifications: + + public interface IHasModificationTime + { + DateTime? LastModificationTime { get; set; } + } + + public interface IModificationAudited : IHasModificationTime + { + long? LastModifierUserId { get; set; } + } + +ASP.NET Boilerplate automatically sets these properties when +updating an entity. You just have to define them for your entity. + +If you want to implement all of the audit properties, you can directly +implement the **IAudited** interface: + + public interface IAudited : ICreationAudited, IModificationAudited + { + + } + +As a shortcut, you can derive from the **AuditedEntity** class instead of +directly implementing **IAudited**. The AuditedEntity class also has a +generic version for different types of Id properties. + +Note: ASP.NET Boilerplate gets the current user's Id from [ABP +Session](/Pages/Documents/Abp-Session). + +#### Soft Delete + +Soft delete is a commonly used pattern to mark an Entity as deleted +instead of actually deleting it from database. For instance, you may not +want to hard delete a User from the database since it has many relations to +other tables. The **ISoftDelete** interface is used for this purpose: + + public interface ISoftDelete + { + bool IsDeleted { get; set; } + } + +ASP.NET Boilerplate implements the soft delete pattern out-of-the-box. When +a soft-delete entity is being deleted, ASP.NET Boilerplate detects this, +prevents deleting, sets IsDeleted as true, and then updates the entity in the +database. It also does not retrieve (select) soft deleted entities from +the database by automatically filtering them. + +If you use soft delete, you may also want to store information of when an +entity was deleted and who deleted it. You can implement the +**IDeletionAudited** interface, shown below: + + public interface IDeletionAudited : ISoftDelete + { + long? DeleterUserId { get; set; } + + DateTime? DeletionTime { get; set; } + } + + +As you've probably noticed, IDeletionAudited extends ISoftDelete. ASP.NET Boilerplate +automatically sets these properties when an entity is deleted. + +If you want to implement all the audit interfaces (creation, modification +and deletion) for an entity, you can directly implement **IFullAudited** +since it inherits from the others: + + public interface IFullAudited : IAudited, IDeletionAudited + { + + } + +As a shortcut, you can derive your entity from the **FullAuditedEntity** +class that implements them all. + +- NOTE 1: All audit interfaces and classes have a generic version for + defining the navigation property to your **User** entity (like + ICreationAudited<TUser> and FullAuditedEntity<TPrimaryKey, + TUser>). +- NOTE 2: All of them also have an **AggregateRoot** version, like + AuditedAggregateRoot. + +#### Active/Passive Entities + +Some entities need to be marked as Active or Passive. You may take an +action upon the active/passive states of the entity. You can implement the +**IPassivable** interface that has been created for this reason. It defines +the **IsActive** property. + +If your entity will be active on it's first creation, you can set IsActive to +true in the constructor. + +This is different than soft delete (IsDeleted). If an entity is soft +deleted, it can not be retrieved from the database (ABP prevents it by +default). For active/passive entities, it's completely up to you +on how you control the retrieval of these entities. + +### Entity Change Events + +ASP.NET Boilerplate automatically triggers certain events when an entity +is inserted, updated or deleted. You can register to these events +and perform any logic you need. See the Predefined Events section in the [event +bus documentation](/Pages/Documents/EventBus-Domain-Events) for more +information. + +### IEntity Interfaces + +The **Entity** class implements the **IEntity** interface (and +**Entity<TPrimaryKey>** implements +**IEntity<TPrimaryKey>**). If you do not want to derive from +the Entity class, you can implement these interfaces directly. There are +also corresponding interfaces for other entity classes. We don't recommend +you do this unless you have a good reason not to derive from the +Entity classes. + +### Multi-Lingual Entities + +ASP.NET Boilerplate provides a simple way for defining and using Multi-Lingual entities. For more information see [Multi-Lingual Entities](Multi-Lingual-Entities.md). + +### IExtendableObject Interface + +ASP.NET Boilerplate provides a simple interface, **IExtendableObject**, +to easily associate **arbitrary name-value data** to an entity. Consider +this simple entity: + + public class Person : Entity, IExtendableObject + { + public string Name { get; set; } + + public string ExtensionData { get; set; } + + public Person(string name) + { + Name = name; + } + } + +**IExtendableObject** defines the **ExtensionData** string property +which is used to store **JSON formatted** name value objects. Example: + + var person = new Person("John"); + + person.SetData("RandomValue", RandomHelper.GetRandom(1, 1000)); + person.SetData("CustomData", new MyCustomObject { Value1 = 42, Value2 = "forty-two" }); + +We can use any type of object as a value to the **SetData** method. When we +use the code above, **ExtensionData** will look like this: + + {"CustomData":{"Value1":42,"Value2":"forty-two"},"RandomValue":178} + +We can then use **GetData** to get the values: + + var randomValue = person.GetData("RandomValue"); + var customData = person.GetData("CustomData"); + +While this technique can be very useful in some cases (when you need to +provide the ability to dynamically add extra data to an entity), you +should normally use regular properties. Such dynamic usage is not type +safe and explicit. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-Framework-Core.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-Framework-Core.md" new file mode 100644 index 0000000..efa7c63 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-Framework-Core.md" @@ -0,0 +1,274 @@ +### Introduction + +The [Abp.EntityFrameworkCore](https://www.nuget.org/packages/Abp.EntityFrameworkCore) +NuGet package is used to integrate the Entity Framework (EF) Core ORM +framework. After installing this package, you need to also add a +[DependsOn](Module-System.md) attribute for +**AbpEntityFrameworkCoreModule**. + +### DbContext + +EF Core requires you to define a class derived from DbContext. In ABP, we +need to derive from the **AbpDbContext** as shown below: + + public class MyDbContext : AbpDbContext + { + public DbSet Products { get; set; } + + public MyDbContext(DbContextOptions options) + : base(options) + { + } + } + +The constructor should get a **DbContextOptions<T>** as shown above. +The parameter name must be **options**. It's not possible to change it +because ABP provides it as an anonymous object parameter. + +### Configuration + +#### The Startup Class + +Use the **AddAbpDbContext** method on the service collection in +the **ConfigureServices** method as shown below: + + services.AddAbpDbContext(options => + { + options.DbContextOptions.UseSqlServer(options.ConnectionString); + }); + +For non web projects, we will not have a Startup class. In this case, we +can use the **Configuration.Modules.AbpEfCore().AddDbContext** method in our +[module](Module-System.md) class to configure the DbContext, shown +below: + + Configuration.Modules.AbpEfCore().AddDbContext(options => + { + options.DbContextOptions.UseSqlServer(options.ConnectionString); + }); + +We used the given connection string and Sql Server as the database +provider. Normally, **options.ConnectionString** is the **default connection +string** (see next section). However, ABP can use +IConnectionStringResolver to determine it. This behaviour can be +changed and the connection string can be determined dynamically. The action +passed to AddDbContext is called whenever a DbContext instance will be +created. You also have a chance to return different connection +strings, conditionally. + +Where do we set the default connection string? + +#### In the module PreInitialize method + +You can do it in the PreInitialize method of your module as shown below: + + public class MyEfCoreAppModule : AbpModule + { + public override void PreInitialize() + { + Configuration.DefaultNameOrConnectionString = GetConnectionString("Default"); + ... + } + } + +You can define the GetConnectionString method, which simply returns the +connection string from a configuration file. This is generally in the +appsettings.json file. + +### Repositories + +Repositories are used to abstract data access from higher layers. See the +[repository documentation](Repositories.md) for more info.  + +#### Default Repositories + +[Abp.EntityFrameworkCore](http://www.nuget.org/packages/Abp.EntityFrameworkCore) +implements default repositories for all the entities defined in your +DbContext. You don't have to create repository classes to use predefined +repository methods. Example: + + public class PersonAppService : IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + + _personRepository.Insert(person); + } + } + +The PersonAppService contructor-injects **IRepository<Person>** and +uses the **Insert** method. In this way, you can easily inject +**IRepository<TEntity>** (or IRepository<TEntity, +TPrimaryKey>) and use the predefined methods. + +#### Custom Repositories + +If standard repository methods are not sufficient, you can create custom +repository classes for your entities. + +##### Application Specific Base Repository Class + +ASP.NET Boilerplate provides a base class **EfCoreRepositoryBase** to +implement repositories easily. To implement the **IRepository** interface, +you can simply derive your repository from this class. It's better to +create your own base class that extends EfRepositoryBase. This way, you can +easily add shared and common methods to your repositories. Here's an example base +class for all the repositories of a *SimpleTaskSystem* application: + + //Base class for all repositories in my application + public class SimpleTaskSystemRepositoryBase : EfCoreRepositoryBase + where TEntity : class, IEntity + { + public SimpleTaskSystemRepositoryBase(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + //add common methods for all repositories + } + + //A shortcut for entities which have an integer Id + public class SimpleTaskSystemRepositoryBase : SimpleTaskSystemRepositoryBase + where TEntity : class, IEntity + { + public SimpleTaskSystemRepositoryBase(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + //do not add a method here, add it to the class above (because this class inherits it) + } + +Note that we're inheriting from +EfCoreRepositoryBase<**SimpleTaskSystemDbContext**, TEntity, +TPrimaryKey>. This sets ASP.NET Boilerplate to use the +SimpleTaskSystemDbContext in our repositories. + +By default, all repositories for your given DbContext +(SimpleTaskSystemDbContext in this example) are implemented using +EfCoreRepositoryBase. You can replace it with your own repository base +class by adding the **AutoRepositoryTypes** attribute to your +DbContext as shown below: + + [AutoRepositoryTypes( + typeof(IRepository<>), + typeof(IRepository<,>), + typeof(SimpleTaskSystemEfRepositoryBase<>), + typeof(SimpleTaskSystemEfRepositoryBase<,>) + )] + public class SimpleTaskSystemDbContext : AbpDbContext + { + ... + } + +##### Custom Repository Example + +To implement a custom repository, just derive it from your application +specific base repository class like the one we created above. + +Assume that we have a Task entity that can be assigned to a Person +(entity). We also have a Task which has a State (new, assigned, completed... and so on). +We may need to write a custom method to get the list of Tasks with some +conditions and include the AssisgnedPerson property, pre-fetched (included), in a +single database query. See the following code: + + public interface ITaskRepository : IRepository + { + List GetAllWithPeople(int? assignedPersonId, TaskState? state); + } + + public class TaskRepository : SimpleTaskSystemRepositoryBase, ITaskRepository + { + public TaskRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public List GetAllWithPeople(int? assignedPersonId, TaskState? state) + { + var query = GetAll(); + + if (assignedPersonId.HasValue) + { + query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); + } + + if (state.HasValue) + { + query = query.Where(task => task.State == state); + } + + return query + .OrderByDescending(task => task.CreationTime) + .Include(task => task.AssignedPerson) + .ToList(); + } + } + +**We first defined** ITaskRepository and then implemented it. +The **GetAll()** method returns **IQueryable<Task>**. We then add some +**Where** filters using the given parameters. Finally, we call +**ToList()** to get the list of Tasks. + +You can also use the **Context** object in the repository methods to reach +your DbContext and directly use Entity Framework APIs.  + +**Note**: Define the custom repository **interface** in the +**domain/core** layer, **implement** it in the **EntityFrameworkCore** +project for layered applications. That way, you can inject the interface +from any project without referencing EF Core. + +##### Replacing the Default Repositories + +Even if you created a TaskRepository as shown above, any class can +still [inject](Dependency-Injection.md) IRepository<Task, long> +and use it. That's not a problem in most cases. But what if you did an +**override** on a base method in your custom repository? Say that you have +overidden Delete method in your custom repository to add a custom +behaviour on delete. If a class injects IRepository<Task, long> +and usea the default repository to Delete a task, your custom behaviour +will not work. To overcome this issue, you can replace your custom +repository implementation with the default one like shown below: + + Configuration.ReplaceService>(() => + { + IocManager.IocContainer.Register( + Component.For, ITaskRepository, TaskRepository>() + .ImplementedBy() + .LifestyleTransient() + ); + }); + +We registered the TaskRepository for IRepository<Task, Guid>, +ITaskRepository and TaskRepository. This way, any one of these can be injected +to use the TaskRepository. + +#### Repository Best Practices + +- **Use the default repositories** wherever it's possible. You can use a + default repository even if you have a custom repository for an entity + (if you are using the standard repository methods). +- Always create the **repository base class** for your application for + custom repositories, as defined above. +- Define **interfaces** for your custom repositories in the **domain + layer** (.Core project in the startup template). Then define the custom repository + **classes** in the **.EntityFrameworkCore** project if you want to + abstract EF Core from your domain/application. + +### Other Database Integrations + +This document and the examples are based on using MS SQL Server. +The following documents can be followed for different database integrations. + +- [MySQL Integration](EF-Core-MySql-Integration.md) +- [PostgreSQL Integration](EF-Core-PostgreSql-Integration.md) +- [SQLite Integration](EF-Core-Sqlite-Integration.md) +- [Oracle Integration](EF-Core-Oracle-Integration.md) diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-History.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-History.md" new file mode 100644 index 0000000..db2c8ed --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Entity-History.md" @@ -0,0 +1,167 @@ +### Introduction + +ASP.NET Boilerplate provides an infrastructure to automatically log all +entity and property changes. + +The saved fields for an entity change are: The related **tenant id**, +**entity change set id**, **entity id**, +**entity type name**, **change time** and the **change type**. + +The saved fields for an entity property change are: The related **tenant id**, +**entity change id**, **property name**, **property type name**, +**new value** and the **original value**. + +The entity changes are grouped in a change set for each SaveChanges call. + +The saved fields for an entity change set are: The related **tenant id**, +changer **user id**, **creation time**, **reason**, the client's +**IP address**, the client's **computer name** and the **browser info** (if +entities are changed in a web request). + +The Entity History tracking system uses +[**IAbpSession**](/Pages/Documents/Abp-Session) to +get the current UserId and TenantId. + +No entities are automatically tracked by default. You should configure entities either by using the startup configuration or via attributes. + +#### About IEntityHistoryStore + +The Entity History tracking system uses **IEntityHistoryStore** to +save change information. While you can implement it in your own way, +it's fully implemented in the **Module Zero** project. + +### Configuration + +To configure Entity History, you can use the +**Configuration.EntityHistory** property +in your [module](/Pages/Documents/Module-System)'s PreInitialize method. +Entity History is **enabled by default**. +You can disable it as shown below: + + public class MyModule : AbpModule + { + public override void PreInitialize() + { + Configuration.EntityHistory.IsEnabled = false; + } + + //... + } + +Here are the Entity History configuration properties: + +- **IsEnabled**: Used to enable/disable the tracking system completely. + Default: **true**. +- **IsEnabledForAnonymousUsers**: If this is set to true, the change logs + are saved for users that are not logged in to the application. + Default: **false**. +- **Selectors**: Used to select entities to save change logs. + +**Selectors** is a list of predicates to select entities to save +change logs. A selector has a unique **name** and a **predicate**. +For example, a selector can be used to select **full audited entities**. +It's defined as shown below: + + Configuration.EntityHistory.Selectors.Add( + new NamedTypeSelector( + "Abp.FullAuditedEntities", + type => typeof (IFullAudited).IsAssignableFrom(type) + ) + ); + +You can add your selectors in your module's PreInitialize method. + +### Enable/Disable by attributes + +While you can select tracked entities by configuration, you can use the +**Audited** and **DisableAuditing** attributes for a single +**entity** or an individual **property**. Example: + + [Audited] + public class MyEntity : Entity + { + public string MyProperty1 { get; set; } + + [DisableAuditing] + public int MyProperty2 { get; set; } + + public long MyProperty3 { get; set; } + } + +All properties of MyEntity are tracked except MyProperty2 since it's +explicitly disabled. The Audited attribute can be used to +save change logs for a desired property. + +**DisableAuditing** can be used for an entity or a single **property of an +entity**. Thus, you can **hide sensitive data** in change logs, such as +passwords for example. + +### Reason Property + +The entity change set has a **Reason** property that can be used to understand why a +set of changes has occurred, i.e. the use case that resulted in these changes. + +For example, Person A transfers money from Account A to Account B. Both account +balances change and "Money transfer" is recorded as the Reason for this change set. +Since a balance change can be due to other reasons, the Reason property explains +why these changes were made. + +The **Abp.AspNetCore** package implements **HttpRequestEntityChangeSetReasonProvider**, +which returns the HttpContext.Request's URL as the Reason. + +#### UseCase Attribute + +The preferred approach is using the **UseCase** attribute. Example: + + [UseCase(Description = "Assign an issue to a user")] + public virtual async Task AssignIssueAsync(AssignIssueInput input) + { + // ... + } + +##### UseCase Attribute Restrictions + +You can use the UseCase attribute for: + +- All **public** or **public virtual** methods for classes that are + used via its interface, e.g. an application service used via its interface. +- All **public virtual** methods for self-injected classes, e.g. **MVC + Controllers**. +- All **protected virtual** methods. + +#### IEntityChangeSetReasonProvider + +In some cases, you may need to change/override the Reason value for a limited scope. +You can use the **IEntityChangeSetReasonProvider.Use(...)** method as shown below: + + public class MyService + { + private readonly IEntityChangeSetReasonProvider _reasonProvider; + + public MyService(IEntityChangeSetReasonProvider reasonProvider) + { + _reasonProvider = reasonProvider; + } + + public virtual async Task AssignIssueAsync(AssignIssueInput input) + { + var reason = "Assign an issue to user: " + input.UserId.ToString(); + using (_reasonProvider.Use(reason)) + { + ... + + _unitOfWorkManager.Current.SaveChanges(); + } + } + } + +The Use method returns an IDisposable and it **must be disposed**. Once the return +value is disposed, the Reason is automatically restored to the previous value. + +### Notes + +- A property must be **public** in order to be saved in the change logs. + Private and protected properties are ignored. +- DisableAuditing takes priority over the Audited attribute. +- Entity History only works for entities. +- Entity History only works for scalar properties, e.g. string, int, bool... diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EntityFramework-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EntityFramework-Integration.md" new file mode 100644 index 0000000..67fdf14 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EntityFramework-Integration.md" @@ -0,0 +1,277 @@ +ASP.NET Boilerplate can work with any O/RM framework. It has built-in +integration with **EntityFramework**. This document will explain how to +use EntityFramework with ASP.NET Boilerplate. It's assumed that you're +already familar with EntityFramework at a basic level. + +### NuGet Package + +The NuGet package to use EntityFramework as an O/RM in ASP.NET Boilerplate is +[Abp.EntityFramework](http://www.nuget.org/packages/Abp.EntityFramework). +You should add it to your application. It's better to implement +EntityFramework in a separated assembly (dll) in your application and +depend on that package from this assembly. + +### DbContext + +As you know, to work with EntityFramework, you should define a +**DbContext** class for your application. An example DbContext is shown +below: + + public class SimpleTaskSystemDbContext : AbpDbContext + { + public virtual IDbSet People { get; set; } + public virtual IDbSet Tasks { get; set; } + + public SimpleTaskSystemDbContext() + : base("Default") + { + + } + + public SimpleTaskSystemDbContext(string nameOrConnectionString) + : base(nameOrConnectionString) + { + + } + + public SimpleTaskSystemDbContext(DbConnection existingConnection) + : base(existingConnection, false) + { + + } + + public SimpleTaskSystemDbContext(DbConnection existingConnection, bool contextOwnsConnection) + : base(existingConnection, contextOwnsConnection) + { + + } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().ToTable("StsPeople"); + modelBuilder.Entity().ToTable("StsTasks").HasOptional(t => t.AssignedPerson); + } + } + +It's a regular DbContext class except with the following rules: + +- It's derived from **AbpDbContext** instead of DbContext. +- It should have the constructors like the sample above (constructor + parameter names should also be the same). Explanation: + - The **Default** constructor passes "Default" to the base class as the + connection string. It expects a "Default" named connection + string in the web.config/app.config file. This constructor is + not used by ABP, but used by the EF command-line migration tool + commands (like "update-database"). + - The constructor gets the **nameOrConnectionString** which is used by ABP + to pass the connection name or string on runtime. + - The constructor get the **existingConnection** which can be used for unit + tests, and is not directly used by ABP. + - The constructor gets the **existingConnection** and the + **contextOwnsConnection** is used by ABP on single database/ + multiple dbcontext scenarios to share the same connection & + transaction () when **DbContextEfTransactionStrategy** is used + (see Transaction Management section below). + +EntityFramework can map classes to database tables in a conventional +way. You don't even need to make a configuration unless you make some +custom stuff. In this example, we mapped entities to different tables. +By default, the Task entity maps to the **Tasks** table. We changed it to be +**StsTasks** table. Instead of configuring it with data annotation +attributes, it is recommended that you use fluent configuration. You can choose +what you like. + +### Repositories + +Repositories are used to abstract data access from higher layers. See the +[repository documentation](Repositories.md) for more info.  + +#### Default Repositories + +The [Abp.EntityFramework](http://www.nuget.org/packages/Abp.EntityFramework) +implements default repositories for all the entities defined in your +DbContext. You don't have to create repository classes to use predefined +repository methods. Example: + + public class PersonAppService : IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + + _personRepository.Insert(person); + } + } + +The PersonAppService contructor-injects **IRepository<Person>** and +uses the **Insert** method. This way, you can easily inject +**IRepository<TEntity>** (or IRepository<TEntity, +TPrimaryKey>) and use the predefined methods. + +#### Custom Repositories + +If standard repository methods are not sufficient, you can create custom +repository classes for your entities. + +##### Application Specific Base Repository Class + +ASP.NET Boilerplate provides a base class, **EfRepositoryBase**, to +implement repositories easily. To implement the **IRepository** interface, +you can simply derive your repository from this class. However, it's better to +create your own base class that extends the EfRepositoryBase. Thus, you can +easily add shared/common methods to your repositories or override existing +methods. An example base class for all the repositories of a +*SimpleTaskSystem* application: + + //Base class for all repositories in my application + public class SimpleTaskSystemRepositoryBase : EfRepositoryBase + where TEntity : class, IEntity + { + public SimpleTaskSystemRepositoryBase(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + //add common methods for all repositories + } + + //A shortcut for entities those have integer Id + public class SimpleTaskSystemRepositoryBase : SimpleTaskSystemRepositoryBase + where TEntity : class, IEntity + { + public SimpleTaskSystemRepositoryBase(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + //do not add methods here, add them to the class above (because this class inherits it) + } + +Notice that we're inheriting from +EfRepositoryBase<**SimpleTaskSystemDbContext**, TEntity, and +TPrimaryKey>? This sets ASP.NET Boilerplate to use the +SimpleTaskSystemDbContext in our repositories. + +By default, all the repositories for your given DbContext +(SimpleTaskSystemDbContext in this example) is implemented using +EfRepositoryBase. You can replace it to your own base +repository class by adding the **AutoRepositoryTypes** attribute to your +DbContext as shown below: + + [AutoRepositoryTypes( + typeof(IRepository<>), + typeof(IRepository<,>), + typeof(SimpleTaskSystemEfRepositoryBase<>), + typeof(SimpleTaskSystemEfRepositoryBase<,>) + )] + public class SimpleTaskSystemDbContext : AbpDbContext + { + ... + } + +##### Custom Repository Example + +To implement a custom repository, simply derive it from your application +specific base repository class like the one we created above. + +Assume that we have a Task entity that can be assigned to a Person +(entity) and a Task State (new, assigned, completed... and so on). +We may need to write a custom method to get the list of Tasks, with some +conditions, and with a pre-fetched (included) AssisgnedPerson property; All in a +single database query. See the example code: + + public interface ITaskRepository : IRepository + { + List GetAllWithPeople(int? assignedPersonId, TaskState? state); + } + + public class TaskRepository : SimpleTaskSystemRepositoryBase, ITaskRepository + { + public TaskRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public List GetAllWithPeople(int? assignedPersonId, TaskState? state) + { + var query = GetAll(); + + if (assignedPersonId.HasValue) + { + query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); + } + + if (state.HasValue) + { + query = query.Where(task => task.State == state); + } + + return query + .OrderByDescending(task => task.CreationTime) + .Include(task => task.AssignedPerson) + .ToList(); + } + } + +**We first defined** ITaskRepository and then implemented it. +The **GetAll()** method returns an **IQueryable<Task>**, then we can added +some **Where** filters using the given parameters. Finally, we called +**ToList()** to get the list of Tasks. + +You can also use the **Context** object in repository methods to reach +your DbContext, so that you can directly use the Entity Framework APIs.  + +**Note**: Define the custom repository **interface** in the +**domain/core** layer, **implement** it in the **EntityFramework** +project for layered applications. This way, you can inject the interface +from any project without actually referencing EF. + +#### Repository Best Practices + +- **Use default repositories** wherever it's possible. You can use + the default repository even if you have a custom repository for an entity + (if you use standard repository methods). +- Always create a **repository base class** for your application for + custom repositories, as defined above. +- Define **interfaces** for your custom repositories in the **domain + layer** (.Core project in startup template), and custom repository + **classes** in the **.EntityFramework** project, if you want to abstract + EF from your domain/application. + +### Transaction Management + +ASP.NET Boilerplate has a built-in [unit of work](Unit-Of-Work.md) +system to manage database connection and transactions. Entity framework +has different [transaction management +approaches](https://msdn.microsoft.com/en-us/library/dn456843(v=vs.113).aspx). +ASP.NET Boilerplate uses the ambient TransactionScope approach by default, +but it also has a built-in implementation for the DbContext transaction API. If +you want to switch to the DbContext transaction API, you can configure it in the +PreInitialize method of your [module](Module-System.md) like this: + + Configuration.ReplaceService(DependencyLifeStyle.Transient); + +Remember to add "*using Abp.Configuration.Startup;*" to your code file +to be able to use the ReplaceService generic extension method. + +In addition, your DbContext **should have constructors** as described in the +DbContext section of this document. + +### Data Stores + +Since ASP.NET Boilerplate has built-in integration with EntityFramework, +it can work with the data stores EntityFramework supports. Our free startup +templates are designed to work with Sql Server but you can modify them +to work with a different data store. + +For example, if you want to work with MySql, please refer to [this +document](EF-MySql-Integration.md) diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EventBus-Domain-Events.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EventBus-Domain-Events.md" new file mode 100644 index 0000000..91d5aaa --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/EventBus-Domain-Events.md" @@ -0,0 +1,335 @@ +In C\#, a class can define events and other classes can register with them +to be notified when something happens. This is useful for a desktop +application or standalone windows service, but for a web application +it's a bit problematic since objects are created in a web request and +are short-lived. It's hard to register some class events. +Directly registering to another class's event makes classes tightly +coupled. + +Domain events can be used to decouple business logic and to react to +important domain changes in an application. + +### EventBus + +The EventBus is a **singleton** object that is shared by other classes +to trigger and handle events. To use the event bus, you need to get a +reference to it. You can do that in two ways. + +#### Injecting IEventBus + +You can use [dependency +injection](/Pages/Documents/Dependency-Injection) to get a reference to the +**IEventBus**. Here, we used the property injection pattern: + + public class TaskAppService : ApplicationService + { + public IEventBus EventBus { get; set; } + + public TaskAppService() + { + EventBus = NullEventBus.Instance; + } + } + +Property injection is more proper than a constructor injection when injecting +the event bus. This way, your class can work without the event bus. NullEventBus +implements the [null object +pattern](http://en.wikipedia.org/wiki/Null_Object_pattern). When you +call its methods, it does nothing at all. + +#### Getting The Default Instance + +If you can not inject it, you can directly use **EventBus.Default**. +It's the global event bus and it can be used as shown: + + EventBus.Default.Trigger(...); //trigger an event + +We **do not recommend** that you directly use EventBus.Default, +since it makes unit testing harder. + +### Defining Events + +Before you can trigger an event, you need to first define it. An event is +represented by a class that is derived from **EventData**. Assume that +we want to trigger an event when a task is completed: + + public class TaskCompletedEventData : EventData + { + public int TaskId { get; set; } + } + +This class contains properties that are needed by the class that handles +the event. The **EventData** class defines the **EventSource** (the object that +triggered the event) and the **EventTime** (when it's triggered) properties. + +#### Predefined Events + +##### Handled Exceptions + +ASP.NET Boilerplate defines **AbpHandledExceptionData** and triggers +this event when it automatically handles an exception. This is +especially useful if you want to get more information about exceptions +(ASP.NET Boilerplate automatically logs all exceptions). You can +register to this event to be informed when an exception occurs. + +##### Entity Changes + +There are also generic event data classes for entity changes: +**EntityCreatingEventData<TEntity>, +EntityCreatedEventData<TEntity>**, +**EntityUpdatingEventData<TEntity>, +EntityUpdatedEventData<TEntity>, +EntityDeletingEventData<TEntity>** and +**EntityDeletedEventData<TEntity>**. Also, there are +**EntityChangingEventData<TEntity>** and +**EntityChangedEventData<TEntity>**. A change can be insert, +update or delete. + +'ing' events (e.g. EntityUpdating) are triggered before committing a transaction. +This way, you can rollback the [unit of work](/Pages/Documents/Unit-Of-Work) +and prevent an operation by throwing an exception. 'ed' +events (e.g. EntityUpdated) are triggered after committing a transaction, for which +you cannot rollback the unit of work. + +Entity change events are defined in the **Abp.Events.Bus.Entities** +namespace and are **automatically triggered** by ASP.NET Boilerplate +when an entity is inserted, updated or deleted. If you have a Person +entity, you can register to EntityCreatedEventData<Person> to be +informed when a new Person is created and inserted into the database. These +events also support inheritance. If the Student class is derived from the Person +class and you registered to EntityCreatedEventData<Person>, you +will be informed when a Person **or** Student is inserted. + +### Triggering Events + +Triggering an event is simple: + + public class TaskAppService : ApplicationService + { + public IEventBus EventBus { get; set; } + + public TaskAppService() + { + EventBus = NullEventBus.Instance; + } + + public void CompleteTask(CompleteTaskInput input) + { + //TODO: complete the task in the database... + EventBus.Trigger(new TaskCompletedEventData {TaskId = 42}); + } + } + +There are some overloads of the trigger method: + + EventBus.Trigger(new TaskCompletedEventData { TaskId = 42 }); //Explicitly declare generic argument + EventBus.Trigger(this, new TaskCompletedEventData { TaskId = 42 }); //Set 'event source' as 'this' + EventBus.Trigger(typeof(TaskCompletedEventData), this, new TaskCompletedEventData { TaskId = 42 }); //Call non-generic version (first argument is the type of the event class) + +Another way of triggering events is to use the DomainEvents collection of +an AggregateRoot class (see related section in the [Entity +documentation](Entities.md)). + +### Handling Events + +To handle an event, you should implement the **IEventHandler<T>** +interface as shown below: + + public class ActivityWriter : IEventHandler, ITransientDependency + { + public void HandleEvent(TaskCompletedEventData eventData) + { + WriteActivity("A task is completed by id = " + eventData.TaskId); + } + } + +The IEventHandler defines a HandleEvent method and implements it as shown +above. + +EventBus is integrated into the dependency injection system. We implemented +ITransientDependency (above), so when a TaskCompleted event occurs, it +creates a new instance of the ActivityWriter class, calls its +HandleEvent method, and then disposes it. See [dependency +injection](/Pages/Documents/Dependency-Injection) for more info. + +#### Handling Base Events + +Eventbus supports the **inheritance** of events. For example, you can create +a **TaskEventData** base class with two derived classes: **TaskCompletedEventData** +and **TaskCreatedEventData**: + + public class TaskEventData : EventData + { + public Task Task { get; set; } + } + + public class TaskCreatedEventData : TaskEventData + { + public User CreatorUser { get; set; } + } + + public class TaskCompletedEventData : TaskEventData + { + public User CompletorUser { get; set; } + } + +You can then implement **IEventHandler<TaskEventData>** to handle +both of these events: + + public class ActivityWriter : IEventHandler, ITransientDependency + { + public void HandleEvent(TaskEventData eventData) + { + if (eventData is TaskCreatedEventData) + { + //... + } + else if (eventData is TaskCompletedEventData) + { + //... + } + } + } + +You can implement IEventHandler<EventData> to +handle all the events in an application. You probably don't want that, but +it's possible. + +#### Exception Handlers + +The EventBus **triggers all handlers** even if any of them throw +an exception. If only one of them throws an exception, then it's directly +thrown by the Trigger method. If more than one handler throws an exception, +EventBus throws a single **AggregateException** for all of them. + +#### Handling Multiple Events + +You can handle **multiple events** in a single handler. If so, +you should implement IEventHandler<T> for each event. Example: + + public class ActivityWriter : + IEventHandler, + IEventHandler, + ITransientDependency + { + public void HandleEvent(TaskCompletedEventData eventData) + { + //TODO: handle the event... + } + + public void HandleEvent(TaskCreatedEventData eventData) + { + //TODO: handle the event... + } + } + +### Registration Of Handlers + +We have to register handlers to the event bus in order to handle events. + +#### Automatically + +ASP.NET Boilerplate finds all the classes that implement **IEventHandler** +that are registered with **dependency injection** (for example, by implementing +ITransientDependency as the samples above). It then registers them to +the event bus **automatically**. When an event occurs, it uses +dependency injection to get a reference to the handler and, after handling the event, +releases it. This is the **suggested** way of using the +event bus in ASP.NET Boilerplate. + +#### Manually + +It is also possible to manually register to events, but use it with +caution! In a web application, event registration should be done at +the start of the application. It's not a good approach to register to an event in a +web request, since registered classes remain registered after the request's +completion and would be re-registered for each request. This may cause problems +for your application since a registered class can be called multiple +times. Keep in mind that manual registrations do not use the +dependency injection system. + +There are some overloads of the register method in the event bus. The +simplest one uses a delegate (or a lambda): + + EventBus.Register(eventData => + { + WriteActivity("A task is completed by id = " + eventData.TaskId); + }); + + +When a 'task completed' event occurs, this lambda method is +called. The second one waits for an object that implements +IEventHandler<T>: + + EventBus.Register(new ActivityWriter()); + +The same instance of ActivityWriter is called for events. This method +also has a non-generic overload. Another overload accepts two generic +arguments: + + EventBus.Register(); + +In this case, the event bus creates a new ActivityWriter for each event. It +then calls the ActivityWriter.Dispose method if it's +[disposable](http://msdn.microsoft.com/en-us/library/system.idisposable.aspx). + +Lastly, you can register an **event handler factory** to handle the creation +of handlers. A handler factory has two methods: **GetHandler** and +**ReleaseHandler**. Example: + + public class ActivityWriterFactory : IEventHandlerFactory + { + public IEventHandler GetHandler() + { + return new ActivityWriter(); + } + + public void ReleaseHandler(IEventHandler handler) + { + //TODO: release/dispose the activity writer instance (handler) + } + } + +There is also a special factory class, **IocHandlerFactory**, that +can be used with the dependency injection system to create and release +handlers. ASP.NET Boilerplate also uses this class in automatic +registrations, so if you want to use the dependency injection system, +directly use the automatic registration as defined above. + +### Unregistration + +When you **manually** register to an event bus, you may want to +**unregister** the event later. The simplest way of unregistering an +event is disposing the return value of the **Register** method. Example: + + //Register to an event... + var registration = EventBus.Register(eventData => WriteActivity("A task is completed by id = " + eventData.TaskId) ); + + //Unregister from event + registration.Dispose(); + + +Most likely, the unregistration will be somewhere else and at a later time. Keep the +registration object and dispose it when you want to unregister. All +overloads of the Register method return a disposable object to +unregister to the event. + +The EventBus also provides the **Unregister** method. Example usage: + + //Create a handler + var handler = new ActivityWriter(); + + //Register to the event + EventBus.Register(handler); + + //Unregister from event + EventBus.Unregister(handler); + + +This also provides overloads to unregister delegates and factories. +Unregistering a handler object must be done on the same object which was registered +before. + +Lastly, EventBus provides a **UnregisterAll<T>()** method to +unregister all the handlers of an event and a **UnregisterAll()** method to +unregister all the handlers of all the events. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Feature-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Feature-Management.md" new file mode 100644 index 0000000..bf97b41 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Feature-Management.md" @@ -0,0 +1,261 @@ +### Introduction + +Most **SaaS** (multi-tenant) applications have **editions** (packages) +that have different **features**. This way, they can provide different +**price and feature options** to their tenants (customers). + +ASP.NET Boilerplate provides a **feature system** to make it easier. You +can **define** features, **check** if a feature is **enabled** for a +tenant, and **integrate** the feature system to other ASP.NET Boilerplate +concepts (like [authorization](Authorization.md) and +[navigation](Navigation.md)). + +#### About IFeatureValueStore + +The feature system uses the **IFeatureValueStore** to get the values of features. +While you can implement it in your own way, it's fully implemented in the +**Module Zero** project. If it's not implemented, NullFeatureValueStore +is used to return null for all features (so the default feature values are +used in this case). + +### Feature Types + +There are two fundamental feature types. + +#### Boolean Feature + +Can be "true" or "false". This type of a feature can be **enabled** or +**disabled** (for an edition or for a tenant). + +#### Value Feature + +Can be an **arbitrary value**. While it's stored and retrieved as a string, +numbers also can be stored as strings. + +For example, our application may be a task management application and we +may have a limit for creating tasks in a month. Imagine that we have two +different editions/packages; one allows for creating 1,000 tasks per month, +while the other allows for creating 5,000 tasks per month. This feature +should be stored as a value, not simply as true or false. + +### Defining Features + +A feature should be defined before it is checked. A +[module](/Pages/Documents/Module-System) can define its own features by +deriving from the **FeatureProvider** class. Here's a very simple feature +provider that defines 3 features: + + public class AppFeatureProvider : FeatureProvider + { + public override void SetFeatures(IFeatureDefinitionContext context) + { + var sampleBooleanFeature = context.Create("SampleBooleanFeature", defaultValue: "false"); + sampleBooleanFeature.CreateChildFeature("SampleNumericFeature", defaultValue: "10"); + context.Create("SampleSelectionFeature", defaultValue: "B"); + } + } + +After creating a feature provider, we must register it in our module's +[PreInitialize](/Pages/Documents/Module-System#preinitialize) method +as shown below: + + Configuration.Features.Providers.Add(); + +#### Basic Feature Properties + +A feature definition requires at least two properties: + +- **Name**: A unique name (string) to identify the feature. +- **Default value**: A default value. This is used when we need the + value of the feature and it's not available for current tenant. + +Here, we defined a boolean feature named "SampleBooleanFeature", with the +default value of "false" (not enabled). We also defined two value +features. Note that SampleNumericFeature is defined as a child of +SampleBooleanFeature. + +Tip: Create a const string for a feature name and use it everywhere to +prevent typing errors. + +#### Other Feature Properties + +While the unique name and default value properties are required, there are +some optional properties for more fine-tuned control. + +- **Scope**: A value in the FeatureScopes enum. It can be **Edition** (if + this feature can be set only for edition level), **Tenant** (if this + feature can be set only for tenant level) or **All** (if this + feature can be set for editions and tenants, where a tenant setting + overrides its edition's setting). Default value is **All**. +- **DisplayName**: A localizable string to show the feature's name to + users. +- **Description**: A localizable string to show the feature's detailed + description to users. +- **InputType**: A UI input type for the feature. This can be defined, + and then used while creating an automatic feature screen. +- **Attributes**: An arbitrary custom dictionary of key-value pairs + that are related to the feature. + +Let's see some more detailed definitions for the features above: + + public class AppFeatureProvider : FeatureProvider + { + public override void SetFeatures(IFeatureDefinitionContext context) + { + var sampleBooleanFeature = context.Create( + AppFeatures.SampleBooleanFeature, + defaultValue: "false", + displayName: L("Sample boolean feature"), + inputType: new CheckboxInputType() + ); + + sampleBooleanFeature.CreateChildFeature( + AppFeatures.SampleNumericFeature, + defaultValue: "10", + displayName: L("Sample numeric feature"), + inputType: new SingleLineStringInputType(new NumericValueValidator(1, 1000000)) + ); + + context.Create( + AppFeatures.SampleSelectionFeature, + defaultValue: "B", + displayName: L("Sample selection feature"), + inputType: new ComboboxInputType( + new StaticLocalizableComboboxItemSource( + new LocalizableComboboxItem("A", L("Selection A")), + new LocalizableComboboxItem("B", L("Selection B")), + new LocalizableComboboxItem("C", L("Selection C")) + ) + ) + ); + } + + private static ILocalizableString L(string name) + { + return new LocalizableString(name, AbpZeroTemplateConsts.LocalizationSourceName); + } + } + +Note that the Input type definitions are not used by ASP.NET Boilerplate. +They can be used by applications to create inputs for features. +ASP.NET Boilerplate just provides the infrastructure to make it easier. + +#### Feature Hierarchy + +As shown in the sample feature providers, a feature can have **child features**. +A Parent feature is generally defined as a **boolean** +feature. Child features will be available only if the parent is enabled. +ASP.NET Boilerplate **does not** enforce this, but we recommend it. +The application should take care of it. + +### Checking Features + +We define a feature to check its value in the application to allow or +block some application features per tenant. There are different ways of +checking it. + +#### Using RequiresFeature Attribute + +We can use the **RequiredFeature** attribute for a method or a class as +shown below: + + [RequiresFeature("ExportToExcel")] + public async Task GetReportToExcel(...) + { + ... + } + +This method is executed only if the "ExportToExcel" feature is enabled for +the **current tenant** (current tenant is obtained from +[IAbpSession](/Pages/Documents/Abp-Session)). If it's not enabled, an +**AbpAuthorizationException** is thrown automatically. + +As such, the RequiresFeature attribute should only be used for **boolean type +features**. Otherwise, you may get exceptions. + +##### RequiresFeature attribute notes + +ASP.NET Boilerplate uses the power of dynamic method interception for +feature checking. There are some restrictions for the methods that can use the +RequiresFeature attribute. + +- You can not use it for private methods. +- You can not use it for static methods. +- You can not use it for methods of a non-injected class (We must use + [dependency injection](/Pages/Documents/Dependency-Injection)). + +Also, + +- You can use it for any **public** method if the method is called over an + **interface** (like Application Services used over interface). +- A method should be **virtual** if it's called directly from a class + reference (like ASP.NET MVC or Web API Controllers). +- A method should be **virtual** if it's **protected**. + +#### Using IFeatureChecker + +We can inject and use IFeatureChecker to check a feature manually (it's +automatically injected and directly usable for application services, MVC, +and Web API controllers). + +##### IsEnabled + +This is used to simply check if a given feature is enabled or not. Example: + + public async Task GetReportToExcel(...) + { + if (await FeatureChecker.IsEnabledAsync("ExportToExcel")) + { + throw new AbpAuthorizationException("You don't have this feature: ExportToExcel"); + } + + ... + } + +The IsEnabledAsync and other methods also have sync versions. + +The IsEnabled method should be used for **boolean type features**, +otherwise you may get exceptions. + +If you just want to check a feature and throw an exception as shown in the +example, you can use the **CheckEnabled** method. + +##### GetValue + +Used to get the current value of a feature for value-type features. Example: + + var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth(); + if (createdTaskCountInThisMonth >= FeatureChecker.GetValue("MaxTaskCreationLimitPerMonth").To()) + { + throw new AbpAuthorizationException("You exceed task creation limit for this month, sorry :("); + } + +The FeatureChecker methods also have overrides to check features not only for the +current tenantId, but for a **specified** tenantId as well. + +#### Client Side + +On the client side (JavaScript), we can use the **abp.features** namespace +to get the current values of features. + +##### isEnabled +```csharp + var isEnabled = abp.features.isEnabled('SampleBooleanFeature'); +``` +##### getValue +```csharp + var value = abp.features.getValue('SampleNumericFeature'); +``` +### Feature Manager + +If you need the definitions of features, you can inject and use +**IFeatureManager**. + +### A Note For Editions + +The ASP.NET Boilerplate framework does not have a built-in edition system because +such a system requires a database (to store editions, edition features, +tenant-edition mappings and so on...). Therefore, the edition system is +implemented in [Module Zero](/Pages/Documents/Zero/Edition-Management). +You can use it as a ready-made edition system or implement +one yourself. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Handling-Exceptions.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Handling-Exceptions.md" new file mode 100644 index 0000000..4821659 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Handling-Exceptions.md" @@ -0,0 +1,142 @@ +### Introduction + +This document is for the ASP.NET MVC and Web API. If you're interested in +ASP.NET Core, see the [ASP.NET Core](AspNet-Core.md) documentation. + +In a web application, exceptions are usually handled in MVC Controller +and Web API Controller actions. When an exception occurs, +the application user is informed about the error and with an optional reason. + +If an error occurs in a regular HTTP request, an error page is shown. +If an error occurs in an AJAX request, the server sends the error information +to the client and the client then handles and shows it to the user. + +Handling exceptions in all web requests is tedious, and hard to keep DRY. +ASP.NET Boilerplate **automates** this. You almost never need to +explicitly handle an exception. ASP.NET Boilerplate handles all +exceptions, logs them, and returns an appropriate and formatted response to +the client. It also handles these responses in the client and shows error +messages to the user. + +### Enabling Error Handling + +To enable error handling for ASP.NET MVC Controllers, **customErrors** +mode must be enabled for ASP.NET MVC applications. + + + +It can also be '**RemoteOnly**' if you do not want to handle errors on a +local computer, for instance. Note that this is only required for ASP.NET MVC +Controllers, and not for Web API Controllers. + +If you are already handling exceptions in a global filter, it may +hide exceptions. Thus, ABP's exception handling may not work as you +expected. So if you do this, do it carefully! + +### Non-Ajax Requests + +If a request is not AJAX, an error page is shown. + +#### Showing Exceptions + +Imagine that there is an MVC controller action which throws an arbitrary +exception: + + public ActionResult Index() + { + throw new Exception("A sample exception message..."); + } + +Most likely, this exception would be thrown by another method that is called +from this action. ASP.NET Boilerplate handles this exception, logs it +and shows the '**Error.cshtml**' view. You can **customize** this view to +show the error. Here's an **example** error view (the default Error view in the ASP.NET +Boilerplate templates): + +Default Error view + +ASP.NET Boilerplate hides the details of the exception from users and shows +a standard (and localizable) error message, unless you explicitly throw +a **UserFriendlyException**. + +#### UserFriendlyException + +The UserFriendlyException is a special type of exception that is directly +shown to the user. See the sample code below: + + public ActionResult Index() + { + throw new UserFriendlyException("Ooppps! There is a problem!", "You are trying to see a product that is deleted..."); + } + +ASP.NET Boilerplate logs it and does not hide the exception: + +User friendly exception + +If you want to show a special error message to users, just throw a +UserFriendlyException (or an exception derived from it). + +#### Error Model + +ASP.NET Boilerplate passes an **ErrorViewModel** object as a model to the +Error view: + + public class ErrorViewModel + { + public AbpErrorInfo ErrorInfo { get; set; } + + public Exception Exception { get; set; } + } + +**ErrorInfo** contains detailed information about the error that can be +shown to the user. The **Exception** object is the thrown exception. You can +check it and show additional information if you want. For example, we +can show validation errors if it's an **AbpValidationException**: + +Validation errors + +### AJAX Requests + +If the return type of an MVC action is a JsonResult (or Task<JsonResult for +async actions), ASP.NET Boilerplate returns a JSON object to the client +when exceptions occur. Sample return object for an error: + + { + "targetUrl": null, + "result": null, + "success": false, + "error": { + "message": "An internal error occurred during your request!", + "details": "..." + }, + "unAuthorizedRequest": false + } + +**success: false** indicates that there is an error. The **error** object +provides the **message** and **details**. + +When you use ASP.NET Boilerplate's infrastructure to make an AJAX request +on the client side, it automatically handles this JSON object and shows an +error message to the user using the [message API](/Pages/Documents/Javascript-API/Message). +See the [AJAX API](/Pages/Documents/Javascript-API/AJAX) documentation for more +information. + +### Exception Event + +When ASP.NET Boilerplare handles an exception, it triggers an +**AbpHandledExceptionData** event. +(See [eventbus documentation](/Pages/Documents/EventBus-Domain-Events) +for more information about Event Bus). Example: + + public class MyExceptionHandler : IEventHandler, ITransientDependency + { + public void HandleEvent(AbpHandledExceptionData eventData) + { + //TODO: Check eventData.Exception! + } + } + +If you put this example class into your application (generally into your +Web project), the **HandleEvent** method will be called for all exceptions +handled by ASP.NET Boilerplate. From there, you can investigate the Exception +object in detail. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Hangfire-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Hangfire-Integration.md" new file mode 100644 index 0000000..f1fd6e6 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Hangfire-Integration.md" @@ -0,0 +1,151 @@ +### Introduction + +[Hangfire](http://hangfire.io/) is a compherensive background job +manager. You can **integrate** ASP.NET Boilerplate with Hangfire to use +it instead of the [default background job +manager](/Pages/Documents/Background-Jobs-And-Workers). You can use the +**same background job API** for Hangfire. As such, your code will be +**independent** of Hangfire. If you like, you can directly use +**Hangfire's API**, too. + +Hangfire Integration depends on the frameworks you are using. + +### ASP.NET Core Integration + +The [**Abp.HangFire.AspNetCore**](https://www.nuget.org/packages/Abp.HangFire.AspNetCore) +package is used to integrate to ASP.NET Core based applications. It +depends on +[Hangfire.AspNetCore](https://www.nuget.org/packages/Hangfire.AspNetCore/). +[This +document](https://www.hangfire.io/blog/2016/07/16/hangfire-1.6.0.html) +describes how to install hangfire to an ASP.NET Core project. It's similar +for ABP based projects too. First install the +[Abp.HangFire.AspNetCore](https://www.nuget.org/packages/Abp.HangFire.AspNetCore) +package to your web project: + + Install-Package Abp.HangFire.AspNetCore + +You can then install any storage for Hangfire. The most common one is SQL +Server (see the +[**Hangfire.SqlServer**](https://www.nuget.org/packages/Hangfire.SqlServer) +NuGet package). After you have installed these NuGet packages, you need to +**configure** your project to use Hangfire. + +First, we change the Startup class to add Hangfire to dependency +injection, and then configure the storage and connection string in the +**ConfigureServices** method: + + services.AddHangfire(config => + { + config.UseSqlServerStorage(_appConfiguration.GetConnectionString("Default")); + }); + +We then add the UseHangfireServer call in the **Configure** method: + + app.UseHangfireServer(); + +If you want to use hangfire's dashboard, you can add it, too: + + app.UseHangfireDashboard(); + +If you want to [authorize](Authorization.md) the dashboard, you can +use AbpHangfireAuthorizationFilter as shown below: + + app.UseHangfireDashboard("/hangfire", new DashboardOptions + { + Authorization = new[] { new AbpHangfireAuthorizationFilter("MyHangFireDashboardPermissionName") } + }); + +The configuration above is the standard way to integrate hangfire to an +ASP.NET Core application. For ABP based projects, we should also +configure our web module to replace Hangfire for ABP's default +background job manager: + + [DependsOn(typeof (AbpHangfireAspNetCoreModule))] + public class MyProjectWebModule : AbpModule + { + public override void PreInitialize() + { + Configuration.BackgroundJobs.UseHangfire(); + } + + //... + } + +We added **AbpHangfireAspNetCoreModule** as a dependency and used the +Configuration.BackgroundJobs.**UseHangfire** method to replace Hangfire +for ABP's default background job manager. + +Hangfire requires the schema creation permission in your database since it +creates its own schema and tables on first run. See the [Hangfire +documentation](http://docs.hangfire.io/en/latest/) for more information. + +### ASP.NET MVC 5.x Integration + +The [**Abp.HangFire**](https://www.nuget.org/packages/Abp.HangFire) NuGet +package is used for ASP.NET MVC 5.x projects: + + Install-Package Abp.HangFire + +You can then install any storage for Hangfire. The most common one is SQL +Server (see the +[**Hangfire.SqlServer**](https://www.nuget.org/packages/Hangfire.SqlServer) +NuGet package). After you have installed these NuGet packages, you can +**configure** your project to use Hangfire as shown below: + + [DependsOn(typeof (AbpHangfireModule))] + public class MyProjectWebModule : AbpModule + { + public override void PreInitialize() + { + Configuration.BackgroundJobs.UseHangfire(configuration => + { + configuration.GlobalConfiguration.UseSqlServerStorage("Default"); + }); + + } + + //... + } + +We added **AbpHangfireModule** as a dependency and used the +Configuration.BackgroundJobs.**UseHangfire** method to enable and +configure Hangfire ("Default" is the connection string in web.config). + +Hangfire requires the schema creation permission in your database since it +creates its own schema and tables on first run. See the [Hangfire +documentation](http://docs.hangfire.io/en/latest/) for more information. + +#### Dashboard Authorization + +Hangfire can show a **dashboard page** so you can see the status of all background +jobs in real time. You can configure it as described in its +[documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html). +By default, this dashboard page is available for all users, and is not +authorized. You can integrate it in to ABP's [authorization +system](Authorization.md) using the **AbpHangfireAuthorizationFilter** +class defined in the Abp.HangFire package. Example configuration: + + app.UseHangfireDashboard("/hangfire", new DashboardOptions + { + Authorization = new[] { new AbpHangfireAuthorizationFilter() } + }); + +This checks if the current user has logged in to the application. If you +want to require an additional permission, you can pass into its +constructor: + + app.UseHangfireDashboard("/hangfire", new DashboardOptions + { + Authorization = new[] { new AbpHangfireAuthorizationFilter("MyHangFireDashboardPermissionName") } + }); + +**Note**: UseHangfireDashboard should be called after the authentication +middleware in your Startup class (probably as the last line). Otherwise, +authorization will always fail. + +#### Limitations + +More than one background jobs in a single transaction isn't supported by Hangfire. Because, Hangfire does not participate the current transaction. It does not use the ambient transaction (TransactionScope). + +It works with default background job manager since it simply performs a db command and it belongs to the current transaction as expected. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Introduction.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Introduction.md" new file mode 100644 index 0000000..c447bd9 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Introduction.md" @@ -0,0 +1,148 @@ +### What is the ASP.NET Boilerplate? + +ASP.NET Boilerplate (**ABP**) is an **[open source](https://github.com/aspnetboilerplate/aspnetboilerplate)** and well-documented **application framework**. +It's not just a framework, it also provides a strong **[architectural model](https://aspnetboilerplate.com/Pages/Documents/NLayer-Architecture)** +based on **Domain Driven Design**, with all the **best practices** in mind. + +ABP works with the latest **ASP.NET Core** & **EF Core** but also supports ASP.NET MVC 5.x & EF 6.x as well. + +### A Quick Sample + +Let's investigate a simple class to see ABP's benefits: + + public class TaskAppService : ApplicationService, ITaskAppService + { + private readonly IRepository _taskRepository; + + public TaskAppService(IRepository taskRepository) + { + _taskRepository = taskRepository; + } + + [AbpAuthorize(MyPermissions.UpdateTasks)] + public async Task UpdateTask(UpdateTaskInput input) + { + Logger.Info("Updating a task for input: " + input); + + var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId); + if (task == null) + { + throw new UserFriendlyException(L("CouldNotFindTheTaskMessage")); + } + + input.MapTo(task); + } + } + +Here we see a sample [Application Service](Application-Services.md) method. An application service, in DDD, +is directly used by the presentation layer to perform the **use cases** of the application. +Think **UpdateTask** as a method that is called by JavaScript via AJAX. + +Let's see some of ABP's benefits here: + +- **[Dependency Injection](/Pages/Documents/Dependency-Injection)**: ABP uses and provides a conventional DI infrastructure. + Since this class is an application service, it's conventionally + registered to the DI container as transient (created per request). It + can simply inject any dependencies (such as the IRepository<Task> in + this sample). +- **[Repository](/Pages/Documents/Repositories)**: ABP can create a default repository for each entity (such as IRepository<Task> in + this example). The default repository has many useful methods such as the + FirstOrDefault method used in this example. We can extend the default + repository to suit our needs. Repositories abstract the DBMS and ORMs and + simplify the data access logic. +- **[Authorization](/Pages/Documents/Authorization)**: ABP can check permissions declaratively. + It prevents access to the UpdateTask method if the current user + has no "update tasks" permission or is not logged in. ABP not only uses declarative + attributes, but it also has additional ways in which you can authorize. +- **[Validation](/Pages/Documents/Validating-Data-Transfer-Objects)**: ABP automatically checks if the input is null. It also validates all + the properties of an input based on standard data annotation attributes + and custom validation rules. If a request is not valid, it throws a + proper validation exception and handles it in the client side. +- **[Audit Logging](/Pages/Documents/Audit-Logging)** : User, browser, IP address, calling service, method, parameters, calling time, + execution duration and some other information is automatically + saved for each request based on conventions and configurations. +- [**Unit Of Work**](/Pages/Documents/Unit-Of-Work): In ABP, each application service method is assumed to be a unit of work by default. + It automatically creates a connection and begins a transaction at + the beginning of the method. If the method successfully completes + without an exception, then the transaction is committed and the connection + is disposed. Even if this method uses different repositories or + methods, all of them will be atomic (transactional). All changes + on entities are automatically saved when a transaction is committed. + We don't even need to call the \_repository.Update(task) method as + shown above. +- [**Exception Handling**](/Pages/Documents/Handling-Exceptions): We almost never have to manually handle exceptions in ABP on a web application. All exceptions are automatically handled by default! If an exception + occurs, ABP automatically logs it and returns a proper result to the + client. For example, if this is an AJAX request, it returns a + JSON object to the client indicating that an error occurred. It hides the actual + exception from the client unless the exception is a + UserFriendlyException, as used in this sample. It also understands + and handles errors on the client side and show appropriate messages to the + users. +- **[Logging](/Pages/Documents/Logging)**: As you see, we can write logs using the Logger object defined in the base class. + Log4Net is used by default, but it's changeable and configurable. +- **[Localization](/Pages/Documents/Localization)**: Note that we used the 'L' method while throwing the exception? + This way, it's automatically localized based on the current user's culture. See the [localization](/Pages/Documents/Localization) document for more. +- **[Auto Mapping](/Pages/Documents/Data-Transfer-Objects)**: In the last line, we're using ABP's MapTo extension method to map input + properties to entity properties. It uses the AutoMapper library to + perform the mapping. We can easily map properties from one object + to another based on naming conventions. +- **[Dynamic API Layer](/Pages/Documents/Dynamic-Web-API)**: TaskAppService is a simple class, actually. We generally have to write a wrapper API Controller to expose methods to JavaScript clients, but ABP + automatically does that on runtime. This way, we can use application + service methods directly from clients. +- **[Dynamic JavaScript AJAX Proxy](/Pages/Documents/Dynamic-Web-API#dynamic-javascript-proxies)** : ABP creates proxy methods those make calling application + service methods as simple as calling JavaScript methods on the client. + +We can see the benefits of ABP in this simple class. All these tasks normally take significant time, +but are automatically handled by the framework. + +Besides this simple example, ABP provides a strong infrastructure and development model for +[modularity](/Pages/Documents/Module-System), [multi-tenancy](Multi-Tenancy.md), [caching](Caching.md), [background jobs](Background-Jobs-And-Workers.md), [data filters](/Pages/Documents/Data-Filters), [setting management](/Pages/Documents/Setting-Management), [domain events](EventBus-Domain-Events.md), unit & integration testing and so on... You focus on your business code and don't repeat yourself! + +### Getting Started + +You can start with the startup templates or the introduction tutorials. + +#### Startup Templates + +Directly create a modern looking startup project from the [startup templates](/Templates). + +Startup template + +Startup templates provides a basic layout and some common features for an application. There are several startup templates with different options. + +##### ASP.NET Core + +* [Single Page Application with ASP.NET Core & Angular](Zero/Startup-Template-Angular.md) +* [Multi-Page Application with ASP.NET Core & jQuery](Zero/Startup-Template-Core.md) + +##### ASP.NET MVC 5.x + +* [ASP.NET MVC 5.x & AngularJS 1.x / ASP.NET MVC 5.x & jQuery](Zero/Startup-Template.md) + +See the [download page](/Templates) for other combinations. + +#### Introduction Tutorials + +Step by step tutorials introduces the framework and explains how to create your application based on the startup templates. + +##### ASP.NET Core + +- [Introduction with ASP.NET Core & Entity Framework Core](Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html) +- [Developing a multi-tenant (SaaS) application with ASP.NET Core, EntityFramework Core & Angular](Articles/Developing-MultiTenant-SaaS-ASP.NET-CORE-Angular/index.html) + +##### ASP.NET MVC 5.x + +- [Introduction with ASP.NET MVC 5.x, Web API 2.x, EntityFramework 6.x & AngularJS 1.x](Articles/Introduction-With-AspNet-MVC-Web-API-EntityFramework-and-AngularJs/index.html) +- [Developing a multi-tenant (SaaS) application with ASP.NET MVC 5.x, EntityFramework 6.x & AngularJS 1.x](Articles/Developing-a-Multi-Tenant-SaaS-Application-with-ASP.NET-MVC-EntityFramework-AngularJs/index.html) + +### Samples + +There are many sample projects developed with the framework. See [the samples page](/Samples). + +### Community + +This is an open source project and open to contributions from the community. + +* Use [the GitHub repository](https://github.com/aspnetboilerplate/aspnetboilerplate) to access the latest **source code**, create [issues](https://github.com/aspnetboilerplate/aspnetboilerplate/issues) and send [pull requests](https://github.com/aspnetboilerplate/aspnetboilerplate/pulls). +* Use [aspnetboilerplate tag on stackoverflow](https://stackoverflow.com/questions/tagged/aspnetboilerplate) to ask questions about the usage. +* Follow [aspboilerplate on twitter](https://twitter.com/aspboilerplate) to be informed about the happenings. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API.md" new file mode 100644 index 0000000..e5dea9b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API.md" @@ -0,0 +1,37 @@ +ASP.NET Boilerplate provides a set of objects and functions that are +used to make JavaScript development easy and standards-based. + +Here is a list of APIs in ASP.NET Boilerplate. +Click the headers to see more documentation about their usage. + +#### [AJAX](/Pages/Documents/Javascript-API/AJAX) + +Used to call server-side services using AJAX and evaluate the return +value. Since ASP.NET Boilerplate's server-side code returns a standard +response for AJAX calls, it's suggested you use this method to handle the +standard return value. + +#### [Notification](/Pages/Documents/Javascript-API/Notification) + +Used to show auto-disappearing notifications. + +#### [Message](/Pages/Documents/Javascript-API/Message) + +Used to show message boxes (dialogs) to the user. + +#### [UI Block & Busy](/Pages/Documents/Javascript-API/UI-Block-Busy) + +Used to make an area (a div, a form, entire page...) blocked for user +inputs. Also used to make an area busy (with a busy indicator). + +#### [Event Bus](/Pages/Documents/Javascript-API/Event-Bus) + +Used to register to and trigger client side global events. + +#### [Logging](/Pages/Documents/Javascript-API/Logging) + +Used to write logs on the client-side. + +#### [Other Utility Functions](/Pages/Documents/Javascript-API/Other-Utilities) + +Some utility functions that make it easy to perform some common stuff. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/AJAX.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/AJAX.md" new file mode 100644 index 0000000..b43cbae --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/AJAX.md" @@ -0,0 +1,253 @@ +### Problems of AJAX Operations + +Performing AJAX calls is frequently used by modern applcations. +Especially in SPAs (Single Page Applications), it's almost the only way +of communicating with the server. An AJAX call consists of some +repeating steps: + +On the client-side, the JavaScript code should supply an URL, +optionally some data, and it should select a method (POST, GET...) to perfom an AJAX +call. It must wait and handle the return value. There may be an error +(network error generally) while performing a call to the server. Or there +may be some error on the server-side and server may send a failed response +with an error message. The client-side code must handle these and optionally +notify a user (like show an error dialog). If there is no error and the server +returns data, the client must also handle it. In addition to this, you generally want +to block some part or a whole area of the screen and show a busy indicator +until an AJAX operation completes. + +Server-code should get the request, perform some server-side code, catch +exceptions and return a valid response to the client. If an error +occurs, it may optionally send an error message to the client. If +it's a validation error, the server may want to the add descriptions to validation problems. +In the case of a successful request, it may send return values to the client. + +### The ASP.NET Boilerplate Way + +ASP.NET Boilerplate automates some of these steps by wrapping AJAX calls +with the **abp.ajax** function. An example ajax call: + + var newPerson = { + name: 'Dougles Adams', + age: 42 + }; + + abp.ajax({ + url: '/People/SavePerson', + data: JSON.stringify(newPerson) + }).done(function(data) { + abp.notify.success('Created new person with id = ' + data.personId); + }); + +abp.ajax gets **options** as an object. You can pass any valid parameter +into jQuery's [$.ajax](http://api.jquery.com/jQuery.ajax/) method. +There are some **defaults** here: the dataType is '**json**', the type +is '**POST**', and the contentType is '**application/json**' (We're +calling JSON.stringify to convert a JavaScript object into a JSON string +before sending it to the server). You can override these defaults by passing +options to abp.ajax. + +abp.ajax returns a **[promise](http://api.jquery.com/deferred.promise/)**, +so you can write done, fail, then (etc) handlers. In this example, we made +a simple AJAX request to the **PeopleController**'s **SavePerson** action. +In the **done** handler, we fetched the database **id** for the newly created +person and showed a success notification (See [notification +API](/Pages/Documents/Javascript-API/Notification)). Let's see the **MVC +controller** for this AJAX call: + + public class PeopleController : AbpController + { + [HttpPost] + public JsonResult SavePerson(SavePersonModel person) + { + //TODO: save new person to database and return new person's id + return Json(new {PersonId = 42}); + } + } + +The **SavePersonModel** contains the Name and Age properties. +The SavePerson action is marked with **HttpPost**, since abp.ajax's default method +is POST. We simplified the method implementation by returning an anonymous +object. + +This seams pretty straightforward, but there are some important things behind +the scenes that are handled by ASP.NET Boilerplate. Let's dive into those +details... + +#### AJAX Return Messages + +When we directly return an object with PersonId = 2, ASP.NET Boilerplate +wraps it with an **MvcAjaxResponse** object. The actual AJAX response is +something like this: + + { + "success": true, + "result": { + "personId": 42 + }, + "error": null, + "targetUrl": null, + "unAuthorizedRequest": false, + "__abp": true + } + +Here all the properties are camelCase (since it's conventional in +JavaScript) even if they are PascalCase on the server-side's code. Here's +an explanation of all the fields: + +- **success**: A boolean value (true or false) that indicates the success + status of the operation. If this is true, abp.ajax resolves the + promise and calls the **done** handler. If it's false (if there is + an exception thrown in the method call), it calls the **fail** handler and + shows the **error** message using the + [abp.message.error](/Pages/Documents/Javascript-API/Message) + function. +- **result**: The actual return value of the controller action. It's valid + if the request was a success and if the server sent a return value. +- **error**: If success is false, this field is an object that + contains the **message** and **details** fields. +- **targetUrl**: This provides a way for the server to + **redirect** the client to another url if needed. +- **unAuthorizedRequest**: This provides a method for the server + to inform the client that this operation is not authorized or the user is + not authenticated. abp.ajax **reloads** the current page if this is + true. +- **\_\_abp**: A special signature that is returned by an ABP wrapped + responses. You don't use this yourself, but abp.ajax handles it. + +This return format is recognized and handled by the **abp.ajax** function. +The done handler in abp.ajax gets the actual return value of the +controller (An object with a personId property) if there is no error. + +#### Handling Errors + +As described above, ASP.NET Boilerplate handles exceptions on the server and +returns an object with an error message like this: + + { + "targetUrl": null, + "result": null, + "success": false, + "error": { + "message": "An internal error occurred during your request!", + "details": "..." + }, + "unAuthorizedRequest": false, + "__abp": true + } + +As you can see, **success is false** and **result is null**. abp.ajax +handles this object and shows an error message to the user using the +[abp.message.error](/Pages/Documents/Javascript-API/Message) function. +If your server-side code throws an exception type of +**UserFriendlyException**, it directly shows the exception's message to the +user. Otherwise, it hides the actual error (writes error to logs) and +shows a standard ''An internal error occurred..." message to the user. +All these are automatically done by ASP.NET Boilerplate. + +You may want to disable displaying the message for a particular AJAX call. +If so, add **abpHandleError: false** into the abp.ajax options. + +##### HTTP Status Codes + +ABP returns the following HTTP status codes when exceptions occur: + +- **401** for unauthenticated requests (Used has not logged in and the + server action needs authentication). +- **403** for unauthorized requests. +- **500** for all other exception types. + +#### WrapResult and DontWrapResult Attributes + +You can control the wrapping using **WrapResult** and the **DontWrapResult** +attributes for an action or all actions in a controller. + +##### ASP.NET MVC Controllers + +ASP.NET Boilerplate **wraps** (as described above) ASP.NET **MVC** +action results **by default** if the return type is a **JsonResult** (or +Task<JsonResult> for async actions). You can change this by using the +**WrapResult** attribute as shown below: + + public class PeopleController : AbpController + { + [HttpPost] + [WrapResult(WrapOnSuccess = false, WrapOnError = false)] + public JsonResult SavePerson(SavePersonModel person) + { + //TODO: save new person to database and return new person's id + return Json(new {PersonId = 42}); + } + } + +As a shortcut, we can simply use the **\[DontWrapResult\]** attribute which is identical +for this example. + +You can change this default behaviour from the [startup +configuration](../Startup-Configuration.md) (using +Configuration.Modules.AbpMvc()...). + +##### ASP.NET Web API Controllers + +ASP.NET Boilerplate **does not wrap** Web API actions **by default** if +an action has successfully executed. You can add WrapResult to actions or +controllers if you need to, but by default it **wraps exceptions**. + +You can change this default behavior from the [startup +configuration](../Startup-Configuration.md) (using +Configuration.Modules.AbpWebApi()...). + +##### Dynamic Web API Layer + +ASP.NET Boilerplate **wraps** dynamic web api layer methods **by +default**. You can change this behavior using the **WrapResult** and +**DontWrapResult** attributes in the **interface** of your application +service. + +You can change this default behaviour from the [startup +configuration](../Startup-Configuration.md) (using +Configuration.Modules.AbpWebApi()...). + +##### ASP.NET Core Controllers + +ABP automatically wraps results for a JsonResult, ObjectResult and any +object which does not implement IActionResult.  See the [ASP.NET Core +documentation](../AspNet-Core.md) for more info. + +You can change this default behavior from the [startup +configuration](../Startup-Configuration.md) (using +Configuration.Modules.AbpAspNetCore()...). + +#### Dynamic Web API Layer + +While ASP.NET Boilerplate provides a mechanism to make AJAX calls easy, +in a real-world application it's typical to write a JavaScript function +for every AJAX call. For example: + + //Create a function to abstract AJAX call + var savePerson = function(person) { + return abp.ajax({ + url: '/People/SavePerson', + data: JSON.stringify(person) + }); + }; + + //Create a new person + var newPerson = { + name: 'Dougles Adams', + age: 42 + }; + + //Save the person + savePerson(newPerson).done(function(data) { + abp.notify.success('created new person with id = ' + data.personId); + }); + +This is good practice, but time-consuming and tedious, because you have to write a +function for every ajax call. ASP.NET can automatically generate these +type of functions for [application +services](/Pages/Documents/Application-Services) and controllers. + +Read the [dynamic web api](/Pages/Documents/Dynamic-Web-API) layer +documentation for the Web API and ASP.NET Core documentation for the [ASP.NET +Core](../AspNet-Core.md) integration. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Event-Bus.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Event-Bus.md" new file mode 100644 index 0000000..76bd401 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Event-Bus.md" @@ -0,0 +1,37 @@ +### Introduction + +The **Pub/sub** event model is widely used on the client-side. ASP.NET +Boilerplate includes a **simple global event bus** to **register** to +and **trigger events**. + +### Registering To Events + +You can use **abp.event.on** to **register** to a **global event**. An +example registration: + + abp.event.on('itemAddedToBasket', function (item) { + console.log(item.name + ' was added to basket!'); + }); + +The firt argument is the **unique name of the event**. The second one is a +**callback function** that is called when the specified event is +triggered. + +You can use the **abp.event.off** method to **unregister** from an event. +Note that the same function should be provided so it can be unregistered. +So for the example above, you must set the callback function to a +variable, then use both the **on** and **off** methods. + +### Trigger Events + +The **abp.event.trigger** is used to **trigger** a **global event**. Example +trigger code for the event registered above: + + abp.event.trigger('itemAddedToBasket', { + id: 42, + name: 'Acme Light MousePad' + }); + +The first argument is the **unique name of the event**. The second one is the +(optional) **event argument**. You can add any number of arguments and +get them in the callback method. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Logging.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Logging.md" new file mode 100644 index 0000000..b03a263 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Logging.md" @@ -0,0 +1,17 @@ +When you want to write a simple log in the client, you can use +console.log('...') API as you may already know. However, it's not supported by all +browsers and your script may break as a result. You must first check if +console is available. You may also want to write logs somewhere else. +You may evem want to write logs at some other level. ASP.NET Boilerplate +defines these safe logging functions: + + abp.log.debug('...'); + abp.log.info('...'); + abp.log.warn('...'); + abp.log.error('...'); + abp.log.fatal('...'); + +You can change the log-level by setting the **abp.log.level** to one of the +abp.log.levels (ex: abp.log.levels.INFO does not write to the debug logs). +These functions write logs to the browser's console by default, but you can +override/extend this behavior if you need to. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Message.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Message.md" new file mode 100644 index 0000000..e4fa090 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Message.md" @@ -0,0 +1,45 @@ +The Message API is used to show a message to the user or to get a +confirmation from the user. + +By default, the Message API is implemented using +[sweetalert](http://t4t5.github.io/sweetalert/). To make +sweetalert work, you should include its CSS & JavaScript files, then +include **abp.sweet-alert.js** to your page. + +### Show message + +Examples: + + abp.message.info('some info message', 'some optional title'); + abp.message.success('some success message', 'some optional title'); + abp.message.warn('some warning message', 'some optional title'); + abp.message.error('some error message', 'some optional title'); + +A success message is shown below: + +Success message using sweetalert + +### Confirmation + +Example: + + abp.message.confirm( + 'User admin will be deleted.', + 'Are you sure?', + function (isConfirmed) { + if (isConfirmed) { + //...delete user + } + } + ); + +The second argument (title) is optional here, so the callback function can be the +second argument instead. + +A confirmation message is shown below: + +Confirmation message using sweetalert + +ASP.NET Boilerplate internally uses the Message API. For example, it calls +abp.message.error if an [AJAX](/Pages/Documents/Javascript-API/AJAX) +call fails. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Notification.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Notification.md" new file mode 100644 index 0000000..f1872f4 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Notification.md" @@ -0,0 +1,25 @@ +We all love to show some fancy auto-disappearing notifications when +something happens, like when an item is saved or a problem has occurred. +ASP.NET Boilerplate defines some standard APIs for that. + + abp.notify.success('a message text', 'optional title'); + abp.notify.info('a message text', 'optional title'); + abp.notify.warn('a message text', 'optional title'); + abp.notify.error('a message text', 'optional title'); + +It can also get a 3rd argument (object) as the **custom options** of the +notification library. + +The Notification API is implemented using the +[toastr](http://codeseven.github.io/toastr/demo.html) library by +default. To make toastr work, you must include toastr's CSS & +JavaScript files, then include **abp.toastr.js** to your page. + +A toastr success notification is shown below: + +Success notification using toastr.js + +You can also implement a notification in your favourite notification +library. Just override all functions in a custom JavaScript file and +include it in to your page instead of abp.toastr.js (You can check this +file to see the implementation, it's pretty simple). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Other-Utilities.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Other-Utilities.md" new file mode 100644 index 0000000..4d6067e --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/Other-Utilities.md" @@ -0,0 +1,32 @@ +ASP.NET Boilerplate provides some common utility functions. + +#### abp.utils.createNamespace + +Used to create deep namespaces at once. Assume that we have a base 'abp' +namespace and want to create or get a 'abp.utils.strings.formatting' +namespace. Instead of this: + + //create or get namespace + abp.utils = abp.utils || {}; + abp.utils.strings = abp.utils.strings || {}; + abp.utils.strings.formatting = abp.utils.strings.formatting || {}; + + //add a function to the namespace + abp.utils.strings.formatting.format = function() { ... }; + +We can write something like this: + + var formatting = abp.utils.createNamespace(abp, 'utils.strings.formatting'; + + //Add a function to the namespace + formatting.format = function() { ... }; + +This simplifies things by safely creating deep namespaces. Note that the first +argument is the root namespace that must exist. + +#### abp.utils.formatString + +Similar to string.Format in C\#. Example usage: + + var str = abp.utils.formatString('Hello {0}!', 'World'); //str = 'Hello World!' + var str = abp.utils.formatString('{0} number is {1}.', 'Secret', 42); //str = 'Secret number is 42' diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/UI-Block-Busy.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/UI-Block-Busy.md" new file mode 100644 index 0000000..1f6b561 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Javascript-API/UI-Block-Busy.md" @@ -0,0 +1,56 @@ +ASP.NET Boilerplate provides some useful APIs to make all or some parts of the +page blocked and/or busy (with a busy indicator). + +### UI Block API + +This API is used to block a whole page or an element on a page with a +transparent overlay. This way, the user can not click it. It's pretty useful +when saving a form or loading an area (a div or even complete page). +Examples: + + abp.ui.block(); //Block the whole page + abp.ui.block($('#MyDivElement')); //You can use any jQuery selection.. + abp.ui.block('#MyDivElement'); //..or a direct selector + abp.ui.unblock(); //Unblock the page + abp.ui.unblock('#MyDivElement'); //Unblock specific element + +The UI Block API is implemented using the +[blockUI](http://malsup.com/jquery/block/) jQuery plug-in by default. To +make it work, you must include it's JavaScript file, then include +**abp.blockUI.js** to your page as the adapter (See this JavaScript file for the +simple implementation and defaults). + +### UI Busy API + +This API is used to make a page/element busy. For example, you may +want to block a form and show a busy indicator while submitting the form +to the server. Examples: + + abp.ui.setBusy('#MyLoginForm'); + abp.ui.clearBusy('#MyLoginForm'); + +Example screenshot: + +A busy div with spin.js + +The parameter should be a jQuery selector (like '\#MyLoginForm') or a +jQuery selection (like $('\#MyLoginForm')). To make the whole page busy, +you can pass null (or 'body') as the selector. + +The setBusy function can take a promise as second parameter and then +automatically unblock the page/element when the promise is completed. Example: + + abp.ui.setBusy( + $('#MyLoginForm'), + abp.ajax({ ... }) + ); + +Since [abp.ajax](/Pages/Documents/Javascript-API/AJAX) returns a promise, +we can directly pass it as a promise. To learn more about promises, see +jQuery's [Deferred Object](http://api.jquery.com/category/deferred-object/) documentation. +The setBusy method also supports Q (and Angular's $http service). + +The UI Busy API is implemented using +[spin.js](http://fgnass.github.io/spin.js/). To make it work, you must +include its JavaScript file, then include **abp.spin.js** to your page +(See this JavaScript file for the simple implementation and defaults). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Localization.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Localization.md" new file mode 100644 index 0000000..1d28259 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Localization.md" @@ -0,0 +1,564 @@ +### Introduction + +Developing a world-ready application, including an application that can be localized into one or more languages, requires localization features. +ASP.NET Boilerplate provides extensive support for the development of world-ready and localized applications. + +### Application Languages + +The first thing to do is to declare which languages are supported. This is +done in the **PreInitialize** method of your +[module](/Pages/Documents/Module-System) as shown below: + + Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true)); + Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr")); + +On the server side, you can [inject](/Pages/Documents/Dependency-Injection) +and use the **ILocalizationManager**. On the client side, you can use the +**abp.localization** JavaScript API to get a list of all available +languages, as well as the current language. famfamfam-flag-england (and tr) is +just a CSS class, which you can change based on your needs. You can then use it +in the UI to show the related flag. + +The ASP.NET Boilerplate [templates](/Templates) use this system to show a +**language-switch** combobox to the user. Create a template and +see the source code for more info. + +### Localization Sources + +Localization texts can be stored in different sources. You can even use +more than one source in the same application (If you have more than one +[module](/Pages/Documents/Module-System), each module can define a +separated localization source, or one module can define multiple +sources). The **ILocalizationSource** interface should be implemented by a +localization source. It is then **register**ed to ASP.NET Boilerplate's +localization configuration. + +Each localization source must have a **unique source name**. There are +pre-defined localization source types, as defined below. + +#### XML Files + +Localization texts can be stored in XML files. The content of an XML file is +something like this: + + + + + + + + + + Hi, + Welcome to Simple Task System! This is a sample + email content. + + + +XML files must be unicode (**utf-8**). **culture="en"** declares that +this XML file contains English texts. For text nodes; the **name** attribute +is used to identify a text. You can use the **value** attribute or **inner +text** (like the last one) to set the value of the localization text. We +create a separated XML file for **each language** as shown below: + +Localization files + +**SimpleTaskSystem** is the **source name** here and +SimpleTaskSystem.xml defines the **default language**. When a text is +requested, ASP.NET Boilerplate gets the text from the current language's XML +file (it finds the current language using +Thread.CurrentThread.**CurrentUICulture**). If it does not exists in the +current language, it gets the text from the default language's XML file. + +##### Registering XML Localization Sources + +XML files can be stored in the **file system** or can be **embedded** into +an assembly. + +For **file system** stored XMLs, we can register the XML localization +source as shown below: + + Configuration.Localization.Sources.Add( + new DictionaryBasedLocalizationSource( + "SimpleTaskSystem", + new XmlFileLocalizationDictionaryProvider( + HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem") + ) + ) + ); + +This is done in the **PreInitialize** event of a module (See the [module +system](/Pages/Documents/Module-System) for more info). ASP.NET +Boilerplate finds all the XML files in a given directory and registers the +localization source. + +For **embedded XML files**, we must mark all localization XML files as an +**embedded resource** (Select XML files, open properties window (F4) and +change Build Action to Embedded Resource). We can then register the +localization source as shown below: + + Configuration.Localization.Sources.Add( + new DictionaryBasedLocalizationSource( + "SimpleTaskSystem", + new XmlEmbeddedFileLocalizationDictionaryProvider( + Assembly.GetExecutingAssembly(), + "MyCompany.MyProject.Localization.Sources" + ) + ) + ); + +**XmlEmbeddedFileLocalizationDictionaryProvider** gets an assembly +containing XML files (GetExecutingAssembly simply refers to current +assembly) and a **namespace** of XML files (namespace is the calculated +assembly name + folder hierarchy of XML files). + +**Note**: When adding a language postfix to embedded XML files, **do not** +use the dot notation like 'MySource.tr.xml', instead use a dash like +'**MySource-tr.xml**' because dot notation causes namespacing problems +when finding resources! + +#### JSON Files + +JSON files can be used to store texts for a localization source. A +sample JSON localization file is shown below: + + { + "culture": "en", + "texts": { + "TaskSystem": "Task system", + "Xtasks": "{0} tasks" + } + } + +JSON files should be unicode (**utf-8**). **culture: "en"** declares +that this JSON file contains English texts. We create a separate JSON +file for **each language** as shown below: + +JSON localization files + +**MySourceName** is the **source name** here, and MySourceName.json +defines the **default language**. It's similar to XML files. + +##### Registering JSON Localization Sources + +JSON files can be stored in the **file system** or can be **embedded** into +an assembly. + +For file system stored JSONs, we can register a JSON localization +source as shown below: + + Configuration.Localization.Sources.Add( + new DictionaryBasedLocalizationSource( + "MySourceName", + new JsonFileLocalizationDictionaryProvider( + HttpContext.Current.Server.MapPath("~/Localization/MySourceName") + ) + ) + ); + +This is done in **PreInitialize** event of a module (See the [module +system](/Pages/Documents/Module-System) for more info). ASP.NET +Boilerplate finds all JSON files in a given directory and registers the +localization source. + +For **embedded JSON files**, we must mark all localization JSON files +as an **embedded resource** (Select JSON files, open properties window (F4) +and change Build Action as Embedded Resource). We can then register the +localization source as shown below: + +  Configuration.Localization.Sources.Add( + new DictionaryBasedLocalizationSource( + "MySourceName", + new JsonEmbeddedFileLocalizationDictionaryProvider( + Assembly.GetExecutingAssembly(), + "MyCompany.MyProject.Localization.Sources" + ) + ) + ); + +**JsonEmbeddedFileLocalizationDictionaryProvider** gets an assembly +containing JSON files (GetExecutingAssembly simply refers to current +assembly) and a **namespace** of JSON files (namespace is the calculated +assembly name + folder hierarchy of JSON files). + +Note: When adding a language postfix to embedded JSON files, **do not** +use the dot notation like 'MySource.tr.json'! Instead, use the dash like +'**MySource-tr.json**', since dot notation causes namespace problems when +finding resources.  + +#### Resource Files + +Localization text can also be stored in .NET's resource files. We can +create a resource file for each language as shown below (Right click +the project, choose add new item, then find resources file). + +Localization resource files + +**MyTexts.resx** contains the default language texts and +MyTexts**.tr**.resx contains texts for the Turkish language. When we open +MyTexts.resx, we can see all the texts: + +Content of a resource file + +In this case, ASP.NET Boilerplate uses .NET's built-in resource manager +for localization. You should configure a localization source for the +resource: + + Configuration.Localization.Sources.Add( + new ResourceFileLocalizationSource( + "MySource", + MyTexts.ResourceManager + )); + +The **uniqe name** of the source is **MySource** here. And +**MyTexts.ResourceManager** is a reference to the resource manager that is +used to get localized texts. This is done in the **PreInitialize** event of +the module (See the [module system](/Pages/Documents/Module-System) for more +info). + +#### Custom Source + +A custom localization source can be implemented to store texts in +different sources such as in a database. You can directly implement the +**ILocalizationSource** interface or you can use the +**DictionaryBasedLocalizationSource** class to make implementation +easier (json and xml localization sources also use it). [Module +zero](Zero/Language-Management.md) implements the source in the database +for example. + +### How the Current Language is Determined + +#### ASP.NET Core + +ASP.NET Core has it's own mechanism to determine the current language. +Abp.AspNetCore package automatically adds ASP.NET Core's +**UseRequestLocalization** middleware to request pipeline. It also adds +some special providers. Here is the default ordered list of all providers, +which determine the current language for an HTTP request: + +- **QueryStringRequestCultureProvider** (ASP.NET Core's default + provider): Uses **culture** & **ui-culture** URL query string values, + if present. Example value: "culture=es-MX&ui-culture=es-MX". +- **AbpUserRequestCultureProvider** (ABP's provider): If the user is known + via **[IAbpSession](Abp-Session.md)** and has explicitly selected a + language before (and saved to + [ISettingManager](Setting-Management.md)), then use the user's + preferred language. If the user is known but has not selected any language + and the **.AspNetCore.Culture** cookie or header has a value, set the user's + language setting with that information and use this value as the + current language. If the user is unknown, this provider does nothing. +- **AbpLocalizationHeaderRequestCultureProvider** (ABP's provider): + Use **.AspNetCore.Culture** header value if present. Example value: + "c=en|uic=en-US". +- **CookieRequestCultureProvider** (ASP.NET Core's default provider): + Use **.AspNetCore.Culture** cookie value if present. Example value: + "c=en|uic=en-US". +- **AbpDefaultRequestCultureProvider** (ABP's provider): If there is + an default/application/tenant **setting value** for the language + (named "Abp.Localization.DefaultLanguageName"), then use the + setting's value. +- **AcceptLanguageHeaderRequestCultureProvider** (ASP.NET Core's + default provider): Use the **Accept-Language** header value if present + (automatically sent by browsers). Example value: + "tr-TR,tr;q=0.8,en-US;q=0.6,en;q=0.4". + +The **UseRequestLocalization** middleware is automatically added when +you call the **app.UseAbp()** method. However, it's suggested that you manually add +it (in the Configure method of the Startup class) after the authentication +middleware if your application uses authentication. Otherwise, the +localization middleware does not know the current user to determine the best +language. Example usage: + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseAbp(options => + { + options.UseAbpRequestLocalization = false; //disable automatic adding of request localization + }); + + //...authentication middleware(s) + + app.UseAbpRequestLocalization(); //manually add request localization + + //...other middlewares + + app.UseMvc(routes => + { + //... + }); + } + +Most of time, you don't need to worry if you are using ABP's +localization system properly. See the ASP.NET Core [localization +document](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) +to understand it better. + +#### ASP.NET MVC 5.x + +ABP automatically determines the current language in every web request and +sets the **current thread's culture (and UI culture)**. This is how ABP +determines it by default. ABP will: + +- Try to get it from a special **query string** value, named + "**Abp.Localization.CultureName**" by default. +- If the user is known via [IAbpSession](Abp-Session.md) and has explicitly + selected a language before (and saved to + [ISettingManager](Setting-Management.md)) then it uses the user's + preferred language. If the user is known but has not selected any language, + and the cookie/header (see below) has a value, it sets the user's setting with + that information. +- Try to get it from a special **header** value, named + "**Abp.Localization.CultureName**" by default. +- Try to get it from a special **header** value, named + "**Abp.Localization.CultureName**" by default. +- Try to get it from a special **cookie** value, named + "**Abp.Localization.CultureName**" by default. +- Try to get it from **default culture** setting (setting name is + "Abp.Localization.DefaultLanguageName", which is a constant defined + in Abp.Localization.LocalizationSettingNames.DefaultLanguage and can + be changed using the [setting management](Setting-Management.md)). +- Try to get it from the **browser's default language** + (HttpContext.Request.UserLanguages). + +If you need to, you can change the special cookie/header/querystring name +in your module's PreInitialize method. Example: + + Configuration.Modules.AbpWeb().Localization.CookieName = "YourCustomName"; + +ABP overrides **Application\_PostAuthenticateRequest** (in global.asax) +to implement that logic. You can override **SetCurrentCulture** in the +global.asax or replace **ICurrentCultureSetter** in order to override +the logic described above. + +### Getting A Localized Text + +After creating a source and registering it to the ASP.NET Boilerplate's +localization system, text can be localized easily.  + +#### Server Side + +On the server side, we can [inject](/Pages/Documents/Dependency-Injection) +**ILocalizationManager** and use it's **GetString** method. + + var s1 = _localizationManager.GetString("SimpleTaskSystem", "NewTask"); + +The GetString method gets the string from the localization source based on the +**current thread's UI culture**. If not found, it falls back to the **default +language**. + +If a given string is not defined anywhere, then it returns the **given +string** by humanizing and wrapping it with **\[** and **\]** by default +(instead of throwing an Exception). Example: If a given text is +"ThisIsMyText", then the result will be "\[This is my text\]". This behavior +is configurable (you can use the Configuration.Localization in the PreInitialize method +of your [module](/Pages/Documents/Module-System) to change it). + +Instead of always repeating your source name, you can first **get the source** and +then get a string from the source: + + var source = _localizationManager.GetSource("SimpleTaskSystem"); + var s1 = source.GetString("NewTask"); + +This returns the text in the current language. There are also overrides of +GetString to get the text in **different languages** and **formatted by +arguments**. + +If we can not inject ILocalizationManager (maybe it's in a static context +that can not be reached by the dependency injection), we can simply use the +**LocalizationHelper** static class. We prefer injecting and using the +ILocalizationManager where it's possible since LocalizationHelper is +static and statics are difficult to test. + +If you need localization in an [**application +service**](/Pages/Documents/Application-Services#applicationservice-class), in +an **MVC Controller**, in a **Razor View**, or in another class derived +from **AbpServiceBase**, there are shortcut **L** methods. + +##### In MVC Controllers + +Localization text is generally needed in an MVC Controller and Views. +There is a shortcut for that. See the sample controller below: + + public class HomeController : SimpleTaskSystemControllerBase + { + public ActionResult Index() + { + var helloWorldText = L("HelloWorld"); + return View(); + } + } + +The **L** method is used to localize a string. You must supply a +source name. It's done in the SimpleTaskSystemControllerBase as shown below: + + public abstract class SimpleTaskSystemControllerBase : AbpController + { + protected SimpleTaskSystemControllerBase() + { + LocalizationSourceName = "SimpleTaskSystem"; + } + } + +Note that it is derived from **AbpController** and therefore, you can easily localize text with the **L** method. + +##### In MVC Views + +The same **L** method also exists in views: + +
+
+
+ + +
+
+ + +
+ +
+
+ +To make this work, you should derive your views from a base class that +sets the source name: + + public abstract class SimpleTaskSystemWebViewPageBase : SimpleTaskSystemWebViewPageBase + { + + } + + public abstract class SimpleTaskSystemWebViewPageBase : AbpWebViewPage + { + protected SimpleTaskSystemWebViewPageBase() + { + LocalizationSourceName = "SimpleTaskSystem"; + } + } + +Then set this view base class in web.config: + + + +All controllers and views are ready with these methods when you create your +solution from one of the ASP.NET Boilerplate [templates](/Templates). + +#### In JavaScript + +ASP.NET Boilerplate also makes it possible to use the same localization text in +JavaScript. First, you need to add the dynamic ABP scripts to +the page: + + + +ASP.NET Boilerplate automatically generates the needed JavaScript code to +get localized text on the client side. You can then easily get a +localized text in JavaScript as shown below: + + var s1 = abp.localization.localize('NewTask', 'SimpleTaskSystem'); + +NewTask is the text name and SimpleTaskSystem is the source name. +Instead of repeating the source name each time, you can first get the source and then get the +text: + + var source = abp.localization.getSource('SimpleTaskSystem'); + var s1 = source('NewTask'); + +##### Format Arguments + +The localization method can also get additional format arguments. Example: + + abp.localization.localize('RoleDeleteWarningMessage', 'MySource', 'Admin'); + + //shortcut if the source is retrieved using getSource as shown above + source('RoleDeleteWarningMessage', 'Admin'); + +if RoleDeleteWarningMessage = 'Role {0} will be deleted', then the localized +text will be 'Role Admin will be deleted'. + +##### Default Localization Source + +You can set a default localization source and use the +abp.localization.localize method without the source name. + + abp.localization.defaultSourceName = 'SimpleTaskSystem'; + var s1 = abp.localization.localize('NewTask'); + +defaultSourceName is global and works for only one source at a time. + +### Extending Localization Sources + +Assume that we use a module which defines it's own localization source. +We may need to change it's localized texts, add new text or translate +to other languages. ASP.NET Boilerplate allows for extending a localization +source. It currently works for XML and JSON files (Actually any +localization source that implements the IDictionaryBasedLocalizationSource +interface). + +ASP.NET Boilerplate also defines some localization sources. For +instance, the **Abp.Web** NuGet package defines a localization source named +"**AbpWeb**" as embedded XML files: + +AbpWeb localization source files + +The default (English) XML file looks like this (only the first two texts are +shown): + + + + + + + ... + + + +To extend AbpWeb source, we can define XML files. Assume that we only +want to change the **InternalServerError** text. We can define an XML file +as shown below: + + + + + + + + +We can then register it on the PreInitialize method of our module: + + Configuration.Localization.Sources.Extensions.Add( + new LocalizationSourceExtensionInfo("AbpWeb", + new XmlEmbeddedFileLocalizationDictionaryProvider( + Assembly.GetExecutingAssembly(), + "MyCompany.MyProject.Localization.Sources" + ) + ) + ); + +ASP.NET Boilerplate overrides (merges) the base localization source with our +XML files. We can also add new language files. + +**Note**: We can use JSON files to extend XML files, or vice verse. The files created for extending localization sources must be marked as **embedded resource**. + +### Getting Languages + +ILanguageManager can be used to get a list of all available languages +and the current language. + +### Best Practices + +XML files, JSON files and Resource files have their own strengths and +weaknesses. We suggest you use XML or JSON files instead of Resource +files, because; + +- XML/JSON files are easy to edit, extend or port. +- XML/JSON files require string keys while getting localized texts + instead of compile-time properties like Resource files. This can be + considered as a weakness. However, it's easier to change the source later. + We can even move the localization to a database without changing the code + which uses localization (**Module Zero** implements it to create a + **database based** and **per-tenant** localization source. See + [documentation](/Pages/Documents/Zero/Language-Management).) + +If you use XML or JSON, we recommend you do not sort the texts by name. +Sort them by creation date! This way, when someone translates it to another +language, he/she can easily see which texts have been added recently. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Logging.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Logging.md" new file mode 100644 index 0000000..c6f769d --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Logging.md" @@ -0,0 +1,201 @@ +### Server Side + +ASP.NET Boilerplate uses Castle Windsor's [logging +facility](http://docs.castleproject.org/Windsor.Logging-Facility.ashx). +It can work with different logging libraries: **Log4Net**, **NLog**, +**Serilog**, and more. Castle provides a **common interface** for all +logger libraries. This way, you're independent from a specific logging library +and can easily change it later if needed. + +**[Log4Net](http://logging.apache.org/log4net/)** is one of the most +popular logging libraries for .NET. The ASP.NET Boilerplate +[templates](/Templates) come with Log4Net properly configured and ready to use. +There is just a single-line of code for the dependency to log4net (as +seen in the [configuration](#config) section), so you can easily change it to +your favourite library. + +#### Getting The Logger + +No matter which logging library you choose, the code to write logs is the +same (thanks to Castle's common ILogger interface). + +First, we need to get a Logger object to write logs. Since ASP.NET +Boilerplate strongly uses [dependency +injection](/Pages/Documents/Dependency-Injection), we can easily inject +a **Logger** object using the [property +injection](/Pages/Documents/Dependency-Injection#property-injection-pattern) +(or constructor injection) pattern. Here's a sample class that writes a log +line: + + using Castle.Core.Logging; //1: Import Logging namespace + + public class TaskAppService : ITaskAppService + { + //2: Getting a logger using property injection + public ILogger Logger { get; set; } + + public TaskAppService() + { + //3: Do not write logs if no Logger supplied. + Logger = NullLogger.Instance; + } + + public void CreateTask(CreateTaskInput input) + { + //4: Write logs + Logger.Info("Creating a new task with description: " + input.Description); + + //TODO: save task to database... + } + } + +**First**, we imported the namespace of Castle's ILogger interface. + +**Secondly**, we defined a public **ILogger** object named Logger. This is +the object we will write logs with. The dependency injection system will set +(inject) this property after creating the TaskAppService object. This is +known as the property injection pattern. + +**Thirdly**, we set Logger to **NullLogger.Instance**. The system will work +fine without this line, but it is best practice to use the property injection +pattern. If no one sets the Logger, it will be **null** and we will get an +"object reference..." exception when we want to use it. This guarantees +that it's not null. So if no one sets the Logger, it will be +NullLogger. This is known as the null object pattern. NullLogger actually +does nothing. It does not write any logs. This way, our class can work with and +without an actual logger. + +**Finally**, we're writing a log text with the **info** level. +There are different levels (see the [configuration](#config) section). + +If we call the CreateTask method and check the log file, we see a log +line like the one shown below: + + INFO 2014-07-13 13:40:23,360 [8 ] SimpleTaskSystem.Tasks.TaskAppService - Creating a new task with description: Remember to drink milk before sleeping! + +#### Base Classes With Logger + +ASP.NET Boilerplate provides **base classes** for MVC controllers, Web +API controllers, [Application +service](/Pages/Documents/Application-Services) classes and more. They +declare a **Logger** property. This way, you can directly use this Logger to +write logs, with no injection needed. Example: + + public class HomeController : SimpleTaskSystemControllerBase + { + public ActionResult Index() + { + Logger.Debug("A sample log message..."); + return View(); + } + } + +Note that SimpleTaskSystemControllerBase is our application specific +base controller that inherits **AbpController.** This way, it can directly +use the Logger. You can also write your own common base class for other +classes. You will then not have to inject a logger each time. + +#### Configuration + +All the configuration is done for Log4Net when you create your application +from the ASP.NET Boilerplate [templates](/Templates). + +The default configuration's log format is as shown below (for each line): + +- **Log level**: DEBUG, INFO, WARN, ERROR or FATAL. +- **Date and time**: The time when the log line was written. +- **Thread number**: The thread number that wrote the log line. +- **Logger name**: This is generally the class name which writes the + log line. +- **Log text**: Actual log text that you write. + +It's defined in the **log4net.config** file of the application as shown +below: + + + + + + + + + + + + + + + + + + + + + + + +Log4Net is highly configurable and is a strong logging library. You can write +logs in different formats and to different targets (text file, +database...). You can set minimum log levels (as set for NHibernate in +this configuration). You can write diffent loggers to different log +files. It can automatically backup and create new log file when it +reaches to a specific size (Rolling file adapter with 10000 KB per file +in this configuration) and so on... Read it's own confuguration +[documentation](http://logging.apache.org/log4net/release/config-examples.html) +for more info. + +Finally, in the Global.asax file, we declare that we use Log4Net with the +log4net.config file: + + public class MvcApplication : AbpWebApplication + { + protected override void Application_Start(object sender, EventArgs e) + { + IocManager.Instance.IocContainer.AddFacility(f => f.UseLog4Net().WithConfig("log4net.config")); + base.Application_Start(sender, e); + } + } + +This is **the only code line we directly depend on for log4net**. Only +the web project depends on log4net library's [nuget +package](https://www.nuget.org/packages/log4net/). You can also easily +change to another library without changing your logging code. + +#### Abp.Castle.Log4Net Package + +ABP uses the Castle Logging Facility for logging and it does not directly +depend on log4net, as declared above. There is, however, a problem with +Castle's Log4Net integration... It does not support the latest log4net. We +created a NuGet package, +[**Abp.Castle.Log4Net**](http://nuget.org/packages/Abp.Castle.Log4Net), +to solve this issue. After adding this package to our solution, all we +have to do is to change the code in the application start method like this: + + public class MvcApplication : AbpWebApplication + { + protected override void Application_Start(object sender, EventArgs e) + { + IocManager.Instance.IocContainer.AddFacility(f => f.UseAbpLog4Net().WithConfig("log4net.config")); + base.Application_Start(sender, e); + } + } + +The only difference is that we used the "**UseAbpLog4Net()**" method +(defined in Abp.Castle.Logging.Log4Net namespace) instead of +"UseLog4Net()". When we use Abp.Castle.Log4Net package, you **do not +need** to use the +[Castle.Windsor-log4net](https://www.nuget.org/packages/Castle.Windsor-log4net) +and +[Castle.Core-log4net](https://www.nuget.org/packages/Castle.Core-log4net/) +packages. + +### Client-Side + +ASP.NET Boilerplate defines a simple JavaScript logging API for the client +side. It logs to the browser's console as default. Here's some JavaScript code to +write logs: + + abp.log.warn('a sample log message...'); + +For more information, see the [logging API +documentation](/Pages/Documents/Javascript-API/Logging). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Controllers.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Controllers.md" new file mode 100644 index 0000000..8e4fcef --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Controllers.md" @@ -0,0 +1,135 @@ +### Introduction + +ASP.NET Boilerplate is integrated in to the **ASP.NET MVC Controllers** via +the **Abp.Web.Mvc** NuGet package. You can create regular MVC Controllers as +you always do. [Dependency +Injection](/Pages/Documents/Dependency-Injection) properly works for +regular MVC Controllers, but you should derive your controllers from +**AbpController**, which provides several benefits and provides for better integration +in to ASP.NET Boilerplate. + +### AbpController Base Class + +This is a simple controller derived from AbpController: + + public class HomeController : AbpController + { + public ActionResult Index() + { + return View(); + } + } + + +#### Localization + +AbpController defines **L** method to make +[localization](/Pages/Documents/Localization) easier. Example: + + public class HomeController : AbpController + { + public HomeController() + { + LocalizationSourceName = "MySourceName"; + } + + public ActionResult Index() + { + var helloWorldText = L("HelloWorld"); + + return View(); + } + } + +You should set **LocalizationSourceName** to make the **L** method work. +You can set it in your own base controller class, so you don't have to repeat it for each +controller. + +#### Others + +You can also use the pre-injected +[AbpSession](/Pages/Documents/Abp-Session), +[EventBus](/Pages/Documents/EventBus-Domain-Events), [PermissionManager, +PermissionChecker](/Pages/Documents/Authorization), +[SettingManager](/Pages/Documents/Setting-Management), [FeatureManager, +FeatureChecker](/Pages/Documents/Feature-Management), +[LocalizationManager](/Pages/Documents/Localization), +[Logger](/Pages/Documents/Logging), and +[CurrentUnitOfWork](/Pages/Documents/Unit-Of-Work) base properties and +more. + +### Filters + +#### Exception Handling & Result Wrapping + +All exceptions are automatically handled, logged, and a proper response +is returned to the client. See the [exception +handling](/Pages/Documents/Handling-Exceptions) documentation for more. + +ASP.NET Boilerplate also **wraps** action results by default if the return +type is **JsonResult** (or Task<JsonResult> for async actions). + +You can change exception handling and wrapping by using the **WrapResult** +and **DontWrapResult** attributes for controllers or actions or from the +[startup configuration](Startup-Configuration.md) (using +Configuration.Modules.AbpMvc()...) globally. See the [ajax +documentation](/Pages/Documents/Javascript-API/AJAX) for more info. + +#### Audit Logging + +The **AbpMvcAuditFilter** is used to integrate to the [audit logging +system](Audit-Logging.md). It logs all requests to all actions by +default (if auditing is not disabled). You can control audit logging +using the **Audited** and **DisableAuditing** attributes for actions and +controllers. + +#### Validation + +The **AbpMvcValidationFilter** automatically checks **ModelState.IsValid** +and prevents execution of the action if it's not valid. It also implements +input DTO validation described in the [validation +documentation](Validating-Data-Transfer-Objects.md). + +#### Authorization + +You can use **AbpMvcAuthorize** attribute for your controllers or +actions to prevent unauthorized users from using your controllers and +actions. Example: + + public class HomeController : AbpController + { + [AbpMvcAuthorize("MyPermissionName")] + public ActionResult Index() + { + return View(); + } + } + +You can define the **AllowAnonymous** attribute for actions or controllers +to suppress authentication/authorization. The AbpController also defines an +**IsGranted** method as a shortcut to check permissions. + +See the [authorization](/Pages/Documents/Authorization) documentation for +more info. + +#### Unit Of Work + +The **AbpMvcUowFilter** is used to integrate to the [Unit of +Work](Unit-Of-Work.md) system. It automatically begins a new unit of +work before an action execution, and if no exception is thrown, completes the unit of work +after the action's execution. + +You can use the **UnitOfWork** attribute to control the behaviour of the UOW for an +action. You can also use the startup configuration to change the default unit of +work attribute for all actions. + +#### Anti Forgery + +The **AbpAntiForgeryMvcFilter** is used to auto-protect MVC actions for +POST, PUT and DELETE requests from CSRF/XSRF attacks. See the [CSRF +documentation](XSRF-CSRF-Protection.md) for more. + +### Model Binders + +The **AbpMvcDateTimeBinder** is used to normalize DateTime (and +Nullable<DateTime>) inputs using the **Clock.Normalize** method. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Views.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Views.md" new file mode 100644 index 0000000..9944522 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/MVC-Views.md" @@ -0,0 +1,17 @@ +### Introduction + +ASP.NET Boilerplate is integrated in to MVC Views via the **Abp.Web.Mvc** NuGet +package. You can create regular MVC Views as you always do. + +### AbpWebViewPage Base Class + +ASP.NET Boilerplate also provides the **AbpWebViewPage** base class, which defines some +useful properties and methods. If you created your project using the +[startup templates](/Templates) then all your views are automatically +inherited from this base class. + +AbpWebViewPage defines an **L** method for +[localization](/Pages/Documents/Localization), **IsGranted** method for +[authorization](/Pages/Documents/Authorization), **IsFeatureEnabled** +and **GetFeatureValue** methods for [feature +management](/Pages/Documents/Feature-Management) and more. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Module-System.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Module-System.md" new file mode 100644 index 0000000..74ef660 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Module-System.md" @@ -0,0 +1,228 @@ +### Introduction + +ASP.NET Boilerplate provides the infrastructure to build modules and +compose them to create an application. A module can depend on another +module. Generally, an assembly is considered a module. If you create +an application with more than one assembly, it's recommended that you create a +module definition for each one. + +The module system is currently focused on the server-side rather than client-side. + +### Module Definition + +A module is defined with a class that is derived from **AbpModule** that is in the [ABP package](https://www.nuget.org/packages/Abp). Say +that we're developing a Blog module that can be used in different +applications. The simplest module definition can be as shown below: + + public class MyBlogApplicationModule : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + } + +The Module definition class is responsible for registering it's classes via +[dependency injection](Dependency-Injection.md), if needed (it can be done +conventionally as shown above). It can also configure the application +and other modules, add new features to the application, and so on... + +### Lifecycle Methods + +ASP.NET Boilerplate calls some specific methods of modules on +application startup and shutdown. You can override these methods to +perform some specific tasks. + +ASP.NET Boilerplate calls these methods **ordered by dependecies**. If +module A depends on module B, module B is initialized before module A. + +The exact order of startup methods: PreInitialize-B, PreInitialize-A, +Initialize-B, Initialize-A, PostInitialize-B and PostInitialize-A. This +is true for all dependency graphs. The **shutdown** method is also similar, +but in **reverse order**. + +#### PreInitialize + +This method is called first, when the application starts. It's the go-to method +to [configure](Startup-Configuration.md) the framework and other +modules before they initialize. + +You can also write some specific code here to run before the dependency +injection registrations. For example, if you create a [conventional +registration](Dependency-Injection.md) class, you should register it +here using the IocManager.AddConventionalRegisterer method. + +#### Initialize + +This is the place where [dependency +injection](/Pages/Documents/Dependency-Injection) registration should be +done. It's generally done using the IocManager.RegisterAssemblyByConvention +method. If you want to define custom dependency registration, see the +[dependency injection documentation](Dependency-Injection.md). + +#### PostInitialize + +This method is called last in the startup process. It's safe to resolve a +dependency here. + +#### Shutdown + +This method is called when the application shuts down. + +### Module Dependencies + +A module can be dependent on another. You need to **explicitly** +declare the dependencies using the **DependsOn** attribute, like below: + + [DependsOn(typeof(MyBlogCoreModule))] + public class MyBlogApplicationModule : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + } + +Here, we declare to ASP.NET Boilerplate that MyBlogApplicationModule +depends on the MyBlogCoreModule and the MyBlogCoreModule should be +initialized before the MyBlogApplicationModule. + +ABP can resolve dependencies recursively beginning from the **startup +module** and initialize them accordingly. The startup module initializes as +the last module. + +### PlugIn Modules + +While modules are investigated beginning from the startup module and go +through the dependencies, ABP can also load modules **dynamically**. +The **AbpBootstrapper** class defines the **PlugInSources** property which can +be used to add sources to dynamically load plugin modules. A plugin +source can be any class implementing the **IPlugInSource** interface. +The **PlugInFolderSource** class implements it to get the plugin modules from +assemblies located in a folder. + +#### ASP.NET Core + +The ABP ASP.NET Core module defines options in the **AddAbp** extension method +to add plugin sources in the **Startup** class: + + services.AddAbp(options => + { + options.PlugInSources.Add(new FolderPlugInSource(@"C:\MyPlugIns")); + }); + +We could use the **AddFolder** extension method for a simpler syntax: + + services.AddAbp(options => + { + options.PlugInSources.AddFolder(@"C:\MyPlugIns"); + }); + +See the [ASP.NET Core document](AspNet-Core.md) for more info on the Startup class. + +#### ASP.NET MVC, Web API + +For classic ASP.NET MVC applications, we can add plugin folders by +overriding the **Application\_Start** in the **global.asax** as shown below: + + public class MvcApplication : AbpWebApplication + { + protected override void Application_Start(object sender, EventArgs e) + { + AbpBootstrapper.PlugInSources.AddFolder(@"C:\MyPlugIns"); + //... + base.Application_Start(sender, e); + } + } + +##### Controllers in PlugIns + +If your modules include MVC or Web API Controllers, +ASP.NET can not investigate your controllers. To overcome this issue, +you can change the global.asax file like below: + + using System.Web; + using Abp.PlugIns; + using Abp.Web; + using MyDemoApp.Web; + + [assembly: PreApplicationStartMethod(typeof(PreStarter), "Start")] + + namespace MyDemoApp.Web + { + public class MvcApplication : AbpWebApplication + { + } + + public static class PreStarter + { + public static void Start() + { + //... + MvcApplication.AbpBootstrapper.PlugInSources.AddFolder(@"C:\MyPlugIns\"); + MvcApplication.AbpBootstrapper.PlugInSources.AddToBuildManager(); + } + } + } + +### Additional Assemblies + +The default implementations for IAssemblyFinder and ITypeFinder (which is +used by ABP to investigate specific classes in the application) only +finds module assemblies and types in those assemblies. We can override the +**GetAdditionalAssemblies** method in our module to include additional +assemblies. + +### Custom Module Methods + +Your modules can also have custom methods that can be used by other +modules that depend on this module. Assume that MyModule2 depends on +MyModule1 and wants to call a method of MyModule1 in the PreInitialize method. + + public class MyModule1 : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + + public void MyModuleMethod1() + { + //this is a custom method of this module + } + } + + [DependsOn(typeof(MyModule1))] + public class MyModule2 : AbpModule + { + private readonly MyModule1 _myModule1; + + public MyModule2(MyModule1 myModule1) + { + _myModule1 = myModule1; + } + + public override void PreInitialize() + { + _myModule1.MyModuleMethod1(); //Call MyModule1's method + } + + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + } + +Here we constructor-injected MyModule1 to MyModule2, so MyModule2 can +call MyModule1's custom method. This is only possible if Module2 depends +on Module1. + +### Module Configuration + +While custom module methods can be used to configure modules, we suggest +you use the [startup configuration](Startup-Configuration.md) system to +define and set the configuration for modules. + +### Module Lifetime + +Module classes are automatically registered as a **singleton**. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Lingual-Entities.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Lingual-Entities.md" new file mode 100644 index 0000000..1f3ff3f --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Lingual-Entities.md" @@ -0,0 +1,158 @@ +### Introduction + +ASP.NET Boilerplate defines two basic interfaces for Multi-Lingual entity definitions to provide a standard model for translating entities. + +#### IMultiLingualEntity + +`IMultiLingualEntity` interface is used to mark multi lingual entities. The entities marked with `IMultiLingualEntity` interface must define language-neutral information. The entities marked with `IMultiLingualEntity` contains a collection of Translations which contains language-dependent information. + +A sample multi lingual entity would be; + + + public class Product : Entity, IMultiLingualEntity + { + public decimal Price { get; set; } + + public ICollection Translations { get; set; } + } + + +#### IEntityTranslation + +IEntityTranslation interface is used to mark translation of a Multi-Lingual entity. The entities marked with IEntityTranslation interface must define language dependent information. The entities marked with IEntityTranslation contains Language field which contains a language code for the translation and a reference to Multi-Lingual entity. + +A sample multi lingual entity would be; + + + public class ProductTranslation : Entity, IEntityTranslation + { + public string Name { get; set; } + + public Product Core { get; set; } + + public int CoreId { get; set; } + + public string Language { get; set; } + } + + #### CreateMultiLingualMap + +When listing Multi-Lingual entities on a user interface, most of the time, only one translation of a Multi-Lingual entity which is in user's current language will be displayed to user. + +For this purpose, ABP defines CreateMultiLingualMap extension method to map a Multi-Lingual entity and one of it's Translation to an appropriate Dto class using **AutoMapper**. + +By using CreateMultiLingualMap extension method, only one record from Translations collection of a Multi-Lingual entity will be mapped to target Dto class. This extension method finds the translation with selected UI language first. If there is no translation with selected UI language, then extension method searches for the default language setting (see [Setting-Management](Setting-Management#setting-scope.md)) and uses the translation in default language. If extension method couldn't find any translation in current UI language or default language, it uses one of the existing translations. + +A sample Dto class for sample Product entity above would be; + + + public class ProductListDto + { + // Mapped from Product.Price + public decimal Price { get; set; } + + // Mapped from ProductTranslation.Name + public string Name { get; set; } + } + + +And it's mapping configuration is; + + + Configuration.Modules.AbpAutoMapper().Configurators.Add(configuration => + { + CustomDtoMapper.CreateMappings(configuration, new MultiLingualMapContext( + IocManager.Resolve() + )); + }); + + internal static class CustomDtoMapper + { + public static void CreateMappings(IMapperConfigurationExpression configuration, MultiLingualMapContext context) + { + configuration.CreateMultiLingualMap(context); + } + } + + +SettingManager is required to find default language setting when mapping a multi lingual entity to a Dto class. + +In some cases like editing a multi lingual entity on the UI, all translations may be needed in the Dto class. In such cases, the Dto classes can be defined like below and [Object-To-Object-Mapping](Object-To-Object-Mapping.md) can be used. + + + [AutoMap(typeof(Product))] + public class ProductDto + { + public decimal Price { get; set; } + + public List Translations {get; set;} + } + + + + [AutoMap(typeof(ProductTranslation))] + public class ProductTranslationDto + { + public string Name { get; set; } + } + +### Crud Operations + +#### Creating a MultiLingual Entity with Translation(s) + +A Dto class like the below one can be used for creating a Multi-Lingual entity with it's translations. + + + [AutoMap(typeof(Product))] + public class ProductDto + { + public decimal Price { get; set; } + + public ICollection Translations { get; set; } + } + +After defining such a Dto class, we can use it in our application service to create a Multi-Lingual entity. + + + public class ProductAppService : ApplicationService, IProductAppService + { + private readonly IRepository _productRepository; + + public ProductAppService(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task CreateProduct(ProductDto input) + { + var product = ObjectMapper.Map(input); + await _productRepository.InsertAsync(product); + } + } + +#### Updating a Multi-Lingual Entity with Translation(s) + +We can use similar Dto class for updating our Multi-Lingual entity. A sample application service method for update operation can be defined like below; + + + public async Task UpdateProduct(ProductDto input) + { + var product = await _productRepository.GetAllIncluding(p => p.Translations) + .FirstOrDefaultAsync(p => p.Id == input.Id); + + product.Translations.Clear(); + + ObjectMapper.Map(input, product); + } + +##### Note for EntityFramework 6.x + +For EntityFramework 6.x, all the translations must be deleted from database manually because Entity Framework 6.x doesn't delete related data. Instead, EntityFramework 6.x tries to set CoreId of each Translation entity to null which fails. So, a sample code like the below one might be used to delete translations of a Multi-Lingual entity for EntityFramework 6.x. + + + foreach (var translation in product.Translations.ToList()) + { + await _productTranslationRepository.DeleteAsync(translation); + product.Translations.Remove(translation); + } + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Tenancy.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Tenancy.md" new file mode 100644 index 0000000..3fb62db --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Multi-Tenancy.md" @@ -0,0 +1,302 @@ +### What Is Multi-Tenancy? + +"*Software* ***Multitenancy*** *refers to a software* ***architecture*** +*in which a* ***single instance*** *of a software runs on a server and +serves* ***multiple tenants****. A tenant is a group of users who share +a common access with specific privileges to the software instance. With +a multitenant architecture, a software application is designed to +provide every tenant a* ***dedicated share of the instance including its +data****, configuration, user management, tenant individual +functionality and non-functional properties. Multitenancy contrasts with +multi-instance architectures, where separate software instances operate +on behalf of different tenants*" +([Wikipedia](https://en.wikipedia.org/wiki/Multitenancy)) + +In short, multi-tenancy is a technique that is used to create **SaaS** +(Software as-a Service) applications. + +#### Database & Deployment Architectures + +There are some different multi-tenant database & deployment approaches: + +##### Multiple Deployment - Multiple Database + +This is **not multi-tenancy** actually, but if we run **one instance** +of the application **for each** customer (tenant) with a **separated +database**, we can serve **multiple tenants** on a single server. We +just have to make sure that multiple instances of the application don't +**conflict** with each other on the same server environment. + +This can also be possible for an **existing application** which is not +designed as multi-tenant. It's easier to create such an application since +the application is not aware of multitenancy. There are, however, setup, +utilization and maintenance problems in this approach. + +##### Single Deployment - Multiple Database + +ln this approach, we run a **single instance** of the application on +a server. We have a **master** (host) database to store tenant metadata +(like tenant name and subdomain) and a **separate database** for each +tenant. Once we identify the **current tenant** (for example; from +subdomain or from a user login form), then we can **switch** to that +tenant's database to perform operations. + +In this approach, the application should be designed as multi-tenant at some +level, but most of the application can remain independent from it. + +We create and maintain a **separate database** for each tenant, +this includes **database migrations**. If we have many customers with +dedicated databases, it may take a long time to migrate the database schema during +an application update. Since we have a separated database for each tenant, we +can **backup** its database separately from other tenants. We can also +**move** the tenant database to a stronger server if that tenant needs +it. + +##### Single Deployment - Single Database + +This is the **most ideal multi-tenancy** architecture: We only deploy a +**single instance** of the application with a **single database** on to a +**single server**. We have a **TenantId** (or similar) field in each +table (for a RDBMS) which is used to isolate a tenant's data from +others. + +This type of application is easy to setup and maintain, but harder to create. +This is because we must prevent a Tenant from reading or writing to other +tenant data. We may add a **TenantId filter** for each database read +(select) operation. We may also check it every time we write, to see if this entity is +related to the **current tenant**. This is tedious and error-prone. However, +ASP.NET Boilerplate helps us here by using **automatic [data +filtering](Data-Filters.md)**. + +This approach may have performance problems if we have many tenants with +large data sets. We can use table partitioning or other database features to +overcome this problem. + +##### Single Deployment - Hybrid Databases + +We may want to store tenants in a single databases normally, but may want to +create a separate database for desired tenants. For example, we can +store tenants with big data in their own databases, but store all other +tenants in a single database. + +##### Multiple Deployment - Single/Multiple/Hybrid Database + +Finally, we may want to deploy our application to more than one server +(like web farms) for better application performance, high +availability, and/or scalability. This is independent from the database +approach. + +### Multi-Tenancy in ASP.NET Boilerplate + +ASP.NET Boilerplate can work with all the scenarios described above. + +#### Enabling Multi-Tenancy + +Multi-tenancy is disabled by default for Framework level. We can enable it in PreInitialize method +of our module as shown below: + + Configuration.MultiTenancy.IsEnabled = true;  + +**Note:** Multi-tenancy is enabled in both ASP.NET Core and ASP.NET MVC 5.x startup templates. + +#### Host vs Tenant + +We define two terms used in a multi-tenant system: + +- **Tenant**: A customer which has it's own users, roles, + permissions, settings... and uses the application completely + isolated from other tenants. A multi-tenant application will have + one or more tenants. If this is a CRM application, different tenants + also have their own accounts, contacts, products and orders. So + when we say a '**tenant user**', we mean a user owned by a tenant. +- **Host**: The Host is singleton (there is a single host). The Host is + responsible for creating and managing tenants. A '**host user**' is at a + higher level and independent from all tenants and can control them. + +#### Session + +ASP.NET Boilerplate defines the **IAbpSession** interface to obtain the current +**user** and **tenant** ids. This interface is used in multi-tenancy to +get the current tenant's id by default. Thus, it can filter data based on the +current tenant's id. Here are the rules: + +- If both the UserId and the TenantId is null, then the current user is **not + logged in** to the system. We can not know if it's a host user + or tenant user. In this case, the user can not access + [authorized](/Pages/Documents/Authorization) content. +- If the UserId is not null and the TenantId is null, then we know that + the current user is a **host user**. +- If the UserId is not null and the TenantId is not null, we know + that the current user is a **tenant user**. +- If the UserId is null but the TenantId is not null, that means we know + the current tenant, but the current request is not authorized (user did + not login). See the next section to understand how the current tenant is + determined. + +See the [session documentation](/Pages/Documents/Abp-Session) for more +information. + +#### Determining Current Tenant + +Since all tenant users use the same application, we should have a way of +distinguishing the tenant of the current request. The default session +implementation (ClaimsAbpSession) uses different approaches to find the +tenant related to the current request in this given order: + +1. If the user is logged in, it gets the TenantId from current claims. Claim + name is *http://www.aspnetboilerplate.com/identity/claims/tenantId* + and should contain an integer value. If it's not found in claims + then the user is assumed to be a *host* user. +2. If the user has not logged in, then it tries to resolve the TenantId from the + *tenant resolve contributor*s. There are 3 pre-defined tenant + contributors and are run in a given order (first successful resolver 'wins'): + 1. **DomainTenantResolveContributer**: Tries to resolve tenancy + name from an url, generally from a domain or subdomain. You can + configure the domain format in the PreInitialize method of your module + (like + Configuration.Modules.AbpWebCommon().MultiTenancy.DomainFormat = + "{0}.mydomain.com";). If the domain format is "{0}.mydomain.com" and + the current host of the request is ***acme*.mydomain.com**, then the + tenancy name is resolved as "acme". The next step is to query + ITenantStore to find the TenantId by the given tenancy name. If a + tenant is found, then it's resolved as the current TenantId. + 2. **HttpHeaderTenantResolveContributer**: Tries to resolve + TenantId from an "Abp.TenantId" header value, if present. This is a + constant defined in + Abp.MultiTenancy.MultiTenancyConsts.TenantIdResolveKey. + 3. **HttpCookieTenantResolveContributer**: Tries to resolve + the TenantId from an "Abp.TenantId" cookie value, if present. This uses the + same constant explained above. + +If none of these attempts can resolve a TenantId, then the current requester +is considered to be the host. Tenant resolvers are extensible. You can add +resolvers to the **Configuration.MultiTenancy.Resolvers** collection, or +remove an existing resolver. + +One last thing on resolvers: The resolved tenant id is cached during +the same request for performance reasons. Resolvers are executed +once in a request, and only if the current user has not already logged in. + +##### Tenant Store + +The **DomainTenantResolveContributer** uses ITenantStore to find the tenant id +by tenancy name. The default implementation of **ITenantStore** is +**NullTenantStore** which does not contain any tenant and returns null +for queries. You can implement and replace it to query tenants from any +data source. [Module Zero](Zero/Overall.md) properly implements it by +getting it from its [tenant manager](Zero/Tenant-Management.md). So if you +are using Module Zero, you don't need to worry about the tenant store. + +#### Data Filters + +For the **multi-tenant single database** approach, we must add a +**TenantId** filter to only get the current tenant's entities when +retrieving [entities](/Pages/Documents/Entities) from the database. ASP.NET +Boilerplate automatically does it when you implement one of the two +interfaces for your entity: **IMustHaveTenant** and **IMayHaveTenant**. + +##### IMustHaveTenant Interface + +This interface is used to distinguish the entities of different tenants by +defining a **TenantId** property. An example entity that implements +IMustHaveTenant: + + public class Product : Entity, IMustHaveTenant + { + public int TenantId { get; set; } + + public string Name { get; set; } + + //...other properties + } + +This way, ASP.NET Boilerplate knows that this is a tenant-specific entity +and automatically isolates the entities of a tenant from other tenants. + +##### IMayHaveTenant interface + +We may need to share an **entity type** between host and tenants. As such, an +entity may be owned by a tenant or the host. The IMayHaveTenant interface +also defines **TenantId** (similar to IMustHaveTenant), but it is **nullable** +in this case. An example entity that implements IMayHaveTenant: + + public class Role : Entity, IMayHaveTenant + { + public int? TenantId { get; set; } + + public string RoleName { get; set; } + + //...other properties + } + +We may use the same Role class to store Host roles and Tenant roles. In this +case, the TenantId property says if this is host entity or tenant +entitiy. A **null** value means this is a **host** entity, a +**non-null** value means this entity is owned by a **tenant** where the Id is +the **TenantId**. + +##### Additional Notes + +IMayHaveTenant is not as common as IMustHaveTenant. For example, a Product +class can not be IMayHaveTenant since a Product is related to the actual +application functionality, and not related to managing tenants. So use +the IMayHaveTenant interface carefully since it's harder to maintain code +shared by host and tenants. + +When you define an entity type as IMustHaveTenant or IMayHaveTenant, +**always set the TenantId** when you create a new entity (While ASP.NET +Boilerplate tries to set it from current TenantId, it may not be +possible in some cases, especially for IMayHaveTenant entities). Most of +the time, this will be the only point you deal with the TenantId properties. +You don't need to explicitly write the TenantId filter in Where conditions +while writing LINQ, since it is automatically filtered. + +#### Switching Between Host and Tenants + +While working on a multi-tenant application database, we can get the +**current tenant**. By default, it's obtained from the +[IAbpSession](Abp-Session.md) (as described before). We can change +this behavior and switch to another tenant's database. Example: + + public class ProductService : ITransientDependency + { + private readonly IRepository _productRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public ProductService(IRepository productRepository, IUnitOfWorkManager unitOfWorkManager) + { + _productRepository = productRepository; + _unitOfWorkManager = unitOfWorkManager; + } + + [UnitOfWork] + public virtual List GetProducts(int tenantId) + { + using (_unitOfWorkManager.Current.SetTenantId(tenantId)) + { + return _productRepository.GetAllList(); + } + } + } + +SetTenantId ensures that we are working on a given tenant's data, +independent from the database architecture: + +- If the given tenant has a dedicated database, it switches to that + database and gets products from it. +- If the given tenant does not have a dedicated database (the single database + approach, for example), it adds the automatic TenantId filter to query + only that tenant's products. + +If we don't use SetTenantId, it gets the tenantId from the +[session](Abp-Session.md). There are some guidelines and +best practices here: + +- Use **SetTenantId(null)** to switch to the host. +- Use SetTenantId within a **using** block (as in the example) if there + is not a special case. This way, it automatically restores the tenantId at + the end of the using block and the code calling the GetProducts method + works as before. +- You can use SetTenantId in **nested blocks** if it's needed. +- Since **\_unitOfWorkManager.Current** is only available in a [unit of + work](Unit-Of-Work.md), be sure that your code runs in a UOW. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NHibernate-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NHibernate-Integration.md" new file mode 100644 index 0000000..729664a --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NHibernate-Integration.md" @@ -0,0 +1,205 @@ +ASP.NET Boilerplate can work with any O/RM framework. It has built-in +integration with **NHibernate**. This document will explain how to use +NHibernate with ASP.NET Boilerplate. It's assumed that you're already +familar with NHibernate at some basic level. + +### NuGet package + +The NuGet package to use NHibernate as an O/RM in ASP.NET Boilerplate is +[Abp.NHibernate](http://www.nuget.org/packages/Abp.NHibernate). You +need to add it to your application. It's better to implement NHibernate +in a separated assembly (dll) in your application and depend on that +package from this assembly. + +### Configuration + +To start using NHibernate, you must configure it in the +[PreInitialize](/Pages/Documents/Module-System) method of your module. + + [DependsOn(typeof(AbpNHibernateModule))] + public class SimpleTaskSystemDataModule : AbpModule + { + public override void PreInitialize() + { + var connStr = ConfigurationManager.ConnectionStrings["Default"].ConnectionString; + + Configuration.Modules.AbpNHibernate().FluentConfiguration + .Database(MsSqlConfiguration.MsSql2008.ConnectionString(connStr)) + .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())); + } + + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + } + +The **AbpNHibernateModule** module provides basic functionality and adapters +to make NHibernate work with ASP.NET Boilerplate. + +#### Entity mapping + +In this sample configuration above, we have fluently mapped using all the +mapping classes in the current assembly. An example mapping class can be as +follows: + + public class TaskMap : EntityMap + { + public TaskMap() + : base("TeTasks") + { + References(x => x.AssignedUser).Column("AssignedUserId").LazyLoad(); + + Map(x => x.Title).Not.Nullable(); + Map(x => x.Description).Nullable(); + Map(x => x.Priority).CustomType().Not.Nullable(); + Map(x => x.Privacy).CustomType().Not.Nullable(); + Map(x => x.State).CustomType().Not.Nullable(); + } + } + +**EntityMap** is a class of ASP.NET Boilerplate that extends +**ClassMap<T>**, automatically maps the **Id** property and gets the +**table name** in the constructor. We're deriving from it and mapping +other properties using +[FluentNHibernate](http://www.fluentnhibernate.org/).  If you want, you can +derive directly from ClassMap. You can use the full API of +**FluentNHibernate** and you can use the other mapping techniques, +like mapping XML files. + +### Repositories + +Repositories are used to abstract data access from higher layers. See the +[repository documentation](Repositories.md) for more info.   + +#### Default Implementation + +The [Abp.NHibernate](http://www.nuget.org/packages/Abp.NHibernate) NuGet package +implements default repositories for entities in your application. You +don't have to create repository classes for entities, just use the +predefined repository methods. Example: + + public class PersonAppService : IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + + _personRepository.Insert(person); + } + } + +The PersonAppService contructor-injects **IRepository<Person>** and +uses the **Insert** method. In this way, you can easily inject +**IRepository<TEntity>** (or IRepository<TEntity, +TPrimaryKey>) and use the pre-defined methods. See the [repository +documentation](/Pages/Documents/Repositories) for a list of all the pre-defined +methods. + +#### Custom Repositories + +If you want to add a custom method, as a best practice, you must first +add it to a repository interface, then implement it in a +repository class. ASP.NET Boilerplate provides a base class +**NhRepositoryBase** to implement repositories easily. To implement the +IRepository interface, you can just derive your repository from this +class. + +Assume that we have a Task entity that can be assigned to a Person +(entity) and the Task has a State (new, assigned, completed... and so on). +We may need to write a custom method to get the list of Tasks with some +conditions and with AssisgnedPerson property pre-fetched in a single +database query. See the example code: + + public interface ITaskRepository : IRepository + { + List GetAllWithPeople(int? assignedPersonId, TaskState? state); + } + + public class TaskRepository : NhRepositoryBase, ITaskRepository + { + public TaskRepository(ISessionProvider sessionProvider) + : base(sessionProvider) + { + } + + public List GetAllWithPeople(int? assignedPersonId, TaskState? state) + { + var query = GetAll(); + + if (assignedPersonId.HasValue) + { + query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); + } + + if (state.HasValue) + { + query = query.Where(task => task.State == state); + } + + return query + .OrderByDescending(task => task.CreationTime) + .Fetch(task => task.AssignedPerson) + .ToList(); + } + } + +**GetAll()** returns **IQueryable<Task>**, then we can add some +**Where** filters using given parameters. Finally we can call +**ToList()** to get the list of Tasks. + +You can also use the **Session** object in the repository methods to use the full +API of NHibernate.  + +**Note**: Define the custom repository **interface** in the +**domain/core** layer, **implement** it in the **NHibernate** project +for layered applications. This way, you can inject the interface from any +project without referencing NH. + +##### Application Specific Base Repository Class + +Although you can derive your repositories from the NhRepositoryBase in +ASP.NET Boilerplate, it's better practice to create your own base +class that **extends** the NhRepositoryBase. This way, you can add shared/common +methods to your repositories easily. Example: + + //Base class for all repositories in my application + public abstract class MyRepositoryBase : NhRepositoryBase + where TEntity : class, IEntity + { + protected MyRepositoryBase(ISessionProvider sessionProvider) + : base(sessionProvider) + { + } + + //add common methods for all repositories + } + + //A shortcut for entities that have an integer Id. + public abstract class MyRepositoryBase : MyRepositoryBase + where TEntity : class, IEntity + { + protected MyRepositoryBase(ISessionProvider sessionProvider) + : base(sessionProvider) + { + } + + //do not add any methods here, add the class above (since this inherits it) + } + + public class TaskRepository : MyRepositoryBase, ITaskRepository + { + public TaskRepository(ISessionProvider sessionProvider) + : base(sessionProvider) + { + } + + //Specific methods for task repository + } diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NLayer-Architecture.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NLayer-Architecture.md" new file mode 100644 index 0000000..0d35ef6 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/NLayer-Architecture.md" @@ -0,0 +1,100 @@ +### Introduction + +The layering of an application's codebase is a widely accepted technique to +help reduce complexity and to improve code reusability. To achieve a layered +architecture, ASP.NET Boilerplate follows the principles of **Domain +Driven Design**. + +### Domain Driven Design Layers + +There are four fundamental layers in Domain Driven Design (DDD): + +- **Presentation Layer**: Provides an interface to the user. Uses the + Application Layer to achieve user interactions. +- **Application Layer**: Mediates between the Presentation and Domain + Layers. Orchestrates business objects to perform specific + application tasks. +- **Domain Layer**: Includes business objects and their rules. This is the + heart of the application. +- **Infrastructure Layer**: Provides generic technical capabilities + that support higher layers mostly using 3rd-party libraries. + +### ASP.NET Boilerplate Application Architecture Model + +In addition to DDD, there are also other logical and physical layers in +a modern architected application. The model below is suggested and +implemented for ASP.NET Boilerplate applications. ASP.NET Boilerplate +not only makes implementing this model easier by providing base classes +and services, but also provides [startup templates](/Templates) to +directly start with this model. + +[ASP.NET Boilerplate NLayer Architecture](https://raw.githubusercontent.com/aspnetboilerplate/aspnetboilerplate/master/doc/WebSite/images/abp-nlayer-architecture.png) + +#### Client Applications + +These are remote clients that use the application as a service via HTTP APIs +(API Controllers, [OData](OData-Integration.md) Controllers, maybe even a +GraphQL endpoint). A remote client can be a SPA (Single Page App), a mobile application, or +a 3rd-party consumer. [Localization](Localization.md) and +[Navigation](Navigation.md) can be done inside this applications. + +#### Presentation Layer + +ASP.NET \[Core\] MVC (Model-View-Controller) can be considered to be the +presentation layer. It can be a physical layer (uses application via +HTTP APIs) or a logical layer (directly injects and uses [application +services](Application-Services.md)). In either case it can include +[Localization](Localization.md), [Navigation](Navigation.md), +[Object Mapping](Object-To-Object-Mapping.md), +[Caching](Caching.md), [Configuration +Management](Setting-Management.md), [Audit +Logging](Audit-Logging.md) and so on. It also deals with +[Authorization](Authorization.md), [Session](Abp-Session.md), +[Features](Feature-Management.md) (for +[multi-tenant](Multi-Tenancy.md) applications) and [Exception +Handling](Handling-Exceptions.md). + +#### Distributed Service Layer + +This layer is used to serve application/domain functionality via remote +APIs like REST, OData, GraphQL... They don't contain business logic but +only translate HTTP requests to domain interactions, or can use +application services to delegate the operation. This layer generally +includes [Authorization](Authorization.md), [Caching](Caching.md), +[Audit Logging](Audit-Logging.md), [Object +Mapping](Object-To-Object-Mapping.md), [Exception +Handling](Handling-Exceptions.md), [Session](Abp-Session.md) and so +on... + +#### Application Layer + +The application layer mainly includes [Application +Services](Application-Services.md) that use domain layer and domain +objects ([Domain Services](Domain-Services.md), +[Entities](Entities.md)...) to perform requested application +functionalities. It uses [Data Transfer +Objects](Data-Transfer-Objects.md) to get data from and return data +to the presentation or distributed service layer. It can also deal with +[Authorization](Authorization.md), [Caching](Caching.md), [Audit +Logging](Audit-Logging.md), [Object +Mapping](Object-To-Object-Mapping.md), the [Session](Abp-Session.md) and +so on... + +#### Domain Layer + +This is the main layer that implements our domain logic. It includes +[Entities](Entities.md), [Value Objects](Value-Objects.md), and [Domain +Services](Domain-Services.md) to perform business/domain logic. It can +also include [Specifications](Specifications.md) and trigger [Domain +Events](EventBus-Domain-Events.md). It defines Repository Interfaces +to read and persist entities from the data source (generally a DBMS). + +#### Infrastructure Layer + +The infrastructure layer makes other layers work: It implements +the repository interfaces (using [Entity Framework +Core](Entity-Framework-Core.md) for example) to actually work with a +real database. It may also include an integration to a vendor to [send +emails](Email-Sending.md) and so on. This is not a strict layer below +all layers, but actually supports other layers by implementing the abstract +concepts of them. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Navigation.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Navigation.md" new file mode 100644 index 0000000..14e631b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Navigation.md" @@ -0,0 +1,110 @@ +Every web application has some kind of menu to navigate between pages/screens. +ASP.NET Boilerplate provides a common infrastructure to create and show +a menu to users. + +### Creating Menus + +An application may consist of different +[modules](/Pages/Documents/Module-System) and each module may have it's +own menu items. To define menu items, we need to create a class derived +from the **NavigationProvider**. + +Imagine that we have a main menu like the one shown below: + +- Tasks +- Reports +- Administration + - User management + - Role management + +Here, the Administration menu item has two **sub menu items**. Here's a +navigation provider class to create such a menu: + + public class SimpleTaskSystemNavigationProvider : NavigationProvider + { + public override void SetNavigation(INavigationProviderContext context) + { + context.Manager.MainMenu + .AddItem( + new MenuItemDefinition( + "Tasks", + new LocalizableString("Tasks", "SimpleTaskSystem"), + url: "/Tasks", + icon: "fa fa-tasks" + ) + ).AddItem( + new MenuItemDefinition( + "Reports", + new LocalizableString("Reports", "SimpleTaskSystem"), + url: "/Reports", + icon: "fa fa-bar-chart" + ) + ).AddItem( + new MenuItemDefinition( + "Administration", + new LocalizableString("Administration", "SimpleTaskSystem"), + icon: "fa fa-cogs" + ).AddItem( + new MenuItemDefinition( + "UserManagement", + new LocalizableString("UserManagement", "SimpleTaskSystem"), + url: "/Administration/Users", + icon: "fa fa-users", + requiredPermissionName: "SimpleTaskSystem.Permissions.UserManagement" + ) + ).AddItem( + new MenuItemDefinition( + "RoleManagement", + new LocalizableString("RoleManagement", "SimpleTaskSystem"), + url: "/Administration/Roles", + icon: "fa fa-star", + requiredPermissionName: "SimpleTaskSystem.Permissions.RoleManagement" + ) + ) + ); + } + } + +A MenuItemDefinition can basically have a unique **name**, a localizable +**display name**, an **url** and an **icon**. Also: + +- A menu item may require a permission to show this menu to a + particular user (See the [authorization](/Pages/Documents/Authorization) + document). The **requiredPermissionName** property can be used in this + case. +- A menu item can be dependent on a + [feature](/Pages/Documents/Feature-Management). + The **featureDependency** property can be used in this case. +- A menu item can define **customData** and the **order** in which it appears. + +The **INavigationProviderContext** has methods to get existing menu items, +add menus, and edit menu items. This way, different modules can add their own items +to the menu. + +There may be one or more menus in an application. +The **context.Manager.MainMenu** references the default main menu. We can +create and add more menus using the **context.Manager.Menus** property. + +#### Registering Navigation Provider + +After creating the navigation provider, we need to register it to ASP.NET +Boilerplate's configuration on the **PreInitialize** method of our +[module](/Pages/Documents/Module-System): + + Configuration.Navigation.Providers.Add();  + +### Showing Menu + +The **IUserNavigationManager** can be +[injected](/Pages/Documents/Dependency-Injection) and used to get menu +items and show them to the user. This way, we can create a menu on the server-side. + +ASP.NET Boilerplate automatically generates a **JavaScript API** to get the +menu and items on the client-side. Methods and objects under the **abp.nav** +namespace can be used for this purpose. For instance, +**abp.nav.menus.MainMenu** can be used to get the main menu of the +application. This way, we can create a menu on the client-side. + +The ASP.NET Boilerplate [templates](/Templates) use this system to create +and show a menu to the user. Create a template and see the source code +for more info. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Notification-System.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Notification-System.md" new file mode 100644 index 0000000..99c3208 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Notification-System.md" @@ -0,0 +1,386 @@ +### Introduction + +Notifications are used to **inform** users on specific events in the +system. ASP.NET Boilerplate provides a **pub/sub** (publish/subscribe) based **real-time** +notification system. + +#### Sending Models + +There are two ways of sending notifications to users: + +- The user **subscribes** to a specific notification type. Then we + **publish** a notification of this type which is delivered to all + **subscribed** users. This is the **pub/sub** model. +- We can **directly send** a notification to **target user(s)**. + +#### Notification Types + +There are also two types of notifications: + +- **General notifications** are arbitrary type of notifications. + "Notify me if a user sends me a friendship request" is an example of + this type notifications. +- **Entity notifications** are associated to a specific entity. + "Notify me if a user comment on **this** photo" is an entity-based + notification since it's associated to a specific photo entity. Users + may want to get notifications for some photos, but not for all. + +#### Notification Data + +A notification generally includes **notification data**. For example: +The "Notify me if a user sends me a friendship request" notification may +have two data properties: *sender user name* (which user sent this +friendship request) and a *request note* (a note that the user wrote in +the request). Note that the notification data type is tightly +coupled to the notification types. Different notification types have +different data types. + +Notification data is **optional**. Some notifications may not require +data. There are some pre-defined notification data types that are +enough for most cases. The **MessageNotificationData** can be used for +simple messages and the **LocalizableMessageNotificationData** can be used +for localizable and parametric notification messages. We will see the +example usage in later sections. + +#### Notification Severity + +There are 5 levels of notification severity, defined in +**NotificationSeverity** enum: **Info**, **Success**, **Warn**, +**Error** and **Fatal**. Default value is **Info**. + +#### About Notification Persistence + +See the *Notification Store* section for more information on notification +persistence. + +### Subscribe to Notifications + +The **INotificationSubscriptionManager** provides an API to **subscribe** to +notifications. Examples: + + public class MyService : ITransientDependency + { + private readonly INotificationSubscriptionManager _notificationSubscriptionManager; + + public MyService(INotificationSubscriptionManager notificationSubscriptionManager) + { + _notificationSubscriptionManager = notificationSubscriptionManager; + } + + //Subscribe to a general notification + public async Task Subscribe_SentFrendshipRequest(int? tenantId, long userId) + { + await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "SentFrendshipRequest"); + } + + //Subscribe to an entity notification + public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId) + { + await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId)); + } + } + +First, we [injected](/Pages/Documents/Dependency-Injection) the +**INotificationSubscriptionManager**. The first method subscribes to a +**general notification**, if a user wants to get notified when someone sends +a friendship request. The second method subscribes to a notification related to +a **specific entity** (Photo), if the user wants to get notified if anyone +writes a comment to a specified photo. + +Every notification type should have a **unique name** (like +*SentFrendshipRequest* and *CommentPhoto* in the examples) + +The **INotificationSubscriptionManager** also has the **UnsubscribeAsync, +IsSubscribedAsync, GetSubscriptionsAsync**... methods to manage +subscriptions. + +### Publish Notifications + +**INotificationPublisher** is used to publish notifications. Examples: + + public class MyService : ITransientDependency + { + private readonly INotificationPublisher _notiticationPublisher; + + public MyService(INotificationPublisher notiticationPublisher) + { + _notiticationPublisher = notiticationPublisher; + } + + //Send a general notification to a specific user + public async Task Publish_SentFrendshipRequest(string senderUserName, string friendshipMessage, UserIdentifier targetUserId) + { + await _notiticationPublisher.PublishAsync("SentFrendshipRequest", new SentFrendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId }); + } + + //Send an entity notification to a specific user + public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, UserIdentifier photoOwnerUserId) + { + await _notiticationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId }); + } + + //Send a general notification to all subscribed users in current tenant (tenant in the session) + public async Task Publish_LowDisk(int remainingDiskInMb) + { + //Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!" + var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName")); + data["remainingDiskInMb"] = remainingDiskInMb; + + await _notiticationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn); + } + } + +In the first example, we published a notification to a single user. +*SentFrendshipRequestNotificationData* should be derived from +**NotificationData** like this: + + [Serializable] + public class SentFrendshipRequestNotificationData : NotificationData + { + public string SenderUserName { get; set; } + + public string FriendshipMessage { get; set; } + + public SentFrendshipRequestNotificationData(string senderUserName, string friendshipMessage) + { + SenderUserName = senderUserName; + FriendshipMessage = friendshipMessage; + } + } + +In the second example, we sent a notification to a **specific user** for +a **specific entity**. Notification data classes don't need to be +**serialzable** normally (since JSON serialization is used by default). +But we suggest you mark it as serializable since you may need to move +notifications between applications and may want to use binary +serialization in the future. Also, as declared before, notification data +is optional and may not be required for all notifications. + +**Note**: If we publish a notification to **specific users**, they +**don't need** to be subscribed to those notifications. + +In the third example, we did not define a dedicated notification data +class. Instead, we directly used the built-in +**LocalizableMessageNotificationData** with **dictionary** based data +and then published the notification as '**Warn**'. +**LocalizableMessageNotificationData** can store dictionary-based +arbitrary data (this is also true for custom notification data classes +since they also inherit from **NotificationData** class). We used +"**remainingDiskInMb**" as an argument on +[localization](/Pages/Documents/Localization). The localization message can +include these arguments (like "*Attention! Only {remainingDiskInMb} MBs +left on the disk!*" as an example). We will see how to localize it on +the client-side section. + +### User Notification Manager + +The **IUserNotificationManager** is used to manage the notifications of users. +It has methods to **get**, **update** or **delete** notifications for a +user. You can use it to prepare a notification list page for your +application. + +### Real-Time Notifications + +While you can use IUserNotificationManager to query notifications, we +generally want to push real time notifications to the client. + +The notification system uses **IRealTimeNotifier** to send real time +notifications to users. This can be implemented with any type of real +time communication system. It's implemented using **SignalR** in a +separated package. The [startup templates](/Templates) already have SignalR +installed. See the [SignalR Integration +document](/Pages/Documents/SignalR-Integration) for more information. + +**Note**: The notification system calls **IRealTimeNotifier** asynchronously +in a [**background job**](/Pages/Documents/Background-Jobs-And-Workers). +Because of this, notifications may be sent with a small delay. + +#### Client-Side + +When a real-time notification is received, ASP.NET Boilerplate triggers +a **global event** on the client-side. You can register it like this to get +notifications: + + abp.event.on('abp.notifications.received', function (userNotification) { + console.log(userNotification); + }); + +The **abp.notifications.received** event is triggered for each received real- +time notification. You can register to this event as shown above to get +notifications. See the [JavaScript event +bus](/Pages/Documents/Javascript-API/Event-Bus) documentation for more +information on events. Here's an example of the incoming notification JSON for +"System.LowDisk": + + { + "userId": 2, + "state": 0, + "notification": { + "notificationName": "System.LowDisk", + "data": { + "message": { + "sourceName": "MyLocalizationSourceName", + "name": "LowDiskWarningMessage" + }, + "type": "Abp.Notifications.LocalizableMessageNotificationData", + "properties": { + "remainingDiskInMb": "42" + } + }, + "entityType": null, + "entityTypeName": null, + "entityId": null, + "severity": 0, + "creationTime": "2016-02-09T17:03:32.13", + "id": "0263d581-3d8a-476b-8e16-4f6a6f10a632" + }, + "id": "4a546baf-bf17-4924-b993-32e420a8d468" + } + +In this object; + +- **userId**: The current user id. You don't generally need this since you + know the current user. +- **state**: Value of **UserNotificationState** enum. 0: **Unread**, + 1: **Read**. +- **notification**: Notification details. + - **notificationName**: Unique name of the notification (same + value used while publishing the notification). + - **data**: notification data. In this example, we used + **LocalizableMessageNotificationData** (as published in the + example above). + - **message**: Localizable message information. We can use + **sourceName** and **name** to localize the message on the UI. + - **type**: The notification data type. Full type name, including + namespaces. We can check this type while processing the + notification data. + - **properties**: Dictionary based custom properties. + - **entityType**, **entityTypeName** and **entityId**: Entity + information if this is an entity related notification. + - **severity**: Value of **NotificationSeverity** enum. 0: + **Info**, 1: **Success**, 2: **Warn**, 3: **Error**, 4: + **Fatal**. + - **creationTime**: Time of when this notification was created. + - **id**: Notification id. +- **id**: User notification id. + +You can not only log the notification, but you can use the notification +data to show notification information to the user. Example: + + abp.event.on('abp.notifications.received', function (userNotification) { + if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') { + var localizedText = abp.localization.localize( + userNotification.notification.data.message.name, + userNotification.notification.data.message.sourceName + ); + + $.each(userNotification.notification.data.properties, function (key, value) { + localizedText = localizedText.replace('{' + key + '}', value); + }); + + alert('New localized notification: ' + localizedText); + } else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') { + alert('New simple notification: ' + userNotification.notification.data.message); + } + }); + +To be able to process notification data, we should check the data type. +This example simply gets a message from the notification data. For the +localized message (LocalizableMessageNotificationData), we are +localizing the message and replacing parameters. For a simple message +(MessageNotificationData), we directly get the message. Of course, in a +real project, we will not use the alert function. We can use the +[abp.notify](/Pages/Documents/Javascript-API/Notification) api instead to show +nice UI notifications. + +If you need to implement logic like what is shown above, there is an easier and +scalable way. You can just use a single line of code to show a [UI +notification](/Pages/Documents/Javascript-API/Notification) when a push +notification is received: + + abp.event.on('abp.notifications.received', function (userNotification) { + abp.notifications.showUiNotifyForUserNotification(userNotification); + }); + +This shows a [UI +notification](/Pages/Documents/Javascript-API/Notification) like this +(for System.LowDisk notification published above): + + + +It works for built-in notification data types +(LocalizableMessageNotificationData and MessageNotificationData). If you +have custom notification data types, then you should register data +formatters like this: + + abp.notifications.messageFormatters['MyProject.MyNotificationDataType'] = function(userNotification) { + return ...; //format and return message here + }; + +This way, **showUiNotifyForUserNotification** can create the shown messages for +your data types. If you just need the formatted message, you can +directly use +**abp.notifications.getFormattedMessageFromUserNotification(userNotification)** +which is internally used by showUiNotifyForUserNotification. + +The [startup templates](/Templates) include the code to show UI +notifications when a push notification is received. + +### Notification Store + +The notification system uses **INotificationStore** to persist +notifications. This must be implemented in order to make the notification +system properly work. You can implement it yourself or use +**[Module Zero](/Pages/Documents/Zero/Overall)** which already +implements it. + +### Notification Definitions + +You don't have to **define** a notification to use it. You can just +use any **notification name** without defining it. However, defining it may +bring you some additional benefits. For example, you can then +**investigate** all notifications in your application. In this case, we +can define a **notification provider** for our +[module](/Pages/Documents/Module-System) as shown below: + + public class MyAppNotificationProvider : NotificationProvider + { + public override void SetNotifications(INotificationDefinitionContext context) + { + context.Manager.Add( + new NotificationDefinition( + "App.NewUserRegistered", + displayName: new LocalizableString("NewUserRegisteredNotificationDefinition", "MyLocalizationSourceName"), + permissionDependency: new SimplePermissionDependency("App.Pages.UserManagement") + ) + ); + } + } + +"**App.NewUserRegistered**" is the unique name of the notification. We +defined a localizable **displayName** so we can then show it when +subscribing to the notification on the UI. And finally, we declared that +this notification is available to a user only if he has the +"**App.Pages.UserManagement**" +[permission](/Pages/Documents/Authorization). + +There are also some other parameters that you can investigate in the code. +Note: The notification name is **required** for a notification definition. + +After defining such a notification provider, we must register it in the +[PreInitialize](/Pages/Documents/Module-System#preinitialize) method +of our module, as shown below: + + public class AbpZeroTemplateCoreModule : AbpModule + { + public override void PreInitialize() + { + Configuration.Notifications.Providers.Add(); + } + + //... + } + +Finally, you can inject and use the **INotificationDefinitionManager** in +your application to get notification definitions. You may then want to +prepare an automatic page to allow users to subscribe to those notifications. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Nuget-Packages.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Nuget-Packages.md" new file mode 100644 index 0000000..6931b27 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Nuget-Packages.md" @@ -0,0 +1,115 @@ +### Packages + +ASP.NET Boilerplate is distributed on **NuGet**. + +Here's a list of all the official packages. + +#### [Abp](http://www.nuget.org/packages/Abp) + +Core package. All other packages depend on it. + +#### [Abp.AspNetCore](http://www.nuget.org/packages/Abp.AspNetCore) + +ASP.NET Core integration package. + +#### [Abp.AspNetCore.SignalR](http://www.nuget.org/packages/Abp.AspNetCore.SignalR) + +ASP.NET Core SignalR integration package. + +#### [Abp.Web.Common](http://www.nuget.org/packages/Abp.Web.Common) + +Common web-related classes (Used by both ASP.NET MVC and ASP.NET Core). + +#### [Abp.Web](http://www.nuget.org/packages/Abp.Web) + +Web package for both MVC and Web API use. + +#### [Abp.Web.Mvc](http://www.nuget.org/packages/Abp.Web.Mvc) + +ASP.NET MVC integration package. + +#### [Abp.Web.Api](http://www.nuget.org/packages/Abp.Web.Api) + +ASP.NET Web API integration package. + +#### [Abp.Web.Api.OData](https://www.nuget.org/packages/Abp.Web.Api.OData) + +OData integration package. + +#### [Abp.Web.Resources](http://www.nuget.org/packages/Abp.Web.Resources) + +Client-side scripts package. + +#### [Abp.Web.SignalR](http://www.nuget.org/packages/Abp.Web.SignalR) + +SignalR integration package. + +#### [Abp.Owin](http://www.nuget.org/packages/Abp.Owin) + +OWIN integration package. + +#### [Abp.EntityFramework.Common](http://www.nuget.org/packages/Abp.EntityFramework.Common) + +Common code shared between the Abp.EntityFramework and +Abp.EntityFrameworkCore packages. + +#### [Abp.EntityFramework](http://www.nuget.org/packages/Abp.EntityFramework) + +EntityFramework integration package. + +#### [Abp.EntityFramework.GraphDiff](http://www.nuget.org/packages/Abp.EntityFramework.GraphDiff) + +EntityFramework GraphDiff integration package. + +#### [Abp.EntityFrameworkCore](http://www.nuget.org/packages/Abp.EntityFrameworkCore) + +EntityFrameworkCore integration package. + +#### [Abp.NHibernate](http://www.nuget.org/packages/Abp.NHibernate) + +NHibernate integration package. + +#### [Abp.Dapper](http://www.nuget.org/packages/Abp.Dapper) + +Dapper integration package. + +#### [Abp.FluentMigrator](http://www.nuget.org/packages/Abp.FluentMigrator) + +Some simple extension methods to use ABP with FluentMigrator. + +#### [Abp.AutoMapper](http://www.nuget.org/packages/Abp.AutoMapper) + +AutoMapper integration package for object mapping. + +#### [Abp.HangFire](http://www.nuget.org/packages/Abp.HangFire) + +Hanfire integration for background job management. + +#### [Abp.HangFire.AspNetCore](http://www.nuget.org/packages/Abp.HangFire.AspNetCore) + +Hangfire.AspNetCore integration for background job management. + +#### [Abp.Castle.Log4Net](http://www.nuget.org/packages/Abp.Castle.Log4Net) + +Log4Net adapter to support latest log4net for ABP and Castle. + +#### [Abp.RedisCache](https://www.nuget.org/packages/Abp.RedisCache) + +Redis replacement for ABP's default cache manager. + +#### [Abp.MailKit](https://www.nuget.org/packages/Abp.MailKit) + +MailKit implementation for ABP's IEmailSender. + +#### [Abp.Quartz](https://www.nuget.org/packages/Abp.Quartz) + +Quartz integration package.  + +#### [Abp.TestBase](http://www.nuget.org/packages/Abp.TestBase) + +Base classes to create integration tests for ABP based projects. + +#### [Abp.AspNetCore.TestBase](http://www.nuget.org/packages/Abp.AspNetCore.TestBase) + +Base classes to create integration tests for ASP.NET Core and ABP-based +projects. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-AspNetCore-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-AspNetCore-Integration.md" new file mode 100644 index 0000000..8bac844 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-AspNetCore-Integration.md" @@ -0,0 +1,308 @@ +### Introduction + +**OData** is defined as "An **open protocol** to allow the creation and +consumption of **queryable** and **interoperable RESTful APIs** in a +**simple** and **standard** way" ([odata.org](http://www.odata.org/)). +You can use OData with ASP.NET Boilerplate. The +[Abp.AspNetCore.OData](https://www.nuget.org/packages/Abp.AspNetCore.OData) +NuGet package simplifies its usage. + +### Setup + +#### Install NuGet Package + +We must first install the Abp.AspNetCore.OData NuGet package to our Web.Core +project: + + Install-Package Abp.AspNetCore.OData + +#### Set Module Dependency + +We need to set a dependency on AbpAspNetCoreODataModule for our module. +Example: + + [DependsOn(typeof(AbpAspNetCoreODataModule))] + public class MyProjectWebCoreModule : AbpModule + { + ... + } + +See the [module system](/Pages/Documents/Module-System) to understand module +dependencies. + +#### Configure Your Entities + +OData requires us to declare entities which can be used as OData resources. +We must do this in the Startup class: + + public class Startup + { + public IServiceProvider ConfigureServices(IServiceCollection services) + { + ... + + services.AddOData(); + + // Workaround: https://github.com/OData/WebApi/issues/1177 + services.AddMvcCore(options => + { + foreach (var outputFormatter in options.OutputFormatters.OfType().Where(_ => _.SupportedMediaTypes.Count == 0)) + { + outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata")); + } + foreach (var inputFormatter in options.InputFormatters.OfType().Where(_ => _.SupportedMediaTypes.Count == 0)) + { + inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata")); + } + }); + + return services.AddAbp(...); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseAbp(); + + ... + + app.UseOData(builder => + { + builder.EntitySet("Persons").EntityType.Expand().Filter().OrderBy().Page(); + }); + + // Return IQueryable from controllers + app.UseUnitOfWork(options => + { + options.Filter = httpContext => + { + return httpContext.Request.Path.Value.StartsWith("/odata"); + }; + }); + + app.UseMvc(routes => + { + routes.MapODataServiceRoute(app); + + ... + }); + } + } + +Here, we got the ODataModelBuilder reference and set the Person entity. +You can use EntitySet to add other entities in a similar way. See the [OData +documentation](http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint) +for more information on the builder. + +### Create Controllers + +The Abp.AspNetCore.OData NuGet package includes the **AbpODataEntityController** +base class (which extends standard ODataController) to create your +controllers easier. An example to create an OData endpoint for the Person +entity: + + public class PersonsController : AbpODataEntityController, ITransientDependency + { + public PersonsController(IRepository repository) + : base(repository) + { + } + } + +It's that easy! All the methods of AbpODataEntityController are **virtual**. +This means that you can override the **Get**, **Post**, **Put**, **Patch**, +**Delete** and other actions and add your own logic. + +### Configuration + +Abp.AspNetCore.OData calls the +IRouteBuilder.MapODataServiceRoute method with the conventional +configuration. If you need to, you can set +Configuration.Modules.AbpAspNetCoreOData().**MapAction** to map OData routes +yourself. + +### Examples + +Here are some requests made to the controller defined above. Assume that +the application works on *http://localhost:21021*. We will show some +basic examples. Since OData is a standard protocol, you can easily find more +advanced examples on the web. + +#### Getting List of Entities + +Getting all people. + +##### Request + + GET http://localhost:21021/odata/Persons + +##### Response + + { + "@odata.context":"http://localhost:21021/odata/$metadata#Persons","value":[ + { + "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + },{ + "Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + ] + } + +#### Getting a Single Entity + +Getting the person with Id = 2. + +##### Request + + GET http://localhost:21021/odata/Persons(2) + +##### Response + + { + "@odata.context":"http://localhost:21021/odata/$metadata#Persons/$entity","Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + +#### Getting a Single Entity With Navigation Properties + +Getting the person with Id = 1 including his/her phone numbers. + +##### Request + + GET http://localhost:21021/odata/Persons(1)?$expand=Phones + +##### Response + + { + "@odata.context":"http://localhost:21021/odata/$metadata#Persons/$entity","Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1,"Phones":[ + { + "PersonId":1,"Type":"Mobile","Number":"4242424242","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + },{ + "PersonId":1,"Type":"Mobile","Number":"2424242424","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + ] + } + +#### Querying + +Here's a more advanced query that includes filtering, sorting and getting the top +2 results. + +##### Request + + GET http://localhost:21021/odata/Persons?$filter=Name eq 'Douglas Adams'&$orderby=CreationTime&$top=2 + +##### Response + + { + "@odata.context":"http://localhost:21021/odata/$metadata#Persons","value":[ + { + "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + } + ] + } + +OData supports paging, sorting, filtering, projections and much more. +See [its own documentation](http://www.odata.org/) for more +information. + +#### Creating a New Entity + +In this example, we're creating a new person. + +##### Request + + POST http://localhost:21021/odata/Persons + + { + Name: "Galileo Galilei" + } + +Here, the "Content-Type" header is "application/json". + +##### Response + + { + "@odata.context": "http://localhost:21021/odata/$metadata#Persons/$entity", + "Name": "Galileo Galilei", + "IsDeleted": false, + "DeleterUserId": null, + "DeletionTime": null, + "LastModificationTime": null, + "LastModifierUserId": null, + "CreationTime": "2016-01-12T20:36:04.1628263+02:00", + "CreatorUserId": null, + "Id": 4 + } + +If we get the list again, we can see the new person. We can also update +or delete an existing entity since OData supports it. + +#### Getting MetaData + +We can get the metadata of entities, as shown in this example. + +##### Request + + GET http://localhost:21021/odata/$metadata + +##### Response + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Metadata is used to investigate the service. + +### Sample Project + +You can get the source code of the sample project here: + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-Integration.md" new file mode 100644 index 0000000..0d92128 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OData-Integration.md" @@ -0,0 +1,316 @@ +### Introduction + +**OData** is defined as "An **open protocol** to allow for the creation and +consumption of **queryable** and **interoperable RESTful APIs** in a +**simple** and **standard** way". See [odata.org](http://www.odata.org/). +You can use OData with ASP.NET Boilerplate. + +The [Abp.Web.Api.OData](https://www.nuget.org/packages/Abp.Web.Api.OData) +NuGet package simplifies its usage. + +### Setup + +#### Install NuGet Package + +We should first install the Abp.Web.Api.OData NuGet package to our WebApi +project: + + Install-Package Abp.Web.Api.OData + +#### Set Module Dependency + +We should set the dependency to AbpWebApiODataModule from our module. +Example: + + [DependsOn(typeof(AbpWebApiODataModule))] + public class MyProjectWebApiModule : AbpModule + { + ... + } + +See the [module system documentation](/Pages/Documents/Module-System) to understand module +dependencies. + +#### Configure Your Entities + +OData requires you to declare entities which can be used as OData resources. +We should do this in the +[PreInitialize](/Pages/Documents/Module-System#preinitialize) method +of our module, as shown below: + + [DependsOn(typeof(AbpWebApiODataModule))] + public class MyProjectWebApiModule : AbpModule + { + public override void PreInitialize() + { + var builder = Configuration.Modules.AbpWebApiOData().ODataModelBuilder; + + // Configure your entities here... + builder.EntitySet("Persons"); + } + + ... + } + +Here, we get the ODataModelBuilder reference and set the Person entity. +Similarly, you can use EntitySet to add other entities. See the [OData +documentation](http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint) +for more information on the builder. + +### Create Controllers + +Abp.Web.Api.OData NuGet package includes the **AbpODataEntityController** +base class (which extends standard ODataController) to create your +controllers easily. + +Here's an example on how to create an OData endpoint for the Person +entity: + + public class PersonsController : AbpODataEntityController, ITransientDependency + { + public PersonsController(IRepository repository) + : base(repository) + { + } + } + +It's that easy. All the methods of AbpODataEntityController are **virtual**. +That means you can override **Get**, **Post**, **Put**, **Patch**, +**Delete** and other actions and add your own logic. + +### Configuration + +Abp.Web.Api.OData automatically calls +HttpConfiguration.MapODataServiceRoute method with the conventional +configuration. If you need to, you can set the +Configuration.Modules.AbpWebApiOData().**MapAction** to map OData routes +yourself. + +### Examples + +Here are some example requests to the controller defined above. Assume that +the application works on *http://localhost:61842*. We will show you some +basics. Since OData is a standard protocol, you can easily find more +advanced examples on the web. + +#### Getting a List of Entities + +Getting all people. + +##### Request + + GET http://localhost:61842/odata/Persons + +##### Response + + { + "@odata.context":"http://localhost:61842/odata/$metadata#Persons","value":[ + { + "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + },{ + "Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + ] + } + +#### Getting a Single Entity + +Getting the person with Id = 2. + +##### Request + + GET http://localhost:61842/odata/Persons(2) + +##### Response + + { + "@odata.context":"http://localhost:61842/odata/$metadata#Persons/$entity","Name":"John Nash","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + +#### Getting a Single Entity With Navigation Properties + +Getting the person with Id = 1 including his phone numbers. + +##### Request + + GET http://localhost:61842/odata/Persons(1)?$expand=Phones + +##### Response + + { + "@odata.context":"http://localhost:61842/odata/$metadata#Persons/$entity","Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1,"Phones":[ + { + "PersonId":1,"Type":"Mobile","Number":"4242424242","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + },{ + "PersonId":1,"Type":"Mobile","Number":"2424242424","CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":2 + } + ] + } + +#### Querying + +Here's a more advanced query that includes filtering, sorting and getting the top +2 results. + +##### Request + + GET http://localhost:61842/odata/Persons?$filter=Name eq 'Douglas Adams'&$orderby=CreationTime&$top=2 + +##### Response + + { + "@odata.context":"http://localhost:61842/odata/$metadata#Persons","value":[ + { + "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2015-11-07T20:12:39.363+03:00","CreatorUserId":null,"Id":1 + },{ + "Name":"Douglas Adams","IsDeleted":false,"DeleterUserId":null,"DeletionTime":null,"LastModificationTime":null,"LastModifierUserId":null,"CreationTime":"2016-01-12T20:29:03+02:00","CreatorUserId":null,"Id":3 + } + ] + } + +OData supports paging, sorting, filtering, projections and much more. +See the [OData documentation](http://www.odata.org/) for more +information. + +#### Creating a New Entity + +In this example, we're creating a new person. + +##### Request + + POST http://localhost:61842/odata/Persons + + { + Name: "Galileo Galilei" + } + +Here, the "Content-Type" header is "application/json". + +##### Response + + { + "@odata.context": "http://localhost:61842/odata/$metadata#Persons/$entity", + "Name": "Galileo Galilei", + "IsDeleted": false, + "DeleterUserId": null, + "DeletionTime": null, + "LastModificationTime": null, + "LastModifierUserId": null, + "CreationTime": "2016-01-12T20:36:04.1628263+02:00", + "CreatorUserId": null, + "Id": 4 + } + +If we get the list again, we can see the new person. We can also update +or delete an existing entity as OData supports it. + +#### Getting MetaData + +We can get the metadata of entities, as shown in this example. + +##### Request + + GET http://localhost:61842/odata/$metadata + +##### Response + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Metadata is used to investigate the service. + +### Sample Project + +You can see the source code of the sample project here: + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OWIN.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OWIN.md" new file mode 100644 index 0000000..e7446a6 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/OWIN.md" @@ -0,0 +1,32 @@ +If you are using both **ASP.NET MVC** and **ASP.NET Web API** in your +application, you need to add the +[**Abp.Owin**](https://www.nuget.org/packages/Abp.Owin) NuGet package to +your project. + +### Installation + +Add the [**Abp.Owin**](https://www.nuget.org/packages/Abp.Owin) NuGet +package to your host project (generally, to the **Web** project). + + Install-Package Abp.Owin + +### Usage + +Then call the **UseAbp()** extension method in your OWIN **Startup** file as +shown below: + + [assembly: OwinStartup(typeof(Startup))] + public class Startup + { + public void Configuration(IAppBuilder app) + { + app.UseAbp(); + + //your other configuration... + } + } + +If you are only using OWIN (say, in a self hosted Web API project), you +can use the override of UseAbp which takes a startup module to initialize +ABP framework. Note that this should only be done if ABP is not +initialized in another way. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Object-To-Object-Mapping.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Object-To-Object-Mapping.md" new file mode 100644 index 0000000..396c13e --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Object-To-Object-Mapping.md" @@ -0,0 +1,234 @@ +### Introduction + +It's common to map a similar object to another object. It's also +tedious and repetitive since generally both objects (classes) may have the +same/similar properties mapped to each other. Imagine a typical +[application service](Application-Services.md) method below: + + public class UserAppService : ApplicationService + { + private readonly IRepository _userRepository; + + public UserAppService(IRepository userRepository) + { + _userRepository = userRepository; + } + + public void CreateUser(CreateUserInput input) + { + var user = new User + { + Name = input.Name, + Surname = input.Surname, + EmailAddress = input.EmailAddress, + Password = input.Password + }; + + _userRepository.Insert(user); + } + } + +CreateUserInput is a simple [DTO](Data-Transfer-Objects.md) class and User +is a simple [entity](Entities.md). We manually created a User +entity from the given input. The User entity will have more properties in a +real-world application and manually creating it will become tedious and +error-prone. We also have to change the mapping code when we want to add +new properties to User and CreateUserInput. + +We can use a library to automatically handle our mappings. +[AutoMapper](http://automapper.org/) is one of the best libraries for +object to object mapping. ASP.NET Boilerplate defines an **IObjectMapper** +interface to abstract it and then implements this interface using AutoMapper +in the [Abp.AutoMapper](https://www.nuget.org/packages/Abp.AutoMapper) +package. + +### IObjectMapper Interface + +IObjectMapper is a simple abstraction that has Map methods to map an +object to another. We can replace the above code with this, instead: + + public class UserAppService : ApplicationService + { + private readonly IRepository _userRepository; + private readonly IObjectMapper _objectMapper; + + public UserAppService(IRepository userRepository, IObjectMapper objectMapper) + { + _userRepository = userRepository; + _objectMapper = objectMapper; + } + + public void CreateUser(CreateUserInput input) + { + var user = _objectMapper.Map(input); + _userRepository.Insert(user); + } + } + +Map is a simple method that gets the source object and creates a new +destination object with the type declared as the generic parameter (User +in this sample). The Map method has an overload to map an object to an +**existing** object. Assume that we already have a User entity and want +to update it's properties using an object: + + public void UpdateUser(UpdateUserInput input) + { + var user = _userRepository.Get(input.Id); + _objectMapper.Map(input, user); + } + +### AutoMapper Integration + +The **[Abp.AutoMapper](https://www.nuget.org/packages/Abp.AutoMapper)** +NuGet package ([module](Module-System.md)) implements the IObjectMapper +and provides additional features. + +#### Installation + +First, install the **Abp.AutoMapper** NuGet package to your project: + + Install-Package Abp.AutoMapper + +Then add a dependency for **AbpAutoMapperModule** to your module +definition class: + + [DependsOn(typeof(AbpAutoMapperModule))] + public class MyModule : AbpModule + { + ... + } + +You can then safely [inject](Dependency-Injection.md) and use +IObjectMapper in your code. You can also use +[AutoMapper](http://automapper.org/)'s own API when you need it. + +#### Creating Mappings + +Before using the mapping, AutoMapper requires you to define mappings between classes (by default). +You can see AutoMapper's own +[documentation](http://automapper.org/) for details on mapping. ASP.NET +Boilerplate makes it a bit easier and modular. + +##### Auto Mapping Attributes + +Most of the time you may only want to directly (and conventionally) map classes. +In this case, you can use the **AutoMap**, **AutoMapFrom** and **AutoMapTo** +attributes. For instance, if we want to map the **CreateUserInput** class to +the **User** class in the sample above, we can use the **AutoMapTo** attribute +as shown below: + + [AutoMapTo(typeof(User))] + public class CreateUserInput + { + public string Name { get; set; } + + public string Surname { get; set; } + + public string EmailAddress { get; set; } + + public string Password { get; set; } + } + +The AutoMap attribute maps two classes in both directions. But in this +sample, we only need to map from CreateUserInput to User, so we used +AutoMapTo. + +##### Custom Mapping + +Simple mapping may not be suitable in some cases. For instance, property +names of two classes may be a little different or you may want to ignore +some properties during the mappping. In such cases you should directly +use AutoMapper's API to define the mapping. The Abp.AutoMapper package +defines an API to make custom mapping more modular. + +Assume that we want to ignore Password on mapping and the User has a slightly +different named Email property. We can define the mapping as shown below: + + [DependsOn(typeof(AbpAutoMapperModule))] + public class MyModule : AbpModule + { + public override void PreInitialize() + { + Configuration.Modules.AbpAutoMapper().Configurators.Add(config => + { + config.CreateMap() + .ForMember(u => u.Password, options => options.Ignore()) + .ForMember(u => u.Email, options => options.MapFrom(input => input.EmailAddress)); + }); + } + } + +AutoMapper has many more options and abilities for object to object +mapping. See it's [documentation](http://automapper.org/) for more info. + +#### MapTo Extension Methods + +It's recommended that you inject and use the IObjectMapper interface as defined +above. This makes our project independent from AutoMapper as much as +possible. It also makes unit testing easier since we can replace (mock) +the mapping in unit tests. + +The Abp.AutoMapper module also defines MapTo extension methods which can be +used on any object to map it to another object without injecting +IObjectMapper. Example usage: + + public class UserAppService : ApplicationService + { + private readonly IRepository _userRepository; + + public UserAppService(IRepository userRepository) + { + _userRepository = userRepository; + } + + public void CreateUser(CreateUserInput input) + { + var user = input.MapTo(); + _userRepository.Insert(user); + } + + public void UpdateUser(UpdateUserInput input) + { + var user = _userRepository.Get(input.Id); + input.MapTo(user); + } + } + +The MapTo extension methods are defined in the Abp.AutoMapper namespace, so you must +first import this namespaces into your code file. + +Since the MapTo extension methods are static, they use AutoMapper's static +instance (Mapper.Instance). This is simple and fine for the application +code, but you can have problems in unit tests since the static configuration and +mapper is shared among different tests, all effecting each other. + +#### Unit Tests + +We want to isolate tests from each other. To do that, we should design +our project with the following rules: + +1. Always use IObjectMapper, do not use MapTo extension methods. + +2. Configure the Abp.AutoMapper module to use a local Mapper instance +(registered as a singleton via dependency injection) rather than the static +one (Abp.AutoMapper uses the static Mapper.Instance by default to allow you +to use the MapTo extension methods as defined above): + + Configuration.Modules.AbpAutoMapper().UseStaticMapper = false; + +#### Pre Defined Mappings + +##### LocalizableString -> string + +The Abp.AutoMapper module defines a mapping to convert LocalizableString (or +ILocalizableString) objects to string objects. It makes the conversion +using [ILocalizationManager](Localization.md), so localizable +properties are automatically localized during the mapping process of any +class. + +#### Injecting IMapper + +You may need to directly use AutoMapper's IMapper object instead of +IObjectMapper abstraction. In that case, just inject IMapper in your +classes and use it. The Abp.AutoMapper package registers the IMapper with +[dependency injection](Dependency-Injection.md) as a singleton. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Plugin.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Plugin.md" new file mode 100644 index 0000000..9411afc --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Plugin.md" @@ -0,0 +1,138 @@ +### Introduction + +In this tutorial, a plugin example will be developed to learn how to crate a plugin and use it in an application. This plugin that named `DatabaseMaintainer` will remove audit logs in a period. + +### Create a Plugin + +- Create a class library project + +create-plugin-project + +**NOTE:** `Abp` and `Abp.ZeroCore` packages are needed in this plugin. + +plugin-nuget-packages + +- Add a module that is inherited from `AbpModule` + +```c# +using System.Reflection; +using Abp.Modules; +using Abp.Threading.BackgroundWorkers; +using Abp.Zero; + +namespace DatabaseMaintainer +{ + [DependsOn(typeof(AbpZeroCoreModule))] + public class DatabaseMaintainerModule : AbpModule + { + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + + public override void PostInitialize() + { + var workManager = IocManager.Resolve(); + workManager.Add(IocManager.Resolve()); + } + } +} +``` + +- Add a background worker + +````c# +using System; +using Abp.Auditing; +using Abp.Dependency; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; +using Abp.Threading.BackgroundWorkers; +using Abp.Threading.Timers; +using Abp.Timing; + +namespace DatabaseMaintainer +{ + public class DeleteOldAuditLogsWorker : PeriodicBackgroundWorkerBase, ISingletonDependency + { + private readonly IRepository _auditLogRepository; + + public DeleteOldAuditLogsWorker(AbpTimer timer, IRepository auditLogRepository) + : base(timer) + { + _auditLogRepository = auditLogRepository; + Timer.Period = 5000; + } + + [UnitOfWork] + protected override void DoWork() + { + Logger.Info("---------------- DeleteOldAuditLogsWorker is working ----------------"); + + using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var fiveMinutesAgo = Clock.Now.Subtract(TimeSpan.FromMinutes(5)); + + _auditLogRepository.Delete(log => log.ExecutionTime > fiveMinutesAgo); + + CurrentUnitOfWork.SaveChanges(); + } + } + } +} +```` + +Project solution looks like following: + +plugin-solution + +#### Build the Plugin + +Build project in release mode. `DatabaseMaintainer.dll` will be created in folder +`DatabaseMaintainer\DatabaseMaintainer\bin\Release\netcoreapp2.1`. + +### Add Plugin to the Application + +Following example, it will be loaded from `wwwroot` folder. You can change plugins folder location. +First following line should be added to application `Startup.cs` that you want to add to application (MVC or Host). + +`options.PlugInSources.AddFolder(Path.Combine(_hostingEnvironment.WebRootPath, "Plugins"), SearchOption.AllDirectories);` + +Latest Startup.cs + +```c# +public class Startup +{ + private readonly IConfigurationRoot _appConfiguration; + + public Startup(IHostingEnvironment env) + { + _appConfiguration = env.GetAppConfiguration(); + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + ... + + // Configure Abp and Dependency Injection + return services.AddAbp( + // Configure Log4Net logging + options => options.IocManager.IocContainer.AddFacility( + f => f.UseAbpLog4Net().WithConfig("log4net.config") + ); + + options.PlugInSources.AddFolder(Path.Combine(_hostingEnvironment.WebRootPath, "Plugins"), SearchOption.AllDirectories); + ); + } +... +``` + +And copy `DatabaseMaintainer.dll` from plugin to application `.Mvc/wwwroot/Plugins` folder. + +plugin-wwwroot + +### Run the Application + +Run project and see Logs.txt to check if it works. + +plugin-log diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Quartz-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Quartz-Integration.md" new file mode 100644 index 0000000..bb12c2b --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Quartz-Integration.md" @@ -0,0 +1,85 @@ +### Introduction + +[Quartz](http://www.quartz-scheduler.net/) is a full-featured, open-source job scheduling system that can be used from the smallest apps to large-scale enterprise systems. + +The [Abp.Quartz](https://www.nuget.org/packages/Abp.Quartz) package simply integrates Quartz to ASP.NET Boilerplate. + +ASP.NET Boilerplate has a built-in [persistent background job queue and +background worker](Background-Jobs-And-Workers.md) system. Quartz can +be a good alternative if you have advanced scheduling requirements for +your background workers. [Hangfire](Hangfire-Integration.md) can also +be a good alternative for persistent background job queues. + +### Installation + +Install the [**Abp.Quartz**](https://www.nuget.org/packages/Abp.Quartz) +NuGet package to your project and add a **DependsOn** attribute to your +[module](Module-System.md) for **AbpQuartzModule**: + + [DependsOn(typeof (AbpQuartzModule))] + public class YourModule : AbpModule + { + //... + } + +### Creating Jobs + +To create a new job, you can either implement Quartz's IJob interface, +or derive from the JobBase class (defined in the Abp.Quartz package) that has +some helper properties/methods for logging and localization, for +example. A simple job class is shown below: + + public class MyLogJob : JobBase, ITransientDependency + { + public override Task Execute(IJobExecutionContext context) + { + Logger.Info("Executed MyLogJob..!"); + return Task.CompletedTask; + } + } + +We simply implemented the **Execute** method to write a log. You can see +Quartz's [documentation](http://www.quartz-scheduler.net/) for more info. + +### Schedule Jobs + +The **IQuartzScheduleJobManager** is used to schedule jobs. You can inject +it to your class (or you can Resolve and use it in your module's +PostInitialize method) to schedule jobs. An example Controller that +schedules a job: + + public class HomeController : AbpController + { + private readonly IQuartzScheduleJobManager _jobManager; + + public HomeController(IQuartzScheduleJobManager jobManager) + { + _jobManager = jobManager; + } + + public async Task ScheduleJob() + { + await _jobManager.ScheduleAsync( + job => + { + job.WithIdentity("MyLogJobIdentity", "MyGroup") + .WithDescription("A job to simply write logs."); + }, + trigger => + { + trigger.StartNow() + .WithSimpleSchedule(schedule => + { + schedule.RepeatForever() + .WithIntervalInSeconds(5) + .Build(); + }); + }); + + return Content("OK, scheduled!"); + } + } + +### More + +Please see Quartz's [documentation](http://www.quartz-scheduler.net/) for more information. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Readme.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Readme.md" new file mode 100644 index 0000000..22a87c1 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Readme.md" @@ -0,0 +1,138 @@ +# Abp 框架中文文档 + +## 总体介绍 + +- [入门介绍](Introduction.md) + +- [多层次架构体系](NLayer-Architecture.md) +- [模块系统](Module-System.md) +- [启动配置](Startup-Configuration.md) +- [多租户](Multi-Tenancy.md) +- [集成OWIN](OWIN.md) +- [调试](Debugging.md) + + + +## 公共结构 + +- [依赖注入](Dependency-Injection.md) +- [会话管理(Session)](Abp-Session.md) +- [缓存管理](Caching.md) +- [日志管理](Logging.md) +- [设置管理](Setting-Management.md) +- [时间与时区设置](Timing.md) +- [对象之间的映射(AutoMapper集成)](Object-To-Object-Mapping.md) +- [邮件发送(MailKit集成)](Email-Sending.md) + +## 领域层 + +- [实体](Entities.md) + - [Multi-Lingual Entities ](Multi-Lingual-Entities.md) +- [值对象](Value-Objects.md) +- [仓储](Repositories.md) +- [领域服务](Domain-Services.md) +- [规约模式](Specifications.md) +- [工作单元](Unit-Of-Work.md) +- [领域事件 (事件总线)](EventBus-Domain-Events.md) +- [数据过滤器](Data-Filters.md) + + +## 应用层 + +- [应用服务](Application-Services.md) +- [数据传输对象](Data-Transfer-Objects.md) +- [数据传输对象验证](Validating-Data-Transfer-Objects.md) +- [权限验证](Authorization.md) +- [功能管理](Feature-Management.md) +- [审计日志](Audit-Logging.md) +- [实体历史 ](Entity-History.md) + + +## 分布式服务层 + +- ASP.NET Web API + - [Web API 控制器](Web-API-Controllers.md) + - [动态WebApi层](Dynamic-Web-API.md) + - [集成OData](OData-Integration.md) + - [集成Swagger UI](Swagger-UI-Integration.md) + - [ASPNET Core 集成OData](5.5ABP分布式服务-ASPNETCoreOData集成.md) + + +## 表现层 + +- ASP.NET MVC + - [Mvc控制器(MVC-Controllers)](MVC-Controllers.md) + - [MVC视图(MVC Views)](MVC-Views.md) + - [Handling Exceptions](Handling-Exceptions.md) +- ASP.NET Core + - [ASP.NET Core Integration](AspNet-Core.md) + - [ASP.NET Core OData Integration ](OData-AspNetCore-Integration.md) +- [Localization](Localization.md) +- [导航栏](Navigation.md) +- [Embedded Resources](Embedded-Resource-Files.md) +- [Javascript API](/Pages/Documents/Javascript-API) +- [CSRF和XSRF保护](XSRF-CSRF-Protection.md) + +## 后台服务 + +- [后台作业和后台工人](Background-Jobs-And-Workers.md) +- [集成Hangfire](Hangfire-Integration.md) +- [集成Quartz](Quartz-Integration.md) + +## 实时服务 + +- [通知系统](Notification-System.md) +- [集成SignalR](SignalR-Integration.md) +- [集成SignalR AspNet Core ](SignalR-AspNetCore-Integration.md) + + +## 基础设施层(对象关系映射层) + +- [集成EntityFramework](EntityFramework-Integration.md) +- [集成EntityFramework Core](Entity-Framework-Core.md) +- [集成NHibernate](NHibernate-Integration.md) +- [集成Dapper ](Dapper-Integration.md) + +## 发布信息 + +- [Nuget 包](Nuget-Packages.md) +- [发布和变更日志信息](https://github.com/aspnetboilerplate/aspnetboilerplate/releases) + +## 模块 Zero 文档 + +## 总体介绍 + +- [总体介绍](Zero/Overall.md) +- Startup Templates + - [ASP.NET Core & Angular](Zero/Startup-Template-Angular.md) + - [ASP.NET Core MVC & jQuery](Zero/Startup-Template-Core.md) + - [ASP.NET MVC 5.x / AngularJS 1.x](Zero/Startup-Template.md) + +## 功能 + +- [多租户管理](/Pages/Documents/Zero/Tenant-Management) +- [版本管理](/Pages/Documents/Zero/Edition-Management) +- [用户管理](/Pages/Documents/Zero/User-Management) +- [角色管理](/Pages/Documents/Zero/Role-Management) +- [组织单元管理](/Pages/Documents/Zero/Organization-Units) +- [权限管理](/Pages/Documents/Zero/Permission-Management) +- [多语言管理](/Pages/Documents/Zero/Language-Management) +- [集成 Identity Server](Zero/Identity-Server.md) + +# 其他说明 + +本文档发布在 https://www.52ABP.com/wiki/index + +仓库地址:https://github.com/52ABP/Documents + +[![GitHub issues](https://img.shields.io/github/issues/52ABP/Documents.svg?style=popout)](https://github.com/52ABP/Documents/issues) +[![GitHub forks](https://img.shields.io/github/forks/52ABP/Documents.svg?style=popout)](https://github.com/52ABP/Documents/network) +[![GitHub stars](https://img.shields.io/github/stars/52ABP/Documents.svg?style=popout)](https://github.com/52ABP/Documents/stargazers) +[![GitHub license](https://img.shields.io/github/license/52ABP/Documents.svg?style=popout)](https://github.com/52ABP/Documents/blob/master/LICENSE) + +如果你想为文档做贡献,请遵循以下步骤: +* 如果是一个小的变化(输入错误或语法修正,或者添加一些句子),您可以直接进行并发送一个拉请求。 +* 如果这是一个重大的变化(我们建议您创建一个新文档或者在现有的文档中找一个新区域添加内容), +我们建议您创建一个Issue,写出更加详细的描述。 +* 我们将评估您所要求的更改。如果它被接受了,你可以讲它变更为PR,我们会合并掉PR。 +* 请在当前文件中的遵循相同标准。 \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Repositories.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Repositories.md" new file mode 100644 index 0000000..93ceb90 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Repositories.md" @@ -0,0 +1,323 @@ +The repository pattern "*Mediates between the domain and data mapping layers using a +collection-like interface for accessing domain objects*" (Martin +Fowler). + +Repositories, in practice, are used to perform database operations for +domain objects ([Entity](/Pages/Documents/Entities) and Value types). +Generally, a separate repository is used for each Entity (or Aggregate +Root). + +### Default Repositories + +In ASP.NET Boilerplate, repository classes implement the +**IRepository<TEntity, TPrimaryKey>** interface. ABP can +automatically create default repositories for each entity type. You can +directly [inject](/Pages/Documents/Dependency-Injection) +**IRepository<TEntity>** (or IRepository<TEntity, +TPrimaryKey>). An example [application +service](/Pages/Documents/Application-Services) uses a repository to +insert an entity into a database: + + public class PersonAppService : IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + _personRepository.Insert(person); + } + } + +The PersonAppService contructor-injects **IRepository<Person>** and +uses the **Insert** method. + +### Custom Repositories + +You only create a repository class for an entity when you need to create +custom repository methods for that entity. + +#### Custom Repository Interface + +A repository definition for a Person entity is shown below: + + public interface IPersonRepository : IRepository + { + + } + +IPersonRepository extends **IRepository<TEntity>**. It's used to +define entities which have a primary key type of int (Int32). If your +entity's primary key is not an int, you can extend the +**IRepository<TEntity, TPrimaryKey>** interface as shown below: + + public interface IPersonRepository : IRepository + { + + } + +#### Custom Repository Implementation + +ASP.NET Boilerplate is designed to be independent from a particular ORM +(Object/Relational Mapping) framework or another technique to access a +database. Repositories are implemented in **NHibernate** and +**EntityFramework**, out-of-the-box. See the following documents to implement +repositories in ASP.NET Boilerplate on these frameworks: + +- [NHibernate integration](/Pages/Documents/NHibernate-Integration) +- [EntityFramework + integration](/Pages/Documents/EntityFramework-Integration) + +### Base Repository Methods + +Every repository has some common methods coming from the +IRepository<TEntity> interface. We will investigate most of them +here. + +#### Querying + +##### Getting single entity + + TEntity Get(TPrimaryKey id); + Task GetAsync(TPrimaryKey id); + TEntity Single(Expression> predicate); + Task SingleAsync(Expression> predicate); + TEntity FirstOrDefault(TPrimaryKey id); + Task FirstOrDefaultAsync(TPrimaryKey id); + TEntity FirstOrDefault(Expression> predicate); + Task FirstOrDefaultAsync(Expression> predicate); + TEntity Load(TPrimaryKey id); + +The **Get** method is used to get an Entity with a given primary key (Id). It +throws an exception if there is no entity in the database with the given Id. +The **Single** method is similar to Get but takes an expression rather than +an Id. This way, you can write a lambda expression to get an Entity. Example +usages: + + var person = _personRepository.Get(42); + var person = _personRepository.Single(p => p.Name == "John"); + +Note that the **Single** method throws an exception if there is no entity +with the given conditions or where there is more than one entity. + +Instead of throwing an exception, **FirstOrDefault** is similar but returns +**null** if there is no entity with a given Id or expression. It returns +the first found entity if there are more than one entity for the given +conditions. + +**Load** does not retrieve the entity from the database but creates a proxy +object for lazy-loading. If you only use the Id property, the Entity is not +actually retrieved. It's retrieved from the database only if you access +other properties of the entity. This can be used instead of Get, for +performance reasons. It's implemented in **NHibernate**. If the ORM provider +does not implements it, the Load method works identically to the Get method. + +##### Getting a list of entities + + List GetAllList(); + Task> GetAllListAsync(); + List GetAllList(Expression> predicate); + Task> GetAllListAsync(Expression> predicate); + IQueryable GetAll(); + +The **GetAllList** method is used to retrieve all entities from the database. An overload +of it can be used to filter entities. Examples: + + var allPeople = _personRepository.GetAllList(); + var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42); + +The **GetAll** method returns an IQueryable<T>. This way, you can add Linq methods +after it. Examples: + + //Example 1 + var query = from person in _personRepository.GetAll() + where person.IsActive + orderby person.Name + select person; + var people = query.ToList(); + + //Example 2: + List personList2 = _personRepository.GetAll().Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).Skip(40).Take(20).ToList(); + +When using GetAll, almost all queries can be written in Linq. It +can even be used in a join expression! + +#### About IQueryable<T> + +When you call GetAll() out of a repository method, there must be an open +database connection. This is because of the deferred execution of +IQueryable<T>. It does not perform a database query unless you call the +ToList() method or use the IQueryable<T> in a foreach loop (or +somehow access the queried items). So when you call the ToList() method, +the database connection must be alive. For a web application, you don't need to +worry about that in most cases since the MVC controller methods are units of work +by default and the database connection is available for the entire request. See +the **[UnitOfWork](/Pages/Documents/Unit-Of-Work)** documentation to +understand it better. + +##### Custom return value + +There is also an additional method to provide the power of the IQueryable that +can be usable out of a unit of work. + + T Query(Func, T> queryMethod); + +The Query method accepts a lambda (or method) that receives +IQueryable<T> and returns any type of object. Example: + + var people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).ToList()); + +Since the given lamda (or method) is executed inside the repository method, +it's executed when the database connection is available. You can return a +list of entities, a single entity, or a projection or something else that +executes the query. + +#### Insert + +The IRepository interface defines methods to insert an entity to database: + + TEntity Insert(TEntity entity); + Task InsertAsync(TEntity entity); + TPrimaryKey InsertAndGetId(TEntity entity); + Task InsertAndGetIdAsync(TEntity entity); + TEntity InsertOrUpdate(TEntity entity); + Task InsertOrUpdateAsync(TEntity entity); + TPrimaryKey InsertOrUpdateAndGetId(TEntity entity); + Task InsertOrUpdateAndGetIdAsync(TEntity entity); + +The **Insert** method simply inserts new a entity in to a database and returns the +same inserted entity. The **InsertAndGetId** method returns the Id of a newly +inserted entity. This is useful if the Id is auto-increment and you need the Id +of the newly inserted entity. The **InsertOrUpdate** method inserts or updates a given +entity by checking its Id's value. Lastly, the **InsertOrUpdateAndGetId** method +returns the Id of the entity after inserting or updating it. + +#### Update + +The IRepository interface defines methods to update an existing entity in the +database. It takes the entity to be updated and returns the same entity +object. + + TEntity Update(TEntity entity); + Task UpdateAsync(TEntity entity); + +Most of the time you don't need to explicitly call the Update methods since the +unit of work system automatically saves all changes when the unit of work +completes. See the [unit of work](Unit-Of-Work.md) documentation for more info. + +#### Delete + +The IRepository interface defines methods to delete an existing entity from the +database + + void Delete(TEntity entity); + Task DeleteAsync(TEntity entity); + void Delete(TPrimaryKey id); + Task DeleteAsync(TPrimaryKey id); + void Delete(Expression> predicate); + Task DeleteAsync(Expression> predicate); + +The first method accepts an existing entity, the second one accepts an Id of the +entity to delete. The last one accepts a condition to delete all +entities that fit a given condition. Note that all entities matching a given +predicate may be retrieved from the database and then deleted (based on +repository implementation). So use it carefully! It may cause +performance problems if there are too many entities with a given +condition. + +#### Others + +The IRepository also provides methods to get the count of entities in a table. + + int Count(); + Task CountAsync(); + int Count(Expression> predicate); + Task CountAsync(Expression> predicate); + long LongCount(); + Task LongCountAsync(); + long LongCount(Expression> predicate); + Task LongCountAsync(Expression> predicate); + +#### About Async Methods + +ASP.NET Boilerplate supports an async programming model. The repository +methods have async versions. Here's a sample [application +service](/Pages/Documents/Application-Services) method that uses the async +model: + + public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public async Task GetAllPeople() + { + var people = await _personRepository.GetAllListAsync(); + + return new GetPeopleOutput + { + People = Mapper.Map>(people) + }; + } + } + +The GetAllPeople method is async and uses GetAllListAsync with the await +keyword. + +Async may not be supported by all ORM frameworks. It's supported by +EntityFramework. If it's not supported, the Async repository methods work +synchronously. For example, InsertAsync works the same as Insert in +EntityFramework since EF does not write new entities to the database until the +unit of work completes (a.k.a. DbContext.SaveChanges). + +### Managing Database Connections + +A database connection is not opened or closed in a repository method. +Connection management is made automatically by ASP.NET Boilerplate. + +A database connection is **opened** and a **transaction** automatically begins while +entering a repository method. When the method ends and +returns, all changes are **saved**, the transaction is **committed** and +the database connection is **closed** by ASP.NET Boilerplate. +If your repository method throws any type of Exception, the transaction +is automatically **rolled back** and the database connection is closed. This +is true for all public methods of classes that implement the IRepository +interface. + +If a repository method calls another repository method (even a method +of a different repository) they share the same connection and transaction. +The connection is managed (opened/closed) by the first method that enters a +repository. For more information on database connection management, see the +[UnitOfWork](/Pages/Documents/Unit-Of-Work) documentation. + +### Lifetime of a Repository + +All repository instances are **Transient**. This means they are +instantiated per-usage. See the [Dependency +Injection](/Pages/Documents/Dependency-Injection) documentation for more +information. + +### Repository Best Practices + +- For an entity of T, use IRepository<T> wherever it's possible. + Don't create custom repositories unless it's really needed. + The pre-defined repository methods will be enough for most cases. +- If you are creating a custom repository (by extending + IRepository<TEntity>); + - Repository classes should be stateless. That means you must + not define repository-level state objects and a repository + method call should not effect another call. + - Custom repository methods should not contain business logic or + application logic. It should just perform data-related or + orm-specific tasks. + - While repositories can use dependency injection, define fewer or + no dependencies to other services. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Setting-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Setting-Management.md" new file mode 100644 index 0000000..6078b9a --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Setting-Management.md" @@ -0,0 +1,198 @@ +### Introduction + +Every application needs to store some settings and use these settings +somewhere in the application. ASP.NET Boilerplate provides a strong +infrastructure to store/retrieve **application**, **tenant** and +**user** level settings available on both the **server** and **client** sides. + +A setting is a **name-value string** pair that is generally stored in a +database (or another source). We can store non-string values by +converting it to a string. + +#### About ISettingStore + +The **ISettingStore** interface must be implemented in order to use the setting +system. While you can implement it in your own way, it's fully +implemented in the **Module Zero** project. If it's not implemented, +settings are read from the application's **configuration file** (web.config +or app.config) but those settings cannot be changed. Scoping will also not +work. + +### Defining settings + +A setting must be defined before its use. ASP.NET Boilerplate is designed +to be [modular](/Pages/Documents/Module-System), so different modules +can have different settings. A module must create a class derived from +the **SettingProvider** in order to define its settings. An example setting +provider is shown below: + + public class MySettingProvider : SettingProvider + { + public override IEnumerable GetSettingDefinitions(SettingDefinitionProviderContext context) + { + return new[] + { + new SettingDefinition( + "SmtpServerAddress", + "127.0.0.1" + ), + + new SettingDefinition( + "PassiveUsersCanNotLogin", + "true", + scopes: SettingScopes.Application | SettingScopes.Tenant + ), + + new SettingDefinition( + "SiteColorPreference", + "red", + scopes: SettingScopes.User, + clientVisibilityProvider: new VisibleSettingClientVisibilityProvider() + ) + + }; + } + } + +The **GetSettingDefinitions** method returns the **SettingDefinition** +objects. The SettingDefinition class has some parameters in it's +constructor: + +- **Name** (required): A setting must have a system-wide **unique** + name. It's a good idea to define a const string for a setting name + instead of using a magic string. +- **Default value**: A setting may have a default value. This value + can be null or an empty string. +- **Scopes**: A setting should define it's scope (see below). +- **Display name**: A localizable string that can be used to show + setting's name later in the UI. +- **Description**: A localizable string that can be used to show a + setting's description later in the UI. +- **Group**: Can be used to group settings. This is just for the UI, and not + used in setting management. +- **ClientVisibilityProvider**: Can be used to determine if a setting can be used on the client-side or not. +- **isInherited**: Used to set if this setting is inherited by tenant + and users (See setting scope section). +- **customData**: Can be used to set custom data for this setting + definition. + +After creating a setting provider, we must register it in the PreIntialize +method of our module: + + Configuration.Settings.Providers.Add(); + +The setting providers are registered via [dependency +injection](/Pages/Documents/Dependency-Injection) automatically. A +setting provider can inject any dependency (like a repository) to build +the setting definitions using some other source. + +#### Setting scope + + There are three **setting scopes** (or levels) defined in the +**SettingScopes** enum: + +- **Application**: An application scoped setting is used for + user/tenant independent settings. For example, we can define a + setting named "SmtpServerAddress" to get the server's IP address when + sending emails. If this setting has a single value (not changes + based on users), then we can define it as Application scoped. +- **Tenant**: If the application is multi-tenant, we can define + tenant-specific settings. +- **User**: We can use a user-scoped setting to store/get the value of the + setting specific to each user. + +The SettingScopes enum has a **Flags** attribute, so we can define a setting +with **more than one scope**. + +The setting scope is **hierarchic** by default (unless you set +**isInherited** to false). For example, if we define a setting's scope +as "Application | Tenant | User" and try to get **current value** of the +the setting; + +- We get the user-specific value if it's defined (overrided) for the + user. +- If not, we get the tenant-specific value if it's defined (overrided) + for the tenant. +- If not, we get the application value if it's defined. +- If not, we get the **default value**. + +The default value can be **null** or an **empty** string. It's recommended that you +provide default values for settings where it's possible. + +#### Overriding Setting Definitions + +context.Manager can be used to get a setting definition to change its +values. In this way, you can manipulate setting definitions of [dependent +modules](Module-System.md). + +### Getting setting values + +After defining a setting, we can get its current value on both the server +and client. + +#### Server-side + +##### ISettingManager + +The **ISettingManager** is used to perform setting operations. We can inject +and use it anywhere in the application. ISettingManager defines many +methods to get a setting's value. + +The most-used method is **GetSettingValue** (or GetSettingValueAsync for +an async call). It returns the **current value** of the setting based on the +default value, application, tenant and user settings (as described in +Setting scope section before). Examples: + + //Getting a boolean value (async call) + var value1 = await SettingManager.GetSettingValueAsync("PassiveUsersCanNotLogin"); + + //Getting a string value (sync call) + var value2 = SettingManager.GetSettingValue("SmtpServerAddress"); + +GetSettingValue has generic and async versions as shown above. There are +also methods to get a specific tenant or user's setting value or list of +all setting values. + +Since ISettingManager is widely used, some special **base classes** +(like ApplicationService, DomainService and AbpController) have a +property named **SettingManager**. If we derive from these classes, there's no +need to explicitly inject it. + +##### ISettingDefinitionManager + +Also `ISettingDefinitionManager` can be used to get setting definitions that are defined in `AppSettingProvider`. We can inject +and use it anywhere in the application as well. You can get definition name, default value, display name and etc. by using `ISettingDefinitionManager`. + +#### Client-side + +**ClientVisibilityProvider** property of a setting definition determines the visibility of a setting for the client-side. There are four implementations of ISettingClientVisibilityProvider. + +* **VisibleSettingClientVisibilityProvider**: Makes a setting definition visible to the client-side. +* **HiddenSettingClientVisibilityProvider**: Makes a setting definition hidden to the client-side. +* **RequiresAuthenticationSettingClientVisibilityProvider**: Makes a setting definition visible to the client-side if a user is logged in. +* **RequiresPermissionSettingClientVisibilityProvider**: Makes a setting definition visible to the client side if logged in user has a specific permission. + +If a setting is visible to client-side according to ClientVisibilityProvider of a setting definition , then +you can get it's current value on the client-side using JavaScript. +The **abp.setting** namespace defines the needed functions and objects. Example: + + var currentColor = abp.setting.get("SiteColorPreference"); + +There are also the **getInt** and **getBoolean** methods. You can get all the +values using the **abp.setting.values** object. Note that if you change a +setting on the server-side, the client can not know this change unless the page is +refreshed, settings are somehow reloaded, or manually updated by +code. + +### Changing settings + +The ISettingManager defines the **ChangeSettingForApplicationAsync**, +**ChangeSettingForTenantAsync** and **ChangeSettingForUserAsync** +methods (and sync versions) to change settings for the application, for +a tenant and for a user respectively. + +### About caching + +The Setting Manager caches settings on the server-side, so we should not +directly change a setting value using a repository or database update +query. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-AspNetCore-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-AspNetCore-Integration.md" new file mode 100644 index 0000000..14eded2 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-AspNetCore-Integration.md" @@ -0,0 +1,195 @@ +### Introduction + +The [Abp.AspNetCore.SignalR](http://www.nuget.org/packages/Abp.AspNetCore.SignalR) NuGet +package makes it easier to use **ASP.NET Core SignalR** in ASP.NET Boilerplate-based +applications. + +> NOTE: This package is currently in preview. If you have a problem, please write to the GitHub issues: https://github.com/aspnetboilerplate/aspnetboilerplate/issues/new + +### Installation + +#### Server-Side + +Install the +[**Abp.AspNetCore.SignalR**](http://www.nuget.org/packages/Abp.AspNetCore.SignalR) +NuGet package to your project (generally to your Web layer) and add a +**dependency** to your module: + + [DependsOn(typeof(AbpAspNetCoreSignalRModule))] + public class YourProjectWebModule : AbpModule + { + //... + } + + +Then use the **AddSignalR** and **UseSignalR** methods in your Startup class: + + using Abp.AspNetCore.SignalR.Hubs; + + namespace MyProject.Web.Startup + { + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSignalR(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseSignalR(routes => + { + routes.MapHub("/signalr"); + }); + } + } + } + +#### Client-Side (Angular) + +The **@aspnet/signalr** package should be added in package.json, and the signalr.min.js included under **scripts** in angular.json. + +The **abp.signalr-client.js** script should be included under **assets** in angular.json. + +SignalR cannot send authorization headers, so encryptedAuthToken is sent in the query string. The startup template includes SignalRAspNetCoreHelper. We should call it in ngOnInit in app.component.ts: + + SignalRAspNetCoreHelper.initSignalR(); + +That's all you have to do. SignalR is properly configured and integrated into your project. + +#### Client-Side (jQuery) + +The **abp.signalr-client.js** script should be included on the page. It's located +in the +**[Abp.Web.Resources](https://www.nuget.org/packages/Abp.Web.Resources)** +package (and already installed in the [startup templates](/Templates)). We +should include it after the signalr.min.js: + + + + +That's all you have to do. SignalR is properly configured and integrated into your project. + +### Connection Establishment + +ASP.NET Boilerplate **automatically connects** to the server (from the +client) when **abp.signalr-client.js** is included on your page. This is +generally fine. But there may be some cases where you may not want it. You can add +these lines just before including **abp.signalr-client.js** to disable auto +connecting: + + + +In this case, you can call the **abp.signalr.connect()** function manually +whenever you need to connect to the server. + +ASP.NET Boilerplate also **automatically reconnects** to the server +(from the client) when the client disconnects, if +**abp.signalr.autoConnect** is true. + +The **"abp.signalr.connected"** global event is triggered when the client +connects to the server. You can register to this event to take actions +when the connection is successfully established. See the JavaScript [event +bus documentation](/Pages/Documents/Javascript-API/Event-Bus) for more information +about client-side events. + +### Built-In Features + +You can use all the power of SignalR in your applications. Additionally, the +**Abp.AspNetCore.SignalR** package implements some built-in features. + +#### Notification + +The **Abp.AspNetCore.SignalR** package implements the **IRealTimeNotifier** to send +real-time notifications to clients (see the [notification +system](/Pages/Documents/Notification-System)). This way, your users can get +real-time push notifications! + +#### Online Clients + +ASP.NET Boilerplate provides the **IOnlineClientManager** to get information +about online users (inject IOnlineClientManager and use +GetByUserIdOrNull, GetAllClients, IsOnline methods for example). +The IOnlineClientManager needs a communication infrastructure to properly +work. The **Abp.AspNetCore.SignalR** package provides this infrastructure, so you +can inject and use IOnlineClientManager in any layer of your application +if SignalR is installed. + +### Your SignalR Code + +The **Abp.AspNetCore.SignalR** package also simplifies your SignalR code. Consider +that we want to add a Hub to our application: + + public class MyChatHub : Hub, ITransientDependency + { + public IAbpSession AbpSession { get; set; } + + public ILogger Logger { get; set; } + + public MyChatHub() + { + AbpSession = NullAbpSession.Instance; + Logger = NullLogger.Instance; + } + + public async Task SendMessage(string message) + { + await Clients.All.SendAsync("getMessage", string.Format("User {0}: {1}", AbpSession.UserId, message)); + } + + public override async Task OnConnectedAsync() + { + await base.OnConnectedAsync(); + Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + await base.OnDisconnectedAsync(exception); + Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId); + } + } + + + + routes.MapHub("/signalr-myChatHub"); // Prefix with '/signalr' + +We implemented the **ITransientDependency** interface to simply register our hub to the +[dependency injection](/Pages/Documents/Dependency-Injection) system +(you can make it a singleton based on your needs). We +[property-injected](/Pages/Documents/Dependency-Injection#property-injection-pattern) +the [session](/Pages/Documents/Abp-Session) and +[logger](/Pages/Documents/Logging). +Alternatively, we can inherit AbpHubBase. + +**SendMessage** is a method of our hub that can be used by clients. We +call the **getMessage** function of **all** clients in this method. We can +use the [AbpSession](/Pages/Documents/Abp-Session) to get the current user id +(if user logged in) as done above. We also overrode **OnConnectedAsync** and +**OnDisconnectedAsync**, which is just for demonstration purposes and not needed here. + +Here is the **client-side** JavaScript code to send/receive messages using +our hub. + + var chatHub = null; + + abp.signalr.startConnection(abp.appPath + 'signalr-myChatHub', function (connection) { + chatHub = connection; // Save a reference to the hub + + connection.on('getMessage', function (message) { // Register for incoming messages + console.log('received message: ' + message); + }); + }).then(function (connection) { + abp.log.debug('Connected to myChatHub server!'); + abp.event.trigger('myChatHub.connected'); + }); + + abp.event.on('myChatHub.connected', function() { // Register for connect event + chatHub.invoke('sendMessage', "Hi everybody, I'm connected to the chat!"); // Send a message to the server + }); + +We can then use the **chatHub** anytime we need to send messages to the +server. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-Integration.md" new file mode 100644 index 0000000..374717c --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/SignalR-Integration.md" @@ -0,0 +1,187 @@ +### Introduction + +This document is for .NET Framework 4.6.1. If you're interested in ASP.NET +Core, see the [SignalR AspNetCore Integration](SignalR-AspNetCore-Integration.md) documentation instead. + +The [Abp.Web.SignalR](http://www.nuget.org/packages/Abp.Web.SignalR) NuGet +package makes it easy to use **SignalR** in ASP.NET Boilerplate-based +applications. See the [SignalR documentation](http://www.asp.net/signalr) +for more detailed information on SignalR. + +### Installation + +#### Server-Side + +Install the +[**Abp.Web.SignalR**](http://www.nuget.org/packages/Abp.Web.SignalR) +NuGet package to your project (generally to your Web layer) and add a +**dependency** to your module: + + [DependsOn(typeof(AbpWebSignalRModule))] + public class YourProjectWebModule : AbpModule + { + //... + } + + +Then use the **MapSignalR** method in your OWIN startup class as you always +do: + + [assembly: OwinStartup(typeof(Startup))] + namespace MyProject.Web + { + public class Startup + { + public void Configuration(IAppBuilder app) + { + app.MapSignalR(); + + //... + } + } + } + +**Note:** Abp.Web.SignalR only depends on the Microsoft.AspNet.SignalR.Core +package, so you will also need to **install** the +**[Microsoft.AspNet.SignalR](https://www.nuget.org/packages/Microsoft.AspNet.SignalR)** +package to your Web project, if you haven't installed it before (See the [SignalR +documents](http://www.asp.net/signalr) for more info). + +#### Client-Side + +The **abp.signalr.js** script should be included on the page. It's located +in the +**[Abp.Web.Resources](https://www.nuget.org/packages/Abp.Web.Resources)** +package (It's already installed in the [startup templates](/Templates)). We +should include it after signalr hubs: + + + + + +That's all you have to do! SignalR is properly configured and integrated into your +project. + +### Connection Establishment + +ASP.NET Boilerplate **automatically connects** to the server (from the +client) when **abp.signalr.js** is included on your page. This is +generally fine, but there may be cases where you might not want to. You can add +these lines just before including **abp.signalr.js** to disable auto +connecting: + + + +In this case, you can call the **abp.signalr.connect()** function manually +whenever you need to connect to the server. + +ASP.NET Boilerplate also **automatically reconnects** to the server +(from the client) when the client disconnects, if +**abp.signalr.autoConnect** is true. + +The **"abp.signalr.connected"** global event is triggered when the client +connects to the server. You can register to this event to take actions +when the connection is successfully established. See the JavaScript [event +bus documentation](/Pages/Documents/Javascript-API/Event-Bus) for more +information about client-side events. + +### Built-In Features + +You can use the full power of SignalR in your applications. Additionally, +the **Abp.Web.SignalR** package implements some built-in features. + +#### Notification + +The **Abp.Web.SignalR** package implements the **IRealTimeNotifier** to send +real-time notifications to clients (see the [notification +system](/Pages/Documents/Notification-System)). This way, your users can get +real-time push notifications. + +#### Online Clients + +ASP.NET Boilerplate provides the **IOnlineClientManager** to get information +about online users (inject IOnlineClientManager and use the +GetByUserIdOrNull, GetAllClients, and IsOnline methods, for example). +The IOnlineClientManager needs a communication infrastructure to properly +work. The **Abp.Web.SignalR** package provides that infrastructure, so you +can inject and use IOnlineClientManager in any layer of your application, +if SignalR is installed. + +#### PascalCase vs. camelCase + +The Abp.Web.SignalR package overrides SignalR's default **ContractResolver** +to use the **CamelCasePropertyNamesContractResolver** on serialization. +This way, we can have classes with **PascalCase** properties on the server +and use them as **camelCase** on the client for sending/receiving +objects (because camelCase is preferred notation in JavaScript). If you +want to ignore this for your classes in some assemblies, then you can +add those assemblies to the AbpSignalRContractResolver.**IgnoredAssemblies** +list. + +### Your SignalR Code + +The **Abp.Web.SignalR** package also simplifies your SignalR code. Imagine +that we want to add a Hub to our application: + + public class MyChatHub : Hub, ITransientDependency + { + public IAbpSession AbpSession { get; set; } + + public ILogger Logger { get; set; } + + public MyChatHub() + { + AbpSession = NullAbpSession.Instance; + Logger = NullLogger.Instance; + } + + public void SendMessage(string message) + { + Clients.All.getMessage(string.Format("User {0}: {1}", AbpSession.UserId, message)); + } + + public async override Task OnConnected() + { + await base.OnConnected(); + Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId); + } + + public async override Task OnDisconnected(bool stopCalled) + { + await base.OnDisconnected(stopCalled); + Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId); + } + } + +We implemented the **ITransientDependency** to simply register our hub via the +[dependency injection](/Pages/Documents/Dependency-Injection) system +(you can make it singleton based on your needs). We +[property-injected](/Pages/Documents/Dependency-Injection#property-injection-pattern) +the [session](/Pages/Documents/Abp-Session) and +[logger](/Pages/Documents/Logging). + +**SendMessage** is a method of our hub that can be used by clients. We +call the **getMessage** function of **all** clients in this method. We can +use [AbpSession](/Pages/Documents/Abp-Session) to get the current user id +(if user logged in) as done above. We also made an override of **OnConnected** and +**OnDisconnected**, but for demonstration purposes only. + +Here's the **client-side** JavaScript code to send/receive messages using +our hub. + + var chatHub = $.connection.myChatHub; // Get a reference to the hub + + chatHub.client.getMessage = function (message) { // Register for incoming messages + console.log('received message: ' + message); + }; + + abp.event.on('abp.signalr.connected', function() { // Register to connect event + chatHub.server.sendMessage("Hi everybody, I'm connected to the chat!"); // Send a message to the server + }); + +We can then use the **chatHub** anytime we need to send a message to the +server. See the [SignalR documentation](http://www.asp.net/signalr) for +more information. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Specifications.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Specifications.md" new file mode 100644 index 0000000..05a6d24 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Specifications.md" @@ -0,0 +1,235 @@ +### Introduction + +The ***specification pattern*** *is a particular software design pattern, +whereby business rules can be recombined by chaining the business rules +together using boolean logic* +([Wikipedia](https://en.wikipedia.org/wiki/Specification_pattern)). + +In pratical terms, it's used mostly **to define reusable filters** for +entities or other business objects. + +#### Example + +In this section, we will see **the need for the specification pattern**. +This section is generic and not related to ABP's implementation. + +Assume that you have a service method that calculates the total count of +your customers as shown below: + + public class CustomerManager + { + public int GetCustomerCount() + { + //TODO... + return 0; + } + } + +You probably will want to get a customer count with a filter. For example, +you may have **premium customers** (which have a balance of more than +$100,000) or you may want to filter customers just by **registration +year**. Then you can create other methods like +*GetPremiumCustomerCount()*, *GetCustomerCountRegisteredInYear(int +year)*, *GetPremiumCustomerCountRegisteredInYear(int year)* and more. As +you have more criteria, it's not possible to create a combination for +every possibility. + +One solution to this problem is the **specification pattern**. We could +create a single method that gets **a parameter as the filter**: + + public class CustomerManager + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public int GetCustomerCount(ISpecification spec) + { + var customers = _customerRepository.GetAllList(); + + var customerCount = 0; + + foreach (var customer in customers) + { + if (spec.IsSatisfiedBy(customer)) + { + customerCount++; + } + } + + return customerCount; + } + } + +This way, we can get any object as a parameter that implements the +**ISpecification<Customer>** interface which is defined as shown +below: + + public interface ISpecification + { + bool IsSatisfiedBy(T obj); + } + +We can call **IsSatisfiedBy** with a customer to test if this +customer is intended. This way, we can use same GetCustomerCount with +different filters **without changing the method** itself. + +While this solution is pretty fine in theory, it could be improved to +work better in C\#. For instance, it's **not efficient** to get all +customers from a database to check if they satisfy the given +specification/condition. In the next section, we will see ABP's +implementation which overcomes this problem. + +### Creating Specification Classes + +ABP defines the ISpecification interface as shown below: + + public interface ISpecification + { + bool IsSatisfiedBy(T obj); + + Expression> ToExpression(); + } + +Includes a **ToExpression()** method which returns an expression and is used to +better integrate with **IQueryable** and **Expression trees**. This way, we +can easily pass a specification to a repository to apply a filter at the +database level. + +We generally inherit from the **Specification<T> class** instead of +directly implementing the ISpecification<T> interface. A Specification +class automatically implements the IsSatisfiedBy method, so we only need to +define ToExpression. Let's create some specification classes: + + //Customers with $100,000+ balance are assumed as PREMIUM customers. + public class PremiumCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return (customer) => (customer.Balance >= 100000); + } + } + + //A parametric specification example. + public class CustomerRegistrationYearSpecification : Specification + { + public int Year { get; } + + public CustomerRegistrationYearSpecification(int year) + { + Year = year; + } + + public override Expression> ToExpression() + { + return (customer) => (customer.CreationYear == Year); + } + } + +As you can see, we just implemented simple **lambda expressions** to define the +specifications. Let's use these specifications to get the counts of the +customers: + + count = customerManager.GetCustomerCount(new PremiumCustomerSpecification()); + count = customerManager.GetCustomerCount(new CustomerRegistrationYearSpecification(2017)); + +### Using a Specification With a Repository + +We can now **optimize** CustomerManager to **apply the filter in the +database**: + + public class CustomerManager + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public int GetCustomerCount(ISpecification spec) + { + return _customerRepository.Count(spec.ToExpression()); + } + } + +It's that simple. We can pass any specification to the repositories since the +[repositories](Repositories.md) can work with the expressions as filters. +In this example, CustomerManager is unnecessary since we could directly +use the repository with the specification to query the database. But imagine that +we want to execute a business operation on some customers. In that case, +we could use the specifications with a domain service to specify the customers +to work on. + +### Composing Specifications + +One powerful feature of specifications is that they are **composable** +with **And, Or, Not** and **AndNot** extension methods. Example: + + var count = customerManager.GetCustomerCount(new PremiumCustomerSpecification().And(new CustomerRegistrationYearSpecification(2017))); + +We can even create a new specification class from existing +specifications: + + public class NewPremiumCustomersSpecification : AndSpecification + { + public NewPremiumCustomersSpecification() + : base(new PremiumCustomerSpecification(), new CustomerRegistrationYearSpecification(2017)) + { + } + } + +**AndSpecification** is a subclass of the **Specification** class which +satisfies only if both specifications are satisfied. We can then use +NewPremiumCustomersSpecification just like any other specification: + + var count = customerManager.GetCustomerCount(new NewPremiumCustomersSpecification()); + +### Discussion + +While the specification pattern is older than C\# lambda expressions, it's +generally compared to expressions. Some developers may think it's not +needed anymore and we can directly pass expressions to a repository or +to a domain service as shown below: + + var count = _customerRepository.Count(c => c.Balance > 100000 && c.CreationYear == 2017); + +Since ABP's [Repository](Repositories.md) supports expessions, this is a +completely valid use. You don't have to define or use any +specification in your application and you can go with expressions. + +So, what's the point of a specification? Why and when should we consider +using them? + +#### When To Use? + +Some benefits of using specifications: + +- **Reusabe**: Imagine that you need the PremiumCustomer filter in many + places in your code base. If you go with expressions and do not create + a specification, what happens if you later change the "Premium Customer" + definition? Say you want to change the minimum balance from $100,000 to + $250,000 and add another condition to be a customer older than 3. + If you used a specification, you just change a single class. If + you repeated (copy/pasted) the same expression, you need to change all of + them. +- **Composable**: You can combine multiple specifications to create new + specifications. This is another type of reusability. +- **Named**: PremiumCustomerSpecification better explains the intent + rather than a complex expression. So, if you have an expression that + is meaningful in your business, consider using specifications. +- **Testable**: A specification is a separately (and easily) testable + object. + +#### When To Not Use? + +- **Non business expressions**: Do not use + specifications for non business-related expressions and operations. +- **Reporting**: If you are just creating a report do not create + specifications, but directly use IQueryable. You can even + use plain SQL, Views or another tool for reporting. DDD does not necessarily care about + reporting, so the way you query the underlying data store can be important + from a performance perspective. \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Startup-Configuration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Startup-Configuration.md" new file mode 100644 index 0000000..a582599 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Startup-Configuration.md" @@ -0,0 +1,148 @@ +ASP.NET Boilerplate provides the infrastructure and a model to configure +its [modules](/Pages/Documents/Module-System) on startup. + +### Configuring ASP.NET Boilerplate + +Configuring ASP.NET Boilerplate is made on the **PreInitialize** method of +your module. Example configuration: + + public class SimpleTaskSystemModule : AbpModule + { + public override void PreInitialize() + { + //Add languages for your application + Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true)); + Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr")); + + //Add a localization source + Configuration.Localization.Sources.Add( + new XmlLocalizationSource( + "SimpleTaskSystem", + HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem") + ) + ); + + //Configure navigation/menu + Configuration.Navigation.Providers.Add(); + } + + public override void Initialize() + { + IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); + } + } + +ASP.NET Boilerplate is designed with **[modularity](Module-System.md)** in +mind. Different modules can configure ASP.NET Boilerplate. For example, +different modules can add navigation providers to add their own menu +items to the main menu. (See the +[localization](/Pages/Documents/Localization) and +[navigation](/Pages/Documents/Navigation) documents for details on +configuring them). + +#### Replacing Built-In Services + +The **Configuration.ReplaceService** method can be used to **override** a +built-in service. For example, you can replace IAbpSession service with +your custom implementation as shown below: + + Configuration.ReplaceService(DependencyLifeStyle.Transient); + +The ReplaceService method has an overload to pass an **action** to make a +replacement in a custom way (you can directly use Castle Windsor with its +advanced registration API). + +The same service can be replaced multiple times, especially in different +modules. The last one replaced will be the one that is valid. The module PreInitialize +methods are executed by the [dependency order](Module-System.md). + +### Configuring Modules + +Besides the framework's own startup configuration, a module can extend +the **IAbpModuleConfigurations** interface to provide configuration points +for the module. Example: + + ... + using Abp.Web.Configuration; + ... + public override void PreInitialize() + { + Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true; + } + ... + +In this example, we configured the AbpWebCommon module to send all +exceptions to clients. + +Not every module should define this type of configuration. It's +generally needed when a module will be re-usable in different +applications and needs to be configured on startup. + +### Creating Configuration For a Module + +Assume that we have a module named MyModule and it has some +configuration properties. First, we create a class for these configurable +properties: + + public class MyModuleConfig + { + public bool SampleConfig1 { get; set; } + + public string SampleConfig2 { get; set; } + } + +We then register this class via [Dependency +Injection](Dependency-Injection.md) on the **PreInitialize** method of +MyModule (Thus, it will be injectable): + + IocManager.Register(); + +It should be registered as a **Singleton** like in this example. We can now +use the following code to configure MyModule in our module's +PreInitialize method: + + Configuration.Get().SampleConfig1 = false; + +While we can use the IAbpStartupConfiguration.Get method as shown below, we +can create an extension method to the IModuleConfigurations like this: + + public static class MyModuleConfigurationExtensions + { + public static MyModuleConfig MyModule(this IModuleConfigurations moduleConfigurations) + { + return moduleConfigurations.AbpConfiguration.Get(); + } + } + +Now other modules can configure this module using the extension method: + + Configuration.Modules.MyModule().SampleConfig1 = false; + Configuration.Modules.MyModule().SampleConfig2 = "test"; + +This makes it easy to investigate module configurations and collect them in +a single place (Configuration.Modules...). ABP itself defines extension +methods for it's own module configurations. + +At some point, MyModule needs this configuration. You can inject +MyModuleConfig and use the configured values. Example: + + public class MyService : ITransientDependency + { + private readonly MyModuleConfig _configuration; + + public MyService(MyModuleConfig configuration) + { + _configuration = configuration; + } + + public void DoIt() + { + if (_configuration.SampleConfig2 == "test") + { + //... + } + } + } + +This way, modules can create central configuration points in the ASP.NET +Boilerplate system. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Swagger-UI-Integration.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Swagger-UI-Integration.md" new file mode 100644 index 0000000..f2dee84 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Swagger-UI-Integration.md" @@ -0,0 +1,138 @@ +### Introduction + +From it's web site: "....with a Swagger-enabled API, you get +**interactive documentation**, client SDK generation and +discoverability." + +### ASP.NET Core + +#### Install NuGet Package + +Install the +**[Swashbuckle.AspNetCore](https://www.nuget.org/packages/Swashbuckle.AspNetCore/)** +NuGet package to your **Web** project. + +#### Configure + +Add the following configuration code for Swagger into the **ConfigureServices** method of +your **Startup.cs** + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + //your other code... + + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new Info { Title = "AbpZeroTemplate API", Version = "v1" }); + options.DocInclusionPredicate((docName, description) => true); + }); + + //your other code... + } + +Then, to use swagger, add this code into the **Configure** method of **Startup.cs** + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + //your other code... + + app.UseSwagger(); + //Enable middleware to serve swagger - ui assets(HTML, JS, CSS etc.) + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "AbpZeroTemplate API V1"); + }); //URL: /swagger + + //your other code... + } + +#### Test + +That's it! You can now browse the swagger ui under "**/swagger**". + +### ASP.NET 5.x + +#### Install NuGet Package + +Install the +**[Swashbuckle.Core](https://www.nuget.org/packages/Swashbuckle.Core/)** +NuGet package to your **WebApi** project (or Web project). + +#### Configure + +Add the configuration code for Swagger into the +[Initialize](/Pages/Documents/Module-System) method of your module. +Example: + + public class SwaggerIntegrationDemoWebApiModule : AbpModule + { + public override void Initialize() + { + //your other code... + + ConfigureSwaggerUi(); + } + + private void ConfigureSwaggerUi() + { + Configuration.Modules.AbpWebApi().HttpConfiguration + .EnableSwagger(c => + { + c.SingleApiVersion("v1", "SwaggerIntegrationDemo.WebApi"); + c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); + }) + .EnableSwaggerUi(c => + { + c.InjectJavaScript(Assembly.GetAssembly(typeof(AbpProjectNameWebApiModule)), "AbpCompanyName.AbpProjectName.Api.Scripts.Swagger-Custom.js"); + }); + } + } + +Note that we inject a JavaScript file named "**Swagger-Custom.js**" +while configuring the swagger ui. This script file is used to add a CSRF token +to requests while testing api services in the ui. You may also need to +add this file to your WebApi project as an **embedded resource** and use +it's Logical Name in the InjectJavaScript method while injecting it. + +**IMPORTANT**: The code above will be slightly different for your +project (Namespace will not be AbpCompanyName.AbpProjectName... and +AbpProjectNameWebApiModule will be *YourProjectName*WebApiModule). + +Here's the content of the **Swagger-Custom.js**: + + var getCookieValue = function(key) { + var equalities = document.cookie.split('; '); + for (var i = 0; i < equalities.length; i++) { + if (!equalities[i]) { + continue; + } + + var splitted = equalities[i].split('='); + if (splitted.length !== 2) { + continue; + } + + if (decodeURIComponent(splitted[0]) === key) { + return decodeURIComponent(splitted[1] || ''); + } + } + + return null; + }; + + var csrfCookie = getCookieValue("XSRF-TOKEN"); + var csrfCookieAuth = new SwaggerClient.ApiKeyAuthorization("X-XSRF-TOKEN", csrfCookie, "header"); + swaggerUi.api.clientAuthorizations.add("X-XSRF-TOKEN", csrfCookieAuth); + +See the Swashbuckle +[documentation](https://github.com/domaindrivendev/Swashbuckle) for more +configuration options. + +#### Test + +That's it! Let's browse to **/swagger/ui/index**: + +Swagger UI + +You can see all the Web API Controllers (and also the [dynamic web +api](/Pages/Documents/Dynamic-Web-API) controllers) and test them. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Timing.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Timing.md" new file mode 100644 index 0000000..cf4ea3d --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Timing.md" @@ -0,0 +1,85 @@ +### Introduction + +While some applications target a single timezone, others target +many different timezones. To satisfy such needs and centralize datetime +operations, ABP provides a common infrastructure for datetime operations. + +### Clock + +**Clock** is the main class used to work with **DateTime** values. It +defines the following **static** properties and methods: + +- **Now**: Gets the current time according to the current provider. +- **Kind**: Gets the DateTimeKind of the current provider. +- **SupportsMultipleTimezone**: Gets a value that indicates whether or not a current + provider can be used for applications that need multiple timezones. +- **Normalize**: Normalizes/converts a given DateTime using the current + provider. + +So, instead of using *DateTime.Now*, we use **Clock.Now**, which +abstracts it: + + DateTime now = Clock.Now; + +Clock uses the clock providers inside it. There are three types of +**built-in clock providers**: + +- **ClockProviders.Unspecified** (UnspecifiedClockProvider): This is + the **default** clock provider and behaves just like + **DateTime.Now**. It acts as if you don't use the Clock class at all. +- **ClockProviders.Utc** (UtcClockProvider): Works in UTC datetime. + **DateTime.UtcNow** for Clock.Now. The Normalize method converts a given + datetime to a utc datetime and then sets it's kind to DateTimeKind.UTC. It + **supports multiple timezones**. +- **ClockProviders.Local** (LocalClockProvider): Works in the local + computer's time. The Normalize method converts a given datetime to a local + datetime and sets it's kind to DateTimeKind.Local. + +You can set Clock.Provider in order to use a different clock provider: + + Clock.Provider = ClockProviders.Utc; + +This is generally done at the beginning of an application (do +it in the Application\_Start of a web application). + +#### Client-Side + +The clock can be used on the client-side using the **abp.clock** object in +JavaScript. When you set Clock.Provider on the server-side, ABP +automatically sets the value of **abp.clock.provider** on the client-side. + +### Time Zones + +ABP defines a [setting](Setting-Management.md) named +**Abp.Timing.TimeZone** (*TimingSettingNames.TimeZone* constant) for +storing the selected timezone of the host, tenant and user. ABP assumes that +the value of a timezone setting is a valid **Windows timezone name**. It also +defines a timezone mapping file to convert a Windows Timezone to an +**IANA** timezone since some common libraries are using the IANA +timezone id. **UtcClockProvider** must be used in order to support +**multiple timezones**. Because if UtcClockProvider is used, all +datetime values will be stored in UTC and all datetimes will be sent to +clients in UTC format. Then on the client-side we can convert a UTC +datetime to the user's timezone by using the user's current timezone setting.  + +#### Client-Side + +ABP creates a JavaScript object named **abp.timing.timeZoneInfo** which +contains timezone information for the current user. This information +contains Windows and IANA timezone ids and some extra information for +windows timezone info. This information can be used to make client-side +datetime convertions by showing a datetime to the user in his/her timezone. + +### Binders and Converters + +- ABP automatically normalizes DateTimes received from **clients** in + MVC, Web API and ASP.NET Core applications, based on the current + clock provider. +- ABP automatically normalizes DateTimes received from the **database** + based on the current clock provider, when + [EntityFramework](EntityFramework-Integration.md) or + [NHibernate](NHibernate-Integration.md) modules are used. + +If the UTC clock provider is used, then all DateTimes stored in the database are +assumed as UTC values, and all DateTimes received from clients are assumed +as UTC values unless explicitly specified. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/UI-Alerts.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/UI-Alerts.md" new file mode 100644 index 0000000..9080c68 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/UI-Alerts.md" @@ -0,0 +1,38 @@ +### Introduction + +Alerts is a common way to show messages to the user after a web request. Examples: + +UI Alerts + +ASP.NET Boilerplate provides a simple alert infrastructure for **MVC** applications (both for ASP.NET Core MVC & ASP.NET MVC 5.x). + +> UI Alert system is designed for the actions those return **views**. An action returns a JSON/object result can not use the UI alert system (because it's something related to UI rather than APIs). + +### Add Alerts + +`IAlertManager` can be injected and used to add alerts into its `Alerts` collection. `AbpControllerBase` and `AbpPageModel` base classes already injects it and have a shortcut to use the `Alerts` collection. + +> All alert messages added in the same web request are added in the same collection even if they are added by different classes/controllers/services. + +Example MVC Controller action (that produces the output shown in the figure above): + +```c# +public class AlertsTestController : AbpControllerBase +{ + public IActionResult Index() + { + Alerts.Danger("Danger alert message!", "Test Alert"); + Alerts.Warning("Warning alert message!", "Test Alert"); + Alerts.Info("Info alert message!", "Test Alert"); + Alerts.Success("Success alert message!", "Test Alert"); + + return View(); + } +} +``` + +### Show Alerts + +MVC based [startup templates](https://aspnetboilerplate.com/Templates) already shows alerts on the page by default. So, nothing to do if you are using one of the startup templates. + +If you want to access to the alerts added by the current request, you can always inject the `IAlertManager` and use its `Alerts` collection. \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Unit-Of-Work.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Unit-Of-Work.md" new file mode 100644 index 0000000..3676b41 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Unit-Of-Work.md" @@ -0,0 +1,384 @@ +### Introduction + +Connection and transaction management is one of the most important +concepts in an application that uses a database. You need to know when to open a +connection, when to start a transaction, and how to dispose the connection, +and so on... ASP.NET Boilerplate manages database connections and +transactions by using its **unit of work** system. + +### Connection & Transaction Management in ASP.NET Boilerplate + +ASP.NET Boilerplate **opens** a database connection (it may not be +opened immediately, but opened during the first database usage, based on the ORM +provider implementation) and begins a **transaction** when **entering** +a **unit of work method**. You can use the connection safely in this +method. At the end of the method, the transaction is **committed** and +the connection is **disposed**. If the method throws an **exception**, the +transaction is **rolled back**, and the connection is disposed. In this +way, a unit of work method is **atomic** (a **unit of work**). ASP.NET +Boilerplate does all of this automatically. + +If a unit of work method calls another unit of work method, both use the +same connection & transaction. The first entered method manages the +connection & transaction and then the others reuse it. + +#### Conventional Unit Of Work Methods + +Some methods are unit of work methods by default: + +- All [MVC](MVC-Controllers.md), [Web API](Web-API-Controllers.md) + and [ASP.NET Core MVC](AspNet-Core.md) Controller actions. +- All [Application Service](Application-Services.md) methods. +- All [Repository](Repositories.md) methods. + +Assume that we have an [application +service](/Pages/Documents/Application-Services) method like the one below: + + public class PersonAppService : IPersonAppService + { + private readonly IPersonRepository _personRepository; + private readonly IStatisticsRepository _statisticsRepository; + + public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository) + { + _personRepository = personRepository; + _statisticsRepository = statisticsRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + _personRepository.Insert(person); + _statisticsRepository.IncrementPeopleCount(); + } + } + +In the CreatePerson method, we're inserting a person using the person +repository and incrementing the total people count using the statistics +repository. Both of these repositories **share** the same connection and +transaction, since the application service method is a unit of +work by default. ASP.NET Boilerplate opens a database connection and +starts a transaction when entering the CreatePerson method, and if no exception is thrown, it +commits the transaction at the end of it. If an exception +is thrown, it rolls everything back. In this way, all the database operations in +the CreatePerson method become **atomic** (a **unit of work**).  + +In addition to the default, conventional unit of work classes, you can add +your own convention in the PreInitialize method of your +[module](Module-System.md) like below: + + Configuration.UnitOfWork.ConventionalUowSelectors.Add(type => ...); + +You should check the type and return true if the type must be a +conventional unit of work class. + +#### Controlling the Unit Of Work + +The unit of work **implicitly** works for the methods defined above. In most +cases you don't have to control the unit of work manually for web +applications. You can **explicitly** use it if you want to control the unit +of work somewhere else. There are two approaches for it.  + +##### UnitOfWork Attribute + +The first and preferred approach is to use the **UnitOfWork** attribute. Example: + + [UnitOfWork] + public void CreatePerson(CreatePersonInput input) + { + var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + _personRepository.Insert(person); + _statisticsRepository.IncrementPeopleCount(); + } + +This way, the CreatePerson method becomes a unit of work and manages the database +connection and transaction. Both repositories use the same unit of work. +Note that you do not need the UnitOfWork attribute if this is an application +service method. See the '[unit of work method +restrictions](#unitofwork-attribute-restrictions)' section. + +There are options for the UnitOfWork attribute. See the 'unit of work in +detail' section. The UnitOfWork attribute can also be used on classes to +configure all the methods of them. The method attribute overrides the class +attribute if it exists. + +##### IUnitOfWorkManager + +The second approach is to use the **IUnitOfWorkManager.Begin(...)** method +as shown below: + + public class MyService + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IPersonRepository _personRepository; + private readonly IStatisticsRepository _statisticsRepository; + + public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository) + { + _unitOfWorkManager = unitOfWorkManager; + _personRepository = personRepository; + _statisticsRepository = statisticsRepository; + } + + public void CreatePerson(CreatePersonInput input) + { + var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + + using (var unitOfWork = _unitOfWorkManager.Begin()) + { + _personRepository.Insert(person); + _statisticsRepository.IncrementPeopleCount(); + + unitOfWork.Complete(); + } + } + } + +You can inject and use IUnitOfWorkManager as shown here (Some base +classes already have **UnitOfWorkManager** injected by default: MVC +Controllers, [application services](Application-Services.md), [domain +services](Domain-Services.md)...). This way, you can create a **limited +scope** unit of work. Using this approach, you must call the **Complete** +method manually. If you don't call it, the transaction is rolled back and +the changes are not saved. + +The **Begin** method has overloads that set the **unit of work options**. It's +simpler (and recommended) to use the **UnitOfWork** attribute if you don't otherwise +have a good reason. + +### Unit Of Work in Detail + +#### Disabling Unit Of Work + +You may want to disable the unit of work **for the conventional unit of work +methods** . To do that, use the UnitOfWorkAttribute's **IsDisabled** +property. Example usage: + + [UnitOfWork(IsDisabled = true)] + public virtual void RemoveFriendship(RemoveFriendshipInput input) + { + _friendshipRepository.Delete(input.Id); + } + + +Normally, you don't want to do this, but in some situations you may want +to disable the unit of work: + +- You may want to use the unit of work in a limited scope with + the UnitOfWorkScope class as described above. + +Note that if a unit of work method calls this RemoveFriendship method, +disabling this method is ignored, and it will use the same unit of work with +the caller method. So, disable carefully! The code above +works well since the repository methods are a unit of work by default. + +#### Non-Transactional Unit Of Work + +By its nature, a unit of work is transactional. ASP.NET Boilerplate starts, +commits or rolls back an explicit database-level transaction. +In some special cases, the transaction may cause problems since +it may lock some rows or tables in the database. In these situations, you +may want to disable the database-level transaction. The UnitOfWork attribute can +get a boolean value in its constructor to work as non-transactional. +Example usage: + + [UnitOfWork(isTransactional: false)] + public GetTasksOutput GetTasks(GetTasksInput input) + { + var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); + return new GetTasksOutput + { + Tasks = Mapper.Map>(tasks) + }; + } + +We recommend you use this attribute as **\[UnitOfWork(isTransactional: +false)\]**. It's more readable and explicit, but you could also use +\[UnitOfWork(false)\]. + +Note that ORM frameworks like NHibernate and EntityFramework +internally save changes in a single command. Assume that you updated a +few Entities in a non-transactional UOW. Even in this situation all the +updates are performed at end of the unit of work with a single database +command. If you execute an SQL query directly, it's performed +immediately and not rolled back if your UOW is not transactional. + +There is a restriction for non-transactional UOWs. If you're already in +a transactional unit of work scope, setting isTransactional to false is +ignored (use the Transaction Scope Option to create a non-transactional unit +of work in a transactional unit of work). + +Use a non-transactional unit of work carefully since most of the time things +should be transactional to ensure data integrity. If your method just reads +data and does not change it, it can be safely non-transactional. + +#### A Unit Of Work Method Calls Another + +The unit of work is ambient. If a unit of work method calls another unit of +work method, they share the same connection and transaction. The first method +manages the connection and then the other methods reuse it. + +#### Unit Of Work Scope + +You can create a different and isolated transaction in another +transaction or you can create a non-transactional scope in a transaction. +.NET defines +[TransactionScopeOption](https://msdn.microsoft.com/en-us/library/system.transactions.transactionscopeoption(v=vs.110).aspx) +for that. You can set the Scope option of the unit of work to control it. + +#### Automatically Saving Changes + +If a method is a unit of work, ASP.NET Boilerplate automatically saves all the changes at +the end of the method. Assume that we need a method to +update the name of a person: + + [UnitOfWork] + public void UpdateName(UpdateNameInput input) + { + var person = _personRepository.Get(input.PersonId); + person.Name = input.NewName; + } + +That's all you have to do! The name was updated. We didn't even have to call +the \_personRepository.Update method. The ORM framework keeps track of all +the changes of entities in a unit of work and reflects these changes to the +database. + +Note that we do not need to declare the UnitOfWork attribute for conventional unit of work +methods. + +#### IRepository.GetAll() Method + +When you call the GetAll() method of a repository, there must be an open +database connection since it returns IQueryable. This is needed because +of the deferred execution of IQueryable. It does not perform the database query +unless you call the ToList() method or use IQueryable in a foreach loop, +or somehow access the queried items. So when you call the ToList() method, +the database connection has to be alive. + +Consider the example below: + + [UnitOfWork] + public SearchPeopleOutput SearchPeople(SearchPeopleInput input) + { + // get IQueryable + var query = _personRepository.GetAll(); + + // add some filters if selected + if (!string.IsNullOrEmpty(input.SearchedName)) + { + query = query.Where(person => person.Name.StartsWith(input.SearchedName)); + } + + if (input.IsActive.HasValue) + { + query = query.Where(person => person.IsActive == input.IsActive.Value); + } + + // get paged result list + var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList(); + + return new SearchPeopleOutput { People = Mapper.Map>(people) }; + } + +Here the SearchPeople method is a unit of work since the ToList() method of +IQueryable is called in the method body. The database connection must also be +open when IQueryable.ToList() is executed. + +In most cases you will use the GetAll method safely in a web application, +since all the controller actions are a unit of work by default. This way, the +database connection is available during the entire request. + +#### UnitOfWork Attribute Restrictions + +You can use the UnitOfWork attribute for: + +- All **public** or **public virtual** methods for classes that are + used over an interface (Like an application service used over a service + interface). +- All **public virtual** methods for self-injected classes (Like **MVC + Controllers** and **Web API Controllers**). +- All **protected virtual** methods. + +We recommended you always make the methods **virtual**. You can **not use +the attribute for private methods** because ASP.NET Boilerplate uses dynamic +proxying for that, and because private methods can not be seen from derived +classes. The UnitOfWork attribute (and any proxying) does not work if you +don't use [dependency injection](/Pages/Documents/Dependency-Injection) +and instantiate the class yourself. + +### Options + +There are some options that can be used to change the behavior of a unit of +work. + +First, we can change the default values of all the unit of works in the [startup +configuration](/Pages/Documents/Startup-Configuration). This is +generally done in the PreInitialize method of our +[module](/Pages/Documents/Module-System). + + public class SimpleTaskSystemCoreModule : AbpModule + { + public override void PreInitialize() + { + Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted; + Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30); + } + + //...other module methods + } + +As a second option, we can override the defaults for a particular unit of work. The **UnitOfWork** attribute +constructor and IUnitOfWorkManager.**Begin** method have overloads to set these options. + +As a final option, you can use the startup configuration to configure the default unit +of work attributes for the ASP.NET MVC, Web API and ASP.NET Core MVC +Controllers (see their documentation for more info). + +### Methods + +The UnitOfWork system works seamlessly and invisibly, but in some special +cases you may need to call its methods. + +You can access the current unit of work in one of two ways: + +- You can directly use the **CurrentUnitOfWork** property if your class is + derived from some specific base classes (ApplicationService, + DomainService, AbpController, AbpApiController... etc.) +- You can inject IUnitOfWorkManager into any class and use the + **IUnitOfWorkManager.Current** property. + +#### SaveChanges + +ASP.NET Boilerplate saves all changes at the end of a unit of work. You +don't have to do anything, but sometimes you may want to save changes +to the database in the middle of a unit of work operation. For example, +after saving some changes, we may want to get the Id of a newly inserted +[Entity](/Pages/Documents/Entities) using +[EntityFramework](/Pages/Documents/EntityFramework-Integration). + +You can use the **SaveChanges** or **SaveChangesAsync** method of the current +unit of work. + +Note that if the current unit of work is transactional, all changes in the +transaction are rolled back if an exception occurs. Even the saved changes! + +### Events + +A unit of work has the **Completed**, **Failed** and **Disposed** events. +You can register to these events and perform any operations you need. For +example, you may want to run some code when the current unit of work +successfully completes. Example: + + public void CreateTask(CreateTaskInput input) + { + var task = new Task { Description = input.Description }; + + if (input.AssignedPersonId.HasValue) + { + task.AssignedPersonId = input.AssignedPersonId.Value; + _unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ }; + } + + _taskRepository.Insert(task); + } diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Validating-Data-Transfer-Objects.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Validating-Data-Transfer-Objects.md" new file mode 100644 index 0000000..f286678 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Validating-Data-Transfer-Objects.md" @@ -0,0 +1,198 @@ +### Introduction to validation + +In an application, the inputs should be validated first. The input can be +sent by a user or another application. In a web application, validation is +usually implemented twice: on the client and server sides. Client-side +validation is implemented mostly for user experience. It's better to +check a form first in the client and show invalid fields to the user. +However, server-side validation is unavoidable and more critical. + +Server-side validation is generally implemented in [application +services](/Pages/Documents/Application-Services) or controllers (in +general, all services get data from the presentation layer). An application +service method should first check (validate) the input and then use it. +ASP.NET Boilerplate provides the infrastructure to automatically +validate inputs of an application for: + +- All [application service](Application-Services.md) methods +- All [ASP.NET Core](AspNet-Core.md) MVC controller actions +- All ASP.NET [MVC](MVC-Controllers.md) and [Web + API](Web-API-Controllers.md) controller actions. + +See the Disabling Validation section to disable validation if needed. + +### Using data annotations + +ASP.NET Boilerplate supports data annotation attributes. Assume that +we're developing a Task application service that is used to create a +task by when it gets an input as shown below: + + public class CreateTaskInput + { + public int? AssignedPersonId { get; set; } + + [Required] + public string Description { get; set; } + } + +Here, the **Description** property is marked as **Required**. +AssignedPersonId is optional. There are also many attributes (like +MaxLength, MinLength, RegularExpression...) in the +**System.ComponentModel.DataAnnotations** namespace. See the Task +[application service](/Pages/Documents/Application-Services) +implementation: + + public class TaskAppService : ITaskAppService + { + private readonly ITaskRepository _taskRepository; + private readonly IPersonRepository _personRepository; + + public TaskAppService(ITaskRepository taskRepository, IPersonRepository personRepository) + { + _taskRepository = taskRepository; + _personRepository = personRepository; + } + + public void CreateTask(CreateTaskInput input) + { + var task = new Task { Description = input.Description }; + + if (input.AssignedPersonId.HasValue) + { + task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); + } + + _taskRepository.Insert(task); + } + } + +As you can see, there is no validation code written since ASP.NET Boilerplate does +it automatically. ASP.NET Boilerplate also checks if input is **null** +and throws an **AbpValidationException** if it is, so you don't have to write +**null-check** code (guard clauses). It also throws an +AbpValidationException if any of the input properties are invalid. + +This mechanism is similar to ASP.NET MVC's validation but note that an +application service class is not derived from a Controller, it's a plain +class and can work even outside of a web application. + +### Custom Validation + +If data annotations are not sufficient for your case, you can implement +the **ICustomValidate** interface as shown below: + + public class CreateTaskInput : ICustomValidate + { + public int? AssignedPersonId { get; set; } + + public bool SendEmailToAssignedPerson { get; set; } + + [Required] + public string Description { get; set; } + + public void AddValidationErrors(CustomValidationContext context) + { + if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0)) + { + context.Results.Add(new ValidationResult("AssignedPersonId must be set if SendEmailToAssignedPerson is true!")); + } + } + } + +The ICustomValidate interface declares the **AddValidationErrors** method to be +implemented. We must add the **ValidationResult** objects to the +**context.Results** list if there are validation errors. You can also +use the context.IocResolver to [resolve +dependencies](Dependency-Injection.md) if needed in the validation +process.  + +In addition to ICustomValidate, ABP also supports .NET's standard +IValidatableObject interface. You can also implement it to perform +additional custom validations. If you implement both interfaces, both of +them will be called. + +### Fluent Validation + +In order to use [FluentValidation](https://github.com/JeremySkinner/FluentValidation), you need to install [Abp.FluentValidation](https://www.nuget.org/packages/Abp.FluentValidation) package first. + +``` +Install-Package Abp.FluentValidation +``` + +Then, You should set a dependency to AbpFluentValidationModule from your module. Example: + +``` +[DependsOn(typeof(AbpFluentValidationModule))] +public class MyProjectAppModule : AbpModule +{ + +} +``` + +After all, you can define your [FluentValidation](https://github.com/JeremySkinner/FluentValidation) validators to validate matching input classes. + +As an example, if you have an input class and a Controller which uses this class as it's input parameter; + +``` +public class MyCustomArgument1 +{ + public int Value { get; set; } +} + +public class MyTestController : AbpController { + + public JsonResult GetJsonValue([FromQuery] MyCustomArgument1 arg1) + { + return Json(new MyCustomArgument1 + { + Value = arg1.Value + }); + } +} +``` + +If you want to limit the value of MyCustomArgument1's Value field between 1 and 99, you can define a validator like the one below; + +``` +public class MyCustomArgument1Validator : AbstractValidator +{ + public MyCustomArgument1Validator() + { + RuleFor(x => x.Value).InclusiveBetween(1, 99); + } +} +``` + +ABP will run MyCustomArgument1Validator to validate MyCustomArgument1 class automatically. + +### Disabling Validation + +For automatically validated classes (see Introduction section), you can +use these attributes to control validation: + +- **DisableValidation** attribute can be used for classes, methods or + properties of DTOs to disable validation. +- **EnableValidation** attribute can only be used to enable validation + for a method, if it's disabled for the containing class. + +### Normalization + +We may need to perform an extra operations to prepare DTO parameters +after validation. ASP.NET Boilerplate defines an **IShouldNormalize** +interface that has a **Normalize** method. If you implement this +interface, the Normalize method is called just after validation (and just +before the method call). Assume that our DTO gets a Sorting direction. If +it's not supplied, we want to set a default sorting: + + public class GetTasksInput : IShouldNormalize + { + public string Sorting { get; set; } + + public void Normalize() + { + if (string.IsNullOrWhiteSpace(Sorting)) + { + Sorting = "Name ASC"; + } + } + } diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Value-Objects.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Value-Objects.md" new file mode 100644 index 0000000..22d201c --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Value-Objects.md" @@ -0,0 +1,66 @@ +### Introduction + +"*An object that represents a descriptive aspect of the domain with no +conceptual identity is called a VALUE OBJECT.*" (Eric Evans). + +[Entities](Entities.md) have identities +(Id), Value Objects do not. If the identities of two +Entities are different, they are considered as different +objects/entities even if all the properties of those entities are the +same. Imagine two different people that have the same Name, Surname and Age but +are different people (their identity numbers are different). For an Address class (which +is a classic Value Object), if the two addresses have the same Country, City, and Street number, etc, +they are considered to be the same address. + +In Domain Driven Design (DDD), the Value Object is another type of domain +object which can include business logic and is an essential part of the +domain. + +### Value Object Base Class + +ABP has a **ValueObject<T>** base class which can be inherited in +order to easily create Value Object types. Here's an example **Address** Value +Object type: + + public class Address : ValueObject
+ { + public Guid CityId { get; private set; } //A reference to a City entity. + + public string Street { get; private set; } + + public int Number { get; private set; } + + public Address(Guid cityId, string street, int number) + { + CityId = cityId; + Street = street; + Number = number; + } + } + +The ValueObject base class overrides the equality operator (and other related +operator and methods) to compare the two value objects and assumes that they +are identical if all the properties are the same. For example, all of these tests +pass: + + var address1 = new Address(new Guid("21C67A65-ED5A-4512-AA29-66308FAAB5AF"), "Baris Manco Street", 42); + var address2 = new Address(new Guid("21C67A65-ED5A-4512-AA29-66308FAAB5AF"), "Baris Manco Street", 42); + + Assert.Equal(address1, address2); + Assert.Equal(address1.GetHashCode(), address2.GetHashCode()); + Assert.True(address1 == address2); + Assert.False(address1 != address2); + +Even if they are different objects in memory, they are identical for our +domain. + +### Best Practices + +Here are some best practices when using Value Objects: + +- Design a value object as **immutable** (like the Address above) + if there is not a good reason for designing it as mutable. +- The properties that make up a Value Object should form a conceptual + whole. For example, CityId, Street and Number shouldn't be separate + properties of a Person entity. This also makes the Person entity + simpler. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Web-API-Controllers.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Web-API-Controllers.md" new file mode 100644 index 0000000..ad53453 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Web-API-Controllers.md" @@ -0,0 +1,137 @@ +### Introduction + +ASP.NET Boilerplate is integrated into the **ASP.NET Web API Controllers** via +the **Abp.Web.Api** NuGet package. You can create regular ASP.NET Web API +Controllers like you always do. [Dependency +Injection](/Pages/Documents/Dependency-Injection) properly works for +regular ApiControllers, but you should derive your controllers from +**AbpApiController**, which provides several benefits and integrates better +into ASP.NET Boilerplate. + +### AbpApiController Base Class + +This is a simple api controller derived from **AbpApiController**: + + public class UsersController : AbpApiController + { + + } + +#### Localization + +AbpApiController defines an **L** method to make +[localization](/Pages/Documents/Localization) easier. Example: + + public class UsersController : AbpApiController + { + public UsersController() + { + LocalizationSourceName = "MySourceName"; + } + + public UserDto Get(long id) + { + var helloWorldText = L("HelloWorld"); + + //... + } + } + +You must set the **LocalizationSourceName** property in order to make the **L** method work. +You can set this in your own base api controller class so you don't have to repeat it +for each api controller. + +#### Others + +You can also use the pre-injected +[AbpSession](/Pages/Documents/Abp-Session), +[EventBus](/Pages/Documents/EventBus-Domain-Events), [PermissionManager, +PermissionChecker](/Pages/Documents/Authorization), +[SettingManager](/Pages/Documents/Setting-Management), [FeatureManager, +FeatureChecker](/Pages/Documents/Feature-Management), +[LocalizationManager](/Pages/Documents/Localization), +[Logger](/Pages/Documents/Logging), and +[CurrentUnitOfWork](/Pages/Documents/Unit-Of-Work) base properties (and +more). + +### Filters + +ABP defines some **pre-built filters** for the AspNet Web API. All of them +are added to **all actions of all controllers** by default. + +#### Audit Logging + +The **AbpApiAuditFilter** is used to integrate into the [audit logging +system](Audit-Logging.md). It logs all requests to all actions by +default (if auditing is not disabled). You can control audit logging +using the **Audited** and **DisableAuditing** attributes for actions and +controllers. + +#### Authorization + +You can use the **AbpApiAuthorize** attribute for your api controllers or +actions to prevent unauthorized users from using your controllers and +actions. Example: + + public class UsersController : AbpApiController + { + [AbpApiAuthorize("MyPermissionName")] + public UserDto Get(long id) + { + //... + } + } + +You can define the **AllowAnonymous** attribute for actions or controllers +to suppress authentication/authorization. AbpApiController also defines +the **IsGranted** method as a shortcut to check the permissions in the code. + +See the [authorization](/Pages/Documents/Authorization) documentation for +more info.  + +#### Anti Forgery Filter + +**AbpAntiForgeryApiFilter** is used to automatically protect the ASP.NET Web API +actions from CSRF/XSRF attacks (including the [dynamic web api](Dynamic-Web-API.md)) (for POST, +PUT and DELETE requests). See the [CSRF +documentation](XSRF-CSRF-Protection.md) for more info.  + +#### Unit Of Work + +**AbpApiUowFilter** is used to integrate into the [Unit of +Work](Unit-Of-Work.md) system. It automatically begins a new unit of +work before an action execution and if no exception is thrown, completes the unit of work after +the action execution. + +You can use the **UnitOfWork** attribute to control the behavior of the UOW for an +action. You can also use the startup configuration to change the default unit of +work attribute for all actions. + +#### Result Wrapping & Exception Handling + +ASP.NET Boilerplate **does not wrap** Web API actions **by default** if +an action has successfully executed. It, however, **handles and wraps +exceptions**. You can add the WrapResult/DontWrapResult attributes to actions and +controllers for finer control. You can change this default behavior from the +[startup configuration](Startup-Configuration.md) (using +Configuration.Modules.AbpWebApi()...). See the [AJAX +document](Javascript-API/AJAX.md) for more info about result wrapping.  + +#### Result Caching + +ASP.NET Boilerplate adds the Cache-Control header (no-cache, no-store) to +the response of Web API requests. This way, it prevents browser caching of +responses even for GET requests. This behavior can be disabled through the +configuration. + +#### Validation + +**AbpApiValidationFilter** automatically checks **ModelState.IsValid** +and prevents execution of the action if it's not valid. It also implements +input DTO validation as described in the [validation +documentation](Validating-Data-Transfer-Objects.md). + +### Model Binders + +**AbpApiDateTimeBinder** is used to normalize DateTime (and +Nullable<DateTime>) inputs using the **Clock.Normalize** method. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/XSRF-CSRF-Protection.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/XSRF-CSRF-Protection.md" new file mode 100644 index 0000000..90735c1 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/XSRF-CSRF-Protection.md" @@ -0,0 +1,282 @@ +### Introduction + +"***Cross-Site Request Forgery** (CSRF) is a type of attack that occurs +when a malicious web site, email, blog, instant message, or program +causes a user’s web browser to perform an unwanted action on a trusted +site for which the user is currently authenticated*" +([OWASP](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet)). + +It's also briefly described +[here](http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks) +where it explains how to implement it into ASP.NET Web API. + +ABP framework **simplifies** and **automates** CSRF protection as much +as possible. The [startup templates](/Templates) come with this +**pre-configured** and it works out-of-the-box. In this document, we will +explain how it's integrated into the ASP.NET platforms and how it works. + +#### Http Verbs + +You don't normally need to protect the the **GET**, **HEAD**, +**OPTIONS** and **TRACE** action HTTP verbs since they normally are side-effect +free (they don't change the database). While ABP assumes this and +implements Anti Forgery protection for only the **POST**, **PUT, PATCH** and +**DELETE** verbs, you can change this behavior using the attributes +defined in this document. + +#### Non-Browser Clients + +CSRF is a type of attack that is a problem for browsers because a +browser sends all cookies (including auth cookies) in all requests, +including cross-domain requests. This is not a problem for non-browser +clients, like mobile applications. The ABP framework understands the +difference and automatically **skips anti-forgery validation for non- +browser clients**. + +### ASP.NET MVC + +#### Features + +ASP.NET MVC has it's own built-in AntiForgery system, but there are a few weaknesses: + +- It requires you to add the **ValidateAntiForgeryToken** attribute to all + actions that need to be protected. You could potentially **forget** to add + it for all the needed actions! +- The ValidateAntiForgeryToken attribute only checks the + **\_\_RequestVerificationToken** in the HTML **form fields**. This + makes it very hard or impossible to use it for **AJAX** requests, + especially if you are sending "**application/json**" as the + content-type. In AJAX requests, it's common to set the token in the + **request header**. +- It's **hard to access** the verification token in **JavaScript** + (especially if you don't write your own JavaScript in .cshtml + files). We need to access it to use it in our **AJAX** requests. +- Even if we can access to the token in JavaScript, we must **manually + add** it to the header for every request. + +ABP does following things to overcome these problems: + +- You do not need to add the **ValidateAntiForgeryToken** attribute for **POST**, + **PUT, PATCH** and **DELETE** actions anymore, because they are + **automatically protected** (using **AbpAntiForgeryMvcFilter**). + Automatic protection will be enough for most cases. But you can + disable it for an action or controller using the + **DisableAbpAntiForgeryTokenValidation** attribute and you can + enable it for any action/controller using the + **ValidateAbpAntiForgeryToken** attribute. +- In addition to the HTML **form field**, **AbpAntiForgeryMvcFilter** also checks the token in the **header**. + This way, we can easily use anti-forgery token protections for AJAX requests. +- ABP provides the **abp.security.antiForgery.getToken()** function to get the + token in JavaScript, even if you don't need it often. +- ABP **Automatically** adds an anti-forgery token to the **header** for all + AJAX requests. + +In this way, CSRF protection works almost seamlessly. + +#### Integration + +The startup templates already integrate the CSRF protections out-of-the-box. +If you need to manually add it to your project (maybe you have a legacy project), follow this guide. + +##### Layout View + +We need to add the following code in our **Layout** view: + + @{ + SetAntiForgeryCookie(); + } + +All pages that use this layout will include it. This method is defined +in the base ABP view class. It creates and sets the appropriate token cookies +and makes JavaScript do the side-work. If you have more than one layout, add +this to all of them. + +That's all we have to do for ASP.NET MVC applications. All AJAX requests +will be protected automatically, but we should still use +the **@Html.AntiForgeryToken()** HTML helper for our **HTML forms** which +are **not posted via AJAX**. There is **no need** to add the +ValidateAbpAntiForgeryToken attribute for the corresponding action. + +#### Configuration + +XSRF protection is **enabled by default**. You can disable or configure +it in your [module](Module-System.md)'s PreInitialize method. Example: + + Configuration.Modules.AbpWeb().AntiForgery.IsEnabled = false; + +You can also configure token and cookie names using +*Configuration.Modules.AbpWebCommon().AntiForgery* object. + +### ASP.NET Web API + +#### Features + +The ASP.NET Web API **does not** include an anti-forgery mechanism. However, ASP.NET +Boilerplate provides the infrastructure to add automated CSRF protection for ASP.NET +Web API Controllers. + +#### Integration + +##### With ASP.NET MVC Clients + +If you are using the Web API inside an MVC project, **no additional +configuration is needed**. Even if you are self-hosting your Web API layer +in another process, no configuration is needed as long as you are making +AJAX requests from a configured MVC application. + +##### With Other Clients + +If your clients are different kinds of applications (say, an independent +Angular application which can not use the SetAntiForgeryCookie() method as +described above), then you should provide a way of setting the anti- +forgery token cookie. One possible way of doing this is to create an api +controller like the following: + + using System.Net.Http; + using Abp.Web.Security.AntiForgery; + using Abp.WebApi.Controllers; + + namespace AngularForgeryDemo.Controllers + { + public class AntiForgeryController : AbpApiController + { + private readonly IAbpAntiForgeryManager _antiForgeryManager; + + public AntiForgeryController(IAbpAntiForgeryManager antiForgeryManager) + { + _antiForgeryManager = antiForgeryManager; + } + + public HttpResponseMessage GetTokenCookie() + { + var response = new HttpResponseMessage(); + + _antiForgeryManager.SetCookie(response.Headers); + + return response; + } + } + } + +You can then call this action from the client to set the cookie. + +### ASP.NET Core + +#### Features + +**ASP.NET Core** MVC has a better [Anti +Forgery]() +mechanism compared to previous versions (ASP.NET MVC 5.x): + +- It has the **AutoValidateAntiforgeryTokenAttribute** class that + automates anti-forgery validation for all **POST**, **PUT, PATCH** + and **DELETE** actions. +- It has the **ValidateAntiForgeryToken** and **IgnoreAntiforgeryToken** + attributes to control token validation. +- It automatically adds an anti-forgery security token to HTML forms if you + don't explicitly disable it. So there's no need to call + @Html.AntiForgeryToken() in most cases. +- It can read the request token from the HTTP **header** and the **form + field**. + +ABP adds the following features: + +- ABP **automatically** adds an anti-forgery token to the **header** for all + AJAX requests. +- It also provides an **abp.security.antiForgery.getToken()** function to get the + token in the JavaScript, even you will not need it much. + +#### Integration + +The startup templates are already integrated to use CSRF protections out-of-the-box. +If you need to manually add it to your project (maybe you created your +project before we added it), follow this guide. + +##### Startup Class + +First, we must add the AutoValidateAntiforgeryTokenAttribute to the global +filters while adding MVC in the ConfigureServices method of the Startup class: + + services.AddMvc(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }); + +This way, all MVC actions (except GET, HEAD, OPTIONS and TRACE as declared +before) will be automatically validated for an anti-forgery token. + +##### Layout View + +We must add the following code in our **Layout** view: + + @using Abp.Web.Security.AntiForgery + @inject IAbpAntiForgeryManager AbpAntiForgeryManager + @{ + AbpAntiForgeryManager.SetCookie(Context); + } + +All the pages that use this layout will include it. It creates and sets +the appropriate token cookies and makes JavaScript do all the work. If you have +more than one layout, add this to all of them. + +That's all we must do for ASP.NET Core MVC applications. All AJAX +requests will work automatically. For non-ajax form submits, ASP.NET +Core automatically adds an anti-forgery token field if you use one of +asp-\* tags in your form. So there's normally no need to use @Html.AntiForgeryToken(). + +### Client Libraries + +The anti-forgery token must be provided in the request header for all AJAX +requests, as we declared above. We will see how it's done here. + +#### jQuery + +The abp.jquery.js script defines an AJAX interceptor which adds the anti-forgery +token to the request header for every request. It gets the token from the +**abp.security.antiForgery.getToken()** JavaScript function. + +#### AngularJS + +AngularJS automatically adds the anti-forgery token to all AJAX requests. +See the *Cross Site Request Forgery (XSRF) Protection* section in the AngularJS +[$http document](https://docs.angularjs.org/api/ng/service/$http). ABP +uses the same cookie and header names by default. So, Angular +integration works out of the box. + +#### Other Libraries + +If you are using any other library for AJAX requests, you have three +options: + +##### Intercept XMLHttpRequest + +Since all libraries use JavaScript's native AJAX object, +XMLHttpRequest, you can define a simple interceptor to add the token to +the header: + + (function (send) { + XMLHttpRequest.prototype.send = function (data) { + this.setRequestHeader(abp.security.antiForgery.tokenHeaderName, abp.security.antiForgery.getToken()); + return send.call(this, data); + }; + })(XMLHttpRequest.prototype.send); + +##### Using the Library Interceptor + +A good library provides interception points (like jQuery and AngularJS), +so follow your vendor's documentation to learn how to intercept +requests and manipulate headers. + +##### Add the Header Manually + +As a final option, you can use the abp.security.antiForgery.getToken() method to get +the token and add it to the request header manually for every request. +You probably do not need this and can solve this problem by using the methods described above. + +### Internals + +You may wonder "How does ABP handle this?". Actually, we use the same +mechanism described in the AngularJS documentation mentioned before. ABP +stores the token into a cookie (as described above) and sets the request +headers using that cookie. For validating it, it also integrates well into the +ASP.NET MVC, Web API and Core frameworks. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Edition-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Edition-Management.md" new file mode 100644 index 0000000..14261d0 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Edition-Management.md" @@ -0,0 +1,29 @@ +### Introduction + +Most **SaaS** (multi-tenant) applications have **editions** (packages) +that have different **features**. This way, they can provide different +**price and feature options** to their tenants (customers). + +#### About Features + +See the [feature management +documentation](/Pages/Documents/Feature-Management) to better understand +how features work. + +### Edition Entity + +**Edition** is a simple entity representing an edition (or package) of the +application. It just has the **Name** and **DisplayName** properties. + +### Edition Manager + +**EditionManager** is the **domain service** to manage editions: + + public class EditionManager : AbpEditionManager + { + } + +It's derived from the **AbpEditionManager** class. You can inject and use +the EditionManager to create, delete, and update editions. EditionManager +is also used to **manage the features** of editions. It internally **caches** +edition features for better performance. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Identity-Server.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Identity-Server.md" new file mode 100644 index 0000000..7ed56b3 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Identity-Server.md" @@ -0,0 +1,313 @@ +### Introduction + +[Identity Server](http://identityserver.io/) is an open source **OpenID +Connect** and **OAuth 2.0** framework. It can be used to make your +application an **authentication / single sign on server**. It can also +issue **access tokens** for 3rd party clients. This document describes +how you can integrate IdentityServer4 (version **2.0+**) to your +project. + +#### Startup Project + +This document assumes that you have already created an ASP.NET Core based +project (including Module Zero) from the [startup templates](/Templates) and +have set it up to work. We created an [ASP.NET Core MVC startup +project](Startup-Template-Core.md) for this demonstration. + +### Installation + +There are two NuGet packages: + +- [**Abp.ZeroCore.IdentityServer4**](https://www.nuget.org/packages/Abp.ZeroCore.IdentityServer4) + is the main integration package. +- [**Abp.ZeroCore.IdentityServer4.EntityFrameworkCore**](https://www.nuget.org/packages/Abp.ZeroCore.IdentityServer4.EntityFrameworkCore) + is the storage provider for EF Core. + +Since the EF Core package already depends on the first one, you only have to +install the +[**Abp.ZeroCore.IdentityServer4.EntityFrameworkCore**](https://www.nuget.org/packages/Abp.ZeroCore.IdentityServer4.EntityFrameworkCore) +package to your project. Install it to the project that contains your +DbContext (.EntityFrameworkCore project for default templates): + + Install-Package Abp.ZeroCore.IdentityServer4.EntityFrameworkCore + +Then you can add a dependency to your [module](../Module-System.md) +(generally, to your EntityFrameworkCore project): + + [DependsOn(typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))] + public class MyModule : AbpModule + { + //... + } + +### Configuration + +Configuring and using IdentityServer4 with Abp.ZeroCore is similar to +independently using IdentityServer4. You should read its [own +documentation](https://identityserver4.readthedocs.io) to better understand how +it works. In this document, we only show the additional configuration needed +to integrate it into Abp.ZeroCore. + +#### Startup Class + +In the ASP.NET Core **Startup class**, we must add IdentityServer to the +**service collection** and to the ASP.NET Core **middleware pipeline**. +Highlighted, here are the **differences** from the standard IdentityServer4 usage: + + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + //... + + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources()) + .AddInMemoryApiResources(IdentityServerConfig.GetApiResources()) + .AddInMemoryClients(IdentityServerConfig.GetClients()) + .AddAbpPersistedGrants() + .AddAbpIdentityServer(); ; + + //... + } + + public void Configure(IApplicationBuilder app) + { + //... + + app.UseJwtTokenMiddleware("IdentityBearer"); + app.UseIdentityServer(); + + //... + } + } + +We added **services.AddIdentityServer()** just after +**IdentityRegistrar.Register(services)** and added +**app.UseJwtTokenMiddleware("IdentityBearer")** just after +**app.UseAuthentication()** in the startup project. + +#### IdentityServerConfig Class + +We have used the IdentityServerConfig class to get identity resources, api +resources and clients. You can find more information about this class in +it's own +[documentation](https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html). +For the simplest case, it can be a static class like below: + + public static class IdentityServerConfig + { + public static IEnumerable GetApiResources() + { + return new List + { + new ApiResource("default-api", "Default (all) API") + }; + } + + public static IEnumerable GetIdentityResources() + { + return new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + new IdentityResources.Email(), + new IdentityResources.Phone() + }; + } + + public static IEnumerable GetClients() + { + return new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword).ToList(), + AllowedScopes = {"default-api"}, + ClientSecrets = + { + new Secret("secret".Sha256()) + } + } + }; + } + } + +#### DbContext Changes + +The **AddAbpPersistedGrants()** method is used to save consent responses to +the persistent data store. In order to use it, **YourDbContext** must +implement the **IAbpPersistedGrantDbContext** interface as shown below: + + public class YourDbContext : AbpZeroDbContext, IAbpPersistedGrantDbContext + { + public DbSet PersistedGrants { get; set; } + + public YourDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigurePersistedGrantEntity(); + } + } + +The IAbpPersistedGrantDbContext interface defines the **PersistedGrants** DbSet. We also +must call the modelBuilder.**ConfigurePersistedGrantEntity()** extension +method as shown above in order to configure EntityFramework for the +**PersistedGrantEntity**. + +Note that this change in YourDbContext causes a new database migration. +So remember to use the "Add-Migration" and "Update-Database" commands to +update your database. + +IdentityServer4 will continue to work even if you don't call the +AddAbpPersistedGrants<YourDbContext>() extension method, but user +consent responses will be stored in an in-memory data store in that case +(which is cleared when you restart your application!). + +#### JWT Authentication Middleware + +If we want to authorize clients against the same application we can use the +[IdentityServer authentication +middleware](http://docs.identityserver.io/en/release/topics/apis.html?highlight=UseIdentityServerAuthentication#the-identityserver-authentication-middleware) +for that. + +First, install the IdentityServer4.AccessTokenValidation package from NuGet +to your project: + + Install-Package IdentityServer4.AccessTokenValidation + +We can then add the middleware to the Startup class as shown below: + + services.AddAuthentication().AddIdentityServerAuthentication("IdentityBearer", options => + { + options.Authority = "http://localhost:62114/"; + options.RequireHttpsMetadata = false; + }); + +We added this just after the **services.AddIdentityServer()** line in the startup +project. + +#### IdentityServer4.AccessTokenValidation Status + +The *IdentityServer4.AccessTokenValidation* package is not ready for ASP.NET +Core 2.0 yet (at the time of this writing). See +https://github.com/IdentityServer/IdentityServer4/issues/1055 for more info. + +### Testing + +Our identity server is now ready to get requests from clients. We can +create a console application to make requests and get responses. + +- Create a new **Console Application** inside your solution. +- Add the **IdentityModel** NuGet package to the console application. This + package is used to create clients for OAuth endpoints. + +While the **IdentityModel** NuGet package is enough to create a client and +consume your API, we need to use the API in a more type safe way: We +will convert incoming data to DTOs which are returned by the application services. + +- Add a reference to the **Application** layer from the console application. + This will allow us to use the same DTO classes returned by the + application layer on the client-side. +- Add the **Abp.Web.Common** NuGet package. This will allow us to use + the AjaxResponse class defined in ASP.NET Boilerplate. Otherwise, + we will have to deal with raw JSON strings to handle the server response. + +Change Program.cs as shown below: + + using System; + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + using Abp.Application.Services.Dto; + using Abp.Json; + using IdentityModel.Client; + using Abp.MultiTenancy; + using Abp.Web.Models; + using IdentityServerIntegrationDemo.Users.Dto; + using Newtonsoft.Json; + + namespace IdentityServerIntegrationDemo.ConsoleApiClient + { + class Program + { + static void Main(string[] args) + { + RunDemoAsync().Wait(); + Console.ReadLine(); + } + + public static async Task RunDemoAsync() + { + var accessToken = await GetAccessTokenViaOwnerPasswordAsync(); + await GetUsersListAsync(accessToken); + } + + private static async Task GetAccessTokenViaOwnerPasswordAsync() + { + var disco = await DiscoveryClient.GetAsync("http://localhost:62114"); + + var httpHandler = new HttpClientHandler(); + httpHandler.CookieContainer.Add(new Uri("http://localhost:62114/"), new Cookie(MultiTenancyConsts.TenantIdResolveKey, "1")); //Set TenantId + var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler); + var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "123qwe"); + + if (tokenResponse.IsError) + { + Console.WriteLine("Error: "); + Console.WriteLine(tokenResponse.Error); + } + + Console.WriteLine(tokenResponse.Json); + + return tokenResponse.AccessToken; + } + + private static async Task GetUsersListAsync(string accessToken) + { + var client = new HttpClient(); + client.SetBearerToken(accessToken); + + var response = await client.GetAsync("http://localhost:62114/api/services/app/user/GetAll"); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine(response.StatusCode); + return; + } + + var content = await response.Content.ReadAsStringAsync(); + var ajaxResponse = JsonConvert.DeserializeObject>>(content); + if (!ajaxResponse.Success) + { + throw new Exception(ajaxResponse.Error?.Message ?? "Remote service throws exception!"); + } + + Console.WriteLine(); + Console.WriteLine("Total user count: " + ajaxResponse.Result.TotalCount); + Console.WriteLine(); + foreach (var user in ajaxResponse.Result.Items) + { + Console.WriteLine($"### UserId: {user.Id}, UserName: {user.UserName}"); + Console.WriteLine(user.ToJsonString(indented: true)); + } + } + } + + internal class UserListDto + { + public int Id { get; set; } + public string UserName { get; set; } + } + } + +Before running this application, ensure that your web project set up and +running, because this console application will make a request to the web +application. Also, ensure that the requesting port (62114) is the same +as your web application. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Language-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Language-Management.md" new file mode 100644 index 0000000..ec4b543 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Language-Management.md" @@ -0,0 +1,214 @@ +### Introduction + +ASP.NET Boilerplate defines a strong UI [localization +system](/Pages/Documents/Localization) which is used both on the server and +client sides. It allows us to easily configure application languages and +define localization texts (strings) in different sources (Resource files +and XML files are two pre-defined sources). + +While it's good for most cases, we may want to define languages and +texts **dynamically** and on a **database**. Module Zero allows us to +dynamically manage application **languages** and **texts** **per +tenant**. + +#### About Localization + +We strongly recommend you read the [localization +documentation](/Pages/Documents/Localization) before this document. + +### How To Enable + +#### Startup Template + +If you create your project from the [startup templates](/Templates), you can +skip this section since the template comes with the database-based +localization enabled by default. If you created your project before this +feature, please read this to enable it for your application. + +Database localization is designed to be backwards-compatible with ASP.NET +Boilerplate's existing localization system. It actually replaces all +the existing dictionary-based localization sources with +**MultiTenantLocalizationSource**. + +MultiTenantLocalizationSource wraps existing +**DictionaryBasedLocalizationSource** based sources. We generally +wrap [XML based +localization](/Pages/Documents/Localization#xml-files) sources. It +can not wrap **Resource File** sources since resource files are designed +as hard-coded and static files which are not proper for dynamic +localization. + +Since it's a wrapper, the underlying XML files are used as a fallback source +if a text is not localized in the database. It may seem complicated, +but it's easy to implement for your application. Let's see how to enable the +database-based localization. + +#### EnableDbLocalization + +First, we enable it: + + Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization(); + +This should be done in the **top level** module's +**[PreInitialize](/Pages/Documents/Module-System#preinitialize)** +method (it's the web module for a web application. Import the +Abp.Zero.Configuration namespace (using Abp.Zero.Configuration) to see +the Zero() extension methods). + +This configuration makes all the magic happen, but we must add +some more code to make it work properly. + +#### Seed Database Languages + +Since ABP will get a list of languages from the database, we must +**insert** the default languages into it. If you're using +EntityFramework, you can [use this seed code](https://github.com/aspnetboilerplate/module-zero-template/blob/master/src/AbpCompanyName.AbpProjectName.EntityFramework/Migrations/SeedData/DefaultLanguagesCreator.cs): + +#### Remove Static Language Configuration + +If you have a static language configuration like the one shown below, you can +**delete** these lines from your configuration code, since they will get +the languages from the database. + + Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true)); + +#### Note On Existing XML Localization Sources + +**Do not** delete your XML localization files and source configuration +code. These files are used as a **fallback source** and all +localization keys are obtained from this source. + +So when you need a new localized text, **define it** into the XML files +as you do normally. You must at least define it in the **default** +language's XML file. Note: you don't need to add the default values of +the localized texts to the database migration code. + +### Managing Languages + +The **IApplicationLanguageManager** interface is +[injected](/Pages/Documents/Dependency-Injection) and used to manage +languages. It has methods like GetLanguagesAsync, AddAsync, RemoveAsync, +UpdateAsync... to manage languages for the host and tenants. + +#### Language List Logic + +The list of languages are stored per tenant and for the host, and calculated +as follows: + +- There is a list of languages defined for **the host**. This list + is considered as the **default** for all tenants. +- There is a separated list of languages for **each tenant**. This + list **inherits** the host list and **adds** tenant-specific languages. + Tenants can not delete or update host-defined (default) languages + (but can override localization texts as we will see later). + +#### ApplicationLanguage Entity + +The ApplicationLanguage entity represents a language for a tenant or the +host. + + [Serializable] + [Table("AbpLanguages")] + public class ApplicationLanguage : FullAuditedEntity, IMayHaveTenant + { + //... + } + +Its basic properties are: + +- **TenantId** (nullable): Contains the related tenant's Id if this + language is tenant-specific. It's null if this is a host language. +- **Name**: Name of the language. This **must be a culture code** from + [this list](https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx). +- **DisplayName**: Shown name of the language. This can be an + arbitrary name, but it generally is the + [CultureInfo.DisplayName](https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.displayname(v=vs.110).aspx). +- **Icon**: An arbitrary icon/flag for the language. This can be used + to show flag of the language on the UI. + +The ApplicationLanguage also inherits from **FullAuditedEntity**. +This means it's a **soft-delete** entity and automatically **audited** +(see the [entity document](/Pages/Documents/Entities) for more info). + +The ApplicationLanguage entities are stored in the **AbpLanguages** table in the +database. + +### Managing Localization Texts + +The **IApplicationLanguageTextManager** interface is +[injected](/Pages/Documents/Dependency-Injection) and used to manage +localization texts. It has the needed methods to get/set a localization text +for a tenant or the host. + +#### Localizing A Text + +Let's see what happens when you want to localize a text; + +- First, it tries to get the **current culture** using the + CurrentThread.CurrentUICulture. + - It checks if the given text is defined (overridden) for the **current + tenant** using + [IAbpSession.TenantId](/Pages/Documents/Abp-Session) in + the **current culture** in the database. It returns the value if + it is defined. + - It then checks if a given text is defined (overridden) for the + **host** in the **current culture** in the database. It returns the + value if it is defined. + - It then checks if a given text is defined in the underlying XML + file in the **current culture**. It returns the value if it is defined. +- Second, it will try to find the **fallback culture**. It's calculated like this: + If the current culture is "en-GB", then the fallback culture is "en". + - It checks if a given text is defined (overrided) for the **current + tenant** in the **fallback culture** in the database. It returns the + value if it is defined. + - It then checks if the given text is defined (overridden) for the + **host** in the **fallback culture** in the database. It returns the + value if it is defined. + - It then checks if the given text is defined in the underlying XML + file in the **fallback culture**. It returns the value if it is defined. +- Third, it will try to find the **default culture**. + - It checks if a given text is defined (overridden) for the **current + tenant** in the **default culture** in the database. It returns the + value if it is defined. + - It then checks if the given text is defined (overridden) for the + **host** in the **default culture** in the database. It returns the + value if it is defined. + - It then checks if a given text is defined in the underlying XML + file in the **default culture**. It returns the value if it is defined. +- If all attempts fail, it will get the same text or an throw exception. + - If the given text (key) is not found at all, ABP throws an exception or + returns the same text (key) by wrapping it with \[ and \] (it can + be configured on startup, see the [localization + document](/Pages/Documents/Localization)). + +Getting a localized text is a bit complicated, but it works fast +since it uses the [cache](/Pages/Documents/Caching). + +#### ApplicationLanguageText Entity + +The ApplicationLanguageText entity is used to store localized values in the +database. + + [Serializable] + [Table("AbpLanguageTexts")] + public class ApplicationLanguageText : AuditedEntity, IMayHaveTenant + { + //... + } + +It's basic properties are; + +- **TenantId** (nullable): Contains the related tenant's Id if this + localized text is tenant-specific. It's null if this is a + host-localized text. +- **LanguageName**: Name of the language. This **must be a culture + code** from [this list](https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx). + This matches to the ApplicationLanguage.Name but is not a forced foreign + key to make it independent from the language entry.  + IApplicationLanguageTextManager handles it properly. +- **Source**: Localization source name. +- **Key**: Localization text's key/name. +- **Value**: Localized value. + +ApplicationLanguageText entities are stored in the **AbpLanguageTexts** +table in the database. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Nuget-Packages.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Nuget-Packages.md" new file mode 100644 index 0000000..e0c3888 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Nuget-Packages.md" @@ -0,0 +1,57 @@ +ASP.NET Boilerplate's Module Zero is distributed on **NuGet**. Here's a +list of all the packages. + +### Abp.ZeroCore Packages + +Abp.ZeroCore is built on +[Microsoft.AspNetCore.Identity](http://www.nuget.org/packages/Microsoft.AspNetCore.Identity). + +#### [Abp.ZeroCore](http://www.nuget.org/packages/Abp.ZeroCore) + +The core/main package of Module Zero. + +#### [Abp.ZeroCore.EntityFrameworkCore](http://www.nuget.org/packages/Abp.ZeroCore.EntityFrameworkCore) + +Entity Framework Core integration package for the Abp.ZeroCore package. + +#### [Abp.ZeroCore.IdentityServer4](http://www.nuget.org/packages/Abp.ZeroCore.IdentityServer4) + +IdentityServer4 integration package for the Abp.ZeroCore package. + +#### [Abp.ZeroCore.IdentityServer4.EntityFrameworkCore](http://www.nuget.org/packages/Abp.ZeroCore.IdentityServer4.EntityFrameworkCore) + +Entity Framework Core integration package for the +Abp.ZeroCore.IdentityServer4 package. + +### Abp.Zero + +Abp.Zero is built on +[Microsoft.AspNet.Identity.Core](http://www.nuget.org/packages/Microsoft.AspNet.Identity.Core). + +#### [Abp.Zero](http://www.nuget.org/packages/Abp.Zero) + +Main package of Module Zero. + +#### [Abp.Zero.Owin](http://www.nuget.org/packages/Abp.Zero.Owin) + +Owin integration package for Module Zero. + +#### [Abp.Zero.AspNetCore](http://www.nuget.org/packages/Abp.Zero.AspNetCore) + +ASP.NET Core integration package for Module Zero. + +#### [Abp.Zero.EntityFramework](http://www.nuget.org/packages/Abp.Zero.EntityFramework) + +EntityFramework integration package for Module Zero. + +### Shared + +Shared packages between Abp.Zero.\* and Abp.ZeroCore.\* packages. + +#### [Abp.Zero.Common](http://www.nuget.org/packages/Abp.Zero.Common) + +Common package for both the Abp.Zero and Abp.ZeroCore libraries. + +#### [Abp.Zero.Ldap](http://www.nuget.org/packages/Abp.Zero.Ldap) + +LDAP authentication integration package. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Organization-Units.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Organization-Units.md" new file mode 100644 index 0000000..7a935cf --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Organization-Units.md" @@ -0,0 +1,252 @@ +### Introduction + +Organization units (OU) can be used to **hierarchically group users and +entities**. + +### OrganizationUnit Entity + +An OU is represented by the **OrganizationUnit** entity. The fundamental +properties of this entity are: + +- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. +- **ParentId**: Parent OU's Id. Can be null if this is a root OU. +- **Code**: A hierarchical string code that is unique for a tenant. +- **DisplayName**: Shown name of the OU. + +The OrganizationUnit entity's primary key (Id) is a **long** type and it derives +from the [**FullAuditedEntity**](/Pages/Documents/Entities#auditing) class +which provides audit information and implements the +[**ISoftDelete**](/Pages/Documents/Data-Filters#isoftdelete) interface +(OUs are not deleted from the database, they are just marked as deleted). + +#### Organization Tree + +Since an OU can have a parent, all OUs of a tenant are in a **tree** +structure. There are some rules for this tree; + +- There can be more than one root (where the ParentId is null). +- Maximum depth of the tree is defined as a constant as + OrganizationUnit.**MaxDepth**. The value is **16**. +- There is a limit for the first-level children count of an OU (because of the + fixed OU Code unit length explained below). + +#### OU Code + +OU code is automatically generated and maintained by the OrganizationUnit +Manager. It's a string that looks something like this: + +"**00001.00042.00005**" + +This code can be used to easily query the database for all the children +of an OU (recursively). There are some rules for this code: + +- It must be **unique** for a [tenant](/Pages/Documents/Multi-Tenancy). +- All the children of the same OU have codes that **start with the parent OU's code**. +- It's **fixed length** and based on the level of the OU in the tree, as shown in + the sample. +- While the OU code is unique, it can be **changeable** if you move an OU. +- We must reference an OU by Id, not Code. + +### OrganizationUnit Manager + +The **OrganizationUnitManager** class can be +[injected](/Pages/Documents/Dependency-Injection) and used to manage +OUs. Common use cases are: + +- Create, Update or Delete an OU +- Move an OU in the OU tree. +- Getting information about the OU tree and its items. + +#### Multi-Tenancy + +The OrganizationUnitManager is designed to work for a **single tenant** at a +time. It works for the **current tenant** by default. + +### Common Use Cases + +Here, we will see some common use cases for OUs. You can find the source code of +the samples +[here](https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/OrganizationUnitsDemo). + +#### Creating An Entity That Belongs To An Organization Unit + +The most obvious usage of OUs is to assign an entity to an OU. Let's see a +sample entity: + + public class Product : Entity, IMustHaveTenant, IMustHaveOrganizationUnit + { + public virtual int TenantId { get; set; } + + public virtual long OrganizationUnitId { get; set; } + + public virtual string Name { get; set; } + + public virtual float Price { get; set; } + } + +We simply created the **OrganizationUnitId** property to assign this entity +to an OU. The **IMustHaveOrganizationUnit** defines the OrganizationUnitId +property. We don't have to implement it, but it's recommended because it provides +standardization. There is also the IMayHaveOrganizationId interface which has a +**nullable** OrganizationUnitId property. + +We can now relate a Product to an OU and query the products of a specific +OU. + +**Please note**; The product entity must have a **TenantId** (which is a property +of IMustHaveTenant) to distinguish it from products of different tenants in a +multi-tenant application (see the [Multi-Tenancy +document](/Pages/Documents/Multi-Tenancy#data-filters) for more info). If your +application is not multi-tenant, you don't need this interface and +property. + +#### Getting Entities In An Organization Unit + +Getting the Products of an OU is simple. Let's see this sample [domain +service](/Pages/Documents/Domain-Services): + + public class ProductManager : IDomainService + { + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) + { + _productRepository = productRepository; + } + + public List GetProductsInOu(long organizationUnitId) + { + return _productRepository.GetAllList(p => p.OrganizationUnitId == organizationUnitId); + } + + } + +As shown above, we can simply write a predicate against Product.OrganizationUnitId. + +#### Get Entities In An Organization Unit Including It's Child Organization Units + +We may want to get the Products of an organization unit including child +organization units. In this case, the OU **Code** can help us: + + public class ProductManager : IDomainService + { + private readonly IRepository _productRepository; + private readonly IRepository _organizationUnitRepository; + + public ProductManager( + IRepository productRepository, + IRepository organizationUnitRepository) + { + _productRepository = productRepository; + _organizationUnitRepository = organizationUnitRepository; + } + + [UnitOfWork] + public virtual List GetProductsInOuIncludingChildren(long organizationUnitId) + { + var code = _organizationUnitRepository.Get(organizationUnitId).Code; + + var query = + from product in _productRepository.GetAll() + join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id + where organizationUnit.Code.StartsWith(code) + select product; + + return query.ToList(); + } + } + +First, we got the **code** of the the given OU. Then we created a LINQ expression with a +**join** and a **StartsWith(code)** condition (StartsWith creates a +**LIKE** query in SQL). This way we can hierarchically get the products of an +OU. + +#### Filter Entities For A User + +We may want to get all products that are in the OUs of a specific user. +Example code: + + public class ProductManager : IDomainService + { + private readonly IRepository _productRepository; + private readonly UserManager _userManager; + + public ProductManager( + IRepository productRepository, + UserManager userManager) + { + _productRepository = productRepository; + _organizationUnitRepository = organizationUnitRepository; + _userManager = userManager; + } + + public async Task> GetProductsForUserAsync(long userId) + { + var user = await _userManager.GetUserByIdAsync(userId); + var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user); + var organizationUnitIds = organizationUnits.Select(ou => ou.Id); + + return await _productRepository.GetAllListAsync(p => organizationUnitIds.Contains(p.OrganizationUnitId)); + } + } + +We simply found the Ids of the OUs of the user. We then used a **Contains** condition +while getting the products. We could also create a LINQ query with join +to get the same list, instead. + +We may want to get products in the user's OUs including their child OUs: + + public class ProductManager : IDomainService + { + private readonly IRepository _productRepository; + private readonly IRepository _organizationUnitRepository; + private readonly UserManager _userManager; + + public ProductManager( + IRepository productRepository, + IRepository organizationUnitRepository, + UserManager userManager) + { + _productRepository = productRepository; + _organizationUnitRepository = organizationUnitRepository; + _userManager = userManager; + } + + [UnitOfWork] + public virtual async Task> GetProductsForUserIncludingChildOusAsync(long userId) + { + var user = await _userManager.GetUserByIdAsync(userId); + var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user); + var organizationUnitCodes = organizationUnits.Select(ou => ou.Code); + + var query = + from product in _productRepository.GetAll() + join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id + where organizationUnitCodes.Any(code => organizationUnit.Code.StartsWith(code)) + select product; + + return query.ToList(); + } + } + +We combined **Any** with the **StartsWith** condition into a LINQ join +statement. + +There will most likely be more complex requirements, but they all can be done +with LINQ or SQL. + +### Settings + +You can inject and use the **IOrganizationUnitSettings** interface to get +the Organization Unit's setting values. There currently is just a single +setting that can be changed for your application needs: + +- **MaxUserMembershipCount**: Maximum allowed membership count for a + user. + Default value is **int.MaxValue** which allows a user to be a member + of unlimited OUs at the same time. + The Setting name is a constant defined in + *AbpZeroSettingNames.OrganizationUnits.MaxUserMembershipCount*. + +You can change the setting values using the [setting +manager](/Pages/Documents/Setting-Management). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Overall.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Overall.md" new file mode 100644 index 0000000..d6289c7 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Overall.md" @@ -0,0 +1,27 @@ +### Introduction + +The ASP.NET Boilerplate framework is designed to be independent of any +database schema and to be as generic as possible. Therefore, it leaves +some concepts **abstract** and **optional**, like audit logging, session +management and authorization, which require some sort of **data store**. + +**Module Zero** implements all the fundamental concepts of the ASP.NET +Boilerplate framework such as [tenant management](/Pages/Documents/Zero/Tenant-Management) +(**multi-tenancy**), [role management](/Pages/Documents/Zero/Role-Management), [user management](/Pages/Documents/Zero/User-Management), +[session](/Pages/Documents/Abp-Session), [authorization](/Pages/Documents/Authorization) ([permission management](/Pages/Documents/Zero/Permission-Management)), [setting management](/Pages/Documents/Setting-Management), +[language management](/Pages/Documents/Zero/Language-Management), [audit logging](/Pages/Documents/Audit-Logging) and more. + +### Microsoft ASP.NET Identity + +This module has two versions: + +- The Abp.Zero.\* packages are built on Microsoft ASP.NET Identity and + Entity Framework 6.x. +- The Abp.ZeroCore.\* packages are built on Microsoft ASP.NET Core + Identity and Entity Framework Core. + +See [all packages](Nuget-Packages.md). + +### Source code + +The source code of this module is also maintained in [the main GitHub repository](https://github.com/aspnetboilerplate/aspnetboilerplate/tree/dev/src) (Abp.Zero.* and Abp.ZeroCore.* projects). diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Permission-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Permission-Management.md" new file mode 100644 index 0000000..cc4a332 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Permission-Management.md" @@ -0,0 +1,96 @@ +#### About Authorization + +We strongly recommend that you read the [authorization +documentation](/Pages/Documents/Authorization) before this one. + +### Introduction + +Module Zero implements the **IPermissionChecker** interface of ASP.NET +Boilerplate's authorization system. To define and check permissions, +see the [authorization document](/Pages/Documents/Authorization). In +this document, we will show you how to grant permissions for roles and users. + +### Role Permissions + +If we **grant** a permission to a role, all the users that have this role are +authorized for the permission (unless explicitly prohibited for a +specific user). + +We use the **RoleManager** to change the permissions of a Role. For example, +**SetGrantedPermissionsAsync** can be used to change all the permissions of +a role in one method call: + + public class RoleAppService : IRoleAppService + { + private readonly RoleManager _roleManager; + private readonly IPermissionManager _permissionManager; + + public RoleAppService(RoleManager roleManager, IPermissionManager permissionManager) + { + _roleManager = roleManager; + _permissionManager = permissionManager; + } + + public async Task UpdateRolePermissions(UpdateRolePermissionsInput input) + { + var role = await _roleManager.GetRoleByIdAsync(input.RoleId); + var grantedPermissions = _permissionManager + .GetAllPermissions() + .Where(p => input.GrantedPermissionNames.Contains(p.Name)) + .ToList(); + + await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions); + } + } + +In this example, we get a **RoleId** and a list of granted permission +names (input.GrantedPermissionNames is a List<string>) as an input. +We then use the **IPermissionManager** to find all the **Permission** objects by +name. After that, we call the **SetGrantedPermissionsAsync** method to update +the permissions of the role. + +There are also other methods like GrantPermissionAsync and +ProhibitPermissionAsync to control the permissions one-by-one. + +### User Permissions + +While the role-based permission management can be enough for most +applications, we may need to control the permissions per user. When we +define a permission setting for a user, it overrides the permission setting +defined for the roles of the user. + +As an example, imagine that we have an application service method for prohibiting a +permission for a user: + + public class UserAppService : IUserAppService + { + private readonly UserManager _userManager; + private readonly IPermissionManager _permissionManager; + + public UserAppService(UserManager userManager, IPermissionManager permissionManager) + { + _userManager = userManager; + _permissionManager = permissionManager; + } + + public async Task ProhibitPermission(ProhibitPermissionInput input) + { + var user = await _userManager.GetUserByIdAsync(input.UserId); + var permission = _permissionManager.GetPermission(input.PermissionName); + + await _userManager.ProhibitPermissionAsync(user, permission); + } + } + +The UserManager has many methods to control the permissions of users. In this +example, we're getting a UserId and PermissionName and using the +UserManager's **ProhibitPermissionAsync** method to prohibit a +permission for a user. + +When we **prohibit** a permission for a user, he/she **cannot** be +authorized for this permission even his/her roles are **granted** for +the permission. We can use the same principle for granting permissions. When we +**grant** a permission specifically for a user, this user **is granted** +the permission even if the roles of the user are not granted the +permission. We can use the **ResetAllPermissionsAsync** method to delete +all user-specific permission settings for a user. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Role-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Role-Management.md" new file mode 100644 index 0000000..0a1908f --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Role-Management.md" @@ -0,0 +1,87 @@ +### Role Entity + +The Role entity represents a **role for the application**. It should be +derived from the **AbpRole** class as shown below: + + public class Role : AbpRole + { + //add your own role properties here + } + +This class will be created when you download an ABP template with the option in the below image is selected. + +Login Page + +Roles are +stored in the **AbpRoles** table in the database. You can add your own custom +properties to the Role class (and create database migrations for the +changes). + +AbpRole defines some properties. The most important are: + +- **Name**: Unique name of the role in the tenant. +- **DisplayName**: Shown name of the role. +- **IsDefault**: Is this role assigned to new users by default? +- **IsStatic**: Is this role static? (setup during pre-build, and can not be + deleted). + +Roles are used to **group permissions**. When a user has a role, then +he/she will have all the permissions of that role. A user can have +**multiple** roles. The Permissions of this user will be a merge of all the +permissions of all assigned roles. + +#### Dynamic vs Static Roles + +In Module Zero, roles can be dynamic or static: + +- **Static role**: A static role has a known **name** (like 'admin') + which can not be changed (we can change the **display name**). It + exists on the system startup and can not be deleted. This way, we can + write code based on a static role's name. +- **Dynamic (non static) role**: We can create a dynamic role after + deployment. We can then grant permissions for that role, we can + assign the role to some users, and we can delete it. We do not know the + names of dynamic roles during development. + +Use the **IsStatic** property to set it for a role. We must also +**register** static roles in +the [PreInitialize](/Pages/Documents/Module-System) method of our module. Assume +that we have an "Admin" static role for tenants: + + Configuration.Modules.Zero().RoleManagement.StaticRoles.Add(new StaticRoleDefinition("Admin", MultiTenancySides.Tenant)); + +This way, Module Zero will be aware of static roles. + +#### Default Roles + +One or more roles can be set as **default**. Default roles are assigned +to newly added/registered users by default. This is not a development time +property and can be set or changed after deployment. Use the **IsDefault** +property to set it. + +### Role Manager + +**RoleManager** is a service to perform **domain logic** for roles: + + public class RoleManager : AbpRoleManager + { + //... + } + +You can [inject](/Pages/Documents/Dependency-Injection) and use +the RoleManager to create, delete, update roles, grant permissions for roles +and much more. You can add your own methods here, too. You can also +**override** any method of the **AbpRoleManager** base class for your own +needs. + +Like the UserManager, some methods of the RoleManager also return IdentityResult +as a result instead of throwing exceptions. See the [user +management](/Pages/Documents/Zero/User-Management) document for more +information. + +### Multi-Tenancy + +Similar to user management, role management also works for a tenant +in a multi-tenant application. See the [user +management](/Pages/Documents/Zero/User-Management#multi-tenancy) +document for more information. \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Angular.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Angular.md" new file mode 100644 index 0000000..0ccafab --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Angular.md" @@ -0,0 +1,168 @@ +### Introduction + +The easiest way of starting a new project using ABP with **ASP.NET Core** and **Angular** is to create a +template on the [download page](/Templates). After creating and downloading your project, follow the +below steps to run your application. + +### ASP.NET Core Application + +- Open your solution in **Visual Studio 2017 v15.3.5+** and **build** + the solution. +- Select the '**Web.Host**' project as the **startup project**. +- Check the **connection string** in the **appsettings.json** file of the Web.Host project, change it if you need to. +- Open the **Package Manager Console** and run an **Update-Database** command + to create your database (ensure that the Default project is selected as + **.EntityFrameworkCore** in the Package Manager Console window). +- Run the application. It will show **swagger-ui** if it is successful: + +Swagger UI + +In this template, **multi-tenancy is enabled by default**. If you don't +need it, you can disable it in the Core project's module class. + +If you have problems with running the application, close and then re-open +Visual Studio again. It sometimes fails on the first package restore. + +### Angular Application + +#### Requirements + +The Angular application needs the following tools installed: + +- [nodejs](https://nodejs.org/en/download/) 6.9+ with npm 3.10+ +- Typescript 2.0+ + +We used the [angular-cli](https://cli.angular.io/) to develop the Angular +application. + +#### Restore Packages + +Open a command prompt, navigate to the **angular** folder which contains +the \*.sln file and run the following command to **restore the npm packages**: + + npm install + +Note that the npm install may show some warning messages. This is not +related to our solution and generally it's not a problem. The solution +can also configured to work with [**yarn**](https://yarnpkg.com/) and we +recommend you use it if it is available on your computer. + +#### Run The Application + +In your opened command prompt, run the following command: + + npm start + +Once the application has compiled, you can go to in +your browser. Be sure that the Web.Host application is running at the same +time. When you open the application, you will see the **login page**: + +Login Page + +The Angular client app also has **HMR** (Hot Module Replacement) enabled. +You can use the following command (instead of npm start) to enable HMR +at development time: + + npm run hmr + +#### Login + +You can now login to the application using the default credentials. The default username +is '**admin**' and the password is '**123qwe**'. If you want to +login as a tenant, you need to first switch to that tenant on the login page. By default, there +is a tenant named "Default". Once you login successfully, you will +see a dashboard: + +Dashboard + +This dashboard is just for demonstration purposes and is meant to be a base for your +actual dashboard. + +#### Deployment of Angular Application + +We used the **angular-cli** tooling to build an Angular solution. You can use the +`ng build --prod` command to publish your project. It publishes to the **dist** +folder by default. You can then host this folder on IIS or any web +server you like. + +### Solution Details & Other Features + +#### Token-Based Authentication + +If you want to consume APIs/application services from a mobile +application, you can use the token based authentication mechanism just like +we do for the Angular client. The startup template includes the JwtBearer token +authentication infrastructure. + +We will use **Postman** (a chrome extension) to demonstrate +requests and responses. + +##### Authentication + +Just send a **POST** request to +**http://localhost:21021/api/TokenAuth/Authenticate** with the +**Content-Type="application/json"** header as shown below: + +Swagger UI auth + +We sent the values **usernameOrEmailAddress** and **password**. As seen +above, the result property of the returning JSON will contain the token and expiration +time (this is 24 hours by default and can be configured). We can save +it and use it for the next requests. + +##### About Multi-Tenancy + +The **API will work as host users by default**. You can send the **Abp.TenantId** +header value to work with a specified tenant. It's an integer value and by default is +1 for the default tenant. + +##### Using The API + +After we authenticate and get the **token**, we can use it to call any +**authorized** action. All **application services** can be +used remotely. For example, we can use the **User service** to get a +**list of users**: + +Using API + +We made a **GET** request to +**http://localhost:21021/api/services/app/user/getAll** with +**Content-Type="application/json"** and **Authorization="Bearer +*your-******auth-token*** **"**. All the functionality available on the UI is +also available as the API. + +#### Migrator Console Application + +The startup template includes a tool, Migrator.exe, to easily migrate your +databases. You can run this application to create/migrate the host and +tenant databases. + +Database Migrator + +This application gets the host connection string from its **own +appsettings.json file**. In the beginning, it will be the same as the appsettings.json +in the .Web.Host project. Be sure that the connection string +in the config file is the database you want. After getting the **host** +**connection string**, it first creates the host database and then applies the +migrations if they don't already exist. It then gets the connection strings of +the tenant databases and runs the migrations for those databases. It skips a +tenant if it does not have a dedicated database or if the database has already +been migrated by another tenant (for databases shared between multiple tenants). + +You can use this tool on the development or production environment to +migrate the databases on deployment instead of using EntityFramework's own +tooling (which requires some configuration and can work only for a single +database/tenant in one run). + +#### Unit Testing + +The startup template includes the test infrastructure setup and a few tests +under the .Test project. You can check them and write similar tests +easily. Actually, they are integration tests rather than unit tests +since they test your code with all of ASP.NET Boilerplate's infrastructure +(including validation, authorization, unit of work...). + +### Source Code + +This template is developed as an open source project and is available for free on GitHub: + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Core.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Core.md" new file mode 100644 index 0000000..92a06bf --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template-Core.md" @@ -0,0 +1,118 @@ +### Introduction + +The easiest way of starting a new project using ABP with **ASP.NET Core MVC** is to create a template on the [download page](/Templates). After creating and downloading your project, follow the steps below to run your application. + +- Open your solution in **Visual Studio 2017 v15.3.5+** and **build** + the solution. +- Select the '**Web.Mvc**' project as the startup project. +- Check the **connection string** in the **appsettings.json** file of the Web.Mvc project, change it if you want. +- Open **Package Manager Console** and run the **Update-Database** command + to create your database (ensure that the Default project is selected as + **.EntityFrameworkCore** in the Package Manager Console window). +- Run the application. + +If you have problems with running the application, please try closing and +opening Visual Studio again. It sometimes fails on first package +restore. + +#### Login + +Once you run the application, you will see the following login page: + +Login Page + +The username is '**admin**' and the password is '**123qwe**' by default. There +is also a "Default" tenant. After you login, you can see the +sample dashboard page: + +Dashboard + +#### About Multi-Tenancy + +In this template, **multi-tenancy is enabled by default**. You can +disable it in Core project's module class if you don't need it. + +### Token Based Authentication + +The startup template uses cookie-based authentication for browsers. However, +if you want to consume Web APIs or application services (those that are +exposed via the [dynamic web api](/Pages/Documents/Dynamic-Web-API)) from a +mobile application, you'll probably want a token-based authentication +mechanism. The startup template includes JwtBearer token authentication +infrastructure. + +Here, **Postman** (Chrome extension) will be used to demonstrate +requests and responses. + +#### Authentication + +Just send a **POST** request to +**http://localhost:62114/api/TokenAuth/Authenticate** with a +**Context-Type="application/json"** header as shown below: + +Request for token + +We sent the values **usernameOrEmailAddress** and **password**. As seen +above, the result property of the returning JSON contains the token and expiration +time (which is 24 hours by default and can be configured). We can save +it and use for the next requests. + +**About Multi-Tenancy +**The API will work as host users by default. You can send a **Abp.TenantId** +header value to work with a specified tenant. It's an integer value and +1 for the default tenant by default. + +#### Use API + +After you authenticate and get the **token**, we can use it to call any +**authorized** action. All **application services** can be +used remotely. For example, we can use the **User service** to get a +**list of users**: + +Call API + +Just made a **GET** request to +**http://localhost:62114/api/services/app/user/GetAll** with +**Content-Type="application/json"** and **Authorization="Bearer +*your-*** ***auth-token*** **"**. + +Almost all operations available on the UI are also available as a Web API, +since the UI uses the same Web API, and can be easily consumed. + +### Migrator Console Application + +The startup template includes a tool, Migrator.exe, to easily migrate your +databases. You can run this application to create/migrate the host and +tenant databases. + +Database Migrator + +This application gets the host connection string from it's **own +appsettings.json file**. In the beginning, it will be the +same in the appsettings.json in the .Web.Host project. +Be sure that the connection string +in the config file is the database you want. After getting the **host** +**connection string**, it first creates the host database and applies +migrations if they don't already exist. It then gets the connection strings of the +tenant databases and runs migrations against those databases. It skips a +tenant if it does not have a dedicated database or its database has already +been migrated by another tenant (for shared databases between multiple +tenants). + +You can use this tool on the development or on the production environment to +migrate databases on deployment instead of EntityFramework's own +tooling (which requires some configuration and can only work for a single +database/tenant in one run). + +### Unit Testing + +The startup template includes the test infrastructure setup and a few tests +under the .Test project. You can check them and write similar tests +easily. They are actually integration tests rather than unit tests, +since they test your code with all the ASP.NET Boilerplate infrastructure +(including validation, authorization, unit of work...). + +### Source Code + +This template is developed as an open source project and is available for free on GitHub: + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template.md" new file mode 100644 index 0000000..84f7e49 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Startup-Template.md" @@ -0,0 +1,104 @@ +### Introduction + +The easiest way of starting a new project using ABP with **ASP.NET MVC 5.x (and optionally AngularJS frontend)** is to create a template on the [download page](/Templates). + +After creating and downloading your project: + +- Open your solution in Visual Studio 2017 v15.3.5+. +- Select the '**Web**' project as the startup project. +- Open the Package Manager Console, select the '**EntityFramework**' project + as the **Default project** and run EntityFramework's + '**Update-Database**' command. This will create the database. You + can then change the **connection string** in the web.config. +- Run the application. The default username is '**admin**' and the password is + '**123qwe**'. + +Be sure you have installed Typescript 2.0+ in Visual Studio +because the Abp.Web.Resources NuGet package comes with d.ts and it requires +Typescript 2.0+. + +In this template, **multi-tenancy is enabled by default**. You can +disable it in the Core project's module class if you don't need it. + +### Token-Based Authentication + +The startup template uses cookie-based authentication for browsers. However, +if you want to consume Web APIs or application services (those that are +exposed via the [dynamic web api](/Pages/Documents/Dynamic-Web-API)) from a +mobile application, you probably want a token-based authentication +mechanism. The startup template includes the infrastructure for bearer-token + authentication. The **AccountController** in the **.WebApi** project contains +an **Authenticate** action to get the token. We can then use the token for the +next requests. + +Here **Postman** (a chrome extension) is used to demonstrate +requests and responses. + +#### Authentication + +Just send a **POST** request to +**http://localhost:6634/api/Account/Authenticate** with the +**Context-Type="application/json"** header as shown below: + +Request for token + +We sent a **JSON request body** which includes a **userNameOrEmailAddress** and +**password**. **tenancyName** should also be sent for **tenant** users. +As seen above, the **result** property of the returning JSON contains the token. +We can save it and use it for the next requests. + +#### Use API + +After we authenticate and get the **token**, we can use it to call +**authorized** actions. All **application services** can be +used remotely. For example, we can use the **user service** to get a +**list of roles**: + +Authorization via token + +We just made a **POST** request to +**http://localhost:6634/api/services/app/user/GetRoles** with +**Content-Type="application/json"** and **Authorization="Bearer +*your-*** ***auth-token*** **"** headers. The request body was empty **{}**. +For the most part, request and response bodies will be different for each API. + +Almost all operations available on the UI are also available as a Web API, +since the UI uses the same Web API, and can be easily consumed. + +### Migrator Console Application + +The startup template includes a tool, Migrator.exe, to easily migrate your +databases. You can run this application to create/migrate the host and +tenant databases. + +Database Migrator + +This application gets the host connection string from it's **own +appsettings.json file**. In the beginning, it will be the +same in the appsettings.json in the .Web.Host project. +Be sure that the connection string +in the config file is the database you want. After getting the **host** +**connection string**, it first creates the host database and applies +migrations if they don't already exist. It then gets the connection strings of the +tenant databases and runs migrations against those databases. It skips a +tenant if it does not have a dedicated database or its database has already +been migrated by another tenant (for shared databases between multiple +tenants). + +You can use this tool on the development or on the production environment to +migrate databases on deployment instead of EntityFramework's own +tooling (which requires some configuration and can only work for a single +database/tenant in one run). + +### Unit Testing + +The startup template includes the test infrastructure setup and a few tests +under the .Test project. You can check them and write similar tests +easily. They are actually integration tests rather than unit tests, +since they test your code with all the ASP.NET Boilerplate infrastructure +(including validation, authorization, unit of work...). + +### Source Code + +This template is developed as an open source project and is available for free on GitHub: + diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Tenant-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Tenant-Management.md" new file mode 100644 index 0000000..71c102d --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/Tenant-Management.md" @@ -0,0 +1,86 @@ +### About Multi-Tenancy + +We strongly recommend that you read the [multi-tenancy +documentation](/Pages/Documents/Multi-Tenancy) before this one. + +### Enabling Multi-Tenancy + +ASP.NET Boilerplate and Module Zero can run in **multi-tenant** or +**single-tenant** modes. Multi-tenancy is disabled by default. We can +enable it in the PreInitialize method of our +[module](/Pages/Documents/Module-System) as shown below: + + [DependsOn(typeof(AbpZeroCoreModule))] + public class MyCoreModule : AbpModule + { + public override void PreInitialize() + { + Configuration.MultiTenancy.IsEnabled = true; + } + + ... + } + +Note: Even if our application is not multi-tenant, we must define a +default tenant (see Default Tenant section of this document).  + +When we create a project [template](/Templates) based on ASP.NET +Boilerplate and Module Zero, we have the **Tenant** entity and the +**TenantManager** domain service. + +### Tenant Entity + +The Tenant entity represents a Tenant of the application. + + public class Tenant : AbpTenant + { + + } + +It's derived from a generic **AbpTenant** class. Tenant entities are +stored in the **AbpTenants** table in the database. You can add your own custom +properties to the Tenant class. + +The AbpTenant class defines some base properties, the most important are: + +- **TenancyName**: This is the **unique** name of a tenant in the + application. It should not normally be changed. It can be used to + allocate subdomains to tenants like '**mytenant**.mydomain.com'. + As such, it cannot contain spaces. + Tenant.**TenancyNameRegex** constant defines the naming rule. +- **Name**: An arbitrary, human-readable, long name of the tenant. +- **IsActive**: True, if this tenant can use the application. If it's + false, no user of this tenant can login to the application. + +The AbpTenant class inherits **FullAuditedEntity**. This means it +has creation, modification and deletion **audit properties**. It is also +**[Soft-Delete](/Pages/Documents/Data-Filters#isoftdelete)**, so +when we delete a tenant, it's not deleted from the database, just marked as +deleted. + +Finally, the **Id** of AbpTenant is defined as an **int**. + +### Tenant Manager + +The Tenant Manager is a service to perform the **domain logic** for tenants: + + public class TenantManager : AbpTenantManager + { + public TenantManager(IRepository tenantRepository) + : base(tenantRepository) + { + + } + } + +The TenantManager is also used to manage the tenant +[features](/Pages/Documents/Feature-Management). You can add your own +methods here. You can also override any method of the AbpTenantManager base +class for your own needs. + +### Default Tenant + +ASP.NET Boilerplate and Module Zero assume that there is a pre-defined +tenant where the TenancyName is '**Default**' and the Id is **1**. In a +single-tenant application, this is used as the only tenant. In a +multi-tenant application, you can delete it or make it passive. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/User-Management.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/User-Management.md" new file mode 100644 index 0000000..30757d4 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/User-Management.md" @@ -0,0 +1,299 @@ +### User Entity + +The User entity represents a **user of the application**. It should be +derived from the **AbpUser** class as shown below: + + public class User : AbpUser + { + //add your own user properties here + } + +This class will be created when you download an ABP template with the option in the below image is selected. + +Login Page + +Users are +stored in the **AbpUsers** table in the database. You can add custom +properties to the User class (and create database migrations for the +changes). + +The AbpUser class defines some base properties. Some of the properties are: + +- **UserName**: Login name of the user. Should be **unique** for a + [tenant](/Pages/Documents/Zero/Tenant-Management). +- **EmailAddress**: Email address of the user. Should be **unique** + for a [tenant](/Pages/Documents/Zero/Tenant-Management). +- **Password**: Hashed password of the user. +- **IsActive**: True, if this user can login to the application. +- **Name** and **Surname** of the user. + +There are also some properties like **Roles**, **Permissions**, +**Tenant**, **Settings**, **IsEmailConfirmed**, and so on. Check the AbpUser +class for more information. + +The AbpUser class is inherited from **FullAuditedEntity**. That means it has +creation, modification and deletion audit properties. It's also implements +**[Soft-Delete](/Pages/Documents/Data-Filters#isoftdelete)** , so +when we delete a user, it's not deleted from database, just marked as +deleted. + +The AbpUser class implements the +[IMayHaveTenant](/Pages/Documents/Data-Filters#imayhavetenant) filter +to properly work in a multi-tenant application. + +Finally, the **Id** of the User is defined as **long**. + +### User Manager + +**UserManager** is a service to perform **domain logic** for users: + + public class UserManager : AbpUserManager + { + //... + } + +You can [inject](/Pages/Documents/Dependency-Injection) and use +UserManager to create, delete, update users, grant permissions, change +roles for users and much more. You can add your own methods here. Also, +you can **override** any method of the **AbpUserManager** base class for +your own needs. + +#### Multi-Tenancy + +*If you're not creating a multi-tenant application, you can skip this +section. See the [multi-tenancy documentation](../Multi-Tenancy.md) for +more information about multi-tenancy.* + +UserManager is designed to work for a **single tenant** at a time. It +works with the **current tenant** by default. Let's see some usages of +the UserManager: + + public class MyTestAppService : ApplicationService + { + private readonly UserManager _userManager; + + public MyTestAppService(UserManager userManager) + { + _userManager = userManager; + } + + public void TestMethod_1() + { + //Find a user by email for current tenant + var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com"); + } + + public void TestMethod_2() + { + //Switch to tenant 42 + CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42); + + //Find a user by email for tenant 42 + var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com"); + } + + public void TestMethod_3() + { + //Disabling MayHaveTenant filter, so we can reach all users + using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + //Now, we can search for a user name in all tenants + var users = _userManager.Users.Where(u => u.UserName == "sampleuser").ToList(); + + //Or we can add TenantId filter if we want to search for a specific tenant + var user = _userManager.Users.FirstOrDefault(u => u.TenantId == 42 && u.UserName == "sampleuser"); + } + } + } + +#### User Login + +Module Zero defines LoginManager which has a **LoginAsync** method used +for logging into the application. It checks all logic for the login and returns a +login result. The LoginAsync method also **automatically saves all login +attempts** to the database (even if it's a failed attempt). You can use the +**UserLoginAttempt** entity to query it. + +#### About IdentityResults + +Some methods of UserManager return IdentityResult as a result instead of +throwing exceptions for some cases. This is the nature of ASP.NET Identity +Framework. Module Zero also follows it, so we should check this +returning result object to know if the operation succeeded. + +Module Zero defines the **CheckErrors** extension method that automatically +checks errors and throws an exception (a localized +[UserFriendlyException](/Pages/Documents/Handling-Exceptions#userfriendlyexception)) +if needed. Example usage: + + (await UserManager.CreateAsync(user)).CheckErrors(); + +To get localized exceptions, we must provide a +[ILocalizationManager](/Pages/Documents/Localization) instance: + + (await UserManager.CreateAsync(user)).CheckErrors(LocalizationManager); + +### External Authentication + +The Login method of Module Zero authenticates a user from the **AbpUsers** +table in the database. Some applications may require you to authenticate +users from some external sources (like active directory, from another +database's tables, or even from a remote service). + +For such cases, UserManager defines an extension point named 'external +authentication source'. We can create a class derived from +**IExternalAuthenticationSource** and register it to the configuration. +There is a **DefaultExternalAuthenticationSource** class to simplify +the implementation of IExternalAuthenticationSource. Let's see an example: + + public class MyExternalAuthSource : DefaultExternalAuthenticationSource, ITransientDependency + { + public override string Name + { + get { return "MyCustomSource"; } + } + + public override Task TryAuthenticateAsync(string userNameOrEmailAddress, string plainPassword, Tenant tenant) + { + //TODO: authenticate user and return true or false + } + } + +In the TryAuthenticateAsync method, we can check the user name and password from +some source and return true if a given user is authenticated by it. +We can also override the CreateUser and UpdateUser methods to +control user creation and updating for this source. + +When a user is authenticated by an external source, Module Zero checks if +this user exists in the database (AbpUsers table). If not, it calls +CreateUser to create the user, otherwise it calls UpdateUser to allow the +authentication source to update existing user information. + +We can define more than one external authentication source in an +application. The AbpUser entity has an AuthenticationSource property that +shows which source authenticated this user. + +To register our authenciation source, we can use some code like this in the +[PreInitialize](/Pages/Documents/Module-System) method of our module: + + Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add(); + +#### LDAP/Active Directory + +LdapAuthenticationSource is an implementation of external authentication +to make users login with their LDAP (active directory) user name and +password. + +If we want to use LDAP authentication, we must first add the +[Abp.Zero.Ldap](https://www.nuget.org/packages/Abp.Zero.Ldap) NuGet +package to our project (generally to the Core (domain) project). We then +must extend the **LdapAuthenticationSource** for our application as shown +below: + + public class MyLdapAuthenticationSource : LdapAuthenticationSource + { + public MyLdapAuthenticationSource(ILdapSettings settings, IAbpZeroLdapModuleConfig ldapModuleConfig) + : base(settings, ldapModuleConfig) + { + } + } + +Lastly, we must set a module dependency to **AbpZeroLdapModule** and +**enable** LDAP with the auth source created above: + + [DependsOn(typeof(AbpZeroLdapModule))] + public class MyApplicationCoreModule : AbpModule + { + public override void PreInitialize() + { + Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource)); + } + + ... + } + +After these steps, the LDAP module will be enabled for your application, but +LDAP auth is not enabled by default. We can enable it using the settings. + +##### Settings + +The **LdapSettingNames** class defines constants for setting names. You can +use these constant names while changing settings (or getting settings). +LDAP settings are **per-tenant** (for multi-tenant applications), so +different tenants have different settings (see the setting definitions on +[github](https://github.com/aspnetboilerplate/module-zero/blob/master/src/Abp.Zero.Ldap/Ldap/Configuration/LdapSettingProvider.cs)).  + +As you can see in the MyLdapAuthenticationSource **constructor**, +LdapAuthenticationSource expects **ILdapSettings** as a constructor +argument. This interface is used to get the LDAP settings like domain, user +name and password to connect to Active Directory. The default implementation +(**LdapSettings** class) gets these settings from the [setting +manager](/Pages/Documents/Setting-Management). + +If you work with Setting manager, then there's no problem. You can change the LDAP +settings using the [setting manager +API](/Pages/Documents/Setting-Management). If you want, you can add some +initial seed data to the database to enable LDAP auth by default. + +Note: If you don't define a domain, username and password, LDAP +authentication works for the current domain if your application runs in a +domain with appropriate privileges. + +##### Custom Settings + +If you want to define another setting source, you can implement a custom +ILdapSettings class as shown below: + + public class MyLdapSettings : ILdapSettings + { + public async Task GetIsEnabled(int? tenantId) + { + return true; + } + + public async Task GetContextType(int? tenantId) + { + return ContextType.Domain; + } + + public async Task GetContainer(int? tenantId) + { + return null; + } + + public async Task GetDomain(int? tenantId) + { + return null; + } + + public async Task GetUserName(int? tenantId) + { + return null; + } + + public async Task GetPassword(int? tenantId) + { + return null; + } + } + +Then register it to IOC in PreInitialize method of your module: + + [DependsOn(typeof(AbpZeroLdapModule))] + public class MyApplicationCoreModule : AbpModule + { + public override void PreInitialize() + { + IocManager.Register(); //change default setting source + Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource)); + } + + ... + } + +Then you can get the LDAP settings from another source. + +#### Social Logins + +See the [social authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/) +document for more info about social logins. diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/_Empty.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/_Empty.md" new file mode 100644 index 0000000..a67ab75 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/Zero/_Empty.md" @@ -0,0 +1,3 @@ +### Introduction + +.... diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/_Empty.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/_Empty.md" new file mode 100644 index 0000000..a67ab75 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/_Empty.md" @@ -0,0 +1,3 @@ +### Introduction + +.... diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/bootstrap.min.css" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/bootstrap.min.css" new file mode 100644 index 0000000..9aebd31 --- /dev/null +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/bootstrap.min.css" @@ -0,0 +1,37 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}} + +.bs-callout { + margin: 20px 0; + padding: 15px 30px 15px 15px; + border-left: 5px solid #eee; +} +.bs-callout h4 { + margin-top: 0; +} +.bs-callout p:last-child { + margin-bottom: 0; +} +.bs-callout code, +.bs-callout .highlight { + background-color: #fff; +} + +/* Themes for different contexts */ +.bs-callout-danger { + background-color: #fcf2f2; + border-color: #dFb5b4; +} +.bs-callout-warning { + background-color: #fefbed; + border-color: #f1e7bc; +} +.bs-callout-info { + background-color: #f0f7fd; + border-color: #d0e3f0; +} \ No newline at end of file diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpLayers.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpLayers.png" new file mode 100644 index 0000000..58e1555 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpLayers.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpWeb-localization-source.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpWeb-localization-source.png" new file mode 100644 index 0000000..b7d6f5f Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/AbpWeb-localization-source.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure.png" new file mode 100644 index 0000000..b3f40b8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure2.png" new file mode 100644 index 0000000..76b3014 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/StsSolutionStructure2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/UI-Alerts.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/UI-Alerts.png" new file mode 100644 index 0000000..3c15e41 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/UI-Alerts.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/abp-nlayer-architecture.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/abp-nlayer-architecture.png" new file mode 100644 index 0000000..a532907 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/abp-nlayer-architecture.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/aspnet-core-token-auth.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/aspnet-core-token-auth.png" new file mode 100644 index 0000000..4a8e0b0 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/aspnet-core-token-auth.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/confirmation_message.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/confirmation_message.png" new file mode 100644 index 0000000..428ddb4 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/confirmation_message.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/create-plugin-project.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/create-plugin-project.png" new file mode 100644 index 0000000..85bc047 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/create-plugin-project.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/database-migrator.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/database-migrator.png" new file mode 100644 index 0000000..bb7ddda Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/database-migrator.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/debug-options.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/debug-options.png" new file mode 100644 index 0000000..47bb6f6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/debug-options.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-csproj.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-csproj.png" new file mode 100644 index 0000000..cb48c51 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-csproj.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-xproj.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-xproj.png" new file mode 100644 index 0000000..f782c4f Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-project-xproj.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-sample-file-csproj.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-sample-file-csproj.png" new file mode 100644 index 0000000..15d28ce Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/embedded-resource-sample-file-csproj.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/enable-sourcelink.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/enable-sourcelink.png" new file mode 100644 index 0000000..ce79516 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/enable-sourcelink.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-default.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-default.png" new file mode 100644 index 0000000..ce76cbf Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-default.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-user-friendly.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-user-friendly.png" new file mode 100644 index 0000000..dc0a087 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-user-friendly.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-validation.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-validation.png" new file mode 100644 index 0000000..fe21331 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/error-page-validation.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/include_module_zero_checkbox.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/include_module_zero_checkbox.png" new file mode 100644 index 0000000..569dfd8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/include_module_zero_checkbox.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/json-localization-source-files.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/json-localization-source-files.png" new file mode 100644 index 0000000..50c29a3 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/json-localization-source-files.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-authenticate.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-authenticate.png" new file mode 100644 index 0000000..1e337cb Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-authenticate.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-request.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-request.png" new file mode 100644 index 0000000..a7b4b41 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/jwt-token-request.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files.png" new file mode 100644 index 0000000..575c85f Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files2.png" new file mode 100644 index 0000000..3fe93c2 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/localization_files2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-home.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-home.png" new file mode 100644 index 0000000..b4f3940 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-home.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-login.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-login.png" new file mode 100644 index 0000000..1530042 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/module-zero-core-template-ui-login.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/mysql-integration-summary.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/mysql-integration-summary.png" new file mode 100644 index 0000000..71ceecc Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/mysql-integration-summary.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/notification-warn.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/notification-warn.png" new file mode 100644 index 0000000..193b543 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/notification-warn.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-log.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-log.png" new file mode 100644 index 0000000..24660b7 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-log.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-nuget-packages.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-nuget-packages.png" new file mode 100644 index 0000000..5122fc9 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-nuget-packages.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-solution.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-solution.png" new file mode 100644 index 0000000..5139bac Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-solution.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-wwwroot.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-wwwroot.png" new file mode 100644 index 0000000..150cb21 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/plugin-wwwroot.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/postgresql-integration-summary.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/postgresql-integration-summary.png" new file mode 100644 index 0000000..835fb2a Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/postgresql-integration-summary.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files.png" new file mode 100644 index 0000000..a6edfe7 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files_content.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files_content.png" new file mode 100644 index 0000000..421e688 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/resource_files_content.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-authanticate.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-authanticate.png" new file mode 100644 index 0000000..e8417e8 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-authanticate.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-getroles.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-getroles.png" new file mode 100644 index 0000000..3ce4701 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/startup-mvc5-postman-getroles.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_message.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_message.png" new file mode 100644 index 0000000..2e2995f Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_message.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_notification.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_notification.png" new file mode 100644 index 0000000..938362e Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/success_notification.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api-v2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api-v2.png" new file mode 100644 index 0000000..774a4e1 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api-v2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api.png" new file mode 100644 index 0000000..f8fa079 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-api.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-auth.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-auth.png" new file mode 100644 index 0000000..2651216 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-angular-auth.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-module-zero-core-template.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-module-zero-core-template.png" new file mode 100644 index 0000000..fb2ed65 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui-module-zero-core-template.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui.png" new file mode 100644 index 0000000..64d2ffb Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/swagger-ui.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-authenticate.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-authenticate.png" new file mode 100644 index 0000000..5bb3bc6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-authenticate.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request-v2.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request-v2.png" new file mode 100644 index 0000000..c45a7e6 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request-v2.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request.png" new file mode 100644 index 0000000..83db9a0 Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/token-request.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/ui_busy_sample.png" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/ui_busy_sample.png" new file mode 100644 index 0000000..d0c951b Binary files /dev/null and "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/ABP\350\213\261\346\226\207\346\226\207\346\241\243/images/ui_busy_sample.png" differ diff --git "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/Readme.md" "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/Readme.md" index bfcdef0..3a49891 100644 --- "a/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/Readme.md" +++ "b/ABP\346\241\206\346\236\266\344\270\255\346\226\207\346\226\207\346\241\243/Readme.md" @@ -50,7 +50,7 @@ - [集成Swagger UI](5.4ABP分布式服务-集成SwaggerUI.md) - [ASPNET Core 集成OData](5.5ABP分布式服务-ASPNETCoreOData集成.md) -## ABP表现层 +## 表现层 - [MVC Controllers](6.1ABP表现层-Mvc控制器.md) - [MVC视图](6.2ABP表现层-Mvc视图.md) @@ -76,7 +76,7 @@ - [集成SignalR](8.2ABP实时服务-集成SignalR.md) - [集成SignalR AspNet Core](8.3ABP实时服务-集成SignalRAspNetCore.md) -## 基础设施层 +## 基础设施层(对象关系映射层) - [集成EntityFramework](1ABP基础设施层-集成EntityFramework.md) - [集成NHibernate](2ABP基础设施层-集成NHibernate.md) @@ -97,10 +97,10 @@ - [多租户管理](AbpZero/2.1ABPZero-多租户管理.md) - [版本管理](AbpZero/2.2ABPZero-版本管理.md) - - +- [用户管理](http://www.cnblogs.com/farb/p/moduleZeroUserManagement.html) +- [角色管理](http://www.cnblogs.com/farb/p/ModuleZeroRoleManagement.html) - [组织单位管理](AbpZero/2.4ABPZero-组织单位管理.md) - - +- [权限管理](http://www.cnblogs.com/farb/p/ModuleZeroPermissonManagement.html) +- [语言管理](http://www.cnblogs.com/farb/p/ModuleZeroLanguageManagement.html) diff --git a/README.md b/README.md index 8b3165c..a01e942 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 首先表示欢迎! -您可以到 [52ABP文档中心](https://www.52abp.com/Documents/Index) 进行浏览 +您可以到 [52ABP文档中心](https://www.52abp.com/wiki/index) 进行浏览 52ABP涉及到的文档信息库。 @@ -12,7 +12,7 @@ 本文档的Markdown的原始文件全部保存在[Documents](https://github.com/52ABP/Documents)路径中,为了方便大家的管理和维护,我启用了版本管理,版本类别是对应ABP的版本号。 -效果查看:[52ABP文档中心](https://www.52abp.com/Documents/Index) +效果查看:[52ABP文档中心](https://www.52abp.com/wiki/index) 欢迎大家进行共同参与维护内容。 @@ -20,13 +20,13 @@ 在任何形式的参与前,请先阅读 贡献者文档。如果你希望参与贡献,欢迎 [Pull Request](https://github.com/52ABP/Documents/pulls),或给我们 报告 [Bug](https://github.com/52ABP/Documents/issues)。 -> 贡献规则和详情参见 [贡献说明](GettingInvoled/contributing.md) +> 贡献规则和详情参见 [贡献说明](52ABP开发人员中心/52ABP团队欢迎您的到来.md) --- Welcome! -Please view this at [52ABP Wiki Center](https://www.52abp.com/Documents/Index) +Please view this at [52ABP Wiki Center](https://www.52abp.com/wiki/index)