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-entity

Or, if you want to generate a data view instead of a table for the list page:

spiderly add-new-entity --data-view

The CLI will prompt you to enter the entity name in PascalCase (e.g., YourEntityName).

This command will automatically generate:

  1. Backend Entity:

    • Backend\YourAppName.Business\Entities\YourEntityName.cs
  2. List Page:

    • Frontend\src\app\pages\your-entity-name\your-entity-name-list.component.ts
    • Frontend\src\app\pages\your-entity-name\your-entity-name-list.component.html
  3. Details Page:

    • Frontend\src\app\pages\your-entity-name\your-entity-name-details.component.ts
    • Frontend\src\app\pages\your-entity-name\your-entity-name-details.component.html
  4. Routes in Frontend\src\app\app.routes.ts

  5. 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, Version is set to 1; on every update it is incremented inside SaveChanges.
  • 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 stored Version against the one the client sent back. If they differ, the write is rejected with a localized ConcurrencyException.

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 SaveChangesReadonlyObject<ID> has neither.

  • On insert both are set to the current UTC time; on every update ModifiedAt is refreshed while CreatedAt is 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 CreatedAt explicitly before inserting a new row and Spiderly preserves your value — it stamps CreatedAt only when left at its default. This lets a bulk import keep each row's real creation date from the source system. ModifiedAt is 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 propertyGenerated DTO property
[Required] string Namestring Name { get; set; } = null!;
string Slugstring? Slug (plain string if your app is nullable-oblivious)
[Required] int Quantityint? Quantity — value types stay nullable, see below
int Stockint? Stock
any navigationlong? CategoryId + string? CategoryDisplayName
any collectionunchanged, 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