Skip to content

Controllers

Arsenty Politov edited this page Mar 15, 2019 · 1 revision

Controllers

Provides a way to implement Mvc controllers as a set of pre-implemented, but extendable action handlers.

Related packages

  • DevGuild.AspNetCore.Controllers.Mvc
  • DevGuild.AspNetCore.Controllers.Mvc.Crud
  • DevGuild.AspNetCore.ObjectModel
  • DevGuild.AspNetCore.Services.ModelMapping

Description

These packages allow developer to implement different types of the CRUD controllers by simply inheriting from one of the predefined base controllers, or by combining a set of pre-implemented action handlers manually. Predefined controllers are constructed from a predefined set of pre-implemented action handlers.

Each action handler is dedicated to handling one specific action but still could contain multiple public-facing methods for handing different HTTP-methods of a single action. Action handlers have a strict flow of implementation actions predefined, but most of them could be overridden either by inheriting the action handler or by using action handler overrides, which is a preferred method when using action handler in a controller.

Each action handler contains a property Overrides which is strongly-typed for the handler-specific overrides class. This property allows accessing the instance of the overrides class which contains a set of properties that correspond to action handler's implementation methods. Overriding any of these methods can be done by assigning a delegate to the property of an instance of the overrides class that represents the method that need to be overridden.

Action handlers

Models

Provided action handlers imply and support usage of multiple model classes representing a single entity in different situations:

  • Data model - defines the way the entity is stored in the database. Follows the rules of designing classes for EntityFramework.
  • Display view model - contains everything that is required to display an entity on a page. Does not have any restrictions.
    • Usually, data model can also be used as a display view model.
  • Edit view model - defines a model that represents entity for the edit form. Can also contain additional information required for the form rendering. Must be compatible with the model binding.
    • In some cases view model for the create form can be the same as for the edit form.

Examples

Simple example

// Data model
public class Product
{
    public Guid Id { get; set; }

    [Required]
    [MaxLength(200)]
    public String Name { get; set; }
}

// Display view model - similar to data model
public class ProductDisplayModel
{
    public Guid Id { get; set; }

    public String Name { get; set; }
}

// Create view model - does not contain product ID,
// since user only fills the name and ID is generated automatically.
public class ProductCreateModel
{
    [Required]
    [MaxLength(200)]
    public String Name { get; set; }
}

// Edit view model - in addition to product name, contains reference to the original un-edited entity
// in the form of a display view model reference. This can be used, for example,
// to keep display original entity name until changes are saved.
public class ProductEditModel
{
    [BindNever]
    public ProductDisplayModel Original { get; set; }

    [Required]
    [MaxLength(200)]
    public String Name { get; set; }
}

More complex example

// Data model
public class ApplicationUser : User
{
}

// Display view model that aggregates information necessary to display information about user.
// In that case it combines User entity and list of used Roles.
// This list of roles cannot be accessed from instance of ApplicationUser directly and has to be loaded some other way.
public class UserDetailsModel
{
    public ApplicationUser User { get; set; }

    public List<Role> Roles { get; set; }
}

// Create view model that defines fields that need to be filled to create a new user.
// In addition to that, this model contains a list of all available roles,
// that can be used to populate roles-selecting control.
public class UserCreateModel
{
    [BindNever]
    public List<Role> AvailableRoles { get; set; }

    [Required]
    [MaxLength(256)]
    [Display(Name = "Username")]
    public String UserName { get; set; }

    [MaxLength(256)]
    [Display(Name = "E-mail")]
    public String Email { get; set; }

    [Required]
    [Display(Name = "Password")]
    public String Password { get; set; }

    [Required]
    [Compare(nameof(UserCreateModel.Password))]
    [Display(Name = "Confirm password")]
    public String ConfirmPassword { get; set; }

    [Display(Name = "Roles")]
    public List<Guid> SelectedRoles { get; set; }
}

// Edit view model that defines how the user can be edited.
// In this case, password and confirm password fields are not required as they were in create model.
// This allows changing other properties of a user without affecting his password.
public class UserEditModel
{
    [BindNever]
    public UserDetailsViewModel Original { get; set; }

    [BindNever]
    public List<Role> AvailableRoles { get; set; }

    [Required]
    [MaxLength(256)]
    [Display(Name = "Username")]
    public String UserName { get; set; }

    [MaxLength(256)]
    [Display(Name = "E-mail")]
    public String Email { get; set; }

    [Display(Name = "Password")]
    public String Password { get; set; }

    [Compare(nameof(UserEditModel.Password))]
    [Display(Name = "Confirm password")]
    public String ConfirmPassword { get; set; }

    [Display(Name = "Roles")]
    public List<Guid> SelectedRoles { get; set; }
}

Model interfaces

Package DevGuild.AspNetCore.ObjectModel provides various interfaces that can be used by model classes:

  • Index page view models:

    • IEntityIndexModel - interface that must be implemented by index page view model, used by BasicCrudIndexActionHandler. Default implementation is also provided as DefaultEntityIndexModel class.
    • IEntityPaginatedIndexModel - interface that must be implemented by index page view model, used by BasicCrudPaginatedIndexActionHandler. Default implementation is also provided as DefaultEntityPaginatedIndexModel class.
  • Edit page view models:

    • IEditModelOriginalEntity - interface that can be implemented by an edit view model, used by BasicCrudEditActionHandler that will allow action handle to fill the reference to original entity in the edit model automatically, without any additional overrides.
  • Concurrency-checked model:

    • IConcurrencyCheck - interface that can be implemented by a data model, that will be set to new unique value every time the entity is modified. This is performed automatically by action handlers.
    • IEditModelConcurrencyCheck - interface that can be implemented by an edit view model. This will cause action handlers to check whether the concurrency token within edit model is the same as the one stored in the database. If they are not, then any editing actions will fail, since that means that the entity was modified after edit page was loaded but before it was saved and saving current changes can lead to loosing data.
  • Hierarchical models:

    • IHierarchicalCreateModel - interface that must be implemented by create model used by BasicHierarchicalCrudCreateActionHandler.
    • IHierarchicalEntity - interface that must be implemented by data model used by BasicHierarchicalCrudCreateActionHandler.

Model mapping

DevGuild.AspNetCore.Services.ModelMapping package provides model mapping services that are used by action handlers to perform mapping between different type of models. This mapping is performed automatically, but can be configured or completely overridden in the action handlers.

In addition to mapping model properties, provided services are also used to determine model identifiers.

Default behavior and configuration options

  • Properties are mapped by name and type:
    • Property of one model can be mapped to a property of another model with a different name by using [PropertyMapping] attribute to specify mapped name.
    • Mapping to a property of different type is not supported at the moment.
    • Property mapping can be suppressed either entirely, or for a single direction (e.g. property will be mapped from data model to view model but not from view model to data model) by using [PropertyMapping] attribute.
  • If model has an Id property, its treated as its identifier by default:
    • This can be changed by using either [Key] attribute or [KeyMapping] attribute to specify identifier property or properties explicitly.

Permissions

To provide the ability to check, whether the requesting user has necessary permissions to perform a specific action, action handlers use permission validators that must be provided to the most action handlers during their creation. There are 3 permission validators interfaces defined in provided packages:

  • IEntityPermissionsValidator - defines interface to perform permissions validation for basic entities.
  • IDependentEntityPermissionsValidator - defines interface to perform permissions validation for entities that have a dependency on other entities.
  • IHierarchicalEntityPermissionsValidator - defines interface to perform permissions validation for hierarchical entities.

These interfaces define a following sets of methods that are used to perform the validation:

  • CanACTIONAsync - checks if user can perform the ACTION and returns result of this check.
  • DemandCanACTIONAsync - checks if user can perform the ACTION and throws an exception otherwise.
  • RequireACTIONAccessAsync - applies a filter to the provided query, leaving only entities with which user can perform the ACTION.

Default implementations of these services use Permissions system to perform permissions check. These permission validators require a reference to a IPermissionsHub service and a set of security scopes (for entity type, entity and property):

    var permissionsValidator = new DefaultEntityPermissionsValidator<Country>(
        permissionsHub: this.ControllerServices.PermissionsHub,
        typeManagerPath: "/Domain/Country",
        entityManagerPath: "/Domain/Country/Entities/{entity:Country}",
        propertyManagerPath: "/Domain/Country/Properties/{property:String}");

Other services

To simplify injection of dependencies into predefined controllers and action handlers, IEntityControllerServices service is introduced. It aggregates required dependencies in a single service. The following services are available through IEntityControllerServices:

  • IRepository is used to access the database or other data storage.
  • IPermissionsHub is used to check permissions.
  • IViewModelMappingManager is used to map various model types between each other.
  • IServiceProvider can be used to resolve necessary services later.

Predefined controllers

The following predefined controllers are provided by default by these packages:

Other components

Pagination tag helper

Pagination tag helper can be used to render pagination controls, based on a IPaginationInfo that defines following properties:

  • TotalItems - total number of items.
  • TotalPages - total number of pages.
  • ItemsPerPage - maximum number of items per single page.
  • CurrentPage - current page.

Usage:

<pagination
    info="Model.Items"
    page-url-generator="@(page => page == 1 ? Url.Action("Index") : Url.Action("Index", new { page }))">
</pagination>
  • info attribute should be set to an instance of an object that implements IPaginationInfo interface.
  • page-url-generator attribute should be set to a delegate or lambda expression that return page URL based on page number.

Adding services

To add all necessary services for predefined controllers and action handlers to function, simply add the following lines to ConfigureServices method of Startup class:

services.AddModelMapping();
services.AddControllerServices();

Clone this wiki locally