Cireson Platform Developer Tutorial
Abstract
The goal of this tutorial is to create a Cireson Platform Extension that will have an Odata API and UI that will allow us to capture NPS Survey responses and display results.
Prerequisite Skills
- Basic understanding of .Net and C#
Note
These courses are free with your MSDN subscription. Here are the details to set it up.
- Understanding of Entity Framework, and General ORM concepts.
- Understanding of Linq, Linq to Entities.
- Understanding of OData, and Restful Services.
- Working knowledge of SQL Server, and SQL Service Management Studio.
- Familar with REST/API testing tools like POSTMAN, for this tutorial we will use POSTMAN
- Understanding OData: Getting Started with OData
Environment Pre Requisites Platform Dev Env Setup Guide
To get started create a new 'Cireson Platform Extension' Project called:
CiresonPlatformNPS
Entities
Entities are special classes that are used to create and store data, and also used to expose data through an OData API endpoint.
To declare an Entity, add a new item to the Models folder, using the Cireson Entity Model template.
Note
You can quickly create a new item via: (Ctrl+Shift+A)
Task
- Create a Cireson Entity Model named NPSResponse in the Models folder. We will use this to store and retrieve data in our extension.
- Look at the default generated code for the IPlatformEntity. The IPlatformEntity interface tells the Cireson Platform where Entity model inheritance hierarchies start, and has properties that are needed to track and secure your entities.
Entity Properties
Properties describe fields that store data related to the Entity, these can be thought of as Columns in a database, or simple properties on a javascript object. A property represents a single primitive value such as a string or int. Properties for describing Relationships between entities will be discussed later.
Properties can be added to the newly created Entity class using AutoProperties, or Full Properties (Properties with backing data).
In order for a property to be valid, it must implement a get, and a set.
Properties can be Attributed using well known Entity Framework Attributes, as well as Cireson Platform Specific Attributes that will be covered here.
Task
Add a property named Reason with the type string to the NPSResponse class
public String Reason { get; set; }
Note
Type prop then press tab tab and fill in the snippet fields
Add another property named Rating with the type int
public int Rating { get; set; }
Test
- Start the platform console app by clicking the start button
and make sure:
- Run Web Server is checked.
- Basic Authentication is unchecked.
- Ensure you are running the latest version by checking Show-Pre-Release.
- When the console finishes starting, click the upload platform extension button
- Wait for the platform to restart.
Open a browser (Chrome is preffered) and navigate to http://localhost/api/NPSResponse
Note
API Urls are CaseSensitive
- If prompted to login, enter your local computer username and password.
- You should see an empty Entityset.
- From Postman send a POST request to
http://localhost/api/NPSResponse
with the raw JSON body:
{
"Reason": "I like it!",
"Rating": 7
}
- Refresh your browser.
- You should now see the Response you added in the browser:
{
@odata.context: "http://localhost/api/$metadata#NPSResponse",
value: [
{
Id: 1,
ModifiedDate: "2017-09-22T07:23:20.4222759Z",
ModifiedById: 1,
CreatedDate: "2017-09-22T07:23:20.4222759Z",
CreatedById: 1,
Guid: "2cf4786a-08a8-4895-a222-2efd75e16211",
IsDeleted: false,
Reason: "I like it!",
Rating: 7
}
]
}
Entity Attributes
You may have noticed there is no constraint on the Rating property that we just created. NPS allows for ratings of 1-10, but we can add any value we like in the field. We want to constrain the value to a range between 1 and 10. To do this, we will add an Attribute to the Range Property.
Task:
- Add a Range Attribute to the Rating Property
[Range(0,10,ErrorMessage = "NPS\_InvalidRating")]
- Upload the extension to your running platform by clicking
- The Range Attribute will constrain values within the platform, but will not create a constraint on the Database column itself.
- From Postman send a POST request to:
http://localhost/api/NPSResponse
with the raw JSON body:
{
"Reason": "It is super awesome!",
"Rating": 11
}
You should receive an error response since the rating value of 11 is out of the allowed range.
- We'll cover this more later when we add UI elements. For now just be aware that Attributes can be added to affect the way the Platform creates, and manages the API.
Entity Relationships
NavigationProperties
NavigationProperties allow you to create a relationship from one Entity to another RelatedEntity. This is also referred to as a Many To One Relationship as the RelatedEntity may be related to Many Entities. In our NPS app we want every NPSResponse to be related to a Product.
HowTo
- Create a Cireson Entity Model named Product in the Models folder. *Refer to Entity Properties Section above
- Add a Name property to the Product model:
public String Name { get; set; }
- Add a Product NavigationProperty to the NPSResponse Model
Note
You may use the snippet provided for creating a NavigationProperty by pressing CTRL+K CTRL+X within a class. Select CS Snippets -> Cireson Entity Relationship Property and Press Enter
public Product Product { get; set; }
[ForeignKey("Product")]
public long? ProductId { get; set; }
- Upload your platform Extension by clicking
- Lets add a product. From Postman send a POST request to:
http://localhost/api/Product
with the raw JSON body:
{
"Name": "SMP"
}
- Browse to http://localhost/api/Product
- You should now see the Product you added in the browser:
{
@odata.context: "http://localhost/api/$metadata#Product",
value: [
{
Id: 1,
ModifiedDate: "2017-09-22T07:30:29.2975502Z",
ModifiedById: 1,
CreatedDate: "2017-09-22T07:30:29.2975502Z",
CreatedById: 1,
Guid: "771a2931-0c11-4df5-abde-486a5ef7ca4f",
IsDeleted: false,
Name: "SMP"
}
]
}
- Now let's add a new NPSResponse, but this time with a Product NavigationProperty. From Postman send a POST request to:
http://localhost/api/NPSResponse
with the raw JSON body:
{
"Reason": "It needs some help :(",
"Rating": 2,
"ProductId":1
}
Note
The OData Url used to access a NavigationProperty is
/api/{EntitySet}({EntityId})/NavigationPropertyName
- Now let's navigate to a related Product record, using the NavigationProperty we setup, go to: http://localhost/api/NPSResponse(2)/Product
- You should see the Product record that was related to the NPSResponse of id 2.
Note
This Url is fully queryable using standard OData operations, as well as Named Searches
NavigationCollections
One to Many
A NavigationCollection represents the inverse relationship that is defined by a NavigationProperty. If you wish to access a collection of Entities that are related to the current Entity, you will need to create a NavigationCollection in addition to the NavigationProperty. In our NPS app we want to track releases for a Product, so we can visualize how releases affect NPS Scores.
HowTo
- Create a Cireson Entity Model named Release in the Models folder. *Refer to Entity Properties Section above
- Add a Version and Date property to the Release model
public String Version { get; set; }
public DateTime Date { get; set; }
- Upload your platform Extension by clicking
- Now let's add some releases. From Postman send two POST requests to:
http://localhost/api/Release
with the raw JSON body:
{
"Version": "v1.0.1",
"Date": "2016-12-22T07:59:44.8616828Z",
"ProductId": 1
}
and
{
"Version": "v2.0.2",
"Date": "2017-02-22T07:59:44.8616828Z",
"ProductId": 1
}
- Add the Product NavigationProperty to the Release Model
public Product Product { get; set; }
[ForeignKey("Product")]
public long? ProductId { get; set; }
- Add the Release NavigationCollection to the Product model
Note
You may use the snippet provided for creating a NavigationCollection by pressing CTRL+K CTRL+X within a class. Select CS Snippets -> Cireson Entity Collection Property and Press Enter
[AllowExpand]
public ICollection<Release> Releases { get; set; }
Note
The OData Url used to access a NavigationCollection is
/api/{EntitySet}({EntityId})/{NavigationCollection}
- Now let's navigate to the Releases, using the NavigationCollection we setup, go to: http://localhost/api/Product(1)/Releases
- You should see the Releases that were related to the Product of id 1.
Note
This Url is fully queryable using standard OData operations, as well as Named Searches
Many to Many
If an Entity defines a NavigationCollection that represents the inverse relationship to a RelatedEntity's NavigationCollection, a Many to Many relationship is created. From either side, this relationship looks like a One To Many relationship, however each side can reference Many RelatedEntities. In our NPS app we want to add tag(s) to each NPSResponse so that we can filter/group responses by a single tag.
HowTo
- Create a Cireson Entity Model named Tag in the Models folder. *Refer to Entity Properties Section above
- Add a Name property to the Tag model:
public String Name { get; set; }
- Upload your platform Extension
- Now let's add some tags
http://localhost/api/Tag
with the raw JSON body:
{
"Name": "BMS"
}
and
{
"Name": "APAC"
}
- Add the NPSResponse NavigationCollection to the Tag Model
[AllowExpand]
public ICollection<NPSResponse> NPSResponses { get; set; }
- Add the Tag NavigationCollection to the NPSResponse model
[AllowExpand]
public ICollection<Tag> Tags { get; set; }
- Upload your platform Extension
Note
The OData Url used to access a NavigationCollection is
/api/{EntitySet}({EntityId})/{NavigationCollection}
This Url is fully queryable using standard OData operations, as well as Named Searches
- Open your browser and go to: http://localhost/api/NPSResponse(2)/Tags
At this point we have not related any tags so you will not get any results.
- Now we need to relate NPSResponses to Tags. From Postman send a POST request to:
http://localhost/api/NPSResponse(2)/Tags/$ref
with the raw JSON body:
{
"@odata.id": "http://localhost/api/Tags(1)"
}
This call should just return a Status 200 no response body is returned.
- Lets relate a second tag. From Postman send a POST request to:
http://localhost/api/NPSResponse(2)/Tags/$ref
with the raw JSON body:
{
"@odata.id": "http://localhost/api/Tags(2)"
}
- Now open your browser and go to: http://localhost/api/NPSResponse(2)/Tags You should now see the two related Tags
{
@odata.context: "http://localhost/api/$metadata#Tag",
value: [
{
Id: 1,
ModifiedDate: "2017-09-22T07:59:44.8616828Z",
ModifiedById: 1,
CreatedDate: "2017-09-22T07:59:44.8616828Z",
CreatedById: 1,
Guid: "5870e74d-9c11-4f4b-a3d6-8947c67c683e",
IsDeleted: false,
Name: "BMS"
},
{
Id: 2,
ModifiedDate: "2017-09-22T08:01:27.9069164Z",
ModifiedById: 1,
CreatedDate: "2017-09-22T08:01:27.9069164Z",
CreatedById: 1,
Guid: "63193546-e525-42d7-974b-6ed126612ba8",
IsDeleted: false,
Name: "APAC"
}
]
}
- Now lets unrelate a tag. From Postman send a DELETE request to:
http://localhost/api/NPSResponse(2)/Tags/$ref?$id=http://localhost/api/Tag(2)
with no JSON body.
*This call should just return a *Status 200 no response body is returned for DELETE **
- Now open your browser and go to: http://localhost/api/NPSResponse(2)/Tags You should now see only one related Tag.
Inheritance
Inheritance within the Platform behaves as it normally does within the CLR. Within the ORM, an inheritance relationship behaves like a One to One relationship where both Entities share their primary key.
It is important to note that there is a performance impact when using inheritance with large numbers of child classes.
Also, while the Inheritance root class in the CLR is object within the Entity Models, the inheritance root starts at the Entity that implements IPlatformEntity. Any base class that does not implement nor inherit from a class that implements IPlatformEntity will not be queryable within the ORM, or the OData endpoints.
HowTo
You define an Inheritance relationship as you normally would in C#
public class Manufacturer : BaseEntityType
Actions/Functions
Actions and Functions are components that you can use to create OData Compliant Service Operations that can be invoked via standard HTTP calls using POST and GET Methods respectively.
Declaring Functions
Unbound
An unbound function represents a Service Operation that accepts an HTTP GET Request.
HowTo
- Add a new Item to the Actions folder using the Cireson Unbound Function template. Name the new item NPSScore. Click Add.
- Since this function will return an integer we need to change the class declaration to inherit from UnboundFunction<int>:
public class NPSScore : UnboundFunction<int>
- As well as change the method declaration return type to Task<int>:
public override async Task<int> ExecuteAsync()
- For now let's just return a hardcoded value. Replace the contents of the Execute method with the following
return 9;
Test
- Upload your platform Extension
- Open your browser and go to http://localhost/api/NPSScore
Notice the OData API returned the value of 9. We will be adding additional logic later.
Bound
An bound function represents a Service Operation tied to an OData entity instance that accepts an HTTP GET Request.
Task
- Add a new Item to the Actions folder using the Cireson Bound Function template. Name the new item ResponseSummary. Click Add.
- Since this function will return an string we need to change the class declaration to inherit from BoundFunction<string>
public class ResponseSummary : BoundFunction <NPSResponse, string>
- As well as change the method declaration return type to Task<string>
public override async Task <string> ExecuteAsync()
- For now let's just return a hardcoded value. Replace the contents of the Execute method with the following
return await Task.FromResult($"Reason: {Entity.Reason}, Score: {Entity.Rating}");
Test
- Upload your platform Extension
- Open your browser to http://localhost/api/NPSResponse(2)/Action.ResponseSummary
Notice the OData API returned the composed string with NPSResponse 2's data.
CollectionBound
TBD
Declaring Actions
Unbound
An unbound action represents a Service Operation that accepts an HTTP POST Request.
Task
- Add a new Item to the Actions folder using the Cireson Unbound Action template. Name the new item NPSScoreAction. Click Add.
Note
You can quickly create a new item via: (Ctrl+Shift+A)
- Since this function will return an integer we need to change the class declaration to inherit from UnboundAction<int>
public class NPSScoreAction : UnboundAction<int>
- As well as change the method declaration return type to Task<int>
public override async Task<int> ExecuteAsync()
- For now let's just return a hardcoded value. Replace the contents of the Execute method with the following
return await Task.FromResult(9);
Test
- Upload your platform Extension
- Open Postman
- Point to http://localhost/api/NPSScoreAction
- Set the method to POST.
- Run the request.
Bound
An bound action represents a Service Operation tied to an OData entity instance that accepts an HTTP POST Request.
Task
- Add a new Item to the Actions folder using the Cireson Bound Action template. Name the new item SummaryAction. Click Add.
- Since this function will return an string we need to change the class declaration to inherit from BoundAction<NPSResponse, string>
public class SummaryAction : BoundAction <NPSResponse, string>
- As well as change the method declaration return type to Task<string>
public override async Task <string> ExecuteAsync()
For now let's just return a hardcoded value. Replace the contents of the Execute method with the following
return await Task.FromResult($"Reason: {Entity.Reason}, Score: {Entity.Rating}");
Test
Upload your platform Extension
- Open Postman
- Point to http://localhost/api/NPSReponse(1)/SummaryAction
- Set the method to POST
- Run the request
Notice the OData API returned the composed string with NPSResponse 1's data.
CollectionBound
TBD
Declaring Parameters
TBD
API Url Schema
Named Search Handling
The entity you defined earlier will produce an OData endpoint with queryable functionality via the $filter query string operator. See ( http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html)
While $filter provides a very power means of retrieving data from your application, it is often beneficial to allow users a simpler interface to retrieve data. The platform also supports a concept called Named Search Handling. By default, all entity types are given a search handler whose behavior filters each string property on the model using a StartsWith(string) operator.
Note
If the entity has FullText enabled, a freetext search is executed
The search behavior can be invoked from the query string of the api URL like so:
/api/{EntitySet}?search={searchName}:{searchTerm}
The searchName parameter will direct the platform to the appropriate search handler, if omitted, the default search handler will be used.
Multiple search handlers can be invoked from a single search by adding additional {searchName}:{SearchTerm} pairs separated by a space
/api/NPSResponse?search=tag:Test score:8
When the search operator is provided, the Platform will attempt to resolve the correct search handler(s) and execute the search.
Task
- Add an item to the Search folder using the Cireson Named Search Handler template. Name it ScoreSearchHandler. Click Add.
- Replace
[NamedSearch("OptionalSearchName")]
with[NamedSearch("Score")]
- Change the Generic Constraint
where TEntity : ReplaceWithAppropriateEntity
withwhere TEntity : NPSResponse
- Finally, change the SearchExpression method declaration to:
public Expression<Func<TEntity, bool>> SearchExpression(int searchText)
and return value to:return entity => entity.Rating >= searchText;
- Upload your extension
Test
Open your browser, and go to http://localhost/api/NPSResponse?search=score:8
It should return all responses with a score >= 8.
Security
In addition to providing the framework to quickly produce rich RESTful OData compliant APIs, the Cireson Platform also helps to secure your data, and ensure only the properly authorized users are allowed access. There are several tools that you can use to secure your application, which are:
- Endpoint security using Attributes.
- Entity level security using Entity Security Filters.
- Property Level security using Property Security Filters, or Attributes.
Endpoint Security
Endpoint security can be implemented on Entities, Actions, and Functions. By default, any public endpoint will require an authenticated connection to access.
In the event that you require anonymous access to any of your endpoints, you can add the
[AllowAnonymous]
attribute on the class.
The simplest way to secure an endpoint is to use the Cireson.Core.Common.Attributes.AuthorizeAttribute
[Authorize(Roles="Managers")]
The Authorize Attribute is applied to a class, and accepts a parameter for a comma separated list of Roles to allow access. Optionally, a description can be set to define the authorization for the application user.
If additional granularity is required, you can use the Cireson.Core.Common.Attributes.AuthorizeOdataAttribute
[AuthorizeOData("GET","PUT","POST", Roles ="Managers")]
This attribute works the same as Authorize, but adds the ability to secure specific http methods ("GET","POST","PUT","PATCH","DELETE").
Entity Level Security
Entity Level Security allows you to apply security filters on the server to prevent unauthorized users from accessing data. This layer is used by default for any externally exposed entityset, and can be used within functions and actions by injecting, and querying against the ISecuredRepository.
You can create an EntitySecurityFilter by adding a new class using the Cireson Entity Security Filter template.
Task
- Add a new item to the Filters folder using the Cireson Entity Security Filter. Name it MySecurityFilter. Click Add.
- Replace the text
ReplaceWithAppropriateEntity
withNPSResponse
in the file. - This represents the declaration for the security filter.
- Notice the 4 overloaded methods representing filtration expressions for the various access types. Each also receives a ClaimsIdentity representing the currently logged in user.
- Change the ReadAccessFilter method return value to:
return entity => identity.HasRole("Managers");
- Remove all other methods.
- This will allow Managers read access to the entities.
- More complex expressions are allowed as well.
Property Level Security
Property Level Security allows you to block access to specific properties on an object depending on the roles the current user has. For example, you may not want to show the phone number of an employee unless the user is also an employee. By implementing security on the property, you can ensure that the data is not sent to the client unless the appropriate access is granted.
Task
- Add a new item to the Filters folder using the Cireson Property Security Filter. Name it MyPropertyFilter. Click Add.
- Change the generic constraint in the class definition from
where TEntity : class, IPlatformEntity
towhere TEntity : NPSResponse
- Add the following to the constructor:
this.AddProperty(p => p.Reason);
- Notice the two overridden methods for DenyRead, and DenyWrite.
- Returning true from either of these methods will cause the property Reason to be blocked from reading or writing.
- If a user without access attempts to write to a denied property, the request will be ignored.
- Other property modifications in the same request will be persisted.
Consuming External Extensions
Reference via Nuget
Cireson Platform Extensions (CPEX) can reference other CPEX to add, and reuse features and functionality. Next we will add a reference to the Cireson WebUI CPEX to give our application a nice HTML based interface
Task
- Right-Click on your solution file.
- Select Manage Nuget Packages....
We first need to add the Cireson Public Package Feed
- Click the cog/gear to the left of the Package source dropdown
- In the window that opens click the green + button to add a new package source.
- in the bottom of the window replace the input values with:
- Name:
Cireson MyGet Public
- Source:
https://cireson.myget.org/F/public/api/v3/index.json
- Name:
- Click the Update button.
- Click the OK button.
Now lets install a CPEX:
- Make sure you have Cireson Public selected in your Package source dropdown.
- Select the Browse tab to the left and search for WebUI.
- Ensure the Include PreRelease option is checked.
- Select Cireson.Platform.Extension.Webui.
- Click the Install button on the right dialog.
- Click I Accept to accept licenses.
- Wait for Package Manger Console to display:
========== Finished ==========
- Upload your extension
and wait for the platform to restart.
Test
Open your browser and point navigate to http://localhost/ you should now be presented with a default web page view. In the next section, we will explore the WebUI URL convention, and how it relates to the API work we did earlier.
WebUI
The WebUI extension adds a Single Page Application Framework that acts as a rendering engine for the OData APIs that we create using the Platform's SDK. The URL convention is similar to the OData convention with a few differences.
The basic URL is laid out like so:
For Entities:
http://{hostName}/app/{PageName}/{EntitySet}/{EntityId}
For Unbound Actions/Functions:
http://{hostName}/app/{PageName}/{ActionName}/{ActionParameters}
You can omit any segment to the right of the previous segment, so
http://{hostName}/app/{PageName}/{EntitySet} is valid because we omitted EntityId
however
http://{hostName}/app/{PageName}/{EntityId}
is invalid because we omitted the EntitySet segment that is to the left of the EntityId segment.
Test
Open your web browser and go to:
http://localhost/app/Default/NPSResponse
you should see a default rendering of your NPSResponse Entityset in a table.
You can cross reference this URL to the associated API url. http://localhost/api/NPSResponse will show the same data without the HTML.
Static Content
All static content that you wish to expose to the web (HTML, images, etc. ) needs to be placed in the /ContentRoot folder of your project. Let's test this out by adding an html page that we can access through the web browser.
Task
- Add a new HTML Page to the ContentRoot folder named HelloWorld.html.
- Add whatever content you like in the body.
- Upload your Content Root folder's content
- Navigate to http://localhost/app/HelloWorld.html
- You should see a web page with the content you added.
Content Override
Static content is presented by the extension in which it is created, so multiple extensions can add different files, and different extensions can add the same file.
When two extensions have the same files in the ContentRoot folder, or any folder within ContentRoot, only one of the files can be served. We call this Content Overriding.
The way the Platform determines which file gets served is based on two factors:
- The ResourcePriority value specified in the /Extension.Manifest
- If two extensions have the same ResourcePriority value, the extension that was installed last will be chosen.
Note
The ResourcePriority value applies to all static content for that extension
Task
WebUI ships with an /Index.html page in /ContentRoot and has a ResourcePriority of 0.
- Edit the Extension.Manifest file in root and set ResourcePriority to 1000.
- Add a new Index.html file to ContentRoot.
- Put any content you wish in the Index.html file
- Upload your extension
- Navigate to http://localhost/
- You should see your Index.html.
- Delete Index.html.
- Upload your extension.
- Navigate to http://localhost/
- You should see the default WebUI interface.
Template Customizations
The WebUI extension provides a base implementation of templates(HTML) and Behaviors(Javascript) that can be used to quickly create a SPA application around your Platform Extension.
Any template or behavior can be extended to add functionality or visual elements specific to the UI that you want to build.
Let's start by looking at the WebTemplate component.
Look at This
- Navigate to http://localhost/app/Default/NPSResponse and click the first item in the grid.
- You should now see a default generated template for the NPSResponse entity that you clicked.
- In many cases you will want to modify the templates to produce a custom look specific to the entity type being rendered.
- To do this, we first need to identify the template that is being used to generate the content and then add an override for that template.
- In Chrome browser, right-click somewhere in the Entity form, and click Inspect to view the source of the page.
Note
The main directive that is used to render page elements is the <cireson-templated-data> directive, or <ctd> for short.
The <ctd> is a general purpose tag that allows you to discreetly render a data from your API to the screen.
It is important to note that the data coming from the service is not always presented in its entirety during the page load. For example in the case of a NavigationCollection, or a NavigationProperty, the main entity may be available, but the related entity/entities will be loaded from the service as needed.
The reason for this is clear when we look at some of at the possible scenarios. Imagine you are building an extension to track Products, and Product Categories. You can imagine a Product Category can have relationships to a few products, or even a few thousand products. When looking at a specific Product Category, if all the Products were downloaded within the same OData call, the data returned could be prohibitively large. Using a <ctd> allows the WebUI to determine the best time to load the related data, as well as the templates to use to display the data once it is available.
In addition to related entity data, the <ctd> can also be used to render Functions, Actions, Commands, and Primitive Properties.
Looking at the Source for the page, you will see <ctd> tags, and <ng-include> tags. The <ctd> tag represents the intended rendering, and the <ng-include> is what the WebUI actually decided was the appropriate content to render for that tag based on the type of data.
Here is a sample of one such tag that you may see:
<ctd model="model.currentContent" options="{ applyToRoute:true }" state="done">
<ng-include src="template.templateHash" class="cireson-templated-data" ctd-model-type="['ciresonplatformnps.models.npsresponse','$data.entity','$data.base','']" ctd-selected-template="any|ciresonplatformnps.models.npsresponse" ctd-attached-behaviors="$data.entity.entityNavigationBehavior,$data.entity.entityCrudBehavior,$data.entity.EntityDataAccessCommands" style="">
<h1>This is my CiresonPlatformNPS.ContentRoot.Themes.Default.NPSResponse template</h1>
<h2> Add html code here to render the model.</h2>
</ng-include>
</ctd>
Right now, we are only concerned with a couple of the attributes, but we will cover the rest in more detail later.
- The model attribute on the <ctd> indicates the source of the data to be presented, in the above example, the WebPage will automatically load the data that is specified in the URL (this is typically EntitySet, an Entity, or an UnboundFunction or UnboundAction) and present it as the model.currentContet in the page.
- Once the ctd has a Model to render, it first attempts to determine the type of data in the model. It then renders the <ng-include> so it has a container to inject the appropriate template that can render the Model
- Look at the ctd-model-type, it gives you an indication of the type of data it is rendering ordered by most specific type to least specific type according to the inheritance chain of the class.
- Now look at the ctd-selected-template, this indicates which template was chosen as the template to be used to render the model. The convention for indicating the template is {TemplateKey}|{DataType}.
- With this information, we are now ready to override the default template for our NPSResponse entity.
Task
- Go back to visual studio.
- Create a New Folder inside of ContentRoot named "Themes"
- Inside the Themes folder, create another one named "Default"
- Your ContentRoot folder should now have the folder structure "ContentRoot\Themes\Default"
- This is where we will be placing our templates so the Webui Rendering engine can find them.
- You can create additional folder structures to organize your customizations in a way that is most convenient for your project.
- Add a new item to the Default folder using the Cireson Web Data template. Name it NPSResponse. Click on Add.
- The file will be added as NPSResponse.template.html The actual name of the file is not important, but it does need to have the .html extension for the template engine to use it.
- Open the file and you will see some boilerplate declarations:
- The only part of the template html that will be sent to the client is what is inside the <template> tag the rest is used serverside to determine which templates are presented to specific clients.
- You can add multiple <DataTemplate> tags in a single html file if you would like to organize related templates that way.
- Since we want this template to be used as the default renderer for NPSResponse objects, we can leave
key="any"
and changetype="any"
totype="EntitySet<ciresonplatformnps.models.npsresponse>"
Note
This is the full namespace + name of the class. It may not be the same as what is shown, so make sure the type matches the type namespace.name from your project.
- Save your template and upload your extension
- Open your browser and go to http://localhost/app/Default/NPSResponse
- You should see the contents of your template replace the default.
Using CTD in your templates
Now we are going to edit our template to display information from our model. We can use standard HTML markup, Angular Directives, Kendo-UI, etc. However for now, we just want to display a textbox for our NPSResponse.Rating and NPSResponse.Reason input.
Task
1.Replace the html inside the <template> tag with the following:
<h1>NPSResponse</h1>
<ctd property-name="Rating"></ctd>
<ctd property-name="Reason"></ctd>
- Upload your extension
- Refresh your page.
- Notice you now have 2 fields Displaying the [Rating] and the [Reason].
- Also notice that the Rating field is a numeric field with a spin button.
- This is because we did not tell the WebUI how to render the data, only that we wanted to render it at that location.
- The Template Engine used the same matching technique to determine the proper rendering for the data based on the type.
- You will also notice that the save buttons are no longer there. This is because the Save buttons were declared as part of the default template. To get the button back on our page, we simply need to tell the WebUI to render the proper commands, and we can do this with another ctd.
- In your template add the following code below the Reason ctd
<ctd model="model.commandGroups.Primary"></ctd>
<ctd model="model.commandGroups.Secondary"></ctd>
- Upload and refresh.
- We'll get into more detail as far as what we just did when we cover behaviors, commands, and services later on, but for now just realize that we were able to consume a lot of functionality with a few lines of code. Cool!
Behavior Customizations
Behaviors allow you to run javascript code that is tied to a specific model type, or injected manually at certain points in a page. Behaviors allow you to expand the functionality of the client experience well beyond what is default.
Behaviors are, like templates, attached to type and name. If two behaviors have the same type AND name then the one with the higher resource override is attached.
If you want to attach a behavior manually you would specify a type of "key" and a specific name. You can then attach manually through the template by including the below element at the top of the template.
<attached-behavior behaviors="behaviorName,otherBehaviorName" />
Behavior files are constructed as an XML file with the following elements:
- Roles
- Currently not used, but will eventually tie into the roles of the current user.
- Dependencies
- List of dependencies that the behavior will have available to it.
- These dependencies are angular services.
- The name of the variable available in any of the behavior script blocks will be the dependency name.
- Bootstrap
- Can contain sub-elements which contains a function that returns a promise.
- The promises for all bootstrap elements will be resolved prior to the init, or the attach events running.
- The bootstrap executes only once per model instance.
- The result of the promise is assigned to a property on the boostrap object that is the same name as the sub-element name, in this case 'propertyName'.
<script event="bootstrap" name="propertyName"> return promise; </script>
- Resolve
- Used to eager load additional navigation properties / collections.
- This should be used sparingly as it is preferred to allow the rendering engine to lazy load required related data.
<script type="toc.ymltext/javascript"toc.yml event="toc.ymlresolve"toc.yml> function(query) { return query.include('x => x.NavigationProperty'); } </script>
- Script[event='init']
- Init is the section of code you'll want to use to attach functions and properties to the scope and or model only once. The init section will only run once for that behavior and model instance.
- Script[event='attach']
- Attach is the section of code you'll want to use to attach functions and properties to the scope and or model one or many times. This section will run whenever a behavior is attached to model.
- Script[event='detach']
- Detach is the section of code you'll want to use when cleaning up any code when the behavior is attached, such as manually bound events, etc.
- You will have access to the following variables within the behavior events
- scope - Angular scope object.
- model - The data model this behavior is bound to.
- propertyName - The property name that this behavior is referencing (May be null).
- Some properties are meta-properties (navigation relationships) and only reference the means to retrieve the data, in these cases, the data may not be available in the behavior for the parent model, and you will need to apply the behavior to the type of the child model.
- To get the value of other properties (Primitives) you would use model[propertyName].
- element -the DOM element that this behavior is attached to, or in the case of the init event, the first DOM element that was initialized with this behavior.
- attr – All attributes specified on the ctd.
Back to the Server!
Dependency Injection (C#)
In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. Let's use a Cireson Bound Function as an example.
- Add a new item to the Actions folder using the Cireson Bound Function template. Name it NPSBoundFunction. Click Add.
Constructor
We can use dependency injection through the class constructor by introducing a private class variable and a constructor like so:
private readonly ISecuredRepository _securedRepository;
public NPSBoundFunction(ISecuredRepository securedRepository) {
_securedRepository = securedRepository;
}
In this example we are injecting an implementation of a ISecuredRepository into the constructor and assigning it to a local class variable.
Property
We can also use dependency injection through a property by introducing a private class variable and a public property like so:
private ISecuredRepository _securedRepository;
[Dependency]
public ISecuredRepository SecuredRepository{
get { return _securedRepository; }
set { _securedRepository = value; }
}
DataAccess
ISecuredRepository
A connection to a secured repository allows a user to make an entity-level secured connection to a repository. This entity-level connection is used by default.
Using the ISecuredRepository is accomplished by injecting it into the constructor of our TestClass like so:
private ISecuredRepository _securedRepository;
public TestClass(ISecuredRepository securedRepository)
{
_securedRepository = securedRepository;
}
Once injected the _securedRepository can be used to access a repository. In the following example, the code is getting SoftwareProducts in a Cireson Bound Function:
public override async Task<IList<SoftwareProduct>> ExecuteAsync() {
return await _securedRepository.Get<SoftwareProduct>().ToListAsync();
}
Note
The lifespan of the ISecureRepository by default is PerRequest, which means that the same instance of the ISecureRepository will be available on any injection for the current http request.
IRepository
We can also allow users to make a connection to a repository.
Using the IRepository is accomplished by injecting it into the constructor of our TestClass like so:
private IRepository _repository;
public TestClass(IRepository repository) {
_repository = repository;
}
Once injected the _repository can be used to access a repository. In the following example, the code is getting SoftwareProducts in a Cireson Bound Function:
public override async Task<IList<SoftwareProduct>> ExecuteAsync() {
return await _repository.Get<SoftwareProduct>().ToListAsync();
}
Note
The lifespan of the IRepository by default is PerRequest, which means that the same instance of the IRepository will be available on any injection for the current http request.
IDataContext
A connection to a data context allows users to make a connection to a repository.
Using the IDataContext is accomplished by injecting it into the constructor of our TestClass like so:
private IDataContext _dataContext;
public TestClass(IDataContext dataContext) {
_dataContext = dataContext;
}
Once injected the _dataContext can be used to access a repository. In the following example, the code is getting SoftwareProducts in a Cireson Bound Function:
public override async Task<IList<SoftwareProduct>> ExecuteAsync() {
return await _dataContext.Set<SoftwareProduct>().ToListAsync();
}
Note
The lifespan of the IDataContext by default is PerResolve, which means that each time an IDataContext is resolved, either via a constructor, or by calling container.Resolve<IDataContext>() a new instance is created.
Triggers
Description:
Triggers allow you to execute code when an Entity in the system is Added/Modified, or Deleted.
A trigger can execute code before, during, or after a transaction. Triggers are executed during the SaveChanges(), or SaveChangesAsync() process.
Before – This override will fire before the transaction for the current command is created. If this override throws an exception, the Save operation will halt, and no changes will be committed.
During – This override fires within the transaction, and after the requested changes have been committed. Generated Identity values are accessible for the modified entity.
After – This override fires after the changes have been committed. An exception thrown here will not affect the persisting of the data.
Note
Under most common scenarios, only a single ambient transaction will be active during the save operation. However, it is possible for a developer to create a higher order transaction scope, in which case, the trigger overrides are only relevant for the current transaction scope, and a higher order scope may still rollback the changes to the data even after the After override fires.
HowTo
To create a trigger, simply create a new Project Item using the Cireson -> Triggers -> Cireson Entity Trigger, or Cireson Entity Trigger Generic.
The difference being that the Cireson Entity Trigger will fire for the Specific Type, whereas the Cireson Entity Trigger Generic will fire for any Type that matches the Generic Type Constraint provided in the Trigger Class.
Services
IConfigService
The IConfigService is an injectable service that allows access to Configuration Values. The GetSetting, and SetSetting methods store and retrieve values from the SystemSetting EntitySet. If a matching key is not found in SystemSettings, GetSetting will also check for a value in the AppSettings .config file before reverting to the default value.
ICpexConfigurationService
This is an injectable service that allows a Cpex Developer to create structured configuration settings that can be accessed as a single object, but is stored in JSON format by the Type in the SystemSetting EntitySet.
HowTo
To take advantage of this powerful feature, simply create a class that inherits Cireson.Core.Interfaces.Services.SystemSettings.SystemSettingsBase call it "ConnectionSettings":
[SystemSettingInfo(Description = "ContentLibrarySyncSettings")]
public class ConnectionSettings : SystemSettingsBase {
public string ConnectionName { get; set; }
public string Url { get; set; }
public bool Anonymous { get; set; }
public string Secret { get; set; }
}
You can then use the ICpexConfigurationService to retrieve and save the settings as a whole. Additionally, you can use the OData api endpoints /api/Get_SystemSetting('{TypeName}') and /api/Set_SystemSetting('{TypeName}') to get or set your system setting. Default UIs for the SystemSettingsBase types will also be generated by the WebUI extension if you are using it.
IUserSettingService
This injectable service is similar in functionality to the ICpexConfigurationService with the notable difference that this service will retrieve configuration items ONLY for the currently logged in user.
HowTo
Create a User settings class that inherits from Cireson.Core.Interfaces.Services.UserSettings.UserSettingsBase call it "UserConnectionSettings":
[UserSettingInfo(Description = "ContentLibrarySyncSettings")]
public class UserConnectionSettings : UserSettingsBase {
public string ConnectionName { get; set; }
public string Url { get; set; }
public bool Anonymous { get; set; }
public string Secret { get; set; }
}
You can then use the IUserSettingService to retrieve and save the settings as a whole. Additionally, you can use the OData API endpoints /api/Get_UserSetting('{TypeName}') and /api/Set_UserSetting('{TypeName}') to get or set your system setting. Default UIs for the UserSettingsBase types will also be generated by the WebUI extension if you are using it.