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:
- Customizing how Data Models map to RDBMS tables and enlist RDBMS features
- Customize Serialization & API behavior
- Define AutoQuery & CRUD API behavior
- Define Validation Rules and Authorization restrictions
- Annotate & Document APIs
- Customize UI Behavior & Appearance
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 rowIUpdateDb<Table>
- Fully Update an existing Table rowIPatchDb<Table>
- Partially update an existing Table rowIDeleteDb<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; }
//...
}
Navigating Child References​
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.