×
Back to book

Advanced C#Bot

Learn about the more advanced aspects of C#Bot.

This article provides a step by step tutorial for achieving advanced C#Bot development, and making use of the bot to speed up development. The tutorial includes adding custom code into both C#Bot and ReactBot, and considers setting up security, validation and accessing entities' relationships in this custom code.

Note: When you make any changes to code in C#Bot, you should input those changes into a protected region, so that they are not overwritten the next time the bot writes code. To understand more about protected regions, please read Protected Regions.

Basics

Client-Side

In this section, we will go over adding a custom feature to a ReactBot based project.

Note: All file-paths in this article are relevant to [PROJECT ROOT]/clientside/src unless otherwise stated.

Overview of the Task

In this article, we will create a new page which contains a basic entity creation form, and features validation to provide feedback to users when they enter an invalid value.

Overview of the Entity Diagram

In this article, the model is defined as having a single entity called Region with a single attribute called Region Name in the Entity Diagram.

Editing the Security and UI Diagrams

When adding a new feature, the first step is to create a page on the UI Diagram, which will setup routing to the new page. Add a page called Example to the UI Diagram.

Once you have added a page to the UI Diagram, you must give permission to your users to see it. Switch to the Security Diagram and give the visitor group access to read the Example Page.

Now get the bot to rewrite the application.

Editing the Page

After the bot has rewritten the application, there will be a new file called ExamplePage.tsx inside of the folder Views/Pages. Opening this file should show the following contents:

@observer
export default class ExamplePage extends React.Component<RouteComponentProps> {
    // % protected region % [Add class properties here] off begin
    // % protected region % [Add class properties here] end

    public render() {
        let contents = null;

        // % protected region % [Override contents here] off begin
        // % protected region % [Override contents here] end

        return (
            <SecuredPage>
                {contents}
            </SecuredPage>
        );
    }

    // % protected region % [Add class methods here] off begin
    // % protected region % [Add class methods here] end
}

Any human-written code must be written inside of the areas marked as protected regions so the bot knows not to edit them. Code written outside of the marked protected regions will be removed next time the bot rewrites the application.

You may also notice the render function is returning the component wrapped in a <SecuredPage> tag. This ensures the page obeys the rules defined in the security model.

Editing the New Page

To perform a basic edit to the page, the contents variable will need to be updated. Protected regions need to be activated before they can be used. To do so, change the word off in the protected region to on and write the contents inside. It is very important no other changes are made to the protected region comment.
For a basic hello world, inside the protected region change the contents variable to equal hello world.

// % protected region % [Override contents here] on begin
contents = <>hello world</>;
// % protected region % [Override contents here] end

Adding Interactive Data

We now have some text on the screen but the next step is adding data end users can interact with. To do this, we are going to make a basic interface for adding a Region, the model defined in the Entity Diagram.

First of all, an observable variable must be added to the page class to track the region that is going to be saved.

First we need to add some imports, remembering that any custom code must be in an activated protected region.

// % protected region % [Add any extra imports here] on begin
import { action, observable, runInAction } from "mobx";
import { RegionEntity } from "Models/Entities";
import { TextField } from "Views/Components/TextBox/TextBox";
import { Button } from "Views/Components/Button/Button";
// % protected region % [Add any extra imports here] end

Now the class variable can be added. This will be done in the first protected region in the class.

// % protected region % [Add class properties here] on begin
@observable
private region = new RegionEntity();
// % protected region % [Add class properties here] end

Finally we need to add a text field to the page that is bound to the model. Deleting the 'hello world' that was created before, we can change the contents variable as follows.

contents = (
    <div>
        <TextField
            label="Region Name"
            model={this.region}
            modelProperty="regionName" />
        <Button onClick={this.onSave}>Save</Button>
    </div>
);

This creates the dom elements on the page, but now we need to wire up the save function. To do this, create a private class function called 'onSave' in the first protected region. All models that are written by the bot have a method called 'save' that will persist data to the server. Call this method in the onSave method to save the model.

private onSave = () => {
    this.region.save();
}

Now the data has been saved! However we probably want to reset the form afterwards to make a new region. This model that exists on the class is now bound to the one saved to the server, so we need to create a new Region.

private onSave = async () => {
    await this.region.save();
    this.resetRegion();
}

@action
private resetRegion = () => {
    this.region = new RegionEntity();
}

You may notice that changing this region is completed in a separate function which is decorated with an action.
This is a feature of MobX where all changes to observables must be performed inside of actions. To read more on action see the MobX action documentation.

Adding Validation

Models defined in the Entity Diagram can have validators applied to them. For the next step add a required validator to the Region Name attribute in the Entity Diagram and get the bot to rewrite the code.

After this is done, we need to call the validate method on the Region model to validate the data.

<TextField
    label="Region Name"
    model={this.region}
    modelProperty="regionName"
    onAfterChange={this.validateRegion}
    errors={this.errors} />

Now add the following properties in the class.

@observable
private errors: string[];

private validateRegion = async () => {
    await this.region.validate();
    const regionNameErrors = this.region.getErrorsForAttribute("regionName");

    if (regionNameErrors) {
        runInAction(() => this.errors = regionNameErrors);
    }
};

This will cause the model to be validated when the textbox is changed, updating the errors property of the displayed text field to display the validation errors.

Now you should have a class similar to the code below:

@observer
export default class ExamplePage extends React.Component<RouteComponentProps> {
    // % protected region % [Add class properties here] on begin
    @observable
    private region = new RegionEntity();

    @observable
    private errors: string[];

    private onSave = async () => {
        await this.region.save();
        this.resetRegion();
    };

    @action
    private resetRegion = () => {
        this.region = new RegionEntity();
    };

    private validateRegion = async () => {
        await this.region.validate();
        const regionNameErrors = this.region.getErrorsForAttribute("regionName");

        if (regionNameErrors) {
            runInAction(() => this.errors = regionNameErrors);
        }
    };
    // % protected region % [Add class properties here] end

    public render() {
        let contents = null;

        // % protected region % [Override contents here] off begin
        contents = (
            <div>
                <TextField
                    label="Region Name"
                    model={this.region}
                    modelProperty="regionName"
                    onAfterChange={this.validateRegion}
                    errors={this.errors} />
                <Button onClick={this.onSave}>Save</Button>
            </div>
        );
        // % protected region % [Override contents here] end

        return (
            <SecuredPage>
                {contents}
            </SecuredPage>
        );
    }

    // % protected region % [Add class methods here] off begin
    // % protected region % [Add class methods here] end
}

Server-Side

In this section we will be going over how to implement a custom feature inside of C#Bot.

All file-paths in this article are relevant to [PROJECT ROOT]/serverside/src unless otherwise stated.

This walk-through will implement a server-side REST endpoint which fetches data from both an entity and an associated entity.

Overview of the Model

In this article, the model is defined as a single entity called Region with a single attribute called Region Name. This has a one-to-many relation to another entity in Solar System, such that a region can contain many Systems.

Editing the Controller

Adding the Basics

Adding a REST endpoint requires either the creation of a new controller or an existing controller to be updated. For this task, we will be making an endpoint to fetch Regions with custom conditions. We will therefore edit the existing region controller that exists in Controllers/Entities/RegionEntityController.cs.

At the bottom of the Region controller there should be the following lines.

// % protected region % [Add any further endpoints here] off begin
// % protected region % [Add any further endpoints here] end

First of all we need to activate the protected region and place some code inside.

// % protected region % [Add any further endpoints here] on begin
[HttpGet]
[Route("fetch")]
[Authorize]
public async Task<List<RegionEntityDto>> FetchRegionsWithSystems()
{
    return new List<RegionEntityDto>();
}
// % protected region % [Add any further endpoints here] end

This will construct a new endpoint that returns an empty list. You may notice the endpoint is returning a list of RegionDtos instead of a list of Regions. This is because a RegionDTO can be safely returned to an API consumer, whereas a Region has internal properties which should not be returned.

The attributes on this endpoint specify that request must be a GET request, the route is at fetch and the user must be logged in to access the endpoint.

First we should implement some basic functionality to fetch regions from the database.

[HttpGet]
[Route("fetch")]
[Authorize]
public async Task<List<RegionEntityDto>> FetchRegionsWithSystems([FromServices] <YourProjectname>DBContext dbContext)
{
    return await dbContext
        .RegionEntity
        .Select(region => new RegionEntityDto(region))
        .ToListAsync();
}

Using Postman or another API testing tool, if you send a request to http://localhost:5000/api/region/fetch with the authentication cookie set, you should see a list of regions.

Adding Security

The current code will return a list of Regions, however, this will be accessible to any logged in user and does not respect security model rules To lock this down, you must use the CrudService to fetch the data instead. This service has a series of helpers which perform CRUD operations that respect the security model.

[HttpGet]
[Route("fetch")]
[Authorize]
public async Task<List<RegionEntityDto>> FetchRegionsWithSystems([FromServices]<YourProjectname>DBContext dbContext)
{
    return await _crudService.Get<RegionEntity>()
        .Select(region => new RegionEntityDto(region))
        .ToListAsync();
}

The endpoint is now fetching from the CRUD service and matches rules outlined in the security model.

Adding Relations

We can now fetch the Region data, however, we will also want to fetch data related to this particular entity.

Entity Framework allows efficient fetching of related fields. This is done by using the include function on an IQueryable.

The get function on the CRUD service returns an IQueryable so we can fetch related data and return it like so:

[HttpGet]
[Route("fetch")]
[Authorize]
public async Task<object> FetchRegionsWithSystems([FromServices]CsharpReferenceDBContext dbContext)
{
    var regions = _crudService.Get<RegionEntity>().Include(region => region.Systems);
    var systems = regions.SelectMany(region => region.Systems);
    return new
    {
        Regions = await regions.Select(region => new RegionEntityDto(region)).ToListAsync(),
        Systems = await systems.Select(solarSystem => new SolarSystemEntityDto(solarSystem)).ToListAsync(),
    };
}

However, you may notice the related entity does not have any security applied to it. While we can fetch a Region, this does not mean we are allowed to fetch any associated Systems. We are required to manually apply security to the returned object.

[HttpGet]
[Route("fetch")]
[Authorize]
public async Task<object> FetchRegionsWithSystems(
    [FromServices]CsharpReferenceDBContext dbContext,
    [FromServices]IIdentityService identityService,
    [FromServices]UserManager<User> userManager)
{
    var regions = _crudService.Get<RegionEntity>().Include(region => region.Systems);
    var systems = regions
        .SelectMany(region => region.Systems)
        .AddReadSecurityFiltering(identityService, userManager, dbContext);
    return new
    {
        Regions = await regions.Select(region => new RegionEntityDto(region)).ToListAsync(),
        Systems = await systems.Select(solarSystem => new SolarSystemEntityDto(solarSystem)).ToListAsync(),
    };
}

The AddReadSecurityFiltering extension method can be used to apply the security to an IQueryable. This method takes the identityService which contains the user that is performing the changes, and the groups that the user is in, the user manager and the database context of the application. The endpoint will now have security applied to both the Region and System entity.

Security

Security in C#Bot is implemented at several different levels.

Client-Side Security

The implementation of security on the client-side is not designed to keep data safe, but instead to provide a better flow to the user experience.

On the frontend part of the client-side, pages from the UI Diagram can be marked as able to be read by certain user groups. This is implemented using the SecuredPage component that marks the groups that can access a page. If a user tries to access a component inside of a SecuredPage component, they are redirected to a 404 page.

In the administration section of the client-side, a user will not see links to pages for entities that they cannot manipulate or view, and will not display buttons for creating, deleting or modifying entities they do have permission to.

Server-Side Security

Where the client-side implementation of security is focused on the user experience, the server side is designed to ensure that data is not able to be read or modified by unintended audiences. Security is implemented on the model level for each entity in the Entity Diagram. Every model file (located at serverside/src/Models/[ENTITY NAME]/[ENTITY NAME].cs) has a list of ACL's that are assigned to it.

[NotMapped]
public override IEnumerable<IAcl> Acls => new List<IAcl>
{
    new UsersData(),
    new AdministratorsData(),
    // % protected region % [Add any further ACL entries here] off begin
    // % protected region % [Add any further ACL entries here] end
};

These ACL's define what a user group can or cannot do to an entity. If we dive deeper into these ACLs, they all implement the following interface:

public interface IAcl
{
    string Group { get; }
    bool IsVisitorAcl { get; }

    Expression<Func<TModel, bool>> GetReadConditions<TModel>(User user, SecurityContext context)
        where TModel : IOwnerAbstractModel, new();
    Expression<Func<TModel, bool>> GetUpdateConditions<TModel>(User user, SecurityContext context)
        where TModel : IOwnerAbstractModel, new();
    Expression<Func<TModel, bool>> GetDeleteConditions<TModel>(User user, SecurityContext context)
        where TModel : IOwnerAbstractModel, new();
    bool GetCreate(User user, IEnumerable<IAbstractModel> models, SecurityContext context);
    bool GetUpdate(User user, IEnumerable<IAbstractModel> models, SecurityContext context);
    bool GetDelete(User user, IEnumerable<IAbstractModel> models, SecurityContext context);
}

This interface has a group string property that is used for identifying what user group the ACL is applying to, as well as functions that are used to determine what a user can or cannot do.

There are 2 different types of functions on an acl entry.

The first type are the ones that return booleans. These are for when we know what entities we are going to be changing in advance, and therefore can be provided to the acl to to for security.

The second type of function are the ones that return expressions. These functions are used when we do not know the return the entities we are going to operate on in advance and therefore return a filtering rule that is serialised into SQL by entity framework.

It is important to note that ACLs are additive in their permissions. This means the results of all queried ACLs are joined together using a logical OR.

Validation

Validation for C#Bot is implemented on both the client-side and server-side, with the focus on the client-side giving user feedback quickly and on the server-side to maintain the validity of the data being stored.

Client-Side

Client-Side validators are implemented in the client-side models for each attribute. A typical attribute is declared like the following.

@Validators.Required()
@observable
@attribute()
@CRUD({
    name: 'Number of Members in Team',
    displayType: 'textfield',
    headerColumn: true,
    searchable: true,
    searchFunction: 'equal',
    searchTransform: AttrUtils.standardiseInteger
})
public numberOfMembersInTeam: number;

For this topic, the first line is the most important one, which identifies that the input into text-field displayed on the CRUD is required. This is implemented by putting validation metadata onto the model prototype that can be read by the validate function of the model.

Server-Side

Server-side validation is implemented using model validation for ASP.NET. However, since the server doesn't use the full MVC features of ASP.NET, the abstract model class has a validate method to manually validate a model before it is saved.

Database

For the validators that are supported in Entity Framework, the validation attributes that are created on the server-side are persisted to the database as DDL constraints.