XML Tag System
SmartSql uses XML to define SQL statements, but unlike static SQL mappers, it provides a rich tag system for dynamic SQL construction. Tags are evaluated at runtime against the request parameters to conditionally include SQL fragments, iterate over collections, switch on values, and inject generated values. This system is inspired by MyBatis's dynamic SQL but is tailored for .NET types and conventions.
At a Glance
| Aspect | Detail |
|---|---|
| Interface | ITag with IsCondition() and BuildSql() methods |
| Base class | Tag provides property-based condition checking and child tag iteration |
| Container | Each Statement holds a list of ITag children that are processed sequentially |
| Composite tags | Dynamic, Where, Set act as containers that prepend keywords based on child matches |
| Factory | TagBuilderFactory creates tag instances from XML element definitions |
Tag Class Hierarchy
classDiagram
class ITag {
<<interface>>
+Statement Statement
+ITag Parent
+IsCondition(AbstractRequestContext) bool
+BuildSql(AbstractRequestContext) void
}
class Tag {
<<abstract>>
+String Prepend
+String Property
+bool Required
+IList~ITag~ ChildTags
+IsCondition(AbstractRequestContext)* bool
+BuildSql(AbstractRequestContext) void
+BuildChildSql(AbstractRequestContext) void
#EnsurePropertyValue(AbstractRequestContext) Object
}
class SqlText {
+String BodyText
+BuildSql(AbstractRequestContext) void
}
class IdGenerator {
+IIdGenerator IdGen
+bool Assign
}
class Now {
+String Kind
}
class UUID {
+String Format
}
ITag <|.. Tag
ITag <|.. SqlText
ITag <|.. IdGenerator
ITag <|.. Now
ITag <|.. UUID
Tag <|-- CompareTag
Tag <|-- Dynamic
Tag <|-- IsNotEmpty
Tag <|-- IsEmpty
Tag <|-- IsNotNull
Tag <|-- IsProperty
Tag <|-- IsNotProperty
Tag <|-- Placeholder
Tag <|-- OrderBy
Tag <|-- Range
Tag <|-- Env
Tag <|-- Include
Tag <|-- Switch
Tag <|-- For
CompareTag <|-- StringCompareTag
CompareTag <|-- NumericalCompareTag
StringCompareTag <|-- IsEqual
StringCompareTag <|-- IsNotEqual
StringCompareTag <|-- IsTrue
StringCompareTag <|-- IsFalse
StringCompareTag <|-- Switch_Case
NumericalCompareTag <|-- IsGreaterThan
NumericalCompareTag <|-- IsLessThan
NumericalCompareTag <|-- IsGreaterEqual
NumericalCompareTag <|-- IsLessEqual
Dynamic <|-- Where
Dynamic <|-- Set2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
How Statements Process Tags
When PrepareStatementMiddleware calls Statement.BuildSql(), each statement iterates through its child tags. Each tag checks its condition against the request parameters, and if the condition passes, appends its SQL fragment to the SqlBuilder.
flowchart TD
subgraph Processing["Statement.BuildSql Flow"]
style Processing fill:#161b22,stroke:#30363d,color:#e6edf3
Start["Statement.BuildSql(request)"] --> Loop["For each ITag child"]
Loop --> Check{"tag.IsCondition(request)?"}
Check -->|Yes| Build["tag.BuildSql(request)"]
Build --> Append["Append Prepend + Child SQL to SqlBuilder"]
Append --> Next["Next tag"]
Check -->|No| Skip["Skip tag"]
Skip --> Next
Next --> End{"More tags?"}
End -->|Yes| Loop
End -->|No| Done["request.RealSql = SqlBuilder.ToString()"]
end
style Start fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Loop fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Check fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Build fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Append fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Next fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Skip fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style End fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Done fill:#2d333b,stroke:#6d5dfc,color:#e6edf32
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Tag Types Reference
Conditional Tags
These tags check a condition against request parameters and include their child content only when the condition is met.
IsNotEmpty
Renders child content when the property value is not null, not an empty string, and (for collections) has at least one element.
<IsNotEmpty Property="UserName">
AND UserName = @UserName
</IsNotEmpty>2
3
IsEmpty
The inverse of IsNotEmpty -- renders when the property is null, empty string, or an empty collection.
IsEqual / IsNotEqual
Compares the property value (as a string) to a CompareValue attribute.
<IsEqual Property="Status" CompareValue="1">
AND Active = 1
</IsEqual>2
3
IsGreaterThan / IsLessThan / IsGreaterEqual / IsLessEqual
Numerical comparison tags that parse the property value to Decimal and compare against CompareValue.
<IsGreaterThan Property="Age" CompareValue="18">
AND Age > @Age
</IsGreaterThan>2
3
IsNotNull / IsNull
Checks whether the property value is null or not null.
IsProperty / IsNotProperty
Checks whether the property key exists in the request parameters (regardless of value).
<IsProperty Property="DeptId">
AND DeptId = @DeptId
</IsProperty>
<IsNotProperty Property="DeptId">
AND DeptId IS NULL
</IsNotProperty>2
3
4
5
6
IsProperty additionally supports PropertyChanged tracking for entity proxy change detection.
IsTrue / IsFalse
Boolean comparison tags that check if the property value equals "True" or "False" as a string.
Range
Checks whether a numeric property value falls within a [Min, Max] range.
<Range Property="PageNum" Min="1" Max="100">
-- pagination logic
</Range>2
3
Env
Conditionally includes content based on the current database provider name. Useful for database-specific SQL.
<Env DbProvider="MySql">
LIMIT @PageSize OFFSET @Offset
</Env>
<Env DbProvider="SqlServer">
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
</Env>2
3
4
5
6
Composite / Structural Tags
Where
Extends Dynamic with Prepend = "Where". Renders the WHERE keyword only if at least one child tag condition passes. The first child that matches gets the WHERE keyword prepended; subsequent children get their own Prepend values (typically AND).
<Where>
<IsNotEmpty Property="Name">
AND Name = @Name
</IsNotEmpty>
<IsNotEmpty Property="Status">
AND Status = @Status
</IsNotEmpty>
</Where>
<!-- Produces: WHERE Name = @Name AND Status = @Status -->2
3
4
5
6
7
8
9
Set
Extends Dynamic with Prepend = "Set". Used in UPDATE statements to conditionally include SET clauses.
<Set>
<IsNotEmpty Property="Name">
Name = @Name,
</IsNotEmpty>
<IsNotEmpty Property="Status">
Status = @Status,
</IsNotEmpty>
</Set>2
3
4
5
6
7
8
Dynamic
The base composite tag. Evaluates child tags and renders Prepend before the first matching child. Supports a Min attribute that throws TagMinMatchedFailException if fewer children match than the minimum.
Switch / Case / Default
A switch-case construct that evaluates child Case tags by comparing the property value to CompareValue, falling through to Default if no case matches.
<Switch Property="SortField">
<Case CompareValue="Name">ORDER BY Name ASC</Case>
<Case CompareValue="Date">ORDER BY CreateTime DESC</Case>
<Default>ORDER BY Id ASC</Default>
</Switch>2
3
4
5
For
Iterates over a collection property and generates parameterized SQL for each element. Supports Open, Separator, and Close attributes for controlling delimiters.
<For Property="Ids" Open="(" Close=")" Separator="," Key="id">
@id
</For>
<!-- Produces: (@Ids_For__0, @Ids_For__1, @Ids_For__2) -->2
3
4
The For tag handles both direct values (primitives, strings) and complex objects, creating uniquely named parameters for each iteration to avoid conflicts.
Include
References another statement's SQL content via RefId, enabling SQL fragment reuse.
<Include RefId="BaseColumns">
<!-- Refs another Statement that defines common column selections -->
</Include>2
3
Value Injection Tags
These tags always render and inject values into the parameter collection rather than producing SQL text.
Now
Injects the current date/time into the request parameters. Supports Kind="UTC" for UTC time.
<Now Property="CreateTime" />
<!-- Sets request parameter CreateTime to DateTime.Now -->2
UUID
Generates a new GUID and injects it into the request parameters. Supports a Format attribute for string formatting.
<UUID Property="Id" Format="N" />
<!-- Sets request parameter Id to a GUID formatted as "N" (no hyphens) -->2
Placeholder
Directly interpolates a property value into the SQL string (not as a parameterized value). Used for dynamic table names or column names where parameterization is not possible.
<Placeholder Property="TableName" Prepend="FROM " />
<!-- Produces: FROM Users (literal string substitution) -->2
OrderBy
Generates an ORDER BY clause from a KeyValuePair<string, string> or a collection of key-value pairs where Key is the column name and Value is the direction.
<OrderBy Property="Sort" />
<!-- If Sort = {Key: "Name", Value: "ASC"}, produces: ORDER BY Name ASC -->2
IdGenerator
Uses a registered IIdGenerator (e.g., Snowflake) to generate a unique ID and inject it into the request parameters.
SqlText
SqlText is the leaf node that holds raw SQL text between tags. It also handles automatic IN clause expansion: when it detects an IN @ParamName syntax and the parameter value is an IEnumerable, it expands the parameter into individual numbered parameters.
Tag Hierarchy Diagram
The following diagram shows the abstract inheritance chain that provides common behavior to concrete tags:
graph TD
subgraph Interfaces["Tag Interfaces"]
style Interfaces fill:#161b22,stroke:#30363d,color:#e6edf3
ITag["ITag"]
ITagB["ITagBuilder"]
end
subgraph Base["Abstract Base"]
style Base fill:#161b22,stroke:#30363d,color:#e6edf3
AbsTag["Tag (abstract)"]
CmpTag["CompareTag<T>"]
StrCmp["StringCompareTag"]
NumCmp["NumericalCompareTag"]
end
subgraph Conditional["Conditional Tags"]
style Conditional fill:#161b22,stroke:#30363d,color:#e6edf3
INE["IsNotEmpty"]
IE["IsEmpty"]
INN["IsNotNull"]
ISN["IsNull"]
IP["IsProperty"]
INP["IsNotProperty"]
IEq["IsEqual"]
INEq["IsNotEqual"]
IGT["IsGreaterThan"]
ILT["IsLessThan"]
IGE["IsGreaterEqual"]
ILE["IsLessEqual"]
IT["IsTrue"]
IF["IsFalse"]
Rng["Range"]
end
subgraph Composite["Composite Tags"]
style Composite fill:#161b22,stroke:#30363d,color:#e6edf3
Dyn["Dynamic"]
Whr["Where"]
St["Set"]
Sw["Switch"]
Fr["For"]
Inc["Include"]
EnvT["Env"]
end
subgraph Inject["Value Injection"]
style Inject fill:#161b22,stroke:#30363d,color:#e6edf3
NowT["Now"]
UUIDT["UUID"]
PH["Placeholder"]
OB["OrderBy"]
IDG["IdGenerator"]
ST["SqlText"]
end
ITag --> AbsTag
AbsTag --> CmpTag
AbsTag --> Dyn
AbsTag --> INE
AbsTag --> IE
AbsTag --> INN
AbsTag --> ISN
AbsTag --> IP
AbsTag --> INP
AbsTag --> Rng
AbsTag --> Fr
AbsTag --> Inc
AbsTag --> EnvT
AbsTag --> PH
AbsTag --> OB
CmpTag --> StrCmp
CmpTag --> NumCmp
StrCmp --> IEq
StrCmp --> INEq
StrCmp --> IT
StrCmp --> IF
NumCmp --> IGT
NumCmp --> ILT
NumCmp --> IGE
NumCmp --> ILE
Dyn --> Whr
Dyn --> St
Dyn --> Sw
style ITag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style AbsTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style CmpTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style StrCmp fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style NumCmp fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Dyn fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Whr fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style St fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style INE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style IEq fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Fr fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ING fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style NowT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style UUIDT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style PH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style OB fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style IDG fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Inc fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style EnvT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Rng fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style Sw fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style IGT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
style ILT fill:#2d333b,stroke:#6d5dfc,color:#e6edf32
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
Complete Example
Here is a complete XML statement demonstrating multiple tag types working together:
<Statement Id="QueryUsers">
SELECT * FROM Users
<Where>
<IsNotEmpty Property="Name">
AND Name LIKE CONCAT('%', @Name, '%')
</IsNotEmpty>
<IsEqual Property="Status" CompareValue="1">
AND IsActive = 1
</IsEqual>
<IsGreaterThan Property="MinAge" CompareValue="0">
AND Age >= @MinAge
</IsGreaterThan>
<Env DbProvider="MySql">
LIMIT @PageSize OFFSET @Offset
</Env>
</Where>
<Switch Property="SortField">
<Case CompareValue="Name">ORDER BY Name ASC</Case>
<Default>ORDER BY Id DESC</Default>
</Switch>
</Statement>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Cross-References
- Architecture Overview -- how XML maps fit into the overall architecture
- Middleware Pipeline -- where
PrepareStatementMiddlewareinvokes tag processing - DataSource & Read/Write Splitting -- how
Envtags enable database-specific SQL