_images/platformus.png

Platformus CMS

Platformus is free, open source, and cross-platform developer-friendly CMS based on ASP.NET Core, ExtCore framework, and Magicalizer.

It can be used for rapid development of the websites and admin panels for mobile and web applications. There are built-in features to describe, create, and deliver multilingual and multicultural content as HTML or JSON without writing code, but it is possible to develop custom extensions with the standardized user interface when performance is important. Wide range of the tag helpers makes it simpler and faster.

Platformus CMS is modular and extendable, it can run on different environments (Linux, Mac, Windows, and clouds) and supports different database types (PostgreSQL, SQLite, SQL Server).

_images/111.png _images/23.png _images/32.png

Contents

Getting Started

To start using Platformus CMS you need to either reference it as the NuGet packages (the preferred way) or the source code, prepare the database by executing the database scripts (with both schema and data), and create content.

Using as the NuGet Packages

Using Platformus CMS as the NuGet packages is the preferred way (unless you consider the source code only as a starting point for your own project and do not need any updates in the future). Your host web application can contain any application-specific code, and you can extend the built-in features and modify the default behaviors using the Platformus public API.

  1. Create an ASP.NET Core host web application (or use an existing one):
_images/19.png _images/21.png
  1. Open NuGet Package Manager and add dependencies on the following Platformus packages:
_images/31.png
  • Platformus.Core.Data.EntityFramework.Sqlite
  • Platformus.Images
  • Platformus.Website.Backend
  • Platformus.Website.Data.EntityFramework.Sqlite
  • Platformus.Website.Frontend
  • Platformus.WebApplication

Or you can add them manually by editing .csproj file of your web application project:

<ItemGroup>
  <PackageReference Include="Platformus.Core.Data.EntityFramework.Sqlite" Version="4.0.0" />
  <PackageReference Include="Platformus.Images" Version="4.0.0" />
  <PackageReference Include="Platformus.Website.Backend" Version="4.0.0" />
  <PackageReference Include="Platformus.Website.Data.EntityFramework.SqlServer" Version="4.0.0" />
  <PackageReference Include="Platformus.Website.Frontend" Version="4.0.0" />
  <PackageReference Include="Platformus.WebApplication" Version="4.0.0" />
</ItemGroup>
  1. Open your Startup class.

Add the services.AddPlatformus() extension method call inside the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
  services.AddPlatformus();
}

Add the StorageContextOptions options class configuration inside the ConfigureServices method in order to provide the connection string (of course, you should take it from the application settings):

public void ConfigureServices(IServiceCollection services)
{
  services.Configure<StorageContextOptions>(options =>
    {
      options.ConnectionString = this.configuration.GetConnectionString("Default");
    }
  );

  services.AddPlatformus(this.extensionsPath);
}

Add the applicationBuilder.UsePlatformus() extension method call inside the Configure method:

public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment)
{
  if (webHostEnvironment.IsDevelopment())
    applicationBuilder.UseDeveloperExceptionPage();

  applicationBuilder.UsePlatformus();
}

Don’t forget to include the Platformus.WebApplication.Extensions namespace in order these extension methods to be resolved.

  1. Execute the Platformus database scripts on your database.

5. Run your web application and navigate to /backend to configure Platformus. Use the default “admin@platformus.net” and “admin” credentials to sign in.

Using as the Source Code

Use Platformus CMS as the source code only if you consider it as a starting point for your own project and do not need any updates in the future.

  1. Create an ASP.NET Core host web application (or use an existing one):
_images/110.png _images/22.png

2. Download Platformus sources from the GitHub. Copy them into your solution folder.

  1. Add dependencies on the following projects to your web application project:
  • Platformus.Core.Data.EntityFramework.Sqlite
  • Platformus.Images
  • Platformus.Website.Backend
  • Platformus.Website.Data.EntityFramework.Sqlite
  • Platformus.Website.Frontend
  • Platformus.WebApplication
  1. Open your Startup class.

Add the services.AddPlatformus() extension method call inside the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
  services.AddPlatformus();
}

Add the StorageContextOptions options class configuration inside the ConfigureServices method in order to provide the connection string (of course, you should take it from the application settings):

public void ConfigureServices(IServiceCollection services)
{
  services.Configure<StorageContextOptions>(options =>
    {
      options.ConnectionString = this.configuration.GetConnectionString("Default");
    }
  );

  services.AddPlatformus(this.extensionsPath);
}

Add the applicationBuilder.UsePlatformus() extension method call inside the Configure method:

public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment)
{
  if (webHostEnvironment.IsDevelopment())
    applicationBuilder.UseDeveloperExceptionPage();

  applicationBuilder.UsePlatformus();
}

Don’t forget to include the Platformus.WebApplication.Extensions namespace in order these extension methods to be resolved.

  1. Execute the Platformus database scripts on your database.

5. Run your web application and navigate to /backend to configure Platformus. Use the default “admin@platformus.net” and “admin” credentials to sign in.

Database Scripts

Currently Platformus CMS supports following database types:

  • PostgreSQL database
  • SQLite database
  • SQL Server database

GitHub repository contains SQL scripts for each database type, separately with the schema and initial data.

Tutorial: Basic Content Management

To understand better how the Platformus content management works, let’s use the default personal website sample and add blog feature to it.

There are two ways to work with content using Platformus CMS: using the built-in Website extension and writing a custom one. The first option is suitable when simplicity and speed of development are more important than application performance. It works good for website pages, blog categories, posts, tags etc. The Website extension implements the EAV pattern using the classes and members to describe content, and objects, properties, and relations to store it. Writing custom extension is more complex task, but in this case performance and flexibility are maximized. In this tutorial we will go the first way.

First of all, we need the blog post pages. Each post page should have the same properties as the regular page, but also it needs preview (a small piece of content), image, and creation date. In the Platformus context, all the pages are objects which are described by the classes. Therefore, to create new type of page (and new type of object) we need to create the corresponding class first.

Go to the backend and sign in (navigate to https://localhost:5000/backend/) and then go to the Development/Classes section. There are already two classes here: Page and Regular Page.

_images/1.png

The Page class is abstract, it means that it is used as the base class for the other ones (class copies all the members of its parent class). Click the Members link of the Page class to see the list of its members. As you can see, there are the standard page properties here, like URL, Content, Title, META description, and META keywords:

_images/2.png

Now return to the class list and click the Create class button. Select the Page class as the parent class for our new one. Fill the Code, Name, and Pluralized name fields as shown below:

_images/3.png

Click the Save button. New class is created. Now go to the list of its members. It is empty for now (but don’t forget that our class will have all the members from the Page class, because it is selected as the parent one).

Let’s add the Preview member to our class. Click the Create member button and fill the Code, Name, and Position fields as shown below:

_images/4.png

If we had a lot of the members, we could use tabs to group them on the object edit page, but in our case, we only use it to group the properties that are related to SEO in the parent class. Position is set to 5, because we want our property editor to appear before the Content property one (Content member has position set to 10).

Now click the Property tab and fill the fields as shown below:

_images/5.png

When you change the property data type, the set of the fields on this tab is changed too. You can add your own data types and specify their properties (as well as the client-side editors that are used to edit them). For the properties that have short values we can set the Is property visible in list checkbox, so that properties will be displayed in the object list (we will see that later). Now click the Save button again, our member is created.

Add the Image and Creation date members in the same way (but select the Image and Date property data types for them). Our member list will look like this:

_images/6.png

That’s it, we are done with our data model for now. Let’s add some content. Go to the Content/Objects section. Objects (and again, our pages are objects) are grouped by the parent classes (pluralized names are used to name the groups). Objects of the classes that doesn’t have parent ones go under the Others group. Our Post Page class is already here:

_images/7.png

Click the Create post page button:

_images/8.png

As you can see, all the properties we have defined in the corresponding class are here. Fill the fields and click the Save button. New post is created:

_images/9.png

There are only the properties are displayed whose members have Is property visible in list checkbox checked.

Now we have our post page object created. We can use different ways to present it (view, API, plain text and so on), but now let’s use old good view for that.

Create PostPage.cshtml file inside the Views folder of the web application project with the following content:

@model dynamic
@{
  this.ViewBag.Title = this.Model.Page.Title;
  this.ViewBag.MetaKeywords = this.Model.Page.MetaKeywords;
  this.ViewBag.MetaDescription = this.Model.Page.MetaDescription;
}
<div class="post-page post">
  <h1>
    @Model.Page.Title
  </h1>
  <div class="post__cover">
    <img class="post__cover-image" src="@Model.Page.Image" alt="@Model.Page.Title" />
  </div>
  @Html.Raw(this.Model.Page.Content)
  <div class="post__created">
    @Model.Page.CreationDate
  </div>
</div>

The HTML ifself is very simple. You can see that all the data comes from the view model. There is the Page property which contains all the properties of our post page object that we have described by the class members (and property names are the same as the member codes). This Page property is created for us by the corresponding data source. If your view needs more different data in order to be rendered, just add more data sources that will provide this data to the view model.

Data sources specify the C# classes that implement the IDataProvider interface, you can create your own ones. They can provide data in any way you need: to load some objects, to take it from the web services (weather forecast?), or to return some hardcoded values. All the data sources that are used to process the particular request are grouped inside the endpoint. Endpoints process the requests and return response in Platformus-based web applications (as well as data sources, they specify C# classes that implement the IRequestProcessor interface, and you can create your own implementations). We will see how this all works a bit later in this article. You can still use regular C# controllers to process requests which is simple, but it is related to writing custom extensions and is not considered in this tutorial.

We have described and created the content (our post page object), we have also created the presentation for that content (our view). The last thing we must do to make it all work is to create the endpoint and the data source. Go to the Development/Endpoints section. Click the Create endpoint button and fill the fields as shown below:

_images/10.png _images/11.png

Endpoints define how your Platformus-based web application processes the HTTP requests. By default, if there are no endpoints (and regular C# controllers or pages) configured, you will have 404 response on every request. By specifying the URL template for the endpoint, you tell the instance of the IEndpointResolver interface which endpoint it should use to process the particular request (you can use {*url} one to handle all the requests). It is done the similar way as the MVC routes configuration (endpoint is something like route and controller at once; endpoints support URL parameters too) and it is executed after the default MVC routing. Also, you can specify which C# class (implementation of the IRequestProcessor interface) will process the request and return the result. You can write your own implementations of that interface and use them to process the requests. Specify the view name that we have created earlier that will be used by this endpoint to render the response. Click the Save button to create our new endpoint:

_images/12.png

One more thing about the endpoints. Default implementation of the IEndpointResolver interface checks endpoints, sorted by the position, one by one (whether the current one’s URL template matches the request’s URL or not). That’s why position field value is important. If you have a few endpoints that match the given URL, the first one will be used.

The last thing we have to do is to add the data source that will load the post page object by the value of the URL property and assign it to the view model’s Page property (that will also be created). Click the Data sources link and then the Create data source button. Fill all the fields as shown below and click the Save button:

_images/13.png

That’s it. Now we can test how our post page is displayed. Navigate to https://localhost:5000/en/blog/my-first-blog-post:

_images/14.png

It works! But we also need to have a page with all the posts. We will make it quickly, because now you know enough. This page will display the posts, so we don’t need to create any new class (just create the regular page object with the URL property value set to /blog). All we need is to create new view, endpoint and two data sources for it. Let’s start from the BlogPage.cshtml view:

@model dynamic
@{
  this.ViewBag.Title = this.Model.Page.Title;
  this.ViewBag.MetaKeywords = this.Model.Page.MetaKeywords;
  this.ViewBag.MetaDescription = this.Model.Page.MetaDescription;
}
@Html.Raw(this.Model.Page.Content)
<div class="blog">
  @foreach (var post in this.Model.Posts)
  {
    @Html.Partial("_Post", post as object)
  }
</div>

As you can see, we will have a data source that will provide the Posts view model property for us. Also we have to create the _Post.cshtml partial view (inside the Shared folder):

@model dynamic
<div class="posts__post post">
  <h2>
    <a href="/@System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName@Model.Url">@Model.Title</a>
  </h2>
  <div class="post__cover">
    <a href="/@System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName@Model.Url">
      <img class="post__cover-image" src="@Model.Image" alt="@Model.Title" />
    </a>
  </div>
  @Html.Raw(this.Model.Preview)
  <div class="post__created">
    @Model.CreationDate
  </div>
</div>

Now create the new endpoint (you have to have the separated endpoint for each page template (or view)):

_images/15.png _images/16.png

Because the page that will display the list of the posts is the page too, add the Page data source for our new endpoint (the same way we have done that for the previous one). It will load our regular page object that holds Content and other properties of this page.

But in order to be able to display the posts on this page, we must add one more data source:

_images/17.png

As you can see, another C# class is selected as data provider for this data source. It provides more properties for us. For example, it allows to specify the class of the objects to load etc.

Everything is done. Now you can navigate to https://localhost:5000/en/blog and see the result:

_images/18.png

Click the image to go to the post page. You can add the new menu item in the menu to have your blog there.

Fundamentals

Platformus CMS is modular and extendable, so its behavior is very dependent on the set of the extensions. The main and the only required extension is the Platformus.Core. It doesn’t provide any content management, instead it implements such basic things like reusable admin panel (or backend) UI, user access control, configuration, localization etc.

Depending on your needs and requirements you can either use other existing extensions to manage your content or write your own one(s). Anyway, the main purpose of the Platformus CMS is to save you from writing routine code, to speed up development, and to provide you with convenient and standardized admin panel UI. Also, you can write and then reuse small typical extensions from project to project.

Standard Extensions

There are 4 standard extensions:

Let’s look at a few usage examples to choose the best way to go. All these approaches do not exclude each other and can be combined.

Mobile app API with admin panel

You can create an empty web application, add dependencies on the Platformus.Core extension packages, and write a custom extension that will contain all the application-specific things: entities, models, DTOs, controllers, admin panel sections etc.

In this case you have the very basic things out of the box and do not need to think about backend UI, combining it using the built-in tag helpers like bricks. At the same time, CMS has no effect on performance, so you can get maximum of what the .NET gives.

Website

Most of the websites’ content is changed from time to time, so if you are using a cache, it could be not so important how many milliseconds it takes to retrieve from a database and display your data.

Development time is much more important in such cases, so you can use the Platformus.Website extension. It provides features to describe your content with classes and then create and use it as objects. This extension allows to avoid programming and, in many cases, trivial configuration and writing Razor views could be enough.

Ecommerce

Platformus CMS contains the Platformus.Ecommerce extension which contains standard ecommerce features such categories and products, filter, cart etc. It can be used as the NuGet packages, or as the source code. The second option could be useful when you need a very specific ecommerce app, so you can just use the source code as a simple barebone and implement features you need.

Platformus.Core

This is the only required extension for any Platformus-based web application. It contains such basic things like reusable admin panel (or backend) UI, user access control based on permissions and roles, configuration, localization etc.

Admin Panel Sections

Permissions

Permissions are used to control user access to a web application resources. You can manage them (create, edit, and delete) from the backend using the Audience/Permissions section:

_images/114.png

Each permission has name, code, and position:

_images/26.png
Code

Code might be used to check permissions from code (see examples below).

Position

Position might be used to sort the permissions in the correct order.

Once you created a permission, you can assign it to a role (and then to a user). While signing in, all the user roles and all the permissions from that roles are attached to the user as the claims. These claims then can be checked from the code:

if (context.User.HasClaim(PlatformusClaimTypes.Permission, Permissions.ManageUsers))
{
}

Platformus uses authorization policies to control access to the controllers and actions:

[Authorize(Policy = Policies.HasManageUsersPermission)]
public class UsersController : ControllerBase { }

In order to be able to use an authorization policy, it should be added to the authorization options inside the services.AddAuthorization() extension method:

services.AddAuthorization(options =>
  {
    foreach (IAuthorizationPolicyProvider authorizationPolicyProvider in ExtensionManager.GetInstances<IAuthorizationPolicyProvider>())
      options.AddPolicy(authorizationPolicyProvider.Name, authorizationPolicyProvider.GetAuthorizationPolicy());
    }
  );

As you can see, the ExtCore framework’s ExtensionManager class is used to get all the instances of the IAuthorizationPolicyProvider interface implementations. Then method IAuthorizationPolicyProvider.GetAuthorizationPolicy() is used to get the authorization policies.

So, if the permission is used to control access to a controller or action via policy, you need to implement the IAuthorizationPolicyProvider interface and then add corresponding attribute to the controller or action. If you only want to check the permission from code, you don’t have to implement that interface.

Roles

Roles are used to group the permissions. You can’t assign a permission to a user directly, it is only possible to assign a role. You can manage them (create, edit, and delete) from the backend using the Audience/Roles section:

_images/115.png

Each role has name, code, position, and other fields:

_images/27.png
General/Code

Code might be used to check roles from code.

General/Position

Position might be used to sort the roles in the correct order.

Also, on the Permissions tab you can assign the permissions to a role:

_images/35.png

The same as permissions, roles are attached to a user as the claims while signing in. You can check them from the code to control rights and restrict access, but permissions checking is preferred.

Users

Users (as well as permissions and roles) are used to control access to a web application resources. You can manage them (create, edit, and delete) from the backend using the Audience/Users section:

_images/116.png

Each user has name:

_images/28.png

Also, on the Roles tab you can assign the roles to a user:

_images/36.png

As user itself only has a name, he doesn’t store any information about how he signs in. This information is stored using the Credential objects. Each user can have different credentials, and each credential has its type (it can be email and password, Facebook account, Microsoft account and so on). When user signs in, Platformus checks if there is a credential with the given type, identifier, and secret exists. If the credential is found, corresponding user is signed in.

The credential list looks like this:

_images/41.png

Each credential has type, identifier, and secret:

_images/51.png
Credential type

As a developer, you can create your own credential types which depend on the sign in methods your application supports.

Secret

Secret is optional and can be used to store any additional information. For example, it stores passwords (as hashes) for the email and password credential type. If you need to change the password, just type it into this field. Don’t forget to set the Apply PBKDF2 hashing to secret checkbox to apply hashing, otherwise your password will be saved as plain text and signing in won’t work (as it compares hashes).

Configurations

Configurations and variables are used to provide user-defined configuration parameters to the web application (while developer-defined ones can be provided using the appsettings.json file). You can manage them (create, edit, and delete) from the backend using the Administration/Configurations section:

_images/112.png

Each configuration has code and name:

_images/24.png
Code

Code might be used to get configuration from code (see examples below).

Configurations consist of variables. Each variable has code, name, value, and position:

_images/33.png
Code

The same as for configurations, Code is used to get variable from code.

Value

Value contains the variable’s value.

Position

Position might be used to sort the variables in the correct order.

There is the special DefaultConfigurationManager class that you can use to access the configurations. It implements the IConfigurationManager interface and it is registered as a service inside the DI, so you can replace it with your own implementation.

This is the usage example:

public class DefaultController : Controller
{
  public DefaultController(IConfigurationManager configurationManager)
  {
    string emailSmtpServer = configurationManager["Email", "SmtpServer"];
  }
}

This will get value of the variable with code SmtpServer from the configuration with code Email.

Cultures

Cultures are used to specify which languages and data formats your web application supports. You can manage them (create, edit, and delete) from the backend using the Administration/Cultures section:

_images/113.png

Each culture has two-letter language code, name, and other fields:

_images/25.png
Two-letter language code

This code complies with ISO 639-1. It is used, for example, as URL segment to determine request’s language (see below).

Is frontend default

Specifies if this culture should be used as the default one on the frontend (it means that this culture will be used when the culture is not explicitly provided).

Is backend default

Specifies if this cultured should be used as the backend (admin panel) one.

You can see that there is the Neutral culture exists in the list. This culture is used by the Platformus.Website extension to store the culture-neutral string values using the localizations.

When you create your own extension or describe your data model with classes and members using the Platformus.Website one, you can specify whether the particular string property is localizable or not. If it is localizable, N editors will be displayed, one for each of the cultures. It looks like this:

_images/34.png

When your string property is not localizable, the only one editor will be displayed, and the property value will be saved either using the localization with neutral culture (in case the Platformus.Website extension is used) or in the way you want it to be saved.

When using the Platformus.Website extension, by default a short two-letter language code segment is used in the URL on the frontend to specify which culture should be used for the request. For example: /en/some-page. It is done in this way to make it possible for the pages to be indexed by the search engines with the different languages. But if you are sure that your web application will always support the only one language, you can turn off this behavior using the configurations and have shorter URLs. In this case, the default culture will be used to display the content (but you can change the way culture is selected for the requests; for example, the Accept-Language header can be used).

There is the special DefaultCultureManager class that you can use to work with the cultures. It implements the ICultureManager interface and it is registered as a service inside the DI, so you can replace it with your own implementation.

Platformus.Website

This extension provides everything for building simple websites without (or almost without) programming. You can describe your custom content types and manage your data using the automatically generated admin panel UI. For example, you can have blog posts, tags, and comments with all the properties you need.

Also, there are built-in features to work with menus and forms. You can create them in the backend and then display on frontend using the tag helpers.

A basic file manager is also provided by Platformus.Website extension.

Admin Panel Sections

Objects

Object is the central part of the default Platformus CMS data model. It is an elementary piece of the information. It can be your blog post, comment, or tag. Objects are described by classes and members and consist of properties and relations. You can manage them (create, edit, and delete) from the backend using the Content/Objects section:

_images/127.png

The Content/Objects section has subsections that correspond to classes. These subsections are grouped using the parent classes. Classes without parents go under the Other group.

As we said, objects consist of properties and relations and are described by classes and members. Each property has its own client-side editor, which is specified by the data type of the member. As a developer, you can create your own data types and client-side editors. Also, data types specify which primitive storage data type should be used to store the particular property value (integer, decimal, string and datetime are supported). String properties can be localizable and non-localizable.

The object edit page consist of client-side editors grouped by tabs. A typical page can look like this:

_images/219.png

The property editors may look and behave absolutely different. This is the property editor for the Image data type looks like:

_images/314.png

And this is the one for the Date data type:

_images/46.png
Forms

Forms are used to get and process user input on the frontend. You can manage them (create, edit, and delete) from the backend using the Content/Forms section:

_images/124.png

Each form has code, localized name, submit button title, and other fields:

_images/216.png
Code

Code might be used to get forms from code (see examples below).

Produce completed forms

The Produce completed forms checkbox allows to specify if you want completed forms to be created each time user fills the form. You can review completed forms (user input) from the backend any time if they are created.

Form handler C# class name

Defines the implementation of the IFormHandler interface that will handle the user input for this form. There is the only one built-in implementation of this interface: the EmailFormHandler class. It sends the user input to the specified recipients by the email. You can write your own implementations of this interface. For example, you can have form handler that creates comments using the user input on a blog post page.

Each form handler can have different (specified by the developer) parameters, which use different parameter editors. Parameter editors might be created by the developer too. (The built-in form handler has two parameters: Recipient emails and Redirect URL.)

Forms consist of fields. There are different types of fields (and you can add your own ones). Each field has type, localized name, position, and other fields:

_images/311.png
Is required

Prevents user from submitting the form until this field contains value.

Max length

Prevents user from specifying a longer text value than allowed.

Position

Position might be used to sort the fields in the correct order within the form.

The fields of the type Drop down list also have user-defined options.

Once form is created, you can display it on the frontend using the built-in FormViewComponent view component like this (the form code is passed as the parameter to identify the form we want to display):

@await Component.InvokeAsync("Form", new { code = "Feedback", additionalCssClass = "master-detail__form" })

Or using the view component tag helper:

<vc:form code="Feedback" additional-css-class="master-detail__form" />

As you can see, an additional CSS class might be applied using the corresponding optional parameter.

The result can look something like this:

_images/44.png

Forms are displayed using the built-in views (_Form and _Field). The HTML elements have unique CSS classes (the BEM methodology is used), so it is easy to apply styles to them:

<form class="form" action="/en/forms/send" enctype="multipart/form-data" method="post" novalidate="novalidate">
  <input name="formId" id="formId" type="hidden" value="1">
    <div class="form__field field">
      <label class="field__label label" for="field1">Your name</label>
      <input name="field1" class="field__text-box text-box" id="field1" type="text" maxlength="64" data-val-required="" data-val-maxlength-max="64" data-val="true">
    </div>
    <div class="form__field field">
      <label class="field__label label" for="field2">Your email</label>
      <input name="field2" class="field__text-box text-box" id="field2" type="text" maxlength="64" data-val-required="" data-val-maxlength-max="64" data-val="true">
    </div>
    <div class="form__field field">
      <label class="field__label label" for="field3">Your message</label>
      <textarea name="field3" class="field__text-area text-area" id="field3" maxlength="1024" data-val-required="" data-val-maxlength-max="1024" data-val="true"></textarea>
    </div>
  <div class="form__buttons buttons">
    <button class="buttons__button button" type="submit">Send</button>
  </div>
</form>

If you want to change the HTML, just copy the views into your project and they will be used instead of the built-in ones, so you will be able to modify them as you want.

File Manager

File manager allows you to manage your files (upload and delete them) from the backend using the Content/File manager section:

_images/123.png

Once a file is uploaded you can use it in different ways. You can copy a link to the file and paste it somewhere. If it is an image, you can paste it in the HTML editor using the file selector (click the Insert/edit image button and then the Browse one):

_images/215.png

Please note that if your class contains a member with the Image as the data type, you don’t need to upload an image using the file manager manually. The image editor will upload, crop, and save the image for the object property automatically (and the image path will be different: each object has its own images path that includes its identifier).

Classes

Classes are used to describe the objects. Using the members, they describe which properties should the objects have, how these property values should be stored and edited in the backend, and how the objects are related to each other. You can manage them (create, edit, and delete) from the backend using the Development/Classes section:

_images/120.png

Each class has optional parent class, code, name, and other fields:

_images/212.png
Parent class (abstract only)

If you select a parent class, your class will have all the tabs and members of that abstract class. This feature helps to avoid copying the same members again and again. For example, it is good idea to have the Page abstract class with such members, like URL, Content, Title, and META tags. Also, abstract classes are used to group the objects in the Content/Objects section.

Code

It is the unique text identifier of the class.

Name

Name is used to identify the classes in the backend.

Pluralized name

Pluralized name is used in the Content/Objects section.

Is abstract

Specifies whether the class is abstract or not.

Tabs

Each class can have tabs. Tabs are used to group the object property editors on the create or edit object pages. You can manage them (create, edit, and delete) from the backend using the Tabs section of the classes:

_images/128.png

Each tab has name and position:

_images/220.png
Name

Name is used to identify the tabs in the backend.

Position

Position is used to sort the tabs in the lists.

Members

Members are used to describe which properties and relations should the objects of a given class have. You can manage them (create, edit, and delete) from the backend using the Members section of the classes:

_images/125.png

Each member has tab, code, name, and other fields:

_images/217.png
General/Tab

You can select a tab this member properties should belong to. All the properties of the members without a tab selected go under the General one.

General/Code

It is the unique text identifier of the member. This code is used in the different places, like sorting, mapping etc.

General/Name

Name is used to identify the members in the backend.

General/Position

Position is used to sort the members in the lists.

_images/312.png
Property/Property data type

Member can be a property or a relation. If you specify the property data type, member will be considered as a property. Property data type allows to specify how to store the object property value (and which raw storage data type is used for that), how to display and edit it in the backend.

If a property data type is selected, data type parameters will be also displayed. Each data type can have unique parameters. For example, it could be maximum text length for the text or width and height for the image.

_images/45.png
Relation/Relation class

If you specify the relation class, member will be considered as a relation. Relation selector will be displayed as the editor. For example, user will be able to select a category for a blog post or assign tags to it. Also, if any relation class is selected, additional fields will be displayed.

Is relation single parent

Specifies whether objects of this class can have the only one relation to the specified class (and this specified class will be considered as the parent one for the current class). For example, if blog post page can have the only one category page, you can use this option. In the object list, link to the blog post pages will appear in the category page rows, so all the created blog post pages will be automatically related to the parent category page objects. If the checkbox is not set, there will be two separated lists of the objects: Category pages and Post pages, and you will have to select a category page in every post page from the relation selector manually.

Endpoints

The purpose of the endpoints is to take some data provided by the data sources, represent it in some way (HTML, JSON, XML etc.), and return as the response. They also handle access control and caching. You can manage them (create, edit, and delete) from the backend using the Development/Endpoints section:

_images/122.png

Please note, that the default ASP.NET routing still works and it is executed before any endpoint.

Each endpoint has name, URL template, position, and other fields:

_images/214.png
General/Name

Name is used only in the endpoint list to identify endpoints.

General/URL template

URL template is used by the implementation of the IEndpointResolver interface to select the endpoint which should process current request (see below).

General/Position

Position is important, because the endpoint resolver checks the endpoints one by one, and it will return the first matching endpoint from the list, sorted by position.

_images/310.png
Access/Disallow anonymous

This checkbox allows you to specify whether a user must be authenticated in order to be able to get the response from this endpoint. Once the checkbox is checked, the Required permissions list will appear so you can specify which permissions the user must have.

Access/Sign in URL

This URL will be used to redirect user if he must be authenticated or if a required permission is missing.

_images/43.png
Request processing/Request processor C# class name

It allows you to specify, which C# class (implementation of the IRequestProcessor interface) will process the requests (convert request data and data provided by the data sources to the response). It is very important, because you can write your own implementations of this interface. You can return HTML (using or not using views), JSON, files, plain text, redirects, or any other content. There is the only one built-in request processor: the DefaultRequestProcessor one. It returns views the same way as ASP.NET controllers do (you can specify the view name using the parameter).

Please note that the endpoints process requests using the request processors, they do not (and should not) provide data for the responses. In other words, they take prepared data and represent it in some way (HTML, JSON etc.). Data is provided to the endpoints by data sources. Each endpoint can have different data sources at the same time.

_images/52.png
Response caching/Response cache C# class name

It allows you to specify, which C# class (implementation of the IResponseCache is used to cache the endpoint response. There are several built-in implementations of this interface, but you can write your own ones.

Data Sources

Data sources provide the endpoints with data (end then endpoints represent that data in some way and return as the responses). Data sources use the specified implementation of the IDataProvider interface to get, prepare, or generate data (it can be anything: the database records, weather forecast, some hardcoded text). It can be useful because a developer can prepare different data providers that can be then used and combined by a user to provide all the required data for, for example, the specific website pages. You can manage them (create, edit, and delete) from the backend using the Data sources section of the endpoints:

_images/121.png

Each data source has code and data provider C# class name:

_images/213.png
Code

Code is used as the model property name. So, if you have specified some code, your model (for example, a view model) will have a property with this name.

Data provider C# class

It allows you to specify, which C# class (implementation of the IDataProvider interface) will provide data for this data source.

There are few built-in data providers. PageObjectDataProvider loads the object by its URL property value. ObjectsDataProvider loads the objects of the specified class. RelatedObjectsDataProvider loads the objects that are related to the one, which URL property value matches current request’s URL.

Advanced
Custom Form Handlers

The Platformus.Website extension offers the great forms features. You can describe your forms in the backend, and then render them and get user feedback on any frontend view. When user fills the form and clicks the Submit button, data is sent to a server and might be processed in any way you want. User input is handled by the implementations of the IFormHandler interface which can return any IActionResult as the result.

The selected implementation receives the form object, all the user input (string values by field objects), and all the attachments user has uploaded. Field values can be validated using the implementations of the IFieldValidator interface (you can use the ReCaptchaFieldValidator as an example).

Platformus has the only one built-in implementation of the IFormHandler interface: the EmailFormHandler class. It sends the user input to the specified email recipients.

Let’s implement our own form handler, which will just display the user input (but it could do anything else as well). Create the DisplayUserInputFormHandler class inside the main web application project and implement the IFormHandler interface there:

public class DisplayUserInputFormHandler : IFormHandler
{
  public IEnumerable<ParameterGroup> ParameterGroups => new ParameterGroup[] { };
  public string Description => "Our own form handler.";

  public async Task<IActionResult> HandleAsync(HttpContext httpContext, string origin, Form form, IDictionary<Field, string> valuesByFields, IDictionary<string, byte[]> attachmentsByFilenames)
  {
    StringBuilder body = new StringBuilder();

    foreach (KeyValuePair<Field, string> valueByField in valuesByFields)
      body.AppendFormat("<p>{0}: {1}</p>", valueByField.Key.Name.GetLocalizationValue(), valueByField.Value);

    return new ContentResult() { Content = body.ToString() };
  }
}

Now, when our form handler class is added, navigate to the backend’s Content/Forms section and create or edit a form:

_images/119.png

Please note, that our new form handler C# class is automatically resolved and added to the drop down list. Click the Save button.

Now navigate to /en/contacts and fill out the form:

_images/211.png

Click the Send button. Output from our form handler is displayed:

_images/39.png

We could return some view, redirect, or any other action result we need.

Form Handler Parameters and Parameter Groups

As well as the endpoints and data sources, form handlers support parameters and parameter groups. The implementation is absolutely the same, so please just take a look at the endpoints for the sample. Also, please take a look at the built-in form handler to see how it gets the parameter value.

Custom Data Sources

As we know, data providers are used by data sources to provide endpoints with data. It is a regular C# class that implements the IDataProvider interface. There are several built-in data providers, but all of them work with the objects. Let’s create a custom one that will just provide a hardcoded text.

Create the MyDataProvider class inside the main web application project and implement the IDataProvider interface:

public class MyDataProvider : IDataProvider
{
  public string Description => "Provides the hardcoded values.";

  public IEnumerable<ParameterGroup> ParameterGroups => new ParameterGroup[] { };

  public async Task<dynamic> GetDataAsync(HttpContext httpContext, DataSource dataSource)
  {
    ExpandoObjectBuilder expandoObjectBuilder = new ExpandoObjectBuilder();

    expandoObjectBuilder.AddProperty("Value", "Some hardcoded value from our custom data source.");
    return expandoObjectBuilder.Build();
  }
}

Now, when our data provider class is added, navigate to the backend’s Development/Endpoints section, then to the data source list of the Default endpoint, and create one more data source:

_images/117.png

Please note, that our new data provider C# class is automatically resolved and added to the drop down list. Click the Save button. Data source is created:

_images/29.png

All the regular pages now have the Hardcoded property inside their view models (thanks to the DefaultRequestProcessor request processor; your own request processor might act in a different way to process provided data).

Let’s update the RegularPage.cshtml view to display the value from our new data source:

<p>@Model.Hardcoded.Value</p>

Run the web application and check the output:

_images/37.png

Good. Everything works as expected.

Data Source Parameters and Parameter Groups

As well as the form handlers and endpoints, data providers support parameters and parameter groups. The implementation is absolutely the same, so please just take a look at the endpoints for the sample. Also, please take a look at the built-in data providers to see how they get the parameter values.

Custom Endpoints

As we know, endpoints process the requests and prepare responses using the data provided by the data sources. Endpoints use the implementation of the IRequestProcessor interface for that. It can return any IActionResult. The idea is that by changing the request processor class you can represent data in a different way. That’s why the request processor shouldn’t provide data itself but use the one provided with data sources.

The only one built-in implementation of the IRequestProcessor interface (the DefaultRequestProcessor class) returns Razor views with the view model combined from the data provided by the data sources. Let’s create our own request processor which will return JSON instead:

public class MyRequestProcessor : IRequestProcessor
{
  public IEnumerable<ParameterGroup> ParameterGroups => new ParameterGroup[] { };
  public string Description => "Returns data as JSON.";

  public async Task<IActionResult> ProcessAsync(HttpContext httpContext, Platformus.Website.Data.Entities.Endpoint endpoint)
  {
    dynamic viewModel = await this.CreateViewModelAsync(httpContext, endpoint);

    if (viewModel == null)
      return null;

    return new JsonResult(viewModel);
  }

  private async Task<dynamic> CreateViewModelAsync(HttpContext httpContext, Platformus.Website.Data.Entities.Endpoint endpoint)
  {
    ExpandoObjectBuilder expandoObjectBuilder = new ExpandoObjectBuilder();

    foreach (DataSource dataSource in endpoint.DataSources)
    {
      dynamic viewModel = await this.GetDataProvider(dataSource).GetDataAsync(httpContext, dataSource);

      if (viewModel == null)
        return null;

      expandoObjectBuilder.AddProperty(dataSource.Code, viewModel);
    }

    return expandoObjectBuilder.Build();
  }

  private IDataProvider GetDataProvider(DataSource dataSource)
  {
    return StringActivator.CreateInstance<IDataProvider>(dataSource.DataProviderCSharpClassName);
  }
}

Now, when our request processor class is added, navigate to the backend’s Development/Endpoints section and change the default endpoint’s request processor C# class name:

_images/118.png

Please note, that our new request processor C# class is automatically resolved and added to the drop-down list. Click the Save button.

Now navigate to /en/about and our new request processor will process the request:

_images/210.png

Good. Everything works as expected.

Endpoint Parameters and Parameter Groups

Now let’s assume that we want to make it possible to specify the data format for our request processor output. It can be done using the parameters. Override the ParameterGroups property in our endpoint class:

public IEnumerable<ParameterGroup> ParameterGroups => new ParameterGroup[]
{
  new ParameterGroup(
    "Serialization",
    new Parameter(
      "Format",
      "Format",
      ParameterEditorCodes.RadioButtonList,
      new Option[] {
        new Option("JSON", "json"),
        new Option("XML", "xml")
      }
    )
  )
};

This property just returns the parameter groups. Each of them can contain different parameters defined by developer. When user selects the request processor C# class, these parameters will be available for him. Parameters can have different editor types. All the built-in ones are defined inside the ParameterEditorCodes enum, but you can also add your own ones. Using the parameter’s code, you will be able to get the value entered by a user.

Let’s update also the Description property to indicate that now we can return either JSON or XML:

public override string Description => "Returns data as JSON or XML.";

Description is also presented to a user when the request processor is selected.

Now open our endpoint in the backend one more time:

_images/38.png

The request processor parameter is displayed. Let’s see how to get and use the selected value from the code:

public async Task<IActionResult> ProcessAsync(HttpContext httpContext, Platformus.Website.Data.Entities.Endpoint endpoint)
{
  dynamic viewModel = await this.CreateViewModelAsync(httpContext, endpoint);

  if (viewModel == null)
    return null;

  string format = new ParametersParser(endpoint.RequestProcessorParameters).GetStringParameterValue("Format");

  if (format == "json")
    return new JsonResult(viewModel);

  XDocument document= new XDocument(
    new XElement("someKey", "Some value")
  );

  return new ContentResult() { Content = document.ToString(), ContentType = "application/xml" };
}

Please note that we only use the hardcoded XML here, because converting dynamic object into an XML might look complex and isn’t a subject of the article.

Now if you change the data format in the backend, the endpoint output will also be changed:

_images/42.png

Platformus.ECommerce

Implements standard ecommerce features like categories, products, photos, carts, and orders in a very basic way. Can be used as a starting point for ecommerce projects.

Admin Panel Sections

Categories
Products
Carts
Orders
Order States
Delivery Methods
Payment Methods

Platformus.Images

Provides basic image processing (cropping and resizing).

Custom Extensions

It is not difficult to develop a custom Platformus CMS extension. You can add dependency on any C# project in your Platformus-based application like in any other ASP.NET app, controllers and other features will work as you expect. But Platformus provides you with some public API, so you can extend it, add backend (admin panel) sections, security policies etc.

There are 2 main purposes to have custom Platformus extensions.

  1. You can have all your code in the isolated projects, so CMS itself can be updated independently.

2. You can decrease development time reusing code and combining your apps from the existing parts. It could be useful when you develop a lot of apps and have standard approaches of fixing standard tasks. For example, most of the apps have authentication part, some Firebase cloud messaging features. Many of them also have chats on SignalR.

As Platformus CMS is built on top of the ExtCore framework, you can use your custom extension in the different ways: as NuGet packages, source code, or even DLL-files.

Backend Dashboard Widgets

Most of the apps have some key metrics, analytics, or statistics that should be available in a fast and convenient way. It could be number of the registered users or orders for the last week, sales amount etc.

The home page of the Platformus CMS backend (admin panel) is a dashboard where you can add your own widgets. Each widget is a regular view component, so it has its own view and can look and behave in any way.

To add your view component(s) to the dashboard you need to implement the IMetadata interface. It is preferable to use the MetadataBase class to be able to override only the methods you want:

public class MyMetadata : MetadataBase
{
  public override IEnumerable<DashboardWidget> GetDashboardWidgets(HttpContext httpContext)
  {
    return new DashboardWidget[]
    {
      new DashboardWidget("MyViewComponent", 1000)
    };
  }
}

This file can be placed anywhere in the project, it will be resolved automatically by the default implementation of the IDashboardWidgetsProvider interface.

Let’s look at the DashboardWidget class’s properties.

ViewComponentName is the view component name.

Position is used to sort the widgets. Widgets with a lower position are placed higher.

Backend Menu

When you develop a custom Platformus CMS extension you might need to add items to the backend (admin panel) menu. All the backend menu groups and items are defined in the extensions. Each extension can provide one or more implementations of the IMetadata interface which allows to specify, among other things, the menu items grouped by the menu groups. It is preferable to use the MetadataBase class to be able to override only the methods you want:

public class MyMetadata : MetadataBase
{
  public override IEnumerable<MenuGroup> GetMenuGroups(HttpContext httpContext)
  {
    IStringLocalizer<Metadata> localizer = httpContext.GetStringLocalizer<Metadata>();

    return new MenuGroup[]
    {
      new MenuGroup(
        localizer["My Group"],
        1000,
        new MenuItem[]
        {
          new MenuItem("icon--icon1", "/backend/something-1", localizer["Something 1"], 1000, "ManageSomething1"),
          new MenuItem("icon--icon2", "/backend/something-2", localizer["Something 2"], 2000, "ManageSomething2"),
          new MenuItem("icon--icon3", "/backend/something-3", localizer["Something 3"], 3000, "ManageSomething3")
        }
      ),
    };
  }
}

This file can be placed anywhere in the project, it will be resolved automatically by the default implementation of the IMenuGroupsProvider interface.

If the provided menu group’s name matches name of the existing one, the menu items of both will be merged into the single group according to the menu item positions.

Let’s look at the MenuItem class’s properties.

CssClass allows to specify the CSS class that will be added to the menu item HTML tag. Intended to provide a custom icon but can also be used to apply another styling.

Url is the URL where user is navigated when clicks the menu item.

Name is displayed in the menu.

Position is used to sort the menu items (and menu groups). Items with a lower position are placed higher.

PermissionCodes contains an array of the codes of the permissions which are required for user to have in order to see the menu item.

Backend Styles and Scripts

When you develop a custom Platformus CMS extension you might need to add your own CSS styles and JavaScript scripts on the backend (admin panel) pages. Same as for the menu, it can be done by implementing the IMetadata interface or inheriting the MetadataBase class.

Please, take a look at the implementation from the Platformus.Core extension for a sample.

This file can be placed anywhere in the project, it will be resolved automatically by the default implementation of the IStyleSheetsProvider interface and the default implementation of the IScriptsProvider interface.

It is preferable to use links to minified CSS and JavaScript files to save traffic and speed up page loading. Also, you might need to embed these files into your extension to make it atomic.

Embedded Resources

Usually, your extension might need some resources (images, CSS, or JavaScript files etc.). You can add them to the host web application as regular static files, but in most cases, you would like your extensions to be atomic, especially if they are distributed as NuGet packages. It can be done by defining the resources that should be embedded in your *.csproj file:

<ItemGroup>
  <EmbeddedResource Include="wwwroot\**" />
</ItemGroup>

After the resource is embedded, the underlying ExtCore framework will make it available using the HTTP requests. For example, if you embed a file as /wwwroot/images/photo.jpg, it will be available as /wwwroot.images.photo.jpg.

Please look how the static files are embedded and then used in the standard Platformus.Core extension.

Styles and Scripts Minification

Good practice is to minimize number of the HTTP requests by combining the CSS and JavaScript files. Also, these files can be minified to reduce their size by removing extra spaces, line breaks, replacing long variable names with the shorter ones etc. (The JavaScript files could be also obfuscated, which makes reverse engineering much harder.)

By default, Platformus uses the BundlerMinifier.Core package for that. To start using it add the corresponding reference to your *.csproj file:

<ItemGroup>
  <DotNetCliToolReference Include="BundlerMinifier.Core" Version="3.2.449" />
</ItemGroup>

Now, add the following lines to the same project file to run the bundling and minification process automatically on build:

<Target Name="PrecompileScript" BeforeTargets="BeforeBuild">
  <Exec Command="dotnet bundle" />
</Target>

Finally, the BundlerMinifier.Core package needs the bundleconfig.json file to be present in the project’s root folder:

[
  {
    "outputFileName": "wwwroot/areas/backend/css/output.min.css",
    "inputFiles": [
      "Areas/Backend/Styles/input1.css",
      "Areas/Backend/Styles/input2.css",
      "Areas/Backend/Styles/input3.css"
    ]
  },
  {
    "outputFileName": "wwwroot/areas/backend/js/output.min.js",
    "inputFiles": [
      "Areas/Backend/Scripts/input1.js",
      "Areas/Backend/Scripts/input2.js",
      "Areas/Backend/Scripts/input3.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

In this file you can specify which files and how exactly should be minified and combined. You can use the bundleconfig.json file from the Platformus.Core extension as a sample.

Tutorial: Creating a Custom Extension

When you create a Platformus-based application, unless it’s something quite simple, you’ll probably need to create your own Platformus extension. It will contain everything that is related to your app (entities, models, DTOs, services, APIs, UI etc.) to keep it separated and independent from the basic CMS.

Creating an extension is simple. We will go through all the aspects and create a sample mobile app backend with authentication (phone number validation using a fake code from SMS), categories and products API, and corresponding admin panel sections to manage categories and products etc.

Preparing the Solution

Let’s start from the beginning. First, please follow the Using as the NuGet Packages tutorial to create an empty ASP.NET Core Platformus-based web application but keep only the Platformus.WebApplication and Platformus.Core packages dependencies as we do not need the others.