Add New Entity
Learn how to create a new entity in your project, including adding it to the backend, updating the database, and generating the corresponding frontend code.
Overview
The EF Core entity and its attributes form the foundation of everything in Spiderly. All other components are built and generated based on these entities.
In this step-by-step guide, you'll learn how to create a new entity in your project using the Spiderly CLI, which automates the entire process including backend entity creation, frontend page generation, routing, and navigation menu setup.
Add New Entity
Run the following command from the root of your application:
spiderly add-new-entityOr, if you want to generate a data view instead of a table for the list page:
spiderly add-new-entity --data-viewThe CLI will prompt you to enter the entity name in PascalCase (e.g., YourEntityName).
This command will automatically generate:
-
Backend Entity:
Backend\YourAppName.Business\Entities\YourEntityName.cs
-
List Page:
Frontend\src\app\pages\your-entity-name\your-entity-name-list.component.tsFrontend\src\app\pages\your-entity-name\your-entity-name-list.component.html
-
Details Page:
Frontend\src\app\pages\your-entity-name\your-entity-name-details.component.tsFrontend\src\app\pages\your-entity-name\your-entity-name-details.component.html
-
Routes in
Frontend\src\app\app.routes.ts -
Menu Item in
Frontend\src\app\business\layout\layout.component.ts
Customize the Entity
After generation, open the entity file at Backend\YourAppName.Business\Entities\YourEntityName.cs and customize it according to your needs.
The entity class inherits from BusinessObject<ID> which supports create, read, update and delete operations. If you need a read-only entity that does not support create, update, or delete operations from the UI, change the base class to ReadonlyObject<ID>.
Optimistic concurrency
Every BusinessObject<ID> carries a [ConcurrencyCheck] Version column that gives you optimistic concurrency for free — ReadonlyObject<ID> has none. Spiderly manages it end to end, so you never set or increment it yourself:
- On insert,
Versionis set to1; on every update it is incremented insideSaveChanges. - The value round-trips to the client on the entity's DTO, so the client always holds the version it last read.
- On update, the generated
Save{Entity}reloads the row and compares the storedVersionagainst the one the client sent back. If they differ, the write is rejected with a localizedConcurrencyException.
This means two users editing the same record can't silently overwrite each other — the second save fails loudly instead of clobbering the first. No per-entity configuration is required. (The check runs on update only: deletes use a bulk ExecuteDeleteAsync and are not version-guarded, and inserts have no version to race — guard duplicate creation with a unique index instead.)
Audit timestamps
Every BusinessObject<ID> also carries CreatedAt and ModifiedAt columns that Spiderly stamps inside SaveChanges — ReadonlyObject<ID> has neither.
- On insert both are set to the current UTC time; on every update
ModifiedAtis refreshed whileCreatedAtis left untouched (it is immutable after creation). - In normal CRUD you never assign them by hand — the generated save path owns them.
- One exception, for imports and migrations: set
CreatedAtexplicitly before inserting a new row and Spiderly preserves your value — it stampsCreatedAtonly when left at its default. This lets a bulk import keep each row's real creation date from the source system.ModifiedAtis always stamped with the write time, so it reflects when the row entered this database.
Example of a customized entity:
namespace YourAppName.Business.Entities
{
[SpiderlyEntity]
[DoNotAuthorize]
public class YourEntityName : BusinessObject<long>
{
[StringLength(75, MinimumLength = 1)]
[Required]
public string Name { get; set; }
[UIControlType(nameof(UIControlTypeCodes.TextArea))]
[StringLength(500, MinimumLength = 1)]
public string Description { get; set; }
}
}The [SpiderlyEntity] attribute is required — source generators only enroll classes that carry it. The CLI adds it for you; on hand-created entities, add it yourself. Hand-written DTOs use [SpiderlyDTO]. Generated DTOs ({Entity}DTO, {Entity}SaveBodyDTO, {Entity}MainUIFormDTO) are emitted by Spiderly and need no marker.
Generated DTO Nullability
[Required] decides it for reference types only:
| Entity property | Generated DTO property |
|---|---|
[Required] string Name | string Name { get; set; } = null!; |
string Slug | string? Slug (plain string if your app is nullable-oblivious) |
[Required] int Quantity | int? Quantity — value types stay nullable, see below |
int Stock | int? Stock |
| any navigation | long? CategoryId + string? CategoryDisplayName |
| any collection | unchanged, with = new() |
Value types stay nullable even under [Required], and that is deliberate. A generated
{Entity}DTO serves four roles at once: response shape, request shape, the Angular form model (an
empty numeric input posts null), and the sparse placeholder carrier for
[ComplexManyToManyList] grids, where an all-null row is the "no record" sentinel. The last two
need null to be representable, and int cannot hold it. Reference types are safe to tighten
because a non-nullable string still accepts null at runtime — = null! is an assertion for the
compiler and for you, not a runtime constraint.
These annotations do not reach your OpenAPI document unless you opt in:
spiderly.AddSwagger(options => options.SupportNonNullableReferenceTypes());Spiderly does not turn this on for you, because it narrows your published contract — members become required and non-nullable, which regenerates every consumer's typed client. Treat it as its own deploy, coordinated with those consumers. Apps created by spiderly init have it already, since a brand-new app has no consumers to coordinate with. It is also a no-op until your app is on <Nullable>enable</Nullable>.
AddSwagger's callback runs after Spiderly's own defaults, and is the seam for any other SwaggerGenOptions setting you need — security definitions, schema filters, and so on.
Database Table Naming
Spiderly creates database tables with the exact same name as your entity class — singular, PascalCase. For example, a YourEntityName class produces a "YourEntityName" table, not "YourEntityNames".
Add Your Entity to the Database
Open a terminal in the Backend folder and run the following commands to create and apply a migration:
spiderly add-migration YourMigrationName
spiderly update-database