Using AutoQuery's AutoGen enables the quickest way to modernize an existing database by generating Data Models & AutoQuery CRUD APIs from RDBMS table schemas. From Locode's point of view, the result is indistinguishable to Code-First where instead of developers defining Data Models & API Contracts in code they're dynamically generated by AutoGen at runtime, on Startup.
The difference is how APIs & Types are customized, with Code-First Types able to naturally access the Declarative Dev Model using C# Attributes, Database-First Models instead need to dynamically add attributes at runtime using AutoGen's Type & Service filters.
Regardless of how they're created & customized, each have access to Locode's instant modern UI around AutoQuery services.
To get started quickly watch this video for a step-by-step walkthrough into creating a Database-first Locode App:
Northwind example​
We have an example of this in the Northwind demo which provides a way to manage all the data in the Northwind database with some customizations to improve usability, all in ~120 lines of C#.
Create your project​
Starting with the basic 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
Configuring database connection​
Once you have the new web project open, you will need to configure the following.
- Database type (PostgreSQL, SQL Server, MySQL, or SQLite)
- Database connection string
- AutoQuery Generated Services
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 (MyLocodeApp in the link above) project.
Configure.Db.cs​
Below we have an example using sqlite
of the configuration to add an IDbConnectionFactory
dependency into IoC created by this command.
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));
});
}
The example above is using an in-:memory:
SQLite database, but we want to use a pre-existing database the connection string will need to be updated.
To use the Northwind sample database, we can download and copy it into the AppHost project with the Configure.Db.cs
file and replace the
:memory:
connection string with the file name northwind.sqlite
. Another easy way to download northwind.sqlite
is by using the x
tool with the following
command run from the AppHost directory.
x mix northwind.sqlite
Now our application can communicate with the Northwind sample database, we will need to configure AutoQuery to use AutoGen to generate our CRUD services from our database schema.
Configure.AutoQuery.cs​
With the database connection configured, next you will need to configure AutoQuery to scan your database schema and generate the required CRUD services.
This feature is known as AutoGen
and can be enabled by instantiating the GenerateCrudServices
option on the AutoQueryFeature
plugin with the AutoRegister
flag set to true
.
public class ConfigureAutoQuery : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureAppHost(appHost => {
appHost.Plugins.Add(new AutoQueryFeature {
MaxLimit = 1000,
// Add this line, Configures Generated CRUD services with defaults
GenerateCrudServices = new GenerateCrudServices()
{
AutoRegister = true
}
});
});
}
The AutoQueryFeature
plugin will automatically use your registered IDbConnectionFactory
to communicate with your database and generate services
for the public
schema. Running the application after these changes, we will have a Query
, Create
, Update
and Delete
services ready to use for each table.
Multiple Schemas​
By default, GenerateCrudServices
with AutoRegister
will create services for each table in the public
schema.
If you want to enable services for tables in other schemas, you can use the CreateServices
option. For example, if
you have a schema by the name of dbo
and public
, you would use the following options.
appHost.Plugins.Add(new AutoQueryFeature {
MaxLimit = 1000,
//IncludeTotal = true,
GenerateCrudServices = new GenerateCrudServices()
{
// Configure which schemas should be used, `public` is the default.
CreateServices = new List<CreateCrudServices>
{
new CreateCrudServices(),
new CreateCrudServices { Schema = "dbo" }
}
}
});
Multiple database connections​
If you are using Named connections with OrmLite, you can also specify these connections in the CreateServices
list.
Named connection registration can be done using the IDbConnectionFactory
and RegisterConnection
method.
// SqlServer with a named "Reporting" PostgreSQL connection as a part of the same `dbFactory`
var dbFactory = new OrmLiteConnectionFactory(connString, SqlServer2012Dialect.Provider);
container.Register<IDbConnectionFactory>(dbFactory);
dbFactory.RegisterConnection("Reporting", pgConnString, PostgreSqlDialect.Provider);
The string name provided to RegisterConnection
must match that provided to the NamedConnection
property on CreateCrudServices
.
appHost.Plugins.Add(new AutoQueryFeature {
MaxLimit = 1000,
//IncludeTotal = true,
GenerateCrudServices = new GenerateCrudServices()
{
// Configure multiple databases, `public` is the default schema.
CreateServices = new List<CreateCrudServices>
{
new CreateCrudServices(),
new CreateCrudServices { NamedConnection = "Reporting" }
}
}
});
Multiple Schemas with Named Connections​
These options can be combined so that specific schemas on named connections can also be used.
appHost.Plugins.Add(new AutoQueryFeature {
MaxLimit = 1000,
//IncludeTotal = true,
GenerateCrudServices = new GenerateCrudServices()
{
// Configure multiple databases, `public` is the default schema.
CreateServices = new List<CreateCrudServices>
{
new CreateCrudServices { NamedConnection = "Reporting" },
new CreateCrudServices { NamedConnection = "Finance", Schema = "trading" }
}
}
});
Customizing Locode App​
Locode includes a declarative dev model where attributes can be used to add additional metadata to your services and data model that can be used to enlist additional functionality and enhance the Locode App's UI.
Dynamically adding attributes​
The use of C# attributes to configure your AutoQuery service metadata is optimal for code-first DTOs & Data Models
but when starting with a Database First dev model, the Types are generated and only exist at runtime which typically
would prohibit them from being annotated with C# attributes, however
AutoQuery AutoGen's ServiceFilter
and TypeFilter
lets you
dynamically apply attributes to its generated types at startup.
Exporting to code-first Types​
In addition to enabling access to the same rich declarative functionality ServiceStack makes available via attributes, it also annotates the code-generated types which are retained when moving from database-first to code-first dev model by exporting to code-first types with:
x csharp https://localhost:5001 -path /crud/all/csharp
Annotated Example of Northwind's Customizations​
To help illustrate some customizations available we've annotated the customizations made to the Northwind Sample SQLite Database that was used to create the custom northwind.locode.dev Locode App:
Modifying Dynamic Types at Runtime​
Annotating the Database First code-gen types can be done via the ServiceFilter
and TypeFilter
properties on
GenerateCrudServices
instructions when registering the AutoQueryFeature
.
The ServiceFilter
is called for every Service Operation whilst the TypesFilter
is called for every code-gen Type
including Request & Response DTOs.
Plugins.Add(new AutoQueryFeature {
MaxLimit = 100,
GenerateCrudServices = new GenerateCrudServices {
AutoRegister = true,
ServiceFilter = (op, req) => {
// Annotate all Auto generated Request DTOs with [Tag("Northwind")] attribute
op.Request.AddAttributeIfNotExists(new TagAttribute("Northwind"));
},
TypeFilter = (type, req) =>
{
// Configure to use Icon for this Type
if (Icons.TryGetValue(type.Name, out var icon))
type.AddAttribute(new IconAttribute { Svg = Svg.Create(icon) });
// Is Employee Data Model or any AutoQuery Employee Request DTO
if (type.Name == "Employee" || type.IsCrudCreateOrUpdate("Employee"))
{
// Remove unused `Photo` column
type.Properties.RemoveAll(x => x.Name == "Photo");
// Reorder columns from db-schema to the order we want them to appear in the UI
type.ReorderProperty("PhotoPath", before: "Title")
.AddAttribute(new FormatAttribute(FormatMethods.IconRounded));
type.ReorderProperty("ReportsTo", after: "Title");
if (type.IsCrud()) // Is AutoQuery Employee Request DTO
{
// Configure to use File Input & upload to 'employees' Managed File Upload location
type.Property("PhotoPath")
.AddAttribute(new InputAttribute { Type = Input.Types.File })
.AddAttribute(new UploadToAttribute("employees"));
// Use TextArea control for larger text inputs
type.Property("Notes")
.AddAttribute(new InputAttribute { Type = Input.Types.Textarea });
}
else if (type.Name == "Employee") // Employee Data Model
{
// Configure Employee FK Relation, utilizing UX-friendly LastName & Employee Lookup
type.Property("ReportsTo").AddAttribute(
new RefAttribute { Model = "Employee", RefId = "Id", RefLabel = "LastName" });
// Format to use `tel:` link allowing Phone call to be initiated from UI
type.Property("HomePhone").AddAttribute(new FormatAttribute(FormatMethods.LinkPhone));
}
}
else if (type.Name == "Order")
{
// Customize all Date Columns to use UX-friendly Date Format
type.Properties.Where(x => x.Name.EndsWith("Date")).Each(p =>
p.AddAttribute(new IntlDateTime(DateStyle.Medium)));
// Format number as USD Currency using JavaScript's Intl.NumberFormat
type.Property("Freight").AddAttribute(new IntlNumber { Currency = NumberCurrency.USD });
// Configure Shipper FK Relation, utilizing UX-friendly CompanyName & Shipper Lookup
type.Property("ShipVia").AddAttribute(
new RefAttribute { Model = "Shipper", RefId = "Id", RefLabel = "CompanyName" });
}
else if (type.Name == "OrderDetail")
{
// Format number as USD Currency using JavaScript's Intl.NumberFormat
type.Property("UnitPrice").AddAttribute(new IntlNumber { Currency = NumberCurrency.USD });
// Format as % using JavaScript's Intl.NumberFormat
type.Property("Discount").AddAttribute(new IntlNumber(NumberStyle.Percent));
}
else if (type.Name == "EmployeeTerritory")
{
// Configure Territory FK Relation & Lookup, utilizing UX-friendly TerritoryDescription
type.Property("TerritoryId").AddAttribute(new RefAttribute {
Model = "Territory", RefId = "Id", RefLabel = "TerritoryDescription" });
}
else if (type.Name is "Customer" or "Supplier" or "Shipper")
{
// Format to use `tel:` link allowing Phone call to be initiated from UI
type.Property("Phone").AddAttribute(new FormatAttribute(FormatMethods.LinkPhone));
type.Property("Fax")?.AddAttribute(new FormatAttribute(FormatMethods.LinkPhone));
}
},
},
});
Code-gen Customization Helpers​
A number of UX-Friendly extension methods are available to reduce effort for applying common customizations to
MetadataType
and MetadataPropertyType
blueprints used in code generating .NET Types:
Type Methods | Description |
---|---|
AddAttribute() | Add Attribute to Type |
AddAttributeIfNotExists() | Add Attribute to Type if not already exists |
Property() | Resolve Property from Type |
ReorderProperty() | Reorder where the DB Column appears in Type (changes API & UI ordering) |
EachProperty() | Apply custom lambda to each matching property |
RemoveProperty() | Omit properties from inclusion in code-gen type |
Property Methods | Description |
---|---|
AddAttribute() | Add Attribute to Property |
AddAttributeIfNotExists() | Add Attribute to Property if not already exists |
Format search results​
The Declarative Attributes docs contains a more complete reference of built-in customizations, but we'll cover a few the Northwind Locode App uses to illustrate some potential enhancements available.
An effortless way to add a lot of value to DB Apps is to mark up the raw data stored in RDBMS tables into a UX-friendly
view, an example of this in Northwind is using FormatMethods.LinkPhone
on the Phone
and Fax
properties for
the Customer
, Supplier
, and Shipper
tables:
if (type.Name is "Customer" or "Supplier" or "Shipper")
{
type.Property("Phone").AddAttribute(new FormatAttribute(FormatMethods.LinkPhone));
type.Property("Fax")?.AddAttribute(new FormatAttribute(FormatMethods.LinkPhone));
}
To format the phone numbers in tel:
HTML links enabling 1-click to call, directly from the search results page:
A complete list of built-in functions can be found in the Format Functions docs, another example used in
Northwind is FormatMethods.IconRounded
on PhotoPath
:
if (type.Name == "Employee" || type.IsCrudCreateOrUpdate("Employee"))
{
type.ReorderProperty("PhotoPath", before: "Title")
.AddAttribute(new FormatAttribute(FormatMethods.IconRounded));
}
To apply the iconRounded
JavaScript function to render a preview of the Employee profile directly in the search results:
ReorderProperty
is used to change ordering of Table columns which changes the order they're displayed in APIs and UIs.
TIP
The original images in Northwind were migrated to reference external images using the following SQL:
update Employee set PhotoPath = "/profiles/employees/" || Employee.Id || ".jpg"
Where its uploads are now managed by the configured FilesUploadFeature
and built-in File Input UI controls.
ECMAScript Internationalization APIs​
ECMAScript's rich Intl.NumberFormat
, Intl.DateTimeFormat
and Intl.RelativeTimeFormat
APIs are also available from the
typed [Intl*] Attributes which OrderDetail
makes use of to format UnitPrice
in USD Currency and Discount
in a % percentage format:
if (type.Name == "OrderDetail")
{
type.Property("UnitPrice").AddAttribute(new IntlNumber { Currency = NumberCurrency.USD });
type.Property("Discount").AddAttribute(new IntlNumber(NumberStyle.Percent));
}
This can give a much more contextual view of the data in the returning from our services.
For more customization options, see the Declarative Attributes docs.