Locode - Code-First

Code-First is the natural development model of AutoQuery Services which facilitates the majority of a System and its UI can be developed from simple, declarative C# POCOs to define the underlying RDBMS Schema Data Models and the precise typed API DTO Contracts of their surrounding AutoQuery & CRUD APIs. The Data and Service models can be further enhanced by ServiceStack's vast declarative attributes where a significant amount of behavior, functionality and customization can be defined, ranging from:

To get started quickly, we've created a video containing a step-by-step walkthrough of creating a Code-First CRUD App with Locode:

Bookings MVP

A simple example of using Locode for a back office bookings system would be a single table that a staff member populates.

Create your project

To start off, we will create a project from the basic web template using the ServiceStack website. The link below will create a new project with the name "BookingsLocode".

The web template for a ServiceStack application will provide the basic solution structure with a sample Hello World service. This can be done using the ServiceStack website under Get Started.

Alternatively, templates can be created using the dotnet CLI tool x. The dotnet x tool can be installed using the following command:

dotnet tool install --global x

Once installed, a new web template can be created using:

x new web MyProjectName

Mix in a database and AutoQuery

We can use the dotnet x tool to mix in specific database support and AutoQuery quickly using the command run from the project directory.

x mix sqlite autoquery

TIP

Replace sqlite with postgres, sqlserver, or mysql or other RDBMS providers

This command will create two files, Configure.Db.cs and Configure.AutoQuery.cs and install required NuGet dependencies into the AppHost (BookingsLocode in the link above) project.

Bookings table

With our App now setup to use SQLite & AutoQuery, we'll define our Booking table where our data will be stored in:

public class Booking
{
    [AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public RoomType RoomType { get; set; }
    public int RoomNumber { get; set; }
    public DateTime BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    public decimal Cost { get; set; }
    public string Notes { get; set; }
    public bool? Cancelled { get; set; }
}

public enum RoomType
{
    Single,
    Double,
    Queen,
    Twin,
    Suite,
}

With our table schema defined in code, we can use OrmLite to create the table for us if it doesn't already exist, which we do in the mix generated Configure.Db.cs where our SQLite connection is defined, using CreateTableIfNotExists() to create the Booking table and populate it with Seed data when it's first created:

public class ConfigureDb : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) => {
            services.AddSingleton<IDbConnectionFactory>(new OrmLiteConnectionFactory(
                context.Configuration.GetConnectionString("DefaultConnection")
                ?? ":memory:",
                SqliteDialect.Provider));
        })
        // Create non-existing Table and add Seed Data Example
        .ConfigureAppHost(appHost => {
            using var db = appHost.Resolve<IDbConnectionFactory>().Open();
            if (db.CreateTableIfNotExists<Booking>())
            {
                // Seed data
                db.Insert(new Booking {
                    Name = "Test",
                    Cost = 123,
                    RoomNumber = 321,
                    RoomType = RoomType.Queen,
                    Notes = "Testing more",
                    BookingStartDate = new DateTime(2022, 1, 1),
                    BookingEndDate = new DateTime(2022, 1, 5)
                });
            }
        });
}

This configures our App's database ready for use, but we still don't have any AutoQuery APIs using them defined.

AutoQuery APIs

To create an AutoQuery API to query our Booking RDBMS table, our Request DTO just needs to inherit QueryDb<Table> with Booking table they want the API to query:

public class QueryBookings : QueryDb<Booking> {}

This empty Request DTO alone is all it takes to create an AutoQuery API that can query each Booking column using any of the Implicit Conventions registered in the AutoQueryFeature plugin, e.g:

However, to aid in the discovery of popular Booking table queries and make them easily accessible to all of ServiceStack's Typed Service Clients or gRPC Clients it's recommended to formalize queries you want to make available by adding typed properties to the Request DTO, e.g:

public class QueryBookings : QueryDb<Booking> 
{
    public int[] Ids { get; set; }
    //...
}

Where they can also be consumed by every Service Client with an end-to-end Typed API, e.g:

// C#
var client = new JsonApiClient("https://vue-vite-api.jamstacks.net");
var api = await client.ApiAsync(new QueryBookings { Ids = new[] { 1,2,3 }));

TypeScript Example:

// TypeScript
let client = new JsonServiceClient("https://vue-vite-api.jamstacks.net")
let api = await client.api(new QueryBookings({ Ids: [1,2,3] }))

User-defined Routes

As AutoQuery APIs are themselves normal ServiceStack APIs they benefit from the entire customizability and ecosystem available to ServiceStack APIs, like Routing where the API can be made available under custom user-defined using the [Route] attribute:

[Route("/bookings")]
public class QueryBookings : QueryDb<Booking> 
{
    public int[] Ids { get; set; }
}

To also make the QueryBookings API available from the /bookings path, e.g:

AutoQuery CRUD APIs

To enable Auto CRUD behavior on your Table your Request DTOs can implement any of the following interfaces to create APIs with its respective CRUD behavior:

  • ICreateDb<Table> - Insert a new Table row
  • IUpdateDb<Table> - Fully Update an existing Table row
  • IPatchDb<Table> - Partially update an existing Table row
  • IDeleteDb<Table> - Delete an existing Table row

The Create and Update Request DTOs properties define which columns are updatable from the API:

public class CreateBooking
    : ICreateDb<Booking>, IReturn<IdResponse>
{
    public string Name { get; set; }
    public RoomType RoomType { get; set; }
    public int RoomNumber { get; set; }
    public DateTime BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    public decimal Cost { get; set; }
    public string Notes { get; set; }
}

public class UpdateBooking
    : IPatchDb<Booking>, IReturn<IdResponse>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public RoomType? RoomType { get; set; }
    public int? RoomNumber { get; set; }
    public DateTime? BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    public decimal? Cost { get; set; }
    public bool? Cancelled { get; set; }
    public string Notes { get; set; }
}

Only a single Update DTO needs to be implemented to enable Update functionality in Locode. If IPatchDb<Table> is implemented will use it to only update modified fields, whereas if only IUpdateDb<Table> is implemented, Locode needs to send all fields to perform a full update. If you have AutoQuery CRUD Events enabled it's recommended to use IPatchDb<Table> for the audit logs to only capture which fields were updated.

To enable delete functionality in Locode create a Request DTO that implements IDeleteDb<Table> with the primary key of the table:

public class DeleteBooking : IDeleteDb<Booking>, IReturnVoid
{
    public int Id { get; set; }
}

Although not used by Locode, Delete APIs supports the same querying behavior as AutoQuery APIs where you could enable create an API that supports multiple and batch deletes with the fields you want to delete by, e.g:

public class DeleteBookings : IDeleteDb<Booking>, IReturnVoid
{
    public int[]? Ids { get; set; }
    public bool? Cancelled { get; set; }
}

Locode is a capability-based UI that only enables functionality for CRUD APIs that exist and the currently authenticated user has access to. As these public APIs don't have any auth restrictions applied to them, they can be used immediately by non-authenticated users without signing in to query, insert, update and delete from the Booking Table:

Clicking on our Booking services on the left-hand menu utilizes the QueryBooking AutoQuery API, we can see the test seed data that was populated.

Using the New Booking button gives us a metadata driven Form IO derived from the properties of the CreateBooking Request DTO:

This form also allows editing existing bookings using the Edit button in the first column given its functionality is enabled with the application having the IPatch<Booking> API defined.

POCO References

When it can be inferred Locode automatically detects and linkify POCO references for easy navigation which is used a lot in https://talent.locode.dev like navigating to a Job's Job Applications:

defined by its POCO References:

public class JobApplication : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }

    [References(typeof(Job))]
    public int JobId { get; set; }

    [References(typeof(Contact))]
    public int ContactId { get; set; }

    [Reference]
    public Contact Applicant { get; set; }
    //...
}

The references support also allows adding related records by navigating to the child relation then adding the child record where it will preserve and pre-populate the parent id as seen when navigating to a Job's Applications:

public class Job : AuditBase
{
    //...
    public List<JobApplication> Applications { get; set; } = new();
}

Where it will pre-populate the Job Id reference making it easy to add multiple 1:Many Job Applications:

Checkout Talent.cs DTOs for more Reference examples.

Customizing Locode App

We've walked through a simple example of how to create CRUD APIs for our Booking RDBMS table which Locode uses to power its instant CRUD UI letting your App users start managing its data immediately.

This touches on some basic functionality to get started in Locode, next we'll explore its declarative dev model with the different ways you can annotate your services and data model to customize its behavior & appearance and enhance it with additional functionality using the available composable built-in declarative C# attributes.