DiGi.User.WebAPI is a C# engineering and architectural software library suite designed for BIM and CAD integrations (such as Revit, RhinoCommon, Grasshopper, and Dynamo BIM).
The repository contains the following core components and assemblies:
- DiGi.User.WebAPI (Path:
DiGi.User.WebAPI\DiGi.User.WebAPI)
This project strictly separates Data Models (anemic schemas) from Business/Calculation Logic (static extension methods). All new features must strictly follow this pattern.
graph TD
subgraph "Data Models (Anemic)"
C[Classes]
I[Interfaces]
E[Enums]
end
subgraph "Business Logic (Static Extensions)"
Q[Query Class]
M[Modify Class]
Cr[Create Class]
end
C -->|Query / Read| Q
C -->|Modify / Mutate| M
C -->|Create / Instantiate| Cr
- Classes: Place in the
/Classesdirectory (Namespace:[Project].Classes). Keep them simple and lightweight (properties and basic constructors only). Do NOT put complex logic inside these classes. - Interfaces: Place in the
/Interfacesdirectory (Namespace:[Project].Interfaces). - Enums: Place in the
/Enumsdirectory (Namespace:[Project].Enums).
ALL complex functionalities, including operations on classes, interfaces, and enums, MUST be implemented as Extension Methods inside static partial classes in /Query, /Modify, /Create, or /Convert directories:
- Query (Read/Extract): Static partial class
Queryreturning results based on a query without modifying the source object. - Modify (Update/Mutate): Static partial class
Modifymodifying the state or properties of the existing object in place. - Create (Instantiate): Static partial class
Createinstantiating and returning a new object. - Convert (Parse/Format/Transform): Static partial class
Convertconverting, formatting, or transforming an object into another representation.
To maintain codebase health, performance, and compatibility within Visual Studio 2026 / C# 10+ environments, all developers and AI agents must strictly comply with these guidelines.
- English only — identifiers and comments.
- Explicit typing — no
var(unless the compiler forces it, e.g. anonymous types). Use target-typednew(...)when the type is declared (avoids IDE0090):PointNode pointNode = new();. Use collection expressions[]for collections (avoids IDE0028):List<int> numbers = [];,int[] array = [1, 2, 3];. - Variable naming: start with the type name in camelCase, adding a
_-suffixed qualifier when needed (PointNode pointNode_Base,pointNode_Temp).- Collections: don't prefix with the collection type — use the element type pluralized (
FilterConditions, notConditions/listConditions;FilterGroups, notGroups/listGroups). - Property matching its value type: if a value type is fully descriptive and unique in the class, name the property after the type (
public AggregateFunction AggregateFunction { get; set; }). - Primitives may use plain camelCase (
double tolerance,string name,int count).
- Collections: don't prefix with the collection type — use the element type pluralized (
- Zero warnings/analyzer messages — nullability, parameter validation, clean code.
- C# 10+ (
LangVersion≥ 10) — modern features (enhanced pattern matching, target-typednew, collection expressions, etc.) are fine within these architectural constraints. Namespaces must be block-scoped (as in every example below); file-scoped namespaces are disallowed and theDiGi.Template.editorconfigenforces this (csharp_style_namespace_declarations = block_scoped). - Line breaks in parameters: If a method or constructor has fewer than 6 input parameters, do not break lines between parameters.
- Correct:
public void Calculate(double centerX, double centerY, double radius, double? storeyHeight = null) { }
- Incorrect:
public void Calculate( double centerX, double centerY, double radius, double? storeyHeight = null) { }
- Correct:
- Async method naming: All asynchronous method names must end with
Async.- Example:
public async Task<IActionResult> GetDetailsByReferenceAsync([FromQuery(Name = "reference")] string? reference)
- Example:
- Project Structure: Assume the C# codebase consists of multiple SEPARATE projects, not a single monolithic solution. Handle namespaces and references accordingly.
- Output Optimization: Prioritize highest code quality and output token minimization. Skip conversational filler, polite introductions, and conclusions. Output only the necessary code, logic, or requested explanations.
Data models are strictly separated from business logic (anemic models + static extension methods). Follow this structure for all new features.
Data models:
- Classes →
/Classes, ns[Project].Classes— lightweight (properties + basic constructors only), no complex logic. - Interfaces →
/Interfaces, ns[Project].Interfaces. - Enums →
/Enums, ns[Project].Enums.
Business logic — all complex behavior is an extension method in one of static partial classes; never create a manager/service class:
Query(/Query) — returns a result from a query; does NOT modify the source (e.g. translating dynamic filter groups into SQL/parameterized commands).Modify(/Modify) — modifies the state/properties of the existing object.Create(/Create) — creates and returns a completely new object from input data.Convert(/Convert, subdirs/Convert/To[TargetArea]e.g./Convert/ToSystem,/Convert/ToEPW,/Convert/ToDiGi) — converts/formats/transforms an object or raw components into another representation; method names followTo[TargetArea]_[TargetType](ToSystem_String,ToSystem_DateTime,ToEPW_DateTime).
- Strictly avoid creating private methods within
Query,Convert,Modify, and similar partial utility classes. - If a helper method has well-defined inputs, no side effects, and high reusability, implement it as a public static method within the appropriate partial class (e.g.,
Query,Convert,Modify). - If a helper method is strictly single-use or specific to a narrow scope, implement it as a local function (inline method) directly inside the method you are currently implementing.
public static partial class Convert is the first choice for any method that transforms an object into another representation — including performance-oriented variants that avoid defensive cloning. Never implement a conversion as an instance method on a /Classes model; model classes stay anemic.
The pattern, as established across the Convert folders (reference: DiGi.Geometry/Planar/Convert):
- Folder/file layout:
/Convert/To[TargetArea]/[TargetType].cs— one file per TARGET type, named after the target type; all source-type overloads converting to that target live in that file (e.g.ToNTS/LinearRing.cs,ToDiGi/Polygon2D.cs,ToNTS/Coordinates.cs). - Method shape:
public staticextension method on the SOURCE type; reference-type parameters are nullable; returnnullfor null/invalid input instead of throwing. - Naming: plain
To[TargetArea](...)when the source type has a single natural target in that area — the target is then distinguished by the source overload (ToNTS(this Point2D?)→Coordinate,ToNTS(this Segment2D?)→LineSegment,ToNTS(this IPolygonal2D?)→LinearRing). Use the suffixed formTo[TargetArea]_[TargetType](...)when the same source converts to several targets in one area (ToNTS_LineString(this Segment2D?)andToNTS_Polygon(this IPolygonal2D?)beside the plain overloads above;ToDiGi_Polygon2Ds(this Polygon?)besideToDiGi(this Polygon?)→PolygonalFace2D).
- Enforce a property-like naming convention for methods inside the
Queryclass. - Do not use verbs as prefixes (e.g., avoid
Get,Find,Calculate).- Example:
GetBoundingBox()must be namedBoundingBox().
- Example:
- Exceptions: Verbs indicating boolean checks or safe-retrieval patterns are required. Allowed prefixes are
Is,Has, andTry.- Examples:
IsPlanar(),HasMaterial(),TryConvert().
- Examples:
Runtime assets a project copies to its output belong in one of two solution-root folders, chosen by
sensitivity. Secrets, credentials and machine-specific configuration MUST go in user files/,
never in files/. Both are copied to the build output by a .csproj target; the difference is git.
files/— committed to source control. Non-sensitive, environment-agnostic deployment assets shared by everyone (e.g.web.config,app_offline.htm.bak). Copied by aCopyFilestarget:<Target Name="CopyFiles" AfterTargets="Build"> <ItemGroup> <_Files Include="$(ProjectDir)..\files\**\*.*" /> </ItemGroup> <Copy SourceFiles="@(_Files)" DestinationFiles="@(_Files->'$(OutputPath)%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" /> </Target>
user files/— git-ignored. Fragile / user-specific / secret data: database connection configs (*.confwith host/user/password), API keys, local paths, per-machine settings. Copied by aCopyUserFilestarget with the identical shape but..\user files\**\*.*. The consuming code reads these from next to the executing assembly at runtime, so the app works locally and on the server without the secrets ever entering the repo.
Enforcement: the solution-root .gitignore must contain the case-insensitive rule
[Uu]ser [Ff]iles/. Verify with git check-ignore -v "user files/<file>" — git must report the rule
as the reason the file is ignored. If a new solution needs runtime secrets and lacks this rule, add
it before dropping any secret in. Reference implementations: DiGi.GIS.PostgreSQL.UI,
DiGi.GIS.PostgreSQL.WebAPI (both hold GIS_PostgreSQL_Main.conf in an ignored user files/).
Decision rule when placing a runtime asset: would committing it leak a secret, or break another
developer's / the server's machine-specific setup? If yes → user files/; otherwise → files/.
- Script configurations (PowerShell): PowerShell scripts requiring machine-specific, secret, or environment-specific paths (e.g., local backup paths or cloud storage directories) must load these settings from a
.conffile inside theuser files/directory, rather than hardcoding them in the scripts or introducing custom.gitignorerecords.
All public constructors, properties, methods, and enum values must be fully documented using XML comments:
-
Code preservation & sync: Edit only
///comments — never change C# logic. Add missing tags, and rewrite any existing comment that is outdated, inaccurate, or describes logic/parameters that no longer exist. -
Explicit typing: No
varin any code snippet you touch. -
Partial classes: Don't document the class declaration itself when marked
partial; document only its members. -
Exhaustive coverage: Every public member must have an accurate, up-to-date description.
-
Quality over speed — prioritize accuracy and alignment with the code's actual behavior.
-
Reference context: For each referenced library, ingest its sibling XML doc file (
LibraryName.dll→LibraryName.xml, same directory) for accurate cross-referencing, terminology, and external type/parameter descriptions. -
Signature matching: Docs must match signatures exactly — remove
<param>tags for parameters that no longer exist, add tags for new ones. Document all<param>,<returns>, and<typeparam>to avoid CS1591/CS1573. -
Single summary: Exactly one
<summary>per element. When updating, overwrite the old one — never append. Do a final pass to strip any redundant tags. -
No empty lines inside doc blocks (no blank line or bare
///) — they break Visual Studio IntelliSense tooltip rendering. Use<para>for paragraph breaks:// INCORRECT — a blank line splits the block /// <summary> /// Calculates the total volume of the selected Revit elements. /// This operation might take a while on large BIM models. /// </summary> // CORRECT — use <para> /// <summary> /// Calculates the total volume of the selected Revit elements. /// <para>This operation might take a while on large BIM models.</para> /// </summary>
To minimize token consumption and avoid parsing full implementation files, you MUST consult the generated Markdown documentation first when exploring type schemas, namespaces, and public API interfaces:
To save tokens, consult the generated Markdown API docs before parsing .cs source when exploring type schemas, namespaces, or public API.
- Path:
documentation/API/[AssemblyName]/in each active workspace — one directory per assembly, split by namespace (e.g.DiGi.Core.Classes.md). These files hold exact signatures and<summary>descriptions for all public classes, constructors, methods, properties, and enums. - Fallback: if
documentation/API/is absent, scan the C# source and/bin/*.xmlfiles.
Classes under /Classes needing JSON persistence, cloning, or polymorphic deserialization MUST inherit DiGi.Core.Classes.SerializableObject in this exact shape (reflection-driven — no manual JSON parsing).
- Marker interfaces per project under
/Interfaces(mirroringDiGi.GIS.Interfaces.IGISObject/IGISSerializableObject):Every serializable class implements// /Interfaces/I<Project>Object.cs public interface I<Project>Object : DiGi.Core.Interfaces.IObject { } // /Interfaces/I<Project>SerializableObject.cs public interface I<Project>SerializableObject : I<Project>Object, DiGi.Core.Interfaces.ISerializableObject { }
I<Project>SerializableObject(e.g.public class Holiday : SerializableObject, IEPWSerializableObject). - Fields:
private readonly, each[JsonInclude, JsonPropertyName(nameof(PublicPropertyName))]— alwaysnameof(...), never a hardcoded string literal. - Three constructors, always in this order:
- Primary (plain params, assigns fields) — no
base(...)call needed. - Copy
ClassName(ClassName? classNameInstance) : base(classNameInstance), copying every field:- Primitive/value-type fields and strings: copy by value.
List<T>/IList<T>of primitives:new List<T>(source)(ornullif source isnull).IList<T>of nestedSerializableObject-derived items: clone element-by-element filtering nulls (see the excerpt below). Do NOT pipe theIEnumerable<T>.Clone<T>()extension into anIList<T>field — it returnsList<T?>?, a nullable-element mismatch against a non-nullableIList<T>field.- A single nested
SerializableObjectreference:field = Core.Query.Clone(source.field);.
- JSON
ClassName(JsonObject? jsonObject) : base(jsonObject)— pure delegation, empty body.
- Primary (plain params, assigns fields) — no
- Properties:
[JsonIgnore]get-only, returning the backing field (the field attribute handles serialization — do not also serialize through the property). - Project file:
.csprojneeds a<Reference Include="DiGi.Core"><HintPath>..\..\DiGi.Core\bin\DiGi.Core.dll</HintPath></Reference>and a<PackageReference Include="System.Text.Json" .../>matching the version used elsewhere (checkDiGi.Core.csproj).
using DiGi.Core.Classes;
using DiGi.EPW.Interfaces;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace DiGi.EPW.Classes
{
public class Holiday : SerializableObject, IEPWSerializableObject
{
[JsonInclude, JsonPropertyName(nameof(Name))]
private readonly string? name;
[JsonInclude, JsonPropertyName(nameof(Date))]
private readonly string? date;
public Holiday(string? name, string? date)
{
this.name = name;
this.date = date;
}
public Holiday(Holiday? holiday)
: base(holiday)
{
if (holiday != null)
{
name = holiday.name;
date = holiday.date;
}
}
public Holiday(JsonObject? jsonObject)
: base(jsonObject)
{
}
[JsonIgnore]
public string? Name
{
get
{
return name;
}
}
[JsonIgnore]
public string? Date
{
get
{
return date;
}
}
}
}public HolidaysDaylightSaving(HolidaysDaylightSaving? holidaysDaylightSaving)
: base(holidaysDaylightSaving)
{
if (holidaysDaylightSaving != null)
{
leapYearObserved = holidaysDaylightSaving.leapYearObserved;
if (holidaysDaylightSaving.holidays != null)
{
holidays = [];
foreach (Holiday holiday in holidaysDaylightSaving.holidays)
{
if (Core.Query.Clone(holiday) is Holiday holiday_Temp)
{
holidays.Add(holiday_Temp);
}
}
}
}
}public GroundTemperature(GroundTemperature? groundTemperature)
: base(groundTemperature)
{
if (groundTemperature != null)
{
depth = groundTemperature.depth;
monthlyValues = groundTemperature.monthlyValues == null ? null : new List<double>(groundTemperature.monthlyValues);
}
}- One test project per project:
[ProjectName].xUnit(e.g.DiGi.Core.xUnit,DiGi.Geometry.xUnit). public partial class Factsholds all test methods (one shared class per namespace).- Files under
/Facts. - Namespace matches the test project (e.g.
namespace DiGi.Core.xUnit). Xunitis global-usinged by project config — do NOT addusing Xunit;.[Fact]marks test methods.- Name the method after the class/property/method under test (
Color(),PlanarIntersectionResult_Performance()). - XML
<summary>on every test describing what is tested — no empty lines inside the block (they break VS tooltips); use<para>for paragraph breaks.
When a test needs an on-disk input file (.gmf, .json, .epw, …), use the one shared files directory — do NOT add a per-project data folder.
- Location:
DiGi.Test/files/(theDiGi.Testrepo sits beside the otherDiGi.*repos under theDigiProjectworkspace root; from aDiGi.Test/<ProjectName>.xUnit/dir it is../files/). The path is given relative to the workspace root because this guideline lives in the separateDiGi.Maintenancerepo. - Add a fixture: drop the file into
DiGi.Test/files/and reference it by file name only. Files are read in place (not copied to build output) — no<None CopyToOutputDirectory>entry needed. - Resolve the path:
Core.xUnit.Query.FilePath(System.Reflection.Assembly.GetExecutingAssembly(), "<fileName>")returns the absolute path toDiGi.Test/files/<fileName>; for the directory itself useassembly.FilesDirectory(). Both live inDiGi.Core.xUnit/Query/(FilePath.cs,FilesDirectory.cs) and resolve by walking up from the test assembly'sbin/<ProjectName>.xUnit/output.FilePathAsserts the directory resolves, so anull/missing result fails the test cleanly. - No
usingneeded — call it fully qualified asCore.xUnit.Query.FilePath(...); it resolves via the same innermost-enclosing-namespace lookup asCore.xUnit.Query.SerializationCheck(...), as long as the test namespace nests underDiGi. Addusing System.Reflection;(or fully qualifyAssembly). - Example:
References:
using System.Reflection; // ... string? path = Core.xUnit.Query.FilePath(Assembly.GetExecutingAssembly(), "0207_GML.gmf"); Assert.False(string.IsNullOrWhiteSpace(path)); Assert.True(System.IO.File.Exists(path));
DiGi.GIS.xUnit/Facts/OrtoDatas.cs,DiGi.EPW.xUnit/Facts/EPWFile.cs,DiGi.Geometry.xUnit/Facts/InRange.cs. - Large binaries (multi-MB
.gmf, etc.) are git-tracked (not ignored) — prefer a representative-but-minimal sample, and consider Git LFS if size becomes a concern.
- Version branch only: run only when the active branch is a bare SemVer
*.*.*(e.g.0.8.2,1.12.0). Skip anything with text, prefix, or suffix (feature/login,v0.8.2,0.8.2-beta,main). - Differs from main: run only for repos where the active branch differs from
main; skip repos where they are identical.
- Sync with main: merge the version branch into
mainso both hold the exact same codebase, with no pending diffs. - Bump patch: increment the third version digit by 1 (
0.8.2→0.8.3). - Branch off main using that new version name.
- Update
Directory.Build.props(if present): set<Major>/<Minor>/<Build>to the new version's components and commit on the new branch before pushing. - Push & track: push both
mainand the new version branch toorigin, using-uon the new branch so it tracks properly (git push -u origin <version_branch>).