FluentCMS 0.3.2
dotnet add package FluentCMS --version 0.3.2
NuGet\Install-Package FluentCMS -Version 0.3.2
<PackageReference Include="FluentCMS" Version="0.3.2" />
paket add FluentCMS --version 0.3.2
#r "nuget: FluentCMS, 0.3.2"
// Install FluentCMS as a Cake Addin #addin nuget:?package=FluentCMS&version=0.3.2 // Install FluentCMS as a Cake Tool #tool nuget:?package=FluentCMS&version=0.3.2
Welcome to FluentCMS! 🚀
FluentCMS makes content management seamless with its powerful GraphQL API and intuitive drag-and-drop page design features.
If you'd like to contribute, please check out our CONTRIBUTING guide.
Enjoying FluentCMS? Don’t forget to give us a ⭐ and help us grow!
What is Fluent CMS?
Fluent CMS is an open-source Content Management System designed to simplify and accelerate web development workflows. While it's particularly suited for CMS projects, it is also highly beneficial for general web applications, reducing the need for repetitive REST/GraphQL API development.
Effortless CRUD Operations: Fluent CMS includes built-in RESTful APIs for Create, Read, Update, and Delete (CRUD) operations, complemented by a React-based admin panel for efficient data management.
Powerful GraphQL Queries: Access multiple related entities in a single query, enhancing client-side performance, security, and flexibility.
Drag-and-Drop Page Designer: Build dynamic pages effortlessly using the integrated page designer powered by Grapes.js and Handlebars. Easily bind data sources for an interactive and streamlined design experience.
Online Course System Demo
Source Code
Example Blog Project on GitHub
Live Demo
- Public Site: fluent-cms-admin.azurewebsites.net
- Admin Panel: fluent-cms-admin.azurewebsites.net/admin
- Email:
admin@cms.com
- Password:
Admin1!
- Email:
Additional Resources
- GraphQL Playground: fluent-cms-admin.azurewebsites.net/graph
- Documentation: fluent-cms-admin.azurewebsites.net/doc/index.html
Online Course System Backend
This section provides detailed guidance on developing a foundational online course system, encompassing key entities: teacher
, course
, skill
, and material
.
Database Schema
1. Teachers Table
The Teachers
table maintains information about instructors, including their personal and professional details.
Field | Header | Data Type |
---|---|---|
id |
ID | Int |
firstname |
First Name | String |
lastname |
Last Name | String |
email |
String | |
phone_number |
Phone Number | String |
image |
Image | String |
bio |
Bio | Text |
2. Courses Table
The Courses
table captures the details of educational offerings, including their scope, duration, and prerequisites.
Field | Header | Data Type |
---|---|---|
id |
ID | Int |
name |
Course Name | String |
status |
Status | String |
level |
Level | String |
summary |
Summary | String |
image |
Image | String |
desc |
Description | Text |
duration |
Duration | String |
start_date |
Start Date | Datetime |
3. Skills Table
The Skills
table records competencies attributed to teachers.
Field | Header | Data Type |
---|---|---|
id |
ID | Int |
name |
Skill Name | String |
years |
Years of Experience | Int |
created_by |
Created By | String |
created_at |
Created At | Datetime |
updated_at |
Updated At | Datetime |
4. Materials Table
The Materials
table inventories resources linked to courses.
Field | Header | Data Type |
---|---|---|
id |
ID | Int |
name |
Name | String |
type |
Type | String |
image |
Image | String |
link |
Link | String |
file |
File | String |
Relationships
- Teachers to Courses: One-to-Many (Each teacher can teach multiple courses; each course is assigned to one teacher).
- Teachers to Skills: Many-to-Many (Multiple teachers can share skills, and one teacher may have multiple skills).
- Courses to Materials: Many-to-Many (A course may include multiple materials, and the same material can be used in different courses).
Schema Creation via Fluent CMS Schema Builder
Accessing Schema Builder
After launching the web application, locate the Schema Builder menu on the homepage to start defining your schema.
Adding Entities
- Navigate to the Entities section of the Schema Builder.
- Create entities such as "Teacher" and "Course."
- For the
Course
entity, add attributes such asname
,status
,level
, anddescription
.
Defining Relationships
1. Course and Teacher (Many-to-One Relationship)
To establish a many-to-one relationship between the Course
and Teacher
entities, you can include a Lookup
attribute in the Course
entity. This allows selecting a single Teacher
record when adding or updating a Course
.
Attribute | Value |
---|---|
Field | teacher |
Type | Lookup |
Options | Teacher |
Description: When a course is created or modified, a teacher record can be looked up and linked to the course.
2. Course and Materials (Many-to-Many Relationship)
To establish a many-to-many relationship between the Course
and Material
entities, use a Junction
attribute in the Course
entity. This enables associating multiple materials with a single course.
Attribute | Value |
---|---|
Field | materials |
Type | Junction |
Options | Material |
Description: When managing a course, you can select multiple material records from the Material
table to associate with the course.
Admin Panel: Data Management Features
1. List Page
Example Course List Page
The List Page displays entities in a tabular format, enabling sorting, searching, and pagination. Users can efficiently browse or locate specific records.
2. Detail Page
Example Course Detail Page
The Detail Page provides an interface for viewing and managing detailed attributes. Related data such as teachers and materials can be selected or modified.
GraphQL Query
FluentCMS simplifies frontend development by offering robust GraphQL support.
Getting Started
Accessing the GraphQL IDE
To get started, launch the web application and navigate to /graph
. You can also try our online demo.
Singular vs. List Response
For each entity in FluentCMS, two GraphQL fields are automatically generated:
<entityName>
: Returns a record.<entityNameList>
: Returns a list of records.
**Single Course **
{
course {
id
name
}
}
**List of Courses **
{
courseList {
id
name
}
}
Field Selection
You can query specific fields for both the current entity and its related entities. Example Query:
{
courseList{
id
name
teacher{
id
firstname
lastname
skills{
id
name
}
}
materials{
id,
name
}
}
}
Filtering with Value Match
in FluentCMS
FluentCMS provides flexible filtering capabilities using the idSet
field (or any other field), enabling precise data queries by matching either a single value or a list of values.
Filter by a Single Value Example:
{
courseList(idSet: 5) {
id
name
}
}
Filter by Multiple Values Example:
{
courseList(idSet: [5, 7]) {
id
name
}
}
Advanced Filtering with Operator Match
in FluentCMS
FluentCMS supports advanced filtering options with Operator Match
, allowing users to combine various conditions for precise queries.
matchAll
Example:
Filters where all specified conditions must be true.
In this example: id > 5 and id < 15
.
{
courseList(id: {matchType: matchAll, gt: 5, lt: 15}) {
id
name
}
}
matchAny
Example:
Filters where at least one of the conditions must be true.
In this example: name starts with "A"
or name starts with "I"
.
{
courseList(name: [{matchType: matchAny}, {startsWith: "A"}, {startsWith: "I"}]) {
id
name
}
}
Filter Expressions
in FluentCMS
Filter Expressions allow precise filtering by specifying a field, including nested fields using JSON path syntax. This enables filtering on subfields for complex data structures.
Example: Filter by Teacher's Last Name This query returns courses taught by a teacher whose last name is "Circuit."
{
courseList(filterExpr: {field: "teacher.lastname", clause: {equals: "Circuit"}}) {
id
name
teacher {
id
lastname
}
}
}
Sorting
Sorting by a single field
{
courseList(sort:nameDesc){
id,
name
}
}
Sorting by multiple fields
{
courseList(sort:[level,id]){
id,
level
name
}
}
Sort Expressions in FluentCMS
Sort Expressions allow sorting by nested fields using JSON path syntax.
Example: Sort by Teacher's Last Name
{
courseList(sortExpr:{field:"teacher.lastname", order:Desc}) {
id
name
teacher {
id
lastname
}
}
}
Pagination
Pagination on root field
{
courseList(offset:2, limit:3){
id,
name
}
}
Try it here
Pagination on sub field
{
courseList{
id,
name
materials(limit:2){
id,
name
}
}
}
Variable
Variables are used to make queries more dynamic, reusable, and secure.
Variable in Value filter
query ($id: Int!) {
teacher(idSet: [$id]) {
id
firstname
lastname
}
}
Variable in Operator Match
filter
query ($id: Int!) {
teacherList(id:{equals:$id}){
id
firstname
lastname
}
}
Variable in Filter Expression
query ($years: String) {
teacherList(filterExpr:{field:"skills.years",clause:{gt:$years}}){
id
firstname
lastname
skills{
id
name
years
}
}
}
Variable in Sort
query ($sort_field:TeacherSortEnum) {
teacherList(sort:[$sort_field]) {
id
firstname
lastname
}
}
Variable in Sort Expression
query ($sort_order: SortOrderEnum) {
courseList(sortExpr:{field:"teacher.id", order:$sort_order}){
id,
name,
teacher{
id,
firstname
}
}
}
Variable in Pagination
query ($offset:Int) {
teacherList(limit:2, offset:$offset) {
id
firstname
lastname
}
}
Required vs Optional
If you want a variable to be mandatory, you can add a !
to the end of the type
query ($id: Int!) {
teacherList(id:{equals:$id}){
id
firstname
lastname
}
}
Explore the power of FluentCMS GraphQL and streamline your development workflow!
Saved Query
Realtime queries may expose excessive technical details, potentially leading to security vulnerabilities.
Saved Queries address this issue by abstracting the GraphQL query details. They allow clients to provide only variables, enhancing security while retaining full functionality.
Transitioning from Real-Time Queries to Saved Queries
Using OperationName
as the Saved Query Identifier
In FluentCMS, the Operation Name in a GraphQL query serves as a unique identifier for saved queries. For instance, executing the following query automatically saves it as TeacherQuery
:
query TeacherQuery($id: Int) {
teacherList(idSet: [$id]) {
id
firstname
lastname
skills {
id
name
}
}
}
Saved Query Endpoints
FluentCMS generates two API endpoints for each saved query:
List Records:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuerySingle Record:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/single/
Using REST API Query Strings as Variables
The Saved Query API allows passing variables via query strings:
Single Value:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/?id=3Array of Values:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?id=3&id=4
This passes[3, 4]
to theidSet
argument.
Additional Features of Saved Query
Beyond performance and security improvements, Saved Query
introduces enhanced functionalities to simplify development workflows.
Pagination by offset
Built-in variables offset
and limit
enable efficient pagination. For example:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=2&offset=2
offset
Pagination for Subfields
To display a limited number of subfield items (e.g., the first two skills of a teacher), use the JSON path variable, such as skills.limit
:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2
Pagination by cursor
For large datasets, offset
pagination can strain the database. For example, querying with offset=1000&limit=10
forces the database to retrieve 1010 records and discard the first 1000.
To address this, Saved Query
supports cursor-based pagination, which reduces database overhead.
Example response for https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3:
[
{
"hasPreviousPage": false,
"cursor": "eyJpZCI6M30"
},
{
},
{
"hasNextPage": true,
"cursor": "eyJpZCI6NX0"
}
]
If
hasNextPage
of the last record istrue
, use the cursor to retrieve the next page:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3&last=eyJpZCI6NX0Similarly, if
hasPreviousPage
of the first record istrue
, use the cursor to retrieve the previous page:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?limit=3&first=eyJpZCI6Nn0
Cursor-Based Pagination for Subfields
Subfields also support cursor-based pagination. For instance, querying https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery?skills.limit=2 returns a response like this:
[
{
"id": 3,
"firstname": "Jane",
"lastname": "Debuggins",
"hasPreviousPage": false,
"skills": [
{
"hasPreviousPage": false,
"cursor": "eyJpZCI6MSwic291cmNlSWQiOjN9"
},
{
"hasNextPage": true,
"cursor": "eyJpZCI6Miwic291cmNlSWQiOjN9"
}
],
"cursor": "eyJpZCI6M30"
}
]
To fetch the next two skills, use the cursor:
https://fluent-cms-admin.azurewebsites.net/api/queries/TeacherQuery/part/skills?limit=2&last=eyJpZCI6Miwic291cmNlSWQiOjN9
Drag and Drop Page Designer
The page designer utilizes the open-source GrapesJS and Handlebars, enabling seamless binding of GrapesJS Components
with FluentCMS Queries
for dynamic content rendering.
Page Types: Landing Page, Detail Page, and Home Page
Landing Page
A landing page is typically the first page a visitor sees.
- URL format:
/page/<pagename>
- Structure: Comprised of multiple sections, each section retrieves data via a
query
.
Example:
Landing Page
This page fetches data from:
- https://fluent-cms-admin.azurewebsites.net/api/queries/course/?status=featured
- https://fluent-cms-admin.azurewebsites.net/api/queries/course/?level=Advanced
Detail Page
A detail page provides specific information about an item.
- URL format:
/page/<pagename>/<router parameter>
- Data Retrieval: FluentCMS fetches data by passing the router parameter to a
query
.
Example:
Course Detail Page
This page fetches data from:
https://fluent-cms-admin.azurewebsites.net/api/queries/course/one?course_id=22
Home Page
The homepage is a special type of landing page named home
.
- URL format:
/pages/home
- Special Behavior: If no other route matches the path
/
, FluentCMS renders/pages/home
by default.
Example:
The URL /
will be resolved to /pages/home
unless explicitly overridden.
Introduction to GrapesJS Panels
Understanding the panels in GrapesJS is crucial for leveraging FluentCMS's customization capabilities in the Page Designer UI. This section explains the purpose of each panel and highlights how FluentCMS enhances specific areas to streamline content management and page design.
Style Manager:
- Used to customize CSS properties of elements selected on the canvas.
- FluentCMS Integration: This panel is left unchanged by FluentCMS, as it already provides powerful styling options.
Traits Panel:
- Allows modification of attributes for selected elements.
- FluentCMS Integration: Custom traits are added to this panel, enabling users to bind data to components dynamically.
Layers Panel:
- Displays a hierarchical view of elements on the page, resembling a DOM tree.
- FluentCMS Integration: While FluentCMS does not alter this panel, it’s helpful for locating and managing FluentCMS blocks within complex page designs.
Blocks Panel:
- Contains pre-made components that can be dragged and dropped onto the page.
- FluentCMS Integration: FluentCMS enhances this panel by adding custom-designed blocks tailored for its CMS functionality.
By familiarizing users with these panels and their integration points, this chapter ensures a smoother workflow and better utilization of FluentCMS's advanced page-building tools.
Data Binding: Singleton or List
FluentCMS leverages Handlebars expressions for dynamic data binding in pages and components.
Singleton
Singleton fields are enclosed within {{ }}
to dynamically bind individual values.
- Example Page Settings: Page Schema Settings
- Example Query: Retrieve Course Data
- Example Rendered Page: Rendered Course Page
List
Handlebars
supports iterating over arrays using the {{#each}}
block for repeating data structures.
{{#each course}}
<li>{{title}}</li>
{{/each}}
In FluentCMS, you won’t explicitly see the {{#each}}
statement in the Page Designer. If a block's data source is set to data-list
, FluentCMS automatically generates the loop.
- Example Page Settings: Page Schema Settings
- Example Rendered Page: Rendered List Page
- Example Queries:
Steps to Bind a Data Source
To bind a Data List
to a component, follow these steps:
- Drag a block from the Data List category in the Page Designer.
- Open the Layers Panel and select the
Data List
component. - In the Traits Panel, configure the following fields:
Field | Description |
---|---|
Query | The query to retrieve data. |
Qs | Query string parameters to pass (e.g., ?status=featured , ?level=Advanced ). |
Offset | Number of records to skip. |
Limit | Number of records to retrieve. |
Pagination | Options for displaying content: |
- Button: Divides content into multiple pages with navigation buttons (e.g., "Next," "Previous," or numbered buttons). | |
- Infinite Scroll: Automatically loads more content as users scroll. Ideal for a single component at the bottom of the page. | |
- None: Displays all available content at once without requiring additional user actions. |
Online Course System Frontend
Having established our understanding of Fluent CMS essentials like Entity, Query, and Page, we're ready to build a frontend for an online course website.
Key Pages
- Home Page (
home
): The main entry point, featuring sections like Featured Courses and Advanced Courses. Each course links to its respective Course Details page. - Course Details (
course/{course_id}
): Offers detailed information about a specific course and includes links to the Teacher Details page. - Teacher Details (
teacher/{teacher_id}
): Highlights the instructor’s profile and includes a section displaying their latest courses, which link back to the Course Details page.
Home Page
|
|
+-------------------+
| |
v v
Latest Courses Course Details
| |
| |
v v
Course Details <-------> Teacher Details
Designing the Home Page
- Drag and Drop Components: Use the Fluent CMS page designer to drag a
Content-B
component. - Set Data Source: Assign the component's data source to the
course
query. - Link Course Items: Configure the link for each course to
/pages/course/{{id}}
. The Handlebars expression{{id}}
is dynamically replaced with the actual course ID during rendering.
Creating the Course Details Page
- Page Setup: Name the page
course/{course_id}
to capture thecourse_id
parameter from the URL (e.g.,/pages/course/20
). - Query Configuration: The variable
{course_id:20}
is passed to thecourse
query, generating aWHERE id IN (20)
clause to fetch the relevant course data. - Linking to Teacher Details: Configure the link for each teacher item on this page to
/pages/teacher/{{teacher.id}}
. Handlebars dynamically replaces{{teacher.id}}
with the teacher’s ID. For example, if a teacher object has an ID of 3, the link renders as/pages/teacher/3
.
Creating the Teacher Details Page
- Page Setup: Define the page as
teacher/{teacher_id}
to capture theteacher_id
parameter from the URL. - Set Data Source: Assign the
teacher
query as the page’s data source.
Adding a Teacher’s Courses Section
- Drag a
ECommerce A
component onto the page. - Set its data source to the
course
query, filtered by the teacher’s ID (WHERE teacher IN (3)
).
When rendering the page, the PageService
automatically passes the teacher_id
(e.g., {teacher_id: 3}
) to the query.
Optimizing Caching
Fluent CMS employs advanced caching strategies to boost performance.
For detailed information on ASP.NET Core caching, visit the official documentation: ASP.NET Core Caching Overview.
Cache Schema
Fluent CMS automatically invalidates schema caches whenever schema changes are made. The schema cache consists of two types:
Entity Schema Cache
Caches all entity definitions required to dynamically generate GraphQL types.Query Schema Cache
Caches query definitions, including dependencies on multiple related entities, to compose efficient SQL queries.
By default, schema caching is implemented using IMemoryCache
. However, you can override this by providing a HybridCache
. Below is a comparison of the two options:
IMemoryCache
- Advantages:
- Simple to debug and deploy.
- Ideal for single-node web applications.
- Disadvantages:
- Not suitable for distributed environments. Cache invalidation on one node (e.g., Node A) does not propagate to other nodes (e.g., Node B).
HybridCache
- Key Features:
- Scalability: Combines the speed of local memory caching with the consistency of distributed caching.
- Stampede Resolution: Effectively handles cache stampede scenarios, as verified by its developers.
- Limitations:
The current implementation lacks "Backend-Assisted Local Cache Invalidation," meaning invalidation on one node does not instantly propagate to others. - Fluent CMS Strategy:
Fluent CMS mitigates this limitation by setting the local cache expiration to 20 seconds (one-third of the distributed cache expiration, which is set to 60 seconds). This ensures cache consistency across nodes within 20 seconds, significantly improving upon the typical 60-second delay in memory caching.
To implement a HybridCache
, use the following code:
builder.AddRedisDistributedCache(connectionName: CmsConstants.Redis);
builder.Services.AddHybridCache();
Cache Data
Fluent CMS does not automatically invalidate data caches. Instead, it leverages ASP.NET Core's output caching for a straightforward implementation. Data caching consists of two types:
Query Data Cache
Caches the results of queries for faster access.Page Cache
Caches the output of rendered pages for quick delivery.
By default, output caching is disabled in Fluent CMS. To enable it, configure and inject the output cache as shown below:
builder.Services.AddOutputCache(cacheOption =>
{
cacheOption.AddBasePolicy(policyBuilder => policyBuilder.Expire(TimeSpan.FromMinutes(1)));
cacheOption.AddPolicy(CmsOptions.DefaultPageCachePolicyName,
b => b.Expire(TimeSpan.FromMinutes(2)));
cacheOption.AddPolicy(CmsOptions.DefaultQueryCachePolicyName,
b => b.Expire(TimeSpan.FromSeconds(1)));
});
// After builder.Build();
app.UseOutputCache();
Aspire Integration
Fluent CMS leverages Aspire to simplify deployment.
Architecture Overview
A scalable deployment of Fluent CMS involves multiple web application nodes, a Redis server for distributed caching, and a database server, all behind a load balancer.
+------------------+
| Load Balancer |
+------------------+
|
+-----------------+-----------------+
| |
+------------------+ +------------------+
| Web App 1 | | Web App 2 |
| +-----------+ | | +-----------+ |
| | Local Cache| | | | Local Cache| |
+------------------+ +------------------+
| |
| |
+-----------------+-----------------+
| |
+------------------+ +------------------+
| Database Server | | Redis Server |
+------------------+ +------------------+
Local Emulation with Aspire and Service Discovery
Example Web project on GitHub
Example Aspire project on GitHub
To emulate the production environment locally, Fluent CMS leverages Aspire. Here's an example setup:
var builder = DistributedApplication.CreateBuilder(args);
// Adding Redis and PostgreSQL services
var redis = builder.AddRedis(name: CmsConstants.Redis);
var db = builder.AddPostgres(CmsConstants.Postgres);
// Configuring the web project with replicas and references
builder.AddProject<Projects.FluentCMS_Blog>(name: "web")
.WithEnvironment(CmsConstants.DatabaseProvider, CmsConstants.Postgres)
.WithReference(redis)
.WithReference(db)
.WithReplicas(2);
builder.Build().Run();
Benefits:
- Simplified Configuration:
No need to manually specify endpoints for the database or Redis servers. Configuration values can be retrieved using:builder.Configuration.GetValue<string>(); builder.Configuration.GetConnectionString();
- Realistic Testing:
The local environment mirrors the production architecture, ensuring seamless transitions during deployment.
By adopting these caching and deployment strategies, Fluent CMS ensures improved performance, scalability, and ease of configuration.
Query with Document DB
Optimizing query performance by syncing relational data to a document database, such as MongoDB, significantly improves speed and scalability for high-demand applications.
Limitations of ASP.NET Core Output Caching
ASP.NET Core's output caching reduces database access when repeated queries are performed. However, its effectiveness is limited when dealing with numerous distinct queries:
- The application server consumes excessive memory to cache data. The same list might be cached multiple times in different orders.
- The database server experiences high load when processing numerous distinct queries simultaneously.
Using Document Databases to Improve Query Performance
For the query below, FluentCMS joins the post
, tag
, category
, and author
tables:
query post_sync($id: Int) {
postList(idSet: [$id], sort: id) {
id, title, body, abstract
tag {
id, name
}
category {
id, name
}
author {
id, name
}
}
}
By saving each post along with its related data as a single document in a document database, such as MongoDB, several improvements are achieved:
- Reduced database server load since data retrieval from multiple tables is eliminated.
- Reduced application server processing, as merging data is no longer necessary.
Performance Testing
Using K6 scripts with 1,000 virtual users concurrently accessing the query API, the performance difference between PostgreSQL and MongoDB was tested, showing MongoDB to be significantly faster:
export default function () {
const id = Math.floor(Math.random() * 1000000) + 1; // Random id between 1 and 1,000,000
/* PostgreSQL */
// const url = `http://localhost:5091/api/queries/post_sync/?id=${id}`;
/* MongoDB */
const url = `http://localhost:5091/api/queries/post/?id=${id}`;
const res = http.get(url);
check(res, {
'is status 200': (r) => r.status === 200,
'response time is < 200ms': (r) => r.timings.duration < 200,
});
}
/*
MongoDB:
http_req_waiting...............: avg=50.8ms min=774µs med=24.01ms max=3.23s p(90)=125.65ms p(95)=211.32ms
PostgreSQL:
http_req_waiting...............: avg=5.54s min=11.61ms med=4.08s max=44.67s p(90)=13.45s p(95)=16.53s
*/
Synchronizing Query Data to Document DB
Architecture Overview
Enabling Message Publishing in WebApp
To enable publishing messages to the Message Broker, use Aspire to add a NATS resource. Detailed documentation is available in Microsoft Docs.
Add the following line to the Aspire HostApp project:
builder.AddNatsClient(AppConstants.Nats);
Add the following lines to the WebApp project:
builder.AddNatsClient(AppConstants.Nats);
var entities = builder.Configuration.GetRequiredSection("TrackingEntities").Get<string[]>()!;
builder.Services.AddNatsMessageProducer(entities);
FluentCMS publishes events for changes made to entities listed in appsettings.json
:
{
"TrackingEntities": [
"post"
]
}
Enabling Message Consumption in Worker App
Add the following to the Worker App:
var builder = Host.CreateApplicationBuilder(args);
builder.AddNatsClient(AppConstants.Nats);
builder.AddMongoDBClient(AppConstants.MongoCms);
var apiLinksArray = builder.Configuration.GetRequiredSection("ApiLinksArray").Get<ApiLinks[]>()!;
builder.Services.AddNatsMongoLink(apiLinksArray);
Define the ApiLinksArray
in appsettings.json
to specify entity changes and the corresponding query API:
{
"ApiLinksArray": [
{
"Entity": "post",
"Api": "http://localhost:5001/api/queries/post_sync",
"Collection": "post",
"PrimaryKey": "id"
}
]
}
When changes occur to the post
entity, the Worker Service calls the query API to retrieve aggregated data and saves it as a document.
Migrating Query Data to Document DB
After adding a new entry to ApiLinksArray
, the Worker App will perform a migration from the start to populate the Document DB.
Replacing Queries with Document DB
Architecture Overview
To enable MongoDB queries in your WebApp, use the Aspire MongoDB integration. Details are available in Microsoft Docs.
Add the following code to your WebApp:
builder.AddMongoDBClient(connectionName: AppConstants.MongoCms);
var queryLinksArray = builder.Configuration.GetRequiredSection("QueryLinksArray").Get<QueryLinks[]>()!;
builder.Services.AddMongoDbQuery(queryLinksArray);
Define QueryLinksArray
in appsettings.json
to specify MongoDB queries:
{
"QueryLinksArray": [
{ "Query": "post", "Collection": "post" },
{ "Query": "post_test_mongo", "Collection": "post" }
]
}
The WebApp will now query MongoDB directly for the specified collections.
Integrating it into Your Project
Follow these steps to integrate Fluent CMS into your project using a NuGet package.
Create a New ASP.NET Core Web Application.
Add the FluentCMS NuGet Package: To add Fluent CMS, run the following command:
dotnet add package FluentCMS
Modify
Program.cs
: Add the following line beforebuilder.Build()
to configure the database connection (use your actual connection string):builder.AddSqliteCms("Data Source=cms.db"); var app = builder.Build();
Currently, Fluent CMS supports
AddSqliteCms
,AddSqlServerCms
, andAddPostgresCms
.Initialize Fluent CMS: Add this line after
builder.Build()
to initialize the CMS:await app.UseCmsAsync();
This will bootstrap the router and initialize the Fluent CMS schema table.
Optional: Set Up User Authorization: If you wish to manage user authorization, you can add the following code. If you're handling authorization yourself or don’t need it, you can skip this step.
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString)); builder.AddCmsAuth<IdentityUser, IdentityRole, AppDbContext>();
If you'd like to create a default user, add this after
app.Build()
:InvalidParamExceptionFactory.CheckResult(await app.EnsureCmsUser("sadmin@cms.com", "Admin1!", [Roles.Sa]));
Once your web server is running, you can access the Admin Panel at /admin
and the Schema Builder at /schema
.
You can find an example project here.
Adding Business Logic
Learn how to customize your application by adding validation logic, hook functions, and producing events to Kafka.
Adding Validation Logic with Simple C# Expressions
Simple C# Validation
You can define simple C# expressions in the Validation Rule
of attributes using Dynamic Expresso. For example, a rule like name != null
ensures the name
attribute is not null.
Additionally, you can specify a Validation Error Message
to provide users with feedback when validation fails.
Using Regular Expressions
Dynamic Expresso
supports regular expressions, allowing you to write rules like Regex.IsMatch(email, "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$")
.
Note: Since
Dynamic Expresso
doesn't support verbatim strings, you must escape backslashes (\
).
Extending Functionality with Hook Functions
To implement custom business logic, such as verifying that a teacher
entity has valid email and phone details, you can register hook functions to run before adding or updating records:
var registry = app.GetHookRegistry();
// Hook function for pre-add validation
registry.EntityPreAdd.Register("teacher", args =>
{
VerifyTeacher(args.RefRecord);
return args;
});
// Hook function for pre-update validation
registry.EntityPreUpdate.Register("teacher", args =>
{
VerifyTeacher(args.RefRecord);
return args;
});
Producing Events to an Event Broker (e.g., Kafka)
To enable asynchronous business logic through an event broker like Kafka, you can produce events using hook functions. This feature requires just a few additional setup steps:
Add the Kafka producer configuration:
builder.AddKafkaMessageProducer("localhost:9092");
Register the message producer hook:
app.RegisterMessageProducerHook();
Here’s a complete example:
builder.AddSqliteCms("Data Source=cmsapp.db").PrintVersion();
builder.AddKafkaMessageProducer("localhost:9092");
var app = builder.Build();
await app.UseCmsAsync(false);
app.RegisterMessageProducerHook();
With this setup, events are produced to Kafka, allowing consumers to process business logic asynchronously.
Development Guide
The backend is written in ASP.NET Core, the Admin Panel uses React, and the Schema Builder is developed with jQuery.
System Overviews
Backend Server
- Tools:
- ASP.NET Core
- SqlKata: SqlKata
Admin Panel UI
- Tools:
- React
- PrimeReact: PrimeReact UI Library
- SWR: Data Fetching/State Management
Schema Builder UI
- Tools:
- jsoneditor: JSON Editor
Testing Strategy
This chapter describes Fluent CMS's automated testing strategy
Fluent CMS favors integration testing over unit testing because integration tests can catch more real-world issues. For example, when inserting a record into the database, multiple modules are involved:
EntitiesController
EntitiesService
Entity
(in the query builder)- Query executors (e.g.,
SqlLite
,Postgres
,SqlServer
)
Writing unit tests for each individual function and mocking its upstream and downstream services can be tedious. Instead, Fluent CMS focuses on checking the input and output of RESTful API endpoints in its integration tests.
However, certain cases, such as the Hook Registry or application bootstrap, are simpler to cover with unit tests.
Unit Testing /fluent-cms/server/FluentCMS.Test
This project focuses on testing specific modules, such as:
- Hook Registry
- Application Bootstrap
Integration Testing for FluentCMS.Blog /fluent-cms/server/FluentCMS.Blog.Tests
This project focuses on verifying the functionalities of the FluentCMS.Blog example project.
New Feature Testing /fluent-cms/server/FluentCMS.App.Tests
This project is dedicated to testing experimental functionalities, like MongoDB and Kafka plugins.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. |
-
net9.0
- Aspire.NATS.Net (>= 9.0.0)
- Confluent.Kafka (>= 2.6.1)
- DynamicExpresso.Core (>= 2.17.2)
- FluentMigrator.Runner.Postgres (>= 6.2.0)
- FluentMigrator.Runner.Sqlite (>= 6.2.0)
- FluentResults (>= 3.16.0)
- GraphQL (>= 8.2.1)
- GraphQL.Client (>= 6.1.0)
- GraphQL.Client.Serializer.SystemTextJson (>= 6.1.0)
- GraphQL.Server.Transports.AspNetCore (>= 8.2.0)
- GraphQL.Server.Ui.GraphiQL (>= 8.2.0)
- GraphQL.SystemTextJson (>= 8.2.1)
- GraphQL-Parser (>= 9.5.0)
- Handlebars.Net (>= 2.1.6)
- HtmlAgilityPack (>= 1.11.71)
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 9.0.0)
- Microsoft.AspNetCore.OpenApi (>= 9.0.0)
- Microsoft.AspNetCore.WebUtilities (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Sqlite (>= 9.0.0)
- Microsoft.EntityFrameworkCore.SqlServer (>= 9.0.0)
- Microsoft.Extensions.Caching.Hybrid (>= 9.0.0-preview.7.24406.2)
- MongoDB.Driver (>= 2.30.0)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 9.0.2)
- SkiaSharp (>= 3.116.1)
- SqlKata (>= 2.4.0)
- SqlKata.Execution (>= 2.4.0)
- System.Data.SqlClient (>= 4.9.0)
- System.Runtime.Caching (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on FluentCMS:
Repository | Stars |
---|---|
fluent-cms/fluent-cms
A headless CMS with GraphQL API and drag-and-drop page designer
|
Version | Downloads | Last updated |
---|---|---|
0.3.2 | 40 | 12/23/2024 |
0.3.1 | 41 | 12/17/2024 |
0.3.0.2 | 39 | 12/12/2024 |
0.3.0.1 | 43 | 12/12/2024 |
0.3.0 | 47 | 12/11/2024 |
0.2.8.1 | 85 | 11/29/2024 |
0.2.8 | 80 | 11/29/2024 |
0.2.7 | 84 | 11/29/2024 |
0.2.6 | 96 | 11/11/2024 |
0.2.5 | 95 | 10/30/2024 |
0.2.4 | 102 | 10/23/2024 |
0.2.3 | 113 | 10/2/2024 |
0.2.2 | 106 | 10/2/2024 |
0.2.1 | 109 | 9/26/2024 |
0.2.0 | 109 | 9/8/2024 |
0.1.8 | 109 | 9/8/2024 |
0.1.7 | 101 | 9/8/2024 |
0.1.6 | 115 | 9/8/2024 |
0.1.5 | 107 | 9/8/2024 |
0.1.4 | 122 | 8/30/2024 |
0.1.3 | 140 | 8/23/2024 |
0.1.2 | 124 | 8/23/2024 |
0.1.1 | 136 | 8/22/2024 |
0.1.0 | 135 | 8/22/2024 |
0.0.6 | 142 | 8/15/2024 |
0.0.5 | 126 | 8/15/2024 |
0.0.4 | 90 | 8/5/2024 |
0.0.3 | 157 | 7/31/2024 |