# Query JSON like any other property in OrmLite Source: https://servicestack.net/posts/ormlite-json :::youtube e_zQubovAj8 Query JSON data with a Typed C# ORM: No Extra Tables, No Raw SQL ::: JSON columns are the pragmatic answer to data that doesn't fit neatly into columns: a customer's addresses and tags, an order's lines, an event's payload, settings that differ by tenant. Every database can now store them and query inside them. The catch is that every database does it differently. SQLite has `json_extract()`, PostgreSQL has `jsonb_path_query_first()`, SQL Server has `JSON_VALUE()` and MySQL has `JSON_EXTRACT()`, each with its own path syntax and its own rules for nulls, so a JSON query becomes the one place your App stops being portable and typed. OrmLite has been closing that gap one release at a time. v10.2 added `Sql.Json()` for querying JSON in `string` columns, which we covered in [Type-safe JSON access and Upserts](/posts/omlite-json-upsert). **ServiceStack v10.4** takes the last step. When a property is a class or a `List`, OrmLite already stores it as JSON, so there's no longer anything to wrap. You query it like any other property: ```csharp public class Customer { [AutoIncrement] public int Id { get; set; } public string Name { get; set; } public Address Address { get; set; } // stored as JSON public List Tags { get; set; } // stored as JSON public List Lines { get; set; } // stored as JSON } var q = db.From() .Where(x => x.Address.Country.Code == "UK" && x.Tags.Contains("vip") && x.Lines.Count > 1) .OrderBy(x => x.Address.City) .Select(x => new { x.Name, City = x.Address.City }); ``` That's an ordinary typed query. It compiles, refactors with your model, and runs unchanged on **SQLite**, **PostgreSQL**, **SQL Server** and **MySQL/MariaDB**, each using its own native JSON functions. ## What you can write | Expression | Queries | |-|-| | `x.Address.City == "London"` | A property, at any depth: `x.Address.Country.Code` | | `x.Tags.Contains("vip")` | Whether a list or array has a value | | `x.Lines.Count > 1` | How many items a list has, or `Length` of an array | | `x.Lines[0].Quantity >= 2` | An item of a list by its position | | `x.Lines.Any(l => l.Sku == "A-1" && l.Quantity > 1)` | Whether a list has an item matching a condition | | `x.Lines.All(l => l.Shipped)` | Whether every item matches a condition | | `x.Lines.Count(l => l.Quantity > 10) >= 2` | How many items match a condition | They work wherever a typed expression does, so you can filter, sort, select, count, update and delete by what's inside a JSON property, including on joined tables: ```csharp db.Count(x => x.Address.City == "London"); db.UpdateOnly(() => new Customer { Name = "Londoner" }, where: x => x.Address.City == "London"); db.Delete(x => x.Lines.Count == 0); var q = db.From() .Join((c, o) => c.Id == o.CustomerId) .Where((c, o) => o.Source.Channel == "web" && c.Tags.Contains("vip")); ``` Values are always sent as db params, so they're safe to take from user input, and the queries work in compiled queries and [connection filters](/posts/ormlite-multitenancy) too. ### Conditions on the items of a list `x.Lines[0]` can only look at one item, and `x.Lines[0].Sku == sku && x.Lines[1].Quantity > 1` could be matching two different lines. `Any()`, `All()` and `Count()` check each item against a condition, so the *same* item has to match all of it. Conditions can use every property of the item, the text functions of its properties, lists of values, and even the row's own columns: ```csharp // The same item has the Sku and the Quantity, unlike x.Lines[0] db.Select(x => x.Lines.Any(l => l.Sku == sku && l.Quantity > 1)); db.Select(x => x.Lines.Any(l => !l.Shipped && l.Quantity >= x.MinQuantity)); // the row's columns db.Select(x => x.Tags.Any(t => t.StartsWith("vip"))); // lists of values db.Select(x => x.Lines.Count(l => l.Quantity > 10) >= 2); ``` They're a subquery over the list's items, `EXISTS (SELECT 1 FROM ... WHERE ...)`, using `json_each()` in SQLite, `jsonb_path_query()` in PostgreSQL, `OPENJSON()` in SQL Server and `JSON_TABLE()` in MySQL 8.0.4+ and MariaDB 10.6+. Rows without the list, or with an empty one, have no items, so `All()` matches them and `Any()` doesn't. ### PostgreSQL arrays too PostgreSQL stores `string[]`, `int[]` and `long[]` in its own array types like `text[]` rather than JSON. They're queried the same way as lists, with PostgreSQL's array functions: ```csharp db.Select
(x => x.Tags.Contains("vip")); // :0 = ANY("tags") db.Select
(x => x.Tags.Length > 2); // cardinality("tags") > :0 db.Select
(x => x.Tags.Any(t => t.StartsWith("v"))); // EXISTS (SELECT 1 FROM unnest("tags") ...) db.Select
(x => x.Scores.Count(s => s >= 90) >= 2); ``` ### Complex types need to be stored as JSON To query a property, OrmLite needs to have saved it as JSON, as an RDBMS's JSON functions can't read any other format. That's the default for dialects configured with `AddOrmLite()`, and can be enabled on others: ```csharp // The default of dialects configured with AddOrmLite() services.AddOrmLite(options => options.UsePostgres(connectionString)); // Or set on a dialect provider PostgreSqlDialect.Provider.UseJson = true; ``` Querying a complex property on a dialect that doesn't store JSON throws a `NotSupportedException` that says how to enable it, rather than silently returning nothing. If you're switching an existing App to `UseJson`, note that it changes how complex types are saved, so rows saved as JSV need to be converted to JSON before they can be queried. ## JSON in string columns Sometimes the JSON isn't a property of your model. It's an event body, an integration response or an audit snapshot that your App stores as text. A `string` has no properties to query, so `Sql.Json()` says what the document in it looks like, with a C# Data Model: ```csharp public class OrderEvent { public long Id { get; set; } public string Data { get; set; } // JSON of an OrderDocument public OrderDocument Document { get; set; } // A complex property, also stored as JSON } var q = db.From() .Where(x => Sql.Json(x.Data).Customer.Address.State == "WA" && Sql.Json(x.Data).Tags.Contains("priority") && Sql.Json(x.Data).Lines[0].Quantity > 1); ``` Nested member access becomes a nested JSON path, array indexes become zero-based JSON array indexes (constants or captured values), `[DataMember(Name = "shipping_state")]` is honored, and operations on an extracted scalar like `.State.Length == 2` continue through OrmLite's normal expression translation. Selecting a scalar member extracts its C# value, and selecting an object or collection returns a JSON fragment that OrmLite deserializes into your Data Model: ```csharp var q = db.From() .Select(x => new { x.Id, State = Sql.Json(x.Data).Customer.Address.State, Total = Sql.Json(x.Data).Total, Address = Sql.Json(x.Data).Customer.Address, }); var summaries = db.Select(q); // Or a single typed fragment var address = db.Scalar
(db.From() .Where(x => x.Id == id) .Select(x => Sql.Json(x.Data).Customer.Address)); ``` | JSON is in | Use | |-|-| | A complex type property, on a dialect with `UseJson` | The property: `x.Address.City` | | A `string` column | `Sql.Json(x.Data)` with the type of its document | | A complex type property, on a dialect without `UseJson` | `Sql.Json(x.Address)` | | A property with the type of a table the query joins | `Sql.Json(x.Customer)` | The last row is a subtle one. In a query that joins `Customer`, OrmLite has always resolved `x.Customer.Name` to the `Name` column of the joined table. If the property is *also* stored as JSON, e.g. a copy of the customer as they were when an invoice was created, use `Sql.Json(x.Customer).Name` to query the copy instead of the table. ## Explicit paths for everything else Some JSON questions don't have a C# Data Model. The document may have no fixed shape, the path may only be known at runtime, or you may want to ask something about the JSON itself rather than its values. For those there's an explicit SQL/JSON path API. Paths begin with `$` and use the portable subset every database supports: members, quoted members and zero-based array indexes, e.g. `$.Customer.Address.shipping_state` or `$.Lines[0].Quantity`. ```csharp var statePath = "$.Customer.Address.shipping_state"; // chosen at runtime var q = db.From() .Where(x => Sql.JsonValue(x.Data, statePath) == "WA" && Sql.JsonExists(x.Data, "$.Tags[0]")) .OrderByDescending(x => Sql.JsonValue(x.Data, "$.Total")); ``` Query values are db params, and paths are emitted as escaped SQL string literals for the dialect. The full set is: | Task | Typed expression | Explicit path API | | --- | --- | --- | | Read scalar | `Sql.Json(json).Member` | `Sql.JsonValue(json, path)` | | Read object or array | `Sql.Json(json).Member` | `Sql.JsonQuery(json[, path])` | | Array length | `Sql.Json(json).Items.Count` | `Sql.JsonArrayLength(json[, path])` | | Scalar array membership | `Sql.Json(json).Items.Contains(value)` | `Sql.JsonArrayContains(json[, path], value)` | | Array indexing | `Sql.Json(json).Items[index]` | Include `[index]` in the path | | Validate JSON | - | `Sql.IsJson(json)` | | Path exists | - | `Sql.JsonExists(json, path)` | | Read JSON type | - | `Sql.JsonType(json[, path])` | | Document containment | - | `Sql.JsonContains(json, candidate[, path])` | ### Missing, null and absent JSON has a distinction most SQL doesn't: a property can be missing, present with a `null` value, or present with a value. The portable API keeps all three distinguishable where the databases allow it, with `JsonExists()` telling you whether the path exists at all and `JsonType()` returning a normalized `JsonValueType` (`Null`, `String`, `Number`, `Boolean`, `Array` or `Object`) independent of each database's native type names: | Expression | JSON `null` | Missing path | Object or array | | --- | --- | --- | --- | | `JsonExists()` | `true` | `false` | `true` | | `JsonType()` | `JsonValueType.Null` | `null` | `Object` or `Array` | | `JsonValue()` | SQL `NULL` | SQL `NULL` | SQL `NULL` | | `JsonQuery()` | SQL `NULL` | SQL `NULL` | JSON fragment | | `JsonArrayLength()` | SQL `NULL` | SQL `NULL` | Count for arrays; `NULL` for objects | Use them together to tell a property that was never set from one that was explicitly cleared: ```csharp // true when the first element exists, even when it is JSON null Sql.JsonExists(x.Data, "$.NullableTags[0]") var type = db.Scalar(db.From() .Where(x => x.Id == id) .Select(x => Sql.JsonType(x.Data, "$.Customer.Address"))); // JsonValueType.Object ``` ### Validate and contain `Sql.IsJson()` tests whether a string holds a valid JSON value, which is useful for finding bad rows before they break a query: ```csharp var validJsonRows = db.Count(x => Sql.IsJson(x.Data) == true); ``` Extraction functions can report an RDBMS error given malformed JSON, and a database is free to evaluate predicates in any order, so don't rely on `IsJson(...) && ...` to protect another JSON operation. Validate JSON when it's written, or use a database constraint. On PostgreSQL and MySQL, `Sql.JsonContains()` tests whether a document contains a partial document, with the candidate sent as a db param: ```csharp var candidate = new { Customer = new { Address = new { shipping_state = "WA" } } }; var q = db.From() .Where(x => Sql.JsonContains(x.Data, candidate)); var requiredTags = new[] { "priority" }; var tagged = db.From().Where(x => Sql.JsonContains(x.Data, requiredTags, "$.Tags")); ``` SQLite and SQL Server throw `NotSupportedException`, as they have no equivalent operation with the same semantics. ## One query, four databases Each API is translated to the native functions of the configured database: | Operation | SQLite | PostgreSQL | SQL Server | MySQL | | --- | --- | --- | --- | --- | | Validate | `json_valid` | `IS JSON` | `ISJSON` | `JSON_VALID` | | Scalar value | `json_extract` | `jsonb_path_query_first` | `JSON_VALUE` | `JSON_EXTRACT` | | Object/array | `json_extract` | `jsonb_path_query_first` | `JSON_QUERY` | `JSON_EXTRACT` | | Exists | `json_type` | `jsonb_path_exists` | `JSON_PATH_EXISTS` | `JSON_CONTAINS_PATH` | | Type | `json_type` | `jsonb_typeof` | `OPENJSON` | `JSON_TYPE` | | Array length | `json_array_length` | `jsonb_array_length` | `OPENJSON` | `JSON_LENGTH` | | Array membership | `json_each` | `jsonb` containment | `OPENJSON` | `JSON_CONTAINS` | | Document containment | - | `@>` | - | `JSON_CONTAINS` | SQL Server 2016-2019 can use `JsonValue()`, `JsonQuery()`, `JsonType()`, `JsonArrayLength()` and `JsonArrayContains()`. Use `SqlServer2022Dialect.Provider` when you need the complete API, including `JsonExists()`. You can always see what was generated. JSON expressions are normal `SqlExpression` queries, so `ToSelectStatement()` and `Params` show the SQL and its parameters, and they work with the async APIs too. ## Keeping JSON queries fast A query of a JSON property reads the JSON of every row, which is fine until the table gets large. When a property is queried often, e.g. to find customers by `x.Address.City`, store it in a column of its own and index it with `[Index]`. That works the same way on every database: ```csharp public class Customer { [Index] public string City { get; set; } public Address Address { get; set; } } ``` SQLite and PostgreSQL can also index the expression a query reads the property with. Copy the expression from the SQL of the query, as it has to match exactly, and create the index in a migration: ```csharp var q = db.From().Where(x => x.Address.City == "London"); var sql = q.ToSelectStatement(); // ... WHERE (CASE WHEN json_type("Address", '$.City') NOT IN ('object','array','null') // THEN json_extract("Address", '$.City') END = @0) Db.ExecuteSql(""" CREATE INDEX ix_customer_city ON "Customer" ((CASE WHEN json_type("Address", '$.City') NOT IN ('object','array','null') THEN json_extract("Address", '$.City') END)) """); ``` Check the index is used with `EXPLAIN QUERY PLAN` on SQLite or `EXPLAIN` on PostgreSQL, and check it again after upgrading OrmLite, as it's only used while the query's SQL stays the same. SQL Server and MySQL can only index an expression through a computed or generated column, so a column of its own is the better choice for them. The new `db.Explain()` and the Slowest Queries view in the Admin UI help find the queries worth indexing. ## Get Started Querying complex type properties is available in **ServiceStack v10.4**, and works on every dialect configured with `AddOrmLite()`. Upgrade your `ServiceStack.OrmLite.*` packages and remove the `Sql.Json()` wrappers around your complex properties, if you have any. See the docs for the complete reference: - [OrmLite JSON Support](https://docs.servicestack.net/ormlite/json) - complex type queries, `Sql.Json()`, explicit paths, validation, containment and indexing - [Type-safe JSON access and Upserts](/posts/omlite-json-upsert) - the v10.2 introduction to `Sql.Json()` - [v10.4 Release Notes](https://docs.servicestack.net/releases/v10_04) - everything else in this release # Advanced SQL queries in typed OrmLite Source: https://servicestack.net/posts/ormlite-advanced-queries :::youtube 1e5TXrOapqg Advanced SQL with a Typed C# ORM: Window Functions, CTEs & Temporal Tables ::: Typed queries cover the first 90% of an App's SQL very well. Then comes the query that's easy to describe and awkward to write: *every subject under Fiction, at any depth*, *each customer's 3 latest orders*, *everyone who has written or reviewed a book*, *the next page of 50 after the last row I saw*. Those are the queries that sent code back to hand-written SQL strings, with their string concatenation, their per-database differences and their lack of refactoring. **ServiceStack v10.4** adds typed support for the ones that come up most, in the same fluent `SqlExpression` style as the rest of OrmLite. They're parameterized, they compose with your filters, joins and [connection filters](/posts/ormlite-multitenancy), and they run on the databases that support them: - **`WithRecursive()`** for hierarchies like categories, org charts and threaded comments - **`With()`** to name sub queries as common table expressions - **Window functions** for rankings, running totals, previous rows, and `TopPerGroup()` - **`Union()`, `Intersect()` and `Except()`** to combine queries - **`SeekAfter()`** for fast, stable keyset paging - **`SelectLazyAsync()`** to stream large result sets with `await foreach` ## Query hierarchical data with WithRecursive Hierarchies are everywhere, and walking one used to mean a recursive common table expression written by hand. `WithRecursive()` starts with the rows of a seed query and repeatedly adds the rows matching a relationship: ```csharp // Fiction and every subject under it, at any depth var q = db.From() .WithRecursive( seed: db.From().Where(x => x.Name == "Fiction"), recurse: (parent, child) => child.ParentId == parent.Id); var subjects = db.Select(q); ``` ```sql WITH RECURSIVE "cte" ("Id", "ParentId", "Name", "Active") AS ( SELECT "Id", "ParentId", "Name", "Active" FROM "Subject" WHERE ("Name" = @0) UNION ALL SELECT "c"."Id", "c"."ParentId", "c"."Name", "c"."Active" FROM "Subject" "c" INNER JOIN "cte" ON ("c"."ParentId" = "cte"."Id") ) SELECT "Id", "ParentId", "Name", "Active" FROM "cte" "Subject" -- @0 = 'Fiction' ``` Swapping the relationship walks up the hierarchy instead, e.g. the path from a subject to the root: ```csharp var ancestors = db.From() .WithRecursive( seed: db.From().Where(x => x.Id == subjectId), recurse: (child, parent) => parent.Id == child.ParentId); ``` The rest of the query applies to all the rows found, so typed filters, ordering, projections, `Count()`, paging, async APIs and keyset pagination work as usual: ```csharp var q = db.From() .WithRecursive( seed: db.From().Where(x => x.Id == 1), recurse: (parent, child) => child.ParentId == parent.Id) .Where(x => x.Active) .OrderBy(x => x.Name) .Select(x => x.Name); var names = await db.ColumnAsync(q); ``` ```sql WITH RECURSIVE "cte" ("Id", "ParentId", "Name", "Active") AS ( SELECT "Id", "ParentId", "Name", "Active" FROM "Subject" WHERE ("Id" = @0) UNION ALL SELECT "c"."Id", "c"."ParentId", "c"."Name", "c"."Active" FROM "Subject" "c" INNER JOIN "cte" ON ("c"."ParentId" = "cte"."Id") ) SELECT "Name" FROM "cte" "Subject" WHERE "Active"=1 ORDER BY "Name" -- @0 = 1 ``` Real data has edge cases, and the options cover them. `maxDepth` limits how many levels are selected, `Sql.RecursiveDepth()` is the level of each row, and `detectCycles` stops at rows that were already visited, for data that can have loops: ```csharp // Books, its children and grandchildren, a level at a time var q = db.From() .WithRecursive( seed: db.From().Where(x => x.Name == "Books"), recurse: (parent, child) => child.ParentId == parent.Id, maxDepth: 2, detectCycles: true) .OrderBy(x => Sql.RecursiveDepth()) .Select(x => new { x.Id, x.Name, Depth = Sql.RecursiveDepth() }); ``` Raw SQL statements that start with a CTE, e.g. `WITH roots AS (...) SELECT ...`, are now also executed as-is on all databases. ## Name sub queries with With `With()` names a sub query as a common table expression, which the rest of the query reads like a table. A sub query that's needed in several places is written once, and a complex query can be built in steps that are each simple to read. The columns of the sub query are described by a class, in the order it selects them: ```csharp public class AuthorTotal { public string Author { get; set; } public int Books { get; set; } public decimal Total { get; set; } } var totals = db.From() .GroupBy(x => x.Author) .Select(x => new { x.Author, Books = Sql.Count("*"), Total = Sql.Sum(x.Price) }); // Each book since 1970, with how many books its author has var q = db.From() .With(totals) .Join((b, t) => b.Author == t.Author) .Where(b => b.Year >= 1970) .Select((b, t) => new { b.Title, b.Author, t.Books }); ``` ```sql WITH "AuthorTotal" ("Author", "Books", "Total") AS ( SELECT "Author", Count(*) AS Books, Sum("Price") AS Total FROM "Book" GROUP BY "Author" ) SELECT "Book"."Title", "Book"."Author", "AuthorTotal"."Books" AS "Books" FROM "Book" INNER JOIN "AuthorTotal" ON ("Book"."Author" = "AuthorTotal"."Author") WHERE ("Book"."Year" >= @0) -- @0 = 1970 ``` As the sub query is named after a class, it's used with the same typed APIs as a table, including `db.From()` to select from it. A query can have several sub queries where each can read the ones before it, and they can be combined with `WithRecursive()`. ## Rankings, running totals and top rows per group Window functions calculate a value for each row from the rows related to it, like its rank within a group, a running total or the previous row's value. They're the right tool for a whole family of reporting questions, and they now have typed APIs instead of raw SQL: ```csharp var q = db.From() .Select(x => new { x.Id, x.Customer, x.Total, Rank = Sql.Rank(w => w.PartitionBy(x.Customer).OrderByDescending(x.Total)), RunningTotal = Sql.Sum(x.Total, w => w.PartitionBy(x.Customer).OrderBy(x.CreatedDate)), PreviousTotal = Sql.Lag(x.Total, w => w.PartitionBy(x.Customer).OrderBy(x.CreatedDate)), WeeklyAverage = Sql.Avg(x.Total, w => w.OrderBy(x.CreatedDate).RowsBetween(6, 0)), }); ``` ```sql SELECT "Id", "Customer", "Total", RANK() OVER (PARTITION BY "Customer" ORDER BY "Total" DESC) AS "Rank", SUM("Total") OVER (PARTITION BY "Customer" ORDER BY "CreatedDate") AS "RunningTotal", LAG("Total") OVER (PARTITION BY "Customer" ORDER BY "CreatedDate") AS "PreviousTotal", AVG("Total") OVER (ORDER BY "CreatedDate" ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS "WeeklyAverage" FROM "Order" ``` `RowNumber`, `Rank`, `DenseRank`, `Ntile`, `Lag`, `Lead`, `FirstValue`, `LastValue` and the `Sum`, `Count`, `Min`, `Max` and `Avg` aggregates are supported. ### Top rows of each group *Each customer's 3 latest orders* is one of the most common queries that needs window functions, and one of the easiest to get subtly wrong. `TopPerGroup()` returns the first rows of each group in the order of the query: ```csharp // Each customer's 3 latest orders var latest = db.Select(db.From() .OrderByDescending(x => x.CreatedDate) .TopPerGroup(x => x.Customer, take: 3)); ``` ```sql SELECT "Id", "Customer", "Total", "CreatedDate" FROM ( SELECT "Order".*, ROW_NUMBER() OVER (PARTITION BY "Customer" ORDER BY "CreatedDate" DESC) AS "_rn" FROM "Order" ) "Order" WHERE "Order"."_rn" <= 3 ORDER BY "CreatedDate" DESC ``` Window functions are supported on PostgreSQL, SQL Server, SQLite 3.25+, MySQL 8+, MariaDB and Oracle. ## Combine queries with Union, Intersect and Except Typed queries can now be combined with `Union()`, `UnionAll()`, `Intersect()` and `Except()`, without dropping down to raw SQL. Each query selects the same number of compatible columns, and its params are merged automatically: ```csharp // Everyone who has written or reviewed a book var q = db.From().Select(x => x.Author) .Union(db.From().Select(x => x.Reviewer)); var people = db.Column(q); // Fantasy books or anything under $9 var q2 = db.From().Where(x => x.Genre == Genre.Fantasy).Select(x => x.Title) .Union(db.From().Where(x => x.Price < 9m).Select(x => x.Title)); ``` ```sql -- Everyone who has written or reviewed a book SELECT "Author" FROM "Book" UNION SELECT "Reviewer" FROM "BookReview" -- Fantasy books or anything under $9 SELECT "Title" FROM "Book" WHERE ("Genre" = @0) UNION SELECT "Title" FROM "Book" WHERE ("Price" < @1) -- @0 = 'Fantasy', @1 = 9 ``` `Intersect()` returns rows in both queries, and `Except()` returns rows in the first query that aren't in the other, which is a tidy way to ask what's missing: ```csharp var available = db.From().Where(x => x.Available).Select(x => x.Id); var reviewed = db.From().Select(x => x.BookId); var availableAndReviewed = db.Column(available.Clone().Intersect(reviewed)); var awaitingReviews = db.Column(available.Clone().Except(reviewed)); ``` ```sql -- availableAndReviewed SELECT "Id" FROM "Book" WHERE "Available"=1 INTERSECT SELECT "BookId" FROM "BookReview" -- awaitingReviews SELECT "Id" FROM "Book" WHERE "Available"=1 EXCEPT SELECT "BookId" FROM "BookReview" ``` `OrderBy()`, `Skip()` and `Take()` on the first query apply to the combined results, and `Count()` counts them, so a combined query can be paged like any other. Queries being combined keep their own `OrderBy()` and `Take()`: ```csharp // Page through everyone, sorted by name var q = db.From().Select(x => x.Author) .Union(db.From().Select(x => x.Reviewer)) .OrderBy(x => x.Author) .Skip(20).Take(10); // Fantasy books plus the 2 cheapest books var q2 = db.From().Where(x => x.Genre == Genre.Fantasy).Select(x => x.Title) .UnionAll(db.From().OrderBy(x => x.Price).Take(2).Select(x => x.Title)); ``` ```sql -- Page through everyone, sorted by name SELECT * FROM ( SELECT "Author" FROM "Book" UNION SELECT "Reviewer" FROM "BookReview" ) q ORDER BY "Author" LIMIT 10 OFFSET 20 -- Fantasy books plus the 2 cheapest books SELECT "Title" FROM "Book" WHERE ("Genre" = @0) UNION ALL SELECT * FROM (SELECT "Title" FROM "Book" ORDER BY "Price" LIMIT 2) q1 -- @0 = 'Fantasy' ``` Combined queries work with `Select()` into POCOs and the async APIs. Oracle uses `MINUS` for `Except()`, and Firebird, which doesn't support `INTERSECT` or `EXCEPT`, throws a `NotSupportedException`. ## Fast, stable paging with SeekAfter `Skip(n).Take(m)` gets slower the deeper you page, as the database still reads every skipped row, and rows can be skipped or repeated when data changes between requests. `SeekAfter()` pages through results by continuing after the last row of the previous page (keyset pagination), so every page is fast and stable: ```csharp var q = db.From() .OrderByDescending(x => x.CreatedDate).ThenBy(x => x.Id) // end with a unique column .Take(50); if (lastRow != null) q.SeekAfter(lastRow); // continue after the last row of the previous page var page = db.Select(q); ``` ```sql SELECT "Id", "Customer", "Total", "CreatedDate" FROM "Order" WHERE (("CreatedDate" <= @0) AND (("CreatedDate" < @0) OR ("CreatedDate" = @0 AND "Id" > @1))) ORDER BY "CreatedDate" DESC, "Id" LIMIT 50 -- @0 = '2026-09-30 00:00:00', @1 = 42 ``` It generates `(a > @0) OR (a = @0 AND b > @1)` conditions from the query's `ORDER BY`, which work on every database, including with mixed sort directions, after an `a >= @0` bound that lets the database seek to the page in an index. | | Skip / Take | SeekAfter | |-|-|-| | Deep pages | Slower the deeper you page | Same speed at any depth | | Rows added or removed between requests | Rows can be skipped or repeated | Continues where it left off | | Jump to page N | Yes | No, pages are sequential | That makes it a fit for feeds, infinite scrolling, exports and sync APIs. In an API, pass the `ORDER BY` values directly from a cursor, and it combines with filters and with user-selected sort orders from [OrderBySafe()](/posts/ormlite-safe-sql): ```csharp var page = db.Select(db.From() .Where(x => x.Available) .OrderBy(x => x.Price).ThenByDescending(x => x.Year).ThenBy(x => x.Id) .SeekAfter(cursor.Price, cursor.Year, cursor.Id) .Take(50)); ``` Call it after `OrderBy()`, end the order with a unique column such as the primary key, and sort by non-nullable columns. ## Stream large result sets Large exports and background processing shouldn't have to load an entire table into memory or block a thread. `SelectLazyAsync()` and `ColumnLazyAsync()` stream results one row at a time with `await foreach`: ```csharp await foreach (var order in db.SelectLazyAsync( db.From().Where(x => x.Status == Status.Shipped), token)) { await writer.WriteAsync(order); } await foreach (var email in db.ColumnLazyAsync(db.From().Select(x => x.Email))) ... ``` Queries can be typed, parameterized or use [`Sql.Fmt()`](/posts/ormlite-safe-sql). Breaking out of the loop or cancelling the token closes the reader straight away, so the connection can be reused immediately. ## Row-returning, locking and joined writes Three more additions solve the same kind of problem: a write that needs to be one statement to be correct. `UpdateOnlyReturning()` and `DeleteReturning()` return the rows affected by an UPDATE or DELETE in the same statement, instead of a separate query that could see different rows. Deleting and returning in one statement also makes a simple work queue, as each row is only ever returned to one caller: ```csharp List shipped = db.UpdateOnlyReturning(() => new Order { Status = "Shipped" }, where: x => x.Status == "Packed"); List jobs = db.DeleteReturning(x => x.Queue == "emails"); ``` `ForUpdate()` locks the rows a query selects until the end of the transaction, to prevent lost updates on balances, counters and stock levels, and `ForUpdate(skipLocked: true)` lets multiple workers take different items from the same queue table. It uses `FOR UPDATE [SKIP LOCKED]` on PostgreSQL, MySQL 8+, MariaDB 10.6+ and Oracle, `WITH (UPDLOCK, ROWLOCK[, READPAST])` on SQL Server, and is ignored on SQLite so the same code runs in development: ```csharp using var trans = db.OpenTransaction(); var account = db.Single(db.From().Where(x => x.Id == id).ForUpdate()); account.Balance -= amount; // other transactions wait until this one ends db.Update(account); trans.Commit(); ``` `UpdateFrom()` updates rows with values from joined tables in a single statement, without reading them into .NET or writing RDBMS-specific SQL: ```csharp // Apply each UK warehouse's markup to the price of its parts var q = db.From() .Join((p, w) => p.WarehouseId == w.Id) .Where(w => w.Country == "UK"); int updated = db.UpdateFrom((p, w) => new Part { Price = p.Price * w.Markup }, q); ``` ## Get Started These queries are available in **ServiceStack v10.4**. Upgrade your `ServiceStack.OrmLite.*` packages and replace the raw SQL you were maintaining for hierarchies, rankings and paging. See the docs for the complete reference, which has more examples and the database support of each API: - [Recursive Queries](https://docs.servicestack.net/ormlite/recursive-queries) - ancestors, multiple roots, depth, cycles and paging - [Named Sub Queries](https://docs.servicestack.net/ormlite/common-table-expressions) - building queries in steps and combining with recursion - [Window Functions](https://docs.servicestack.net/ormlite/window-functions) - rankings, running totals, moving averages, `TopPerGroup()` and joins - [Union, Intersect & Except](https://docs.servicestack.net/ormlite/set-operations) - paging and ordering combined queries - [Keyset Pagination](https://docs.servicestack.net/ormlite/keyset-pagination) - cursors in APIs and user-supplied sort orders - [Streaming Results](https://docs.servicestack.net/ormlite/streaming) - `SelectLazyAsync()` and `ColumnLazyAsync()` - [Returning Rows](https://docs.servicestack.net/ormlite/returning), [Locking Rows](https://docs.servicestack.net/ormlite/locking) and [Update from Joined Tables](https://docs.servicestack.net/ormlite/update-from) - [v10.4 Release Notes](https://docs.servicestack.net/releases/v10_04) - everything else in this release # Safe SQL by default in OrmLite Source: https://servicestack.net/posts/ormlite-safe-sql :::youtube GGXmyUx5qqY Dynamic SQL Without SQL Injection in .NET: Safe Interpolation & Sorting ::: Typed queries have always been the safe way to use OrmLite. Every value in `db.Select(x => x.Author == author)` is sent as a db parameter, so there's nothing to inject into. SQL injection creeps in where typed queries stop being convenient, and it almost always arrives the same two ways: - **Concatenating a value into raw SQL**, usually with C# string interpolation, because `$"Author = '{author}'"` is so much nicer to write than a parameter object. - **Letting users choose the sort order**, e.g. `?orderBy=-Price`, which is a column name rather than a value, so it can't be a parameter and ends up pasted into `ORDER BY`. **ServiceStack v10.4** gives both a safe API that's as easy to write as the unsafe one, and hardens the defences around everything else: - **`Sql.Fmt()`** keeps writing natural C# interpolated strings, but every interpolated value is sent as a db param - **`OrderBySafe()`** resolves user-supplied field names to properly quoted columns, so only real fields can be used - **Stronger injection protection** for SQL fragments, identifiers, inlined values and schema queries ## Safe interpolated SQL with Sql.Fmt The unsafe version and the safe version differ by a single method call: ```csharp // Unsafe: the value is embedded in the SQL db.Select($"Author = '{request.Author}'"); // Safe: the value is sent as a db param db.Select(Sql.Fmt($"Author = {request.Author}")); ``` `Sql.Fmt()` is a C# interpolated string handler. Rather than building a string, it records the SQL with a placeholder for each interpolated value, and OrmLite generates the SQL and params for your RDBMS: ```csharp var author = request.Author; // user input is never embedded in SQL var books = db.Select(Sql.Fmt($"Author = {author} AND Price < {request.MaxPrice}")); ``` ```sql SELECT "Id", "Title", "Author", "Genre", "Price", "Year", "Available" FROM "Book" WHERE Author = @p0 AND Price < @p1 -- @p0 = 'J.R.R. Tolkien', @p1 = 20 ``` A value that would otherwise be an injection attack is just a value that doesn't match anything: ```csharp var malicious = "x' OR '1'='1"; db.Select(Sql.Fmt($"Author = {malicious}")); // returns no rows ``` Values are converted the same way as in typed queries, so enums, dates, decimals and bools just work, and collections are expanded into `IN` lists: ```csharp var genres = new[] { Genre.Fantasy, Genre.Science }; var books = db.Select(Sql.Fmt($"Genre IN ({genres}) AND Year >= {since}")); ``` ```sql SELECT "Id", "Title", "Author", "Genre", "Price", "Year", "Available" FROM "Book" WHERE Genre IN (@v0,@v1) AND Year >= @p1 -- @v0 = 'Fantasy', @v1 = 'Science', @p1 = 1960 ``` Empty collections match nothing, rather than generating invalid SQL. ### Works with every raw SQL API `Sql.Fmt()` is accepted anywhere OrmLite takes a SQL string, sync or async: `SqlList`, `SqlColumn` and `SqlScalar`, the shorthand `Select`, `Single`, `Exists`, `Column` and `Scalar` APIs, `ExecuteSql`, `Delete`, the collection APIs `ColumnDistinct`, `Lookup`, `Dictionary` and `KeyValuePairs`, `RowCount` and the lazy streaming APIs: ```csharp var genres = new[] { Genre.Fiction, Genre.Science }; Dictionary> titlesByAuthor = db.Lookup( Sql.Fmt($"SELECT Author, Title FROM Book WHERE Genre IN ({genres})")); db.Delete(Sql.Fmt($"Author = {author} AND Year < {1950}")); ``` It's an explicit opt-in, so existing `string` APIs keep their current behaviour, and passing an interpolated string directly to one still concatenates the value. ### Table and column references Values are the only thing that can be a parameter. Table and column names can't, and are the other thing people end up concatenating. `Sql.Fmt()` embeds them as names quoted by the RDBMS dialect, including their schema and naming convention. Naming the references after their table and column keeps the SQL readable, and makes it clear which parts of the SQL are names and which are params: ```csharp var Book = db.TableRef(); var (Title, Author, Price) = db.ColumnRefs(x => new { x.Title, x.Author, x.Price }); var titles = db.SqlColumn(Sql.Fmt($"SELECT {Title} FROM {Book} WHERE {Author}={author}")); db.ExecuteSql(Sql.Fmt($"UPDATE {Book} SET {Price} = {Price} * {0.9m} WHERE {Author} = {author}")); ``` ```sql SELECT "Title" FROM "Book" WHERE "Author"=@p0 -- @p0 = 'J.R.R. Tolkien' UPDATE "Book" SET "Price" = "Price" * @p0 WHERE "Author" = @p1 -- @p0 = 0.9, @p1 = 'J.R.R. Tolkien' ``` The quoted names also make the SQL portable, as they use the table's `[Alias]` and the RDBMS naming convention, e.g. PostgreSQL's convention stores the `BookReview` table as `book_review` and its `BookId` column as `book_id`. A table can also be referenced by its type, and joins can qualify columns with their table using `prefixTable: true`: ```csharp var (Book, BookReview) = db.TableRefs(); var (Id, Title) = db.ColumnRefs(x => new { x.Id, x.Title }, prefixTable:true); var (BookId, Rating) = db.ColumnRefs(x => new { x.BookId, x.Rating }, prefixTable:true); var reviewed = db.SqlColumn(Sql.Fmt( $"SELECT DISTINCT {Title} FROM {Book} JOIN {BookReview} ON {Id}={BookId} WHERE {Rating}>={4}")); ``` ```sql SELECT DISTINCT "Book"."Title" FROM "Book" JOIN "BookReview" ON "Book"."Id"="BookReview"."BookId" WHERE "BookReview"."Rating">=@p0 -- @p0 = 4 ``` Anything else that really is trusted SQL, like a sort direction you chose in code, can be embedded verbatim with `Sql.Raw()`. It's a signal that the value is SQL rather than data, so it's never used with user input. ### Mix it with typed queries `Sql.Fmt()` also works inside typed `SqlExpression` queries through `Where`, `And`, `Or` and `Having`, which is useful for conditions that are hard to express in C#, like aggregates in a `HAVING` clause: ```csharp var Year = db.ColumnRef(x => x.Year); var q = db.From() .Where(x => x.Available) .And(Sql.Fmt($"{Year} >= {minYear}")); // Genres with at least 2 books, one published since 1980 var byGenre = db.From() .GroupBy(x => x.Genre) .Having(Sql.Fmt($"COUNT(*) >= {2} AND MAX({Year}) >= {1980}")) .Select(x => x.Genre); ``` ```sql -- q SELECT "Id", "Title", "Author", "Genre", "Price", "Year", "Available" FROM "Book" WHERE "Available"=1 AND "Year" >= @0 -- @0 = 1970 -- byGenre SELECT "Genre" FROM "Book" GROUP BY "Genre" HAVING COUNT(*) >= @0 AND MAX("Year") >= @1 -- @0 = 2, @1 = 1980 ``` Format specifiers like `{date:yyyy-MM-dd}` are rejected with a `FormatException`, since params are sent as typed values rather than text. Pass the typed value, or format it before interpolating it. ## Let users choose the sort order with OrderBySafe A sort order is a column name, so it can't be a parameter, and it's the most common way user input ends up in SQL. Passing `?orderBy=-Price` straight to `OrderBy(string)` is only ever as safe as the validation behind it. `OrderBySafe()` never embeds its input at all. It resolves field names to properly quoted columns, so only real fields can ever be used: ```csharp // ?orderBy=-Price,Title&skip=20&take=10 var q = db.From() .OrderBySafe(request.OrderBy, [nameof(Book.Title), nameof(Book.Price), nameof(Book.Year)]) .Skip(request.Skip).Take(request.Take); ``` `orderBy` is a comma-delimited list of field names: | orderBy | SQL | |-------------------------|-----------------------------------| | `Price` | `ORDER BY "Price"` | | `-Price` | `ORDER BY "Price" DESC` | | `Price DESC` | `ORDER BY "Price" DESC` | | `price asc, title` | `ORDER BY "Price", "Title"` | | `-Year,Title` | `ORDER BY "Year" DESC, "Title"` | Fields can be prefixed with `-` or suffixed with `DESC` to sort descending, names are case-insensitive and are resolved to the RDBMS column name, e.g. `in_stock` for `InStock` on PostgreSQL. ### Only the fields you allow The list of fields users can sort by does more than prevent injection. Sorting by a field reveals information about its values, even if the field itself is never returned, so an unrestricted sort lets a client probe the contents of fields it can't see, like a `PasswordHash` or `ApiKey`, by watching how the results are ordered: ```csharp static readonly string[] SortableFields = [nameof(Book.Title), nameof(Book.Price), nameof(Book.Year)]; db.From().OrderBySafe("-Year", SortableFields); // OK db.From().OrderBySafe("Author", SortableFields); // throws ArgumentException ``` Limiting sorting to fields with an index also keeps sorting large tables fast. `OrderBySafe(orderBy)` without a list allows any field on the queried tables, including joined tables, so only use it on tables without sensitive fields. ### Everything else is rejected Input is only ever treated as field names and sort directions. Anything else throws an `ArgumentException`, which ServiceStack returns to API clients as a `400 Bad Request`: ```csharp q.OrderBySafe("Unknown"); // not a field q.OrderBySafe("Id--"); // not a field q.OrderBySafe("Id;DROP TABLE Book"); // not a field q.OrderBySafe("(SELECT 1)"); // not a field q.OrderBySafe("Id SIDEWAYS"); // invalid direction ``` An empty `orderBy` leaves the query's existing order unchanged, so it's easy to keep a default sort: ```csharp var q = db.From() .Join((b, r) => b.Id == r.BookId) .OrderBy(x => x.Title) // default order .OrderBySafe(request.OrderBy); // e.g. "-Rating, Reviewer" ``` It also works with the new [keyset pagination](https://docs.servicestack.net/ormlite/keyset-pagination), so a user-selected sort order stays fast on large tables: ```csharp // ?orderBy=-Price,Id var q = db.From().OrderBySafe(request.OrderBy, [nameof(Book.Price), nameof(Book.Id)]) .SeekAfter(lastRow).Take(50); ``` ```sql SELECT "Id", "Title", "Author", "Genre", "Price", "Year", "Available" FROM "Book" WHERE (("Price" <= @0) AND (("Price" < @0) OR ("Price" = @0 AND "Id" > @1))) ORDER BY "Price" DESC, "Id" LIMIT 50 -- @0 = 12.99, @1 = 42 ``` [AutoQuery](https://docs.servicestack.net/autoquery/rdbms) APIs already support validated `?orderBy=` sorting out of the box, so `OrderBySafe()` is for the APIs you write yourself. ## Stronger SQL injection protection Some APIs accept SQL fragments by design, like `Where(string)`, `OrderBy(string)` and `Select(string)`. OrmLite validates those fragments and throws an `ArgumentException` if one looks like SQL injection. Validation is a safety net rather than a substitute for params, but it's the last line of defence for apps that pass fragments around, so this release closes the ways it could be bypassed: - **Tighter fragment validation**: comments (`--`, `/* */`) and statement separators (`;`) are now rejected anywhere in a fragment, closing several ways to bypass the validation. Values quoted with MySQL-style backslash escapes can no longer disguise SQL either, as quoted strings are checked as both ANSI SQL and MySQL would parse them. - **Safer identifiers on every database**: table, column and schema names containing quote characters are now always escaped, including on Oracle and Firebird, which previously left most names unquoted. - **Escaped date formats and currency symbols**: formats in `x.Created.ToString(format)` and the SQL date format and currency helpers are now always escaped. Previously a runtime value could inject SQL on SQLite and MySQL. - **Safer schema queries**: the catalog queries that check whether tables and schemas exist, and that reset sequences, now escape the names they use on PostgreSQL, Oracle and Firebird, as do Firebird's schema lookups like `GetTable()` and `GetColumns()`. Savepoint names are now validated. - **Escaped spatial values on SQL Server**: `SqlGeography`, `SqlGeometry` and `SqlHierarchyId` values inlined in SQL now have embedded quotes escaped. - **`Contains()` values are always db params**: the value of `x.Tags.Contains(value)` on a list or array property was previously written into the SQL as it was, so a value from a user could change the query. It's now a param of a JSON query, and throws where the property can't be queried. Some fragments that were accepted before are now rejected, e.g. `x--y` or a `;` in the middle of a fragment. If you hit one, pass values as parameters or use `Sql.Fmt()`. For trusted SQL you wrote yourself, the `Unsafe*` APIs skip validation: ```csharp q.UnsafeWhere("Id IN (SELECT BookId FROM BookReview WHERE Rating >= {0})", 4); // {0} is sent as a param q.UnsafeOrderBy("CASE WHEN Available THEN 0 ELSE 1 END, Title"); ``` ## Choose the safe API for each job | You need to... | Use | |-------------------------------------------------|---------------------------------------------------------| | Filter by user input | Typed `Where()`, or params with raw SQL | | Write raw SQL with interpolated values | `Sql.Fmt()` | | Let users choose the sort order | `OrderBySafe()` | | Reference a table name in raw SQL | `db.TableRef()` or `typeof(T)` in `Sql.Fmt()` | | Reference a column name in raw SQL | `db.ColumnRef()`, `db.ColumnRefs()` in `Sql.Fmt()` | | Embed trusted SQL that fails validation | `Unsafe*` APIs, e.g. `q.UnsafeWhere()` | ## Get Started `Sql.Fmt()`, `OrderBySafe()` and the stronger injection protection are available in **ServiceStack v10.4**, on every database OrmLite supports. Upgrade your `ServiceStack.OrmLite.*` packages and replace the interpolated strings you pass to raw SQL APIs with `Sql.Fmt()`. See the docs for the complete reference: - [Interpolated SQL](https://docs.servicestack.net/ormlite/sql-fmt) - every supported API, table and column references, and inspecting the generated SQL - [Dynamic Sorting](https://docs.servicestack.net/ormlite/order-by-safe) - syntax, allowed fields, defaults and paging - [SQL Injection Protection](https://docs.servicestack.net/ormlite/sql-injection) - fragment validation, identifier quoting, `Unsafe*` APIs and how to customize validation - [v10.4 Release Notes](https://docs.servicestack.net/releases/v10_04) - everything else in this release # Keep your database in sync with your C# models Source: https://servicestack.net/posts/ormlite-schema-diff :::youtube chmFo1TE-Wc Keep Your Database in Sync With Your C# Models: Schema Diff for .NET ::: In a code-first App your C# classes are your schema. You add two properties to an `Invoice`, build and deploy: ```csharp public class Invoice { [AutoIncrement] public int Id { get; set; } [StringLength(200)] public string Reference { get; set; } [Index] public int CustomerId { get; set; } public decimal Total { get; set; } public DateTime? PaidDate { get; set; } // new public string Currency { get; set; } // new } db.CreateTableIfNotExists(); // the table exists, so it's unchanged var unpaid = db.Select(x => x.PaidDate == null); ``` But the table was created months ago, and `CreateTableIfNotExists` only creates tables that don't exist. Nothing tells you the table didn't change with its model, until the first query that uses the new columns fails: ``` SqlException: Invalid column name 'PaidDate'. Invalid column name 'PaidDate'. Invalid column name 'Currency'. ``` When that happens in production, it's the first you hear of it. **ServiceStack v10.4** adds **Schema Diff** to OrmLite: one call that compares your models with their tables, tells you everything that's different, and writes the migration that fixes it for you to review. ## One call, every difference ```csharp var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer)); if (diff.HasChanges) log.LogWarning("The database doesn't match its models:\n{Diff}", diff); ``` ``` Invoice ~ Reference VARCHAR(50) NOT NULL -> VARCHAR(200) NULL + PaidDate DATETIME NULL + Currency VARCHAR(8000) NULL (renamed from LegacyCode?) - LegacyCode VARCHAR(8000) NULL (not in Invoice) (renamed to Currency?) + index idx_invoice_customerid Customer + table isn't in the database ``` `+` is in your model and not the database, `-` is in the database and not your model, and `~` is in both, but different. The two new columns, a `Reference` column that's now larger and allows nulls, a column the model no longer has, a missing index and a missing table, all in one readable report. It doesn't guess what types your C# maps to. It creates the model's columns in a temporary table and compares them as your database reports them, so a table created from its model never has false differences, whichever [type converters](https://docs.servicestack.net/ormlite/type-converters) or naming strategy you use. Common equivalent types in tables that weren't created by OrmLite, e.g. by Entity Framework, like `TEXT` vs `VARCHAR(n)` or `INTEGER` vs `BIGINT`, aren't reported either. ## More than columns Your models already describe far more of your schema than their columns, and this release lets them describe even more, with attributes for filtered and covering indexes, enum checks and generated columns, instead of SQL you write by hand: ```csharp [CompositeIndex(nameof(TenantId), nameof(Status), Include = [nameof(Name), nameof(Size)])] public class Upload { [AutoIncrement] public int Id { get; set; } public int TenantId { get; set; } [Index(Unique = true, Where = "{DeletedDate} IS NULL")] // unique among rows that aren't deleted public string Name { get; set; } [CheckEnum] // only allow UploadStatus values public UploadStatus Status { get; set; } public long Size { get; set; } [Compute("{Size} / 1024"), Persisted] // calculated by the database public long SizeKb { get; set; } public DateTime? DeletedDate { get; set; } } ``` Because it's all in your model, Schema Diff compares all of it. Change a `PurchaseOrder`'s default, make its foreign key cascade deletes, restrict its `Status` to its enum's values and add a filtered index: ```csharp public class PurchaseOrder { [AutoIncrement] public int Id { get; set; } [ForeignKey(typeof(Supplier), OnDelete = "CASCADE")] // was no action public int SupplierId { get; set; } [CheckEnum] // was any string public OrderStatus Status { get; set; } [Default(5)] // was Default(1) public int Quantity { get; set; } public DateTime CreatedDate { get; set; } [Index(Where = "{ShippedDate} IS NULL")] // the unshipped orders public DateTime? ShippedDate { get; set; } } ``` And each one is found: ``` PurchaseOrder ~ Quantity default DEFAULT ((1)) -> DEFAULT ((5)) ~ foreign key FK_PurchaseOrder_Supplier_SupplierId (SupplierId) REFERENCES Supplier (Id) -> (SupplierId) REFERENCES Supplier (Id) ON DELETE CASCADE + constraint CHK__PurchaseOrder_Status CHECK ("Status" IN ('Pending','Shipped','Delivered')) + index idx_purchaseorder_shippeddate ``` Schema Diff compares: - **Tables and columns**: missing tables, missing and extra columns, and columns of another type, size or nullability - **Defaults**: a `[Default]` that changed, or a default the property doesn't have - **Indexes**: by their columns, order, uniqueness, `Include` columns and `Where` condition - **Foreign keys**: the table they reference and their `OnDelete` and `OnUpdate` actions - **Constraints**: `[Unique]`, `[UniqueConstraint]`, `[CheckConstraint]` and `[CheckEnum]` - **Primary keys** with other columns, and **full-text indexes** of [`[FullTextIndex]`](https://docs.servicestack.net/ormlite/full-text-search) models Databases rewrite conditions, e.g. PostgreSQL writes an `IN` list as `= ANY (ARRAY[...])`, so index conditions, check constraints and defaults are compared the way your database writes them, not as false differences. ## Built not to lose your data A diff that's quick to drop the wrong thing is worse than no diff at all, so Schema Diff errs on the side of keeping your data. **Likely renames are suggested, never assumed.** A column that's no longer in the model and a new column of the same type are probably the same column renamed, so the diff says so: `Currency (renamed from LegacyCode?)`. It's only a guess, so it's never renamed for you. You decide. **Destructive changes are marked.** Every change that can lose data or fail with the rows already in a table, like dropping a column, shrinking one, making an index unique or adding a check constraint, has `IsDestructive` set, and isn't made unless you allow it. **Hand-added indexes are kept.** Someone added an index to the database to speed up a slow query? It's never dropped, even when destructive changes are allowed. Instead the diff tells you the exact attribute to add, so your model has it too: ```csharp // CREATE INDEX ix_purchaseorder_status_created ON "PurchaseOrder" ("Status", "CreatedDate") var diff = db.GetSchemaDiff(); db.ApplySchemaDiff(diff, allowDestructive: true); // Makes 0 changes: the index is kept ``` ``` PurchaseOrder ! index ix_purchaseorder_status_created (Status, CreatedDate) (not in PurchaseOrder, kept: add [CompositeIndex("Status", "CreatedDate", Name = "ix_purchaseorder_status_created")] to PurchaseOrder) ``` ## The migration, written for you to review Knowing what's different is half the job. `ToMigration()` writes the other half, as an OrmLite [DB Migration](https://docs.servicestack.net/ormlite/db-migrations): ```csharp File.WriteAllText("Migrations/Migration1005.cs", diff.ToMigration("Migration1005", "MyApp.Migrations")); ``` ```csharp #warning Generated by Schema Diff: review this migration before running it, then delete this line public class Migration1005 : MigrationBase { public class Invoice { [AutoIncrement] public int Id { get; set; } [StringLength(200)] public string Reference { get; set; } public DateTime? PaidDate { get; set; } public string Currency { get; set; } } public override void Up() { // Reference is VARCHAR(50) NOT NULL Db.AlterColumn(x => x.Reference); Db.AddColumn(x => x.PaidDate); // Currency is likely LegacyCode renamed. If it is, rename it instead of adding it: // Db.RenameColumn(x => x.Currency, "LegacyCode"); Db.AddColumn(x => x.Currency); // LegacyCode (VARCHAR(8000) NULL) isn't in Invoice. If it was renamed, rename it instead: // Db.RenameColumn("LegacyCode", "Currency"); // Db.DropColumn("LegacyCode"); Db.ExecuteSql("CREATE INDEX idx_invoice_customerid ON \"Invoice\" (\"CustomerId\");"); } public override void Down() { /* ... */ } } ``` The migration has its own copy of the model, with only the properties that change, so it keeps working after your model changes again. Look at the likely rename: the migration adds `Currency`, and leaves dropping `LegacyCode` as commented-out code with the rename to use instead, so no data is dropped unless you decide it should be. :::warning A generated migration is a guide, not a finished migration. Schema Diff is new, so every migration it writes starts with a `#warning` that's reported each time your App is built, until you've reviewed it and deleted it. ::: ## From your browser, or the command line If you use ServiceStack, the [Database Admin UI](https://docs.servicestack.net/admin-ui-database#schema-diff) has a **Schema Diff** for each database. It compares the data models of your AutoQuery APIs and the tables your migrations create, shows each difference with the SQL that fixes it and marks the destructive ones. Comparing doesn't change your tables: With the migration class next to them, ready to copy into your App: Or have the `migrate.new` [App Task](https://docs.servicestack.net/app-tasks) write it straight into your App's `Migrations` folder, named after your last migration: :::sh dotnet run --AppTasks=migrate.new ::: It doesn't write anything when your database already matches its models, and never replaces a migration that's already there. ## Find out when your App starts, not in production Better than finding out when a query fails is finding out before it can. Turn on `LogSchemaDiff` in development and the differences of each database are logged in the background every time your App starts: ```csharp services.AddPlugin(new AdminDatabaseFeature { LogSchemaDiff = context.HostingEnvironment.IsDevelopment(), }); ``` ``` Schema Diff: the tables of the main database aren't the same as their models Invoice ~ Reference VARCHAR(50) NOT NULL -> VARCHAR(200) NULL + PaidDate DATETIME NULL + Currency VARCHAR(8000) NULL (renamed from LegacyCode?) - LegacyCode VARCHAR(8000) NULL (not in Invoice) (renamed to Currency?) + index idx_invoice_customerid Ignored: AspNetUsers ``` Or make it a test, so a model that changed without its migration fails the build before it's ever deployed: ```csharp [Test] public void Database_matches_its_models() { using var db = dbFactory.OpenDbConnection(); var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer)); Assert.That(diff.HasChanges, Is.False, diff.ToString()); } ``` Tables OrmLite doesn't manage, like ASP.NET Core Identity's `AspNet*` tables and EF Core's `__EFMigrationsHistory`, are ignored by default, and you can ignore others by name, wildcard or model: ```csharp OrmLiteConfig.SchemaDiff.IgnoreTables.Add("Legacy*"); OrmLiteConfig.SchemaDiff.IgnoreTypes.Add(typeof(AuditArchive)); ``` ## Keep dev databases in sync While you're developing, `ApplySchemaDiff()` makes the changes for you. By default only safe ones, like creating tables, adding columns and indexes, making columns larger or allowing nulls, are applied: ```csharp var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer)); var applied = db.ApplySchemaDiff(diff); // safe changes only db.ApplySchemaDiff(diff, allowDestructive: true); // also drop and alter columns that can lose data ``` It's for development and test databases. The databases you deploy to should get reviewed migrations that are run once and recorded. ## SQLite, rebuilt SQLite can't alter a column, its default, foreign keys or constraints. The only way to change them is to create the table again and copy its rows. So on SQLite those changes are made by rebuilding the table, and the diff says so: ``` PurchaseOrder ~ Quantity default DEFAULT 1 -> DEFAULT 5 (by rebuilding the table) ~ foreign key FK_PurchaseOrder_Supplier_SupplierId (SupplierId) REFERENCES Supplier (Id) -> (SupplierId) REFERENCES Supplier (Id) ON DELETE CASCADE (by rebuilding the table) + constraint CHK__PurchaseOrder_Status CHECK ("Status" IN ('Pending','Shipped','Delivered')) (by rebuilding the table) + index idx_purchaseorder_shippeddate ~ rebuild table to change the default of Quantity, foreign key FK_PurchaseOrder_Supplier_SupplierId, constraint CHK__PurchaseOrder_Status ``` The new `RebuildTable()` API does it from your model, and is also available to your own migrations: ```csharp db.RebuildTable(); ``` It runs in a transaction, keeping the table's rows, its indexes (including ones added by hand), its triggers and its next `AUTOINCREMENT` id. If a row breaks the new table, e.g. it fails a new check constraint, the table is left as it was. ## Know the limits Schema Diff is built to tell you the truth about your database, so here's the truth about Schema Diff: - **Generated migrations are a guide to review**, not finished migrations you can trust without reading - **`ApplySchemaDiff()` is for development and test databases**, and applies changes one at a time, not in a transaction - **A primary key with other columns is only reported**, as changing it needs its foreign keys and data migrated too - **Not compared**: the order of columns, the expressions of generated columns and comments - **Temporary tables**: your connection's user needs to be able to create them, e.g. MySQL's `CREATE TEMPORARY TABLES` privilege, otherwise column types aren't compared and `diff.Warnings` says why Within those limits, your models stay the source of truth, and you find out the moment your database drifts from them. ## Get Started Schema Diff is available in **ServiceStack v10.4** on **SQLite, PostgreSQL, SQL Server, MySQL** and **MariaDB**. Upgrade your `ServiceStack.OrmLite.*` packages, then either open the Schema Diff in the Database Admin UI, turn on `LogSchemaDiff`, or call `db.GetSchemaDiff()` with your models to see how far your database has drifted. See the docs for the complete reference: - [Schema Diff](https://docs.servicestack.net/ormlite/schema-diff) - everything that's compared, the changes, ignoring tables, generated migrations and rebuilding SQLite tables - [Schema Attributes](https://docs.servicestack.net/ormlite/ddl-attributes) - filtered and covering indexes, enum checks, generated columns and comments declared on your models - [DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) - writing, running and reverting OrmLite migrations - [Database Admin UI](https://docs.servicestack.net/admin-ui-database) - browse, query and compare your databases - [v10.4 Release Notes](https://docs.servicestack.net/releases/v10_04) - everything else in this release # Prompt it, Publish it - AI-built works from your .NET APIs Source: https://servicestack.net/posts/ai-chat-projects Your customer, revenue and usage data already lives inside your .NET App, yet answering a simple business question still means opening several screens, exporting CSVs and assembling the story somewhere else. The common shortcut is pasting that data into an external ChatGPT window, which knows nothing about your App, can't fetch anything new and now has a copy of data that was never meant to leave it. :::youtube -BfooJPT3-I Prompt It, Publish It: Create AI Works directly from your .NET APIs ::: [AI Chat](https://docs.servicestack.net/chat/) takes the opposite approach. It's a ChatGPT-like assistant that runs **inside** your App at `/chat`, with your App's users, identity and permissions. With [API Tools](/posts/api-tools) it can call the APIs you've opted in, as the signed-in user, and with **Projects** it has a workspace to turn the results into something more useful than a paragraph of chat. | | What you get | |-|-| | **Same security boundary** | The assistant runs in your App, as the signed-in user, so it can only see what they could | | **No parallel backend** | Your existing Request DTOs are the AI contract, with no AI-specific gateway or schema to keep in sync | | **Always current data** | It discovers and calls your live APIs instead of working from a stale export | | **Humans approve changes** | Reads run immediately, writes pause for an editable approval form | | **Artifacts, not answers** | Generated pages are saved to a project workspace where they can be previewed and refined | | **Explicit publishing** | Nothing leaves the workspace until a user chooses to publish it | To show what that looks like in practice, the video follows two prompts in the [Next SaaS](/react/#next-saas) template, using its seeded example data. ## Your APIs are the assistant's tools There's no new AI-specific layer to build. The APIs the assistant uses are the same Request DTOs your App's UI already calls, annotated with a `[Tool]` attribute that tells it **when** to use them: ```csharp [Tool("the user asks which customers are at risk, healthy, churning, inactive, likely to upgrade or need attention, or how a customer is doing", Safety = ToolSafety.ReadOnly, Keywords = ["health", "at risk", "churn risk", "attention", "inactive", "upsell", "upgrade", "expansion", "engagement"], FollowUps = ["GetSaasCustomer"], Examples = ["""{"grade":"AtRisk","take":10}"""])] [ValidateIsAuthenticated] [Route("/saas/admin/customer-health", "GET")] public class GetCustomerHealth : IGet, IReturn { [Description("Only this customer")] public string? WorkspaceId { get; set; } [Description("Only customers with this grade")] public CustomerHealthGrade? Grade { get; set; } [Description("Include customers whose subscription was canceled")] public bool IncludeChurned { get; set; } [Description("The most customers to return, from 1 to 200")] public int Take { get; set; } = 25; } ``` Or you can opt in whole groups of APIs by tag when registering `ChatFeature`: ```csharp services.AddPlugin(new ChatFeature { // Require authentication to access /chat RequireAuth = true, ApiTools = { IncludeTags = [ ApiTags.Organizations, ApiTags.Team, ApiTags.Billing, ApiTags.Usage, ApiTags.Documents, ApiTags.ApiKeys, ApiTags.Notifications, ApiTags.Audit, ApiTags.Platform, ], // Metering is for the product's own API clients, not for an assistant to consume quota ExcludeTypes = [nameof(RecordUsage)], }, }); ``` Opting an API in doesn't bypass anything. Every call goes through the normal ServiceStack request pipeline, so the same validation, `[ValidateIsAuthenticated]` and `[ValidateHasRole]` attributes, and the capability checks inside your Services, decide what the assistant gets back: ```csharp public async Task Any(GetRevenueMetrics request) { await RequirePlatformAsync(PlatformCapability.ManageBilling); return SaasInsights.GetRevenueMetrics(PlatformDb, config.DefaultCurrency, Math.Clamp(request.Months, 1, 12), DateTime.UtcNow); } ``` If the signed-in user can't manage billing, neither can their assistant. ## Customer Galaxy: make the portfolio explorable The first prompt asks, in business terms, for an interactive 3D view of the SaaS business built from two APIs, saved as a single self-contained `index.html` in the project: The assistant hasn't memorized your database. It uses `api_search` to find the relevant APIs, `api_describe` to learn their schemas, then `api_call` to fetch the data, and in this case reports back that it fetched 51 customers, including 5 churned accounts, and 12 months of revenue history: The result is **Customer Galaxy**: each customer is a planet, its orbit set by subscription plan and its color by the health grade `GetCustomerHealth` returned. The center shows total monthly recurring revenue (MRR), the side panels break down the health spectrum and rank the revenue at risk, and a timeline replays revenue over the last 12 months: A list of metrics has become a view an operator can scan in seconds to see where to look next. Selecting an at-risk planet brings up the API's own signals for that account, e.g. **Ironbark Builders** has a past-due payment and two failed payments in the last 90 days: ### Writes need a human Spotting that signal naturally leads to a follow-up, like asking the assistant why the account is at risk, or to draft a support note. This is where the approval boundary matters. Read-only APIs run immediately, but any API classified as a write or destructive operation pauses for an editable approval form, so the assistant **proposes** the change and a person decides whether it happens. No change reaches your system of record without that approval. ## Monthly Business Review: same context, different artifact The same App context can answer a completely different question for a different audience. The second prompt asks for this month's leadership review, researched from four APIs and saved as a 16:9, ten-slide HTML deck with its data embedded and the date it was prepared: The assistant calls `GetRevenueMetrics`, `GetCustomerHealth` and `GetSaasAnalytics`, then `GetSaasCustomer` for every account graded **AtRisk** or **Watch** to pull in their support notes and recent activity. The result is a deck covering the executive summary, MRR trend and movement, revenue mix, customer health, accounts needing attention, growth opportunities, operations and recommendations: The most useful slide is the retention worklist, where health signals from one API sit next to support notes and activity from another, with a recommended action for each account. That's the kind of cross-referencing that normally takes someone an afternoon of switching between screens: The final slide turns those observations into prioritized next steps, each with an owner to assign: ### A draft to check, not an oracle Notice what the report says about itself: its MRR figures are estimates that exclude discounts, taxes and negotiated Enterprise pricing, a P0 recommendation is to reconcile a data quality mismatch it found, and it states that no actions have been executed. That's the right role for an in-app assistant. It collapses the time spent gathering and assembling evidence, then leaves people to check the conclusions and make the decisions. ## Preview, then explicitly publish Both artifacts are written to their project's folder, which only the signed-in user's chats can access, and can be previewed in chat while you iterate on them. When it's ready to share, a user chooses **Share → Folder** to publish the project as a static site served by the App: Publishing is always a deliberate step. Working drafts never leak to a public path on their own, and later edits are only published when someone clicks **Update folder**. Once published, the two pages handle their data differently, because they were asked to: - **Customer Galaxy** embeds a snapshot of its data, and on load tries to refresh it from the same-origin APIs using the viewer's cookies. A signed-in viewer with access sees a **Live** badge, anyone else sees the snapshot - **Monthly Business Review** stays a dated snapshot on purpose, so it remains a record of the month it describes. Next month's review is a new prompt and a new publish ### Keep the boundary clear The default publish folder is publicly served, and both pages embed their data in the HTML. That's fine for seeded example data, but real customer data should stay inside authenticated chat or be published through a delivery path with access control. The pages also load Three.js and Chart.js from a CDN, which your Content Security Policy needs to allow in production. ## Start with one read-only API Adding AI to an App doesn't have to mean building parallel infrastructure. AI Chat reuses the APIs, permissions and validation you've already deployed, so the quickest way to evaluate it is to opt in a single read-only API your team asks questions about, and see what they do with it. - [AI Chat overview](https://docs.servicestack.net/chat/overview) - [Install AI Chat](https://docs.servicestack.net/chat/install) - [API Tools](https://docs.servicestack.net/chat/api-tools) - [Integrated Auth](https://docs.servicestack.net/chat/auth) - [Projects](https://docs.servicestack.net/chat/projects) - [Publishing](https://docs.servicestack.net/chat/publishing) To try the same demo yourself, create a new [Next SaaS](https://react-templates.net/#next-saas) App: ```bash npx create-net next-saas ProjectName ``` # Multitenancy that can't be forgotten Source: https://servicestack.net/posts/ormlite-multitenancy Multi-tenant Apps live and die by one rule: every query needs to include the tenant. It only takes one forgotten `Where()`, or one `SingleById()`, for a customer to see another customer's data, and nothing fails when it happens. The query runs, the page renders and the tests pass, as they only ever had one tenant's data to look at. :::youtube 86ff8gs4h3w Multi-Tenant .NET Apps: Enforce Tenant Isolation on the Connection, Not Every Query ::: Code reviews and conventions reduce that risk, but every new query, AutoQuery API and background job adds to it. The [Next SaaS](/posts/next-saas) template we released last week had exactly this pattern throughout, a hand-written tenant condition in every query for customer data: ```csharp var widget = Db.Single(x => x.Id == request.Id && x.WorkspaceId == context.Workspace.Id) ?? throw new HttpError(404, "WidgetNotFound", "The widget was not found."); ``` **ServiceStack v10.4** moves that responsibility from each query to the database connection. OrmLite's new **connection filters** and **write rules** are defined once, then enforced on everything the connection does: | | What you get | |-|-| | **Isolation by default** | Every select, update and delete only reaches the tenant's rows, including lookups by id, joins and AutoQuery | | **Fails closed** | A request that hasn't resolved its tenant throws instead of returning every tenant's rows | | **Less code** | Queries and inserts no longer mention the tenant, and can't get it wrong | | **Audit columns you can trust** | Who changed a row and when is recorded on every write, and can't be set by mistake | | **A reviewable opt-out** | Working across tenants is one explicit call you can search for | | **No rewrite** | Existing Services and AutoQuery APIs don't change. It's one extension method and one AppHost filter | With it, the query above is just the query: ```csharp var widget = Db.SingleById(request.Id) // null if it's another organization's widget ?? throw new HttpError(404, "WidgetNotFound", "The widget was not found."); ``` ## Mark the tables a tenant owns Tenant-owned tables are marked with an interface, so a single rule covers all of them, including tables you add later: ```csharp public interface IHasTenantId { int TenantId { get; } } public class Order : IHasTenantId, IAudit { [AutoIncrement] public int Id { get; set; } public int TenantId { get; set; } public int CustomerId { get; set; } public decimal Total { get; set; } public bool IsDeleted { get; set; } // IAudit public string CreatedBy { get; set; } public DateTime CreatedDate { get; set; } public string ModifiedBy { get; set; } public DateTime ModifiedDate { get; set; } } ``` Every table is then either owned by a tenant, with a required tenant column, or it isn't, like shared lookup tables of plans and countries, or tables the platform runs itself like a webhook inbox. ## Define your rules once Your App's rules are declared once in a `FilterSet`, which reads its values from a scope that each connection provides: ```csharp public record TenantUser(int TenantId, string UserId); public static readonly FilterSet UserRules = FilterSet.Create(f => { // The tenant's rows are the only rows it can read or change, and the rows it writes are the tenant's f.Ensure(x => x.TenantId, s => s.TenantId); // Record who changed a row and when f.OnInsert(x => x.CreatedBy, s => s.UserId); f.OnInsert(x => x.CreatedDate, _ => DateTime.UtcNow); f.OnWrite(x => x.ModifiedBy, s => s.UserId); f.OnWrite(x => x.ModifiedDate, _ => DateTime.UtcNow); }); public static IDbConnection ForUser(this IDbConnection db, int tenantId, string userId) => db.UseFilters(UserRules.For(new TenantUser(tenantId, userId))); ``` The scope is typed, so using a set with the wrong scope doesn't compile. Rules can only read values from the scope, so a set behaves the same for every connection that uses it. There are 3 kinds of rules: | Rule | What it does | |-|-| | `Filter` | Restricts the rows a connection can read, update and delete | | `OnInsert`, `OnUpdate`, `OnWrite` | Set a column on every row a connection writes | | `Ensure` | Both: filters to a value, and writes rows with it | `Ensure` guards both sides. Without the filter, an update could move another tenant's row into the connection's tenant, and without the write rule, an insert could be written for another tenant. ## One filter covers your whole App ServiceStack opens the connections used by `Db` in your Services, AutoQuery, AutoQuery CRUD and the API Keys feature for the current request. The new `DbConnectionRequestFilters` in your AppHost configure every one of them, so your rules are registered in one place: ```csharp public override void Configure() { DbConnectionRequestFilters.Add((db, req) => { var userId = req.GetUserId(); // the signed in user, or the user of an API Key if (userId != null) db.ForUser(GetTenantId(userId), userId); // GetTenantId() is defined by your App }); } ``` A filter can also refuse the request, e.g. by throwing an `HttpError` for a user that can't access the tenant it's for. The connection is disposed for you and the error is returned to the client. That's the whole integration. Your existing Services and AutoQuery APIs don't change: ```csharp public class QueryOrders : QueryDb { } public class OrderServices : Service { public object Get(GetOrder request) => Db.SingleById(request.Id) // null if it's another tenant's order ?? throw HttpError.NotFound("Order not found"); } ``` Return `404` for a row that isn't found rather than `403`, as saying a row exists in another tenant leaks information. ## Isolation that doesn't depend on each query Your code is written as if the tenant's rows were the only rows in the table: ```csharp var order = db.SingleById(1); ``` ```sql SELECT "Id", "TenantId", "CustomerId", "Total", "IsDeleted", "CreatedBy", "CreatedDate", "ModifiedBy", "ModifiedDate" FROM "Order" WHERE ("Order"."TenantId" = @_f0) AND ("Id" = @Id) -- @Id = 1, @_f0 = 1 ``` Filters are mandatory conditions that other conditions can narrow but never widen. An `Or()` is wrapped inside the filter, so a client can't escape it with the conditions it sends to an AutoQuery API: ```csharp var q = db.From().Where(x => x.Total > 100).Or(x => x.CustomerId == 1); var orders = db.Select(q); ``` ```sql SELECT "Id", "TenantId", "CustomerId", "Total", "IsDeleted", "CreatedBy", "CreatedDate", "ModifiedBy", "ModifiedDate" FROM "Order" WHERE ("Order"."TenantId" = @0) AND (("Total" > @1) OR ("CustomerId" = @2)) -- @0 = 1, @1 = 100, @2 = 1 ``` They apply wherever OrmLite creates the SQL: joined tables, sub queries, SQL fragments, referenced rows loaded with `LoadSelect()`, and updates and deletes, so a connection can't change rows it can't see: ```csharp db.UpdateOnly(() => new Order { Total = 200 }, where: x => x.Id == 1); db.DeleteById(99); ``` ```sql UPDATE "Order" SET "Total"=@Total WHERE ("Order"."TenantId" = @0) AND (("Id" = @1)) -- @0 = 1, @1 = 1, @Total = 200 DELETE FROM "Order" WHERE "Id" = @0 AND ("Order"."TenantId" = @_f0) -- @0 = 99, @_f0 = 1 ``` Another tenant's rows are treated as if they don't exist: updates and deletes affect 0 rows, and `Save` and `Upsert` try to insert them, which fails on the primary key instead of overwriting the row. ### Writes are protected too Inserts get the connection's tenant, and writing a row for another tenant throws instead of quietly succeeding, as it's a bug in your App: ```csharp db.Insert(new Order { CustomerId = 1, Total = 100 }); // inserted with the connection's TenantId // InvalidOperationException: Order.TenantId must be '1' on this connection db.Insert(new Order { TenantId = 2, CustomerId = 1, Total = 100 }); // Rows can't be moved to another tenant either db.UpdateOnly(() => new Order { TenantId = 2 }, where: x => x.Id == 1); ``` ### Faster than writing the condition yourself Each filter's SQL is generated once and reused by every connection, which only adds the scope's values as db params, so a filtered query is quicker to create than one with the same condition written in it. Conditions that only read the scope are decided before the SQL is generated, so each case gets its own SQL. With this filter an admin's queries don't have a tenant condition at all, and everyone else's only have `"TenantId" = @0`: ```csharp public static readonly FilterSet AccessFilters = FilterSet.Create(f => f.Filter((x, s) => s.IsAdmin || x.TenantId == s.TenantId)); ``` ## Fails closed Confining a connection when it's opened needs the tenant to be known then. Often it isn't. Working out which tenant a request is for usually needs the database, e.g. to look up the user's membership, and a connection that's left unconfined until then is one where a forgotten check returns every tenant's rows. Rules read their scope for each statement, so a connection can refuse to touch tenant data until the request's tenant is resolved: ```csharp public class TenantScope { public int? TenantId { get; set; } public int AssertTenantId() => TenantId ?? throw new InvalidOperationException("Resolve the request's tenant before using tables owned by a tenant."); } public static readonly FilterSet TenantFilters = FilterSet.Create(f => f.Ensure(x => x.TenantId, s => s.AssertTenantId())); public static IDbConnection ForTenant(this IDbConnection db, TenantScope scope) => db.UseFilters(TenantFilters.For(scope)); // Confine a connection to a tenant that's already known, e.g. in a background job public static IDbConnection ForTenant(this IDbConnection db, int tenantId) => db.ForTenant(new TenantScope { TenantId = tenantId }); ``` | When | Tenant-owned tables | |-|-| | Before the tenant is resolved | Any query or write throws | | After it's resolved | Confined to the tenant | Keeping the scope in `IRequest.Items` means every connection the request opens shares it, including the one AutoQuery opens: ```csharp public static TenantScope GetTenantScope(this IRequest req) { if (req.Items.TryGetValue(nameof(TenantScope), out var existing) && existing is TenantScope scope) return scope; req.Items[nameof(TenantScope)] = scope = new TenantScope(); return scope; } ``` When users can belong to more than one tenant, APIs can say which tenant they're for in their Request DTO, so users can work in different tenants in different browser tabs. The first connection the request opens checks the user is a member before resolving the scope: ```csharp public interface IRequireTenant { int TenantId { get; set; } } public class QueryOrders : QueryDb, IRequireTenant { public int TenantId { get; set; } } DbConnectionRequestFilters.Add((db, req) => { var scope = req.GetTenantScope(); db.ForTenant(scope); if (req.Dto is IRequireTenant requireTenant && scope.TenantId == null) { // Whether the user is a member isn't known yet, so it's looked up across tenants var userId = req.GetUserId(); if (!db.WithoutFilters().Exists(x => x.TenantId == requireTenant.TenantId && x.UserId == userId)) throw HttpError.Forbidden("You do not have access to this tenant"); scope.TenantId = requireTenant.TenantId; } }); ``` An API that uses a tenant-owned table and forgets `IRequireTenant` then fails on its first request in development, instead of returning another tenant's data in production. ## Audit columns you can trust Write rules set columns on every row a connection writes. They replace any value from your App, and add themselves to updates of only some columns: ```csharp db.UpdateOnly(() => new Order { Total = 120 }, where: x => x.Id == 1); ``` ```sql UPDATE "Order" SET "Total"=@Total, "ModifiedBy"=@ModifiedBy, "ModifiedDate"=@ModifiedDate WHERE ("Order"."TenantId" = @0) AND (("Id" = @1)) -- @0 = 1, @1 = 1, @Total = 120, @ModifiedBy = 'alice', @ModifiedDate = '2026-10-01 09:30:00' ``` They apply to every insert and update API, including `BulkInsert`, `UpdateFrom` and `Upsert`, so there's no write path where the housekeeping is forgotten. The objects you write are left with the values that were saved, so a new row can be returned from your API without reading it back: ```csharp var order = new Order { CustomerId = 1, Total = 100 }; db.Insert(order); order.TenantId; // 1 order.CreatedBy; // alice ``` ## Soft deletes, and anything else every query needs The same APIs enforce any condition that every query needs. A `Filter` restricts the rows a connection sees without changing what it writes, and combines with your other filters: ```csharp public static readonly FilterSet SoftDeletes = FilterSet.Create(f => f.Filter(x => !x.IsDeleted)); db.ForUser(tenantId, userId).UseFilters(SoftDeletes); var orders = db.Select(x => x.Total > 100); ``` ```sql SELECT "Id", "TenantId", "CustomerId", "Total", "IsDeleted", "CreatedBy", "CreatedDate", "ModifiedBy", "ModifiedDate" FROM "Order" WHERE "Order"."IsDeleted"=0 AND ("Order"."TenantId" = @0) AND (("Total" > @1)) -- @0 = 1, @1 = 100 ``` A row is soft deleted with an update, after which the connection can no longer see or change it: ```csharp db.UpdateOnly(() => new Order { IsDeleted = true }, where: x => x.Id == id); db.SingleById(id); // null ``` ## An opt-out you can review Some code legitimately works across tenants: finding which tenants a user belongs to, a check that must be unique across tenants, admin tasks and reports. `WithoutFilters()` returns the same connection and transaction without its filters and rules: ```csharp var allOrders = db.WithoutFilters().Select(x => x.Total > 100); // Is the address taken by any tenant? var taken = db.WithoutFilters().Exists(x => x.Slug == slug); ``` It's the only way to opt-out, as filters can't be removed from a connection or ignored in a query. That makes tenant isolation auditable: searching your code base for one method finds every place it's bypassed. Wrap it in a method of your own and limit which files can call it, e.g. with a test, so each new use is reviewed. The connection it returns can use filter sets of its own, so you can drop the tenant filter and keep the rest, e.g. an admin connection that sees every tenant and still records who is writing: ```csharp public static IDbConnection AcrossTenants(this IDbConnection db, string userId) => db.WithoutFilters().UseFilters(AuditRules.For(userId)); ``` For platform APIs that act on a single customer, confine the request to that customer instead of opting out, so the rest of the API can't reach anyone else's rows: ```csharp public object Any(GetCustomer request) { RequireAdmin(); Request.GetTenantScope().TenantId = request.TenantId; // every query below is for this customer //... } ``` ## Background jobs A connection a job opens itself isn't confined. Put the tenant in the job's request and confine its connection before it reads anything, so an id for another tenant's row is treated as if it doesn't exist: ```csharp public class DeleteFile { public int TenantId { get; set; } public int FileId { get; set; } } using var db = dbFactory.Open().ForTenant(request.TenantId); var file = db.SingleById(request.FileId); // null if it isn't this tenant's ``` ## API Keys for each tenant The built-in API Key APIs and UIs now use the request's connection, so a filter on its table confines users to seeing and managing the API Keys of their own tenant: ```csharp static readonly FilterSet TenantApiKeys = FilterSet.Create(f => f.Filter((x, tenantId) => x.RefIdStr == tenantId)); DbConnectionRequestFilters.Add((db, req) => db.UseFilters(TenantApiKeys.For(GetTenantId(req)))); ``` APIs protected with `[ValidateIsAuthenticated]` can also now be called with a user's API Key, by registering API Keys as an ASP.NET Core Authentication scheme with the new `AddApiKeyAuth()`: ```csharp services.AddAuthentication().AddApiKeyAuth(); ``` Requests with a User API Key are authenticated as the user it belongs to, so a tenant's API Keys call the same authenticated APIs as its users, through the same confined connections. See [Allow Authenticated User APIs to API Keys](https://docs.servicestack.net/auth/apikeys#allow-authenticated-user-apis-to-api-keys) for how to limit what an API Key can call. ## One set of tests for every table As tenant-owned tables share an interface, one set of tests can cover all of them, including tables added later. The [Untyped APIs](https://docs.servicestack.net/ormlite/untyped-apis) can now read rows of a table when all you have is its `Type`, and they apply connection filters like the typed APIs: ```csharp static readonly Type[] TenantTables = typeof(IHasTenantId).Assembly.GetTypes() .Where(x => x.IsClass && !x.IsAbstract && typeof(IHasTenantId).IsAssignableFrom(x)) .ToArray(); [TestCaseSource(nameof(TenantTables))] public void A_confined_connection_only_reads_its_tenants_rows(Type table) { using var db = dbFactory.Open().ForTenant(1); Assert.That(SelectTenantIds(db, table), Is.EqualTo(new[] { 1 })); } static int[] SelectTenantIds(IDbConnection db, Type table) => db.CreateTypedApi(table).Select().Cast().Select(x => x.TenantId).ToArray(); ``` Seed two tenants, then assert a confined connection can't select, update or delete the other tenant's rows, that inserts get its tenant, and that an unresolved connection throws. The same APIs make it easy to export or delete everything a tenant owns, without listing each table. ## What isn't covered Filters and rules apply to the typed APIs where OrmLite creates the SQL. A few things are still your App's responsibility: | Not covered | What to do | |-|-| | Complete SQL statements in `SqlList`, `SqlScalar` and `ExecuteSql` | Include the tenant in the statement. SQL fragments in typed APIs are filtered | | Connections you open yourself, e.g. `dbFactory.Open()` in a job | Confine them, e.g. with `.ForTenant(tenantId)` | | Files, caches and other stores | Include the tenant in their keys | | Authorization | Confinement decides which rows are reachable. Roles and permissions are still checked by your App | ## Used throughout Next SaaS These features were developed alongside the [Next SaaS](https://react-templates.net/docs/next-saas) template, which now uses them for everything its organizations own. Besides replacing its hand-written tenant conditions, it's a working example of what a production SaaS App needs around them: [![](/img/posts/next-saas/bg.webp)](/posts/next-saas) - APIs **say which organization they're for** in their Request DTO, so users can work in different organizations in different browser tabs - Requests **fail closed** until the user is checked to be a member of that organization - Platform operators working on a customer are **confined to that customer** - **Background jobs** carry their organization and confine their connection - **Export and deletion** of an organization cover every table it owns, including tables added later - **API Keys** are bound to an organization, and only call the APIs that allow them - One set of **isolation tests** runs against every tenant-owned table Each is explained with its code in the new [Multitenancy Guide](https://docs.servicestack.net/ormlite/multitenancy/guide). ## Get Started Connection filters and write rules are available in **ServiceStack v10.4** on every database OrmLite supports. Upgrade your `ServiceStack.*` packages, define a `FilterSet` and register it in a `DbConnectionRequestFilter`. See the docs for the complete reference: - [Multitenancy](https://docs.servicestack.net/ormlite/multitenancy/overview) - how to build a shared-database multi-tenant App, with a checklist for each table and for your App - [Multitenancy Guide](https://docs.servicestack.net/ormlite/multitenancy/guide) - how Next SaaS applies each of these in a complete SaaS App - [Connection Filters & Write Rules](https://docs.servicestack.net/ormlite/connection-filters) - every API, including soft deletes, conditional filters and state kept with a connection - [v10.4 Release Notes](https://docs.servicestack.net/releases/v10_04) - everything else in this release # Follow every request with OpenTelemetry Source: https://servicestack.net/posts/opentelemetry A single API call rarely does its work alone. Placing an order might charge a card through a payment provider, publish a message that sends a confirmation email, and queue a Job that fulfils the order on another server a few seconds later. When a customer says their order never arrived, the evidence is spread across three servers, four log files and a payment dashboard, and there's nothing to tie it together. This is what distributed tracing is for. Each unit of work records a **span**, every span carries the Id of the **trace** it belongs to, and the trace follows the work wherever it goes. You open one trace and see everything that request caused, in order, with how long each step took and where it failed. **ServiceStack v10.3** now emits standard **OpenTelemetry traces and metrics** that connect all of it: - **API operations** get a span inside ASP.NET Core's HTTP span, tagged with the Request DTO, route and outcome - **MQ messages** carry the trace to whichever server processes them, for Background MQ, Redis MQ and RabbitMQ - **Background Jobs** continue the trace of the request that queued them, even after a retry - **Metrics you can alert on** for API durations, activity and failures, without noisy client errors - **Traces in the Profiling UI**, so you can follow a trace without leaving the Admin UI It all works with the tracing backend you already use: Jaeger, Grafana Tempo, Honeycomb, Datadog, the .NET Aspire Dashboard, or anything else that speaks OTLP. ![](/img/posts/opentelemetry/opentelemetry.webp) ## See the whole request Here's that order, first as you'd find it in your logs today, and then as a single trace: The message published on `web-1` carries the W3C `traceparent` in its `Meta`, so the consumer on `worker-2` continues the same trace. The Job queued by `PlaceOrder` stores the trace in its `BackgroundJob.TraceId` column, so when it fails and is retried on `worker-3`, both attempts show up under the request that started them. Nothing had to be passed around by hand. ## Add it to your App Add OpenTelemetry to an existing **.NET 8+** App with: :::sh npx add-in opentelemetry ::: This installs the OpenTelemetry packages and adds a `Configure.Profiling.cs` that registers ASP.NET Core and HttpClient instrumentation alongside ServiceStack's own sources: ```csharp services.AddOpenTelemetry() .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddSource(OperationDiagnostics.Name, MessagingDiagnostics.Name, JobsDiagnostics.Name) .AddOtlpExporter()) .WithMetrics(metrics => metrics .AddAspNetCoreInstrumentation() .AddMeter(OperationDiagnostics.Name, MessagingDiagnostics.Name, JobsDiagnostics.Name) .AddOtlpExporter()); ``` The three ServiceStack sources cover API operations, messaging and Background Jobs, while .NET's own instrumentation covers incoming HTTP requests and outbound HTTP calls. ServiceStack emits standard .NET `ActivitySource` traces and `Meter` metrics and **doesn't depend on the OpenTelemetry SDK**. Your App chooses the exporter, sampling and collector, and nothing is recorded until a listener is registered, so Apps that don't use OpenTelemetry pay nothing for it. :::info The `opentelemetry` add-in includes everything the `profiling` add-in does. Both create a `Configure.Profiling.cs`, so use one or the other. ::: ### Point it at your backend The exporter uses the standard OpenTelemetry environment variables, so the same build can send its traces to a different collector in each environment: | Variable | Used for | | --- | --- | | `OTEL_EXPORTER_OTLP_ENDPOINT` | Your collector's OTLP endpoint. Defaults to `http://localhost:4317` | | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` (default) or `http/protobuf` | | `OTEL_EXPORTER_OTLP_HEADERS` | Credentials your hosted backend needs, e.g. an API key header | | `OTEL_SERVICE_NAME` | The service name shown in your backend. Defaults to your App's name | | `OTEL_TRACE_URL_TEMPLATE` | Optional link from the Profiling UI to your backend | To try it locally without signing up for anything, run the [.NET Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) in Docker. It receives OTLP on the default port, so there's nothing to configure: :::sh docker run --rm -it -p 18888:18888 -p 4317:18889 mcr.microsoft.com/dotnet/aspire-dashboard:latest ::: Open the login link printed in its output, call a few of your APIs, and your traces and metrics appear. Sampling stays in your hands. For example, to export 10% of new traces while always following the decision of an upstream service: ```csharp .WithTracing(tracing => tracing .SetSampler(new ParentBasedSampler(new TraceIdRatioBasedSampler(0.1))) //... ``` ## Metrics you can alert on Traces tell you what happened to one request. Metrics tell you when something is wrong across all of them. ServiceStack records three metrics for every API: | Metric | Type | Description | | --- | --- | --- | | `servicestack.operation.duration` | Histogram (s) | How long API operations took | | `servicestack.operation.active` | UpDownCounter | API operations currently executing | | `servicestack.operation.errors` | Counter | API operations that failed with a server error | An error alert is only useful if it means your App is broken. A user mistyping their email, a request without a valid session or a lookup for an order that doesn't exist are all working as designed. So ServiceStack only counts **5xx responses and unhandled exceptions** as errors. Validation and authorization failures are recorded as `client_error` and keep an `Ok` span status. Try it: Metrics are tagged by operation, HTTP method, **route template** and outcome, never by raw URLs, users or request bodies. Every tag has a small, fixed set of values, so your metrics stay cheap to store and fast to query no matter how many orders you have. Only requests for known APIs are measured, so bots probing `/api/{anything}` can't create new metric series either. The same privacy rule applies to spans: request and response bodies, user ids and exception messages are never added, only the exception type. The messaging meter adds counts of sent and consumed messages and how long processing took, and these join the existing [Background Jobs metrics](https://docs.servicestack.net/background-jobs-monitoring#opentelemetry) for queue wait times, retries and failures. ## Traces in the Profiling UI You don't have to leave the Admin UI to follow a trace. With OpenTelemetry registered, the [Profiling UI](https://docs.servicestack.net/admin-ui-profiling) records each event with its W3C **Trace Id** and **Span Id**, so the API, OrmLite, Redis, HttpClient, MQ and Job events from the same request come up together. Click a Trace Id to see them in order, and switch between two views: - **Details** - the familiar Profiling grid, with each event's full details - **Trace view** - one row per step, indented under its parent span, with its source, duration and any errors Your OrmLite queries and Redis commands now sit right under the API or Job that ran them, and since the selected view is kept in the URL, you can send a teammate a link to the exact trace you're looking at. The Profiling UI only sees what happened on this server. For the rest of the story, set `ExternalTraceUrlTemplate` and the Trace view gets an **Open in trace backend** link to the complete trace: ```csharp services.AddPlugin(new ProfilingFeature { ExternalTraceUrlTemplate = "https://traces.example.org/trace/{traceId}", }); ``` HTTP is also allowed for `localhost`, so you can link to a local backend during development: | Backend | `ExternalTraceUrlTemplate` | | --- | --- | | Jaeger | `http://localhost:16686/trace/{traceId}` | | Aspire Dashboard | `http://localhost:18888/traces/detail/{traceId}` | Profiling works the same as before without OpenTelemetry, grouping events by their existing request identifiers, so you can adopt tracing whenever you're ready. Profiling durations and times are now also measured correctly on Linux and macOS, where they were previously overstated. ## Good to know - **Want more detail inside an API?** Set `OperationDiagnostics.EnableDetailedSpans = true` to add spans for request filters, the service invocation and AutoQuery execution. They're off by default to keep the number of spans per request low. - **Upgrading is safe.** Messages and Jobs without trace context, including those queued before you upgraded, simply start a new trace instead of failing. The existing `IMessage.TraceId` is unchanged. - **Custom MQ clients can join in.** Wrap your publish in `MessagingDiagnostics.StartPublish()` and call `MessagingDiagnostics.Inject(message)`, and your queue's messages join the same trace. - **Sampling doesn't affect profiling.** Sampling only changes what's exported. The Profiling UI keeps its own bounded history, controlled by `ProfilingFeature.Capacity`, and metrics are recorded whether or not a trace is sampled. ## Get Started OpenTelemetry support is available in **ServiceStack v10.3** for all .NET 8+ Apps. To add it: 1. Run `npx add-in opentelemetry` in your App 2. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to your collector, or run the Aspire Dashboard locally 3. Optionally set `OTEL_TRACE_URL_TEMPLATE` to link the Profiling UI to your trace backend See [OpenTelemetry Tracing](https://docs.servicestack.net/admin-ui-profiling#opentelemetry-tracing) for the full list of spans, attributes and configuration, and the [v10.3 Release Notes](https://docs.servicestack.net/releases/v10_03#follow-a-request-with-opentelemetry) for everything else in this release, including [Scalable Background Jobs](/posts/scalable-background-jobs) and [API Rate Limiting](/posts/rate-limiting). # API Rate Limiting Source: https://servicestack.net/posts/rate-limiting Every public API eventually meets a client that calls it too often. It might be a buggy retry loop, a script someone forgot to stop, a customer syncing everything at once or someone trying to guess passwords. Your APIs slow down for everyone else, your database takes the load, and the third-party services you pay for per call send you the bill. Rate limiting stops this at the edge of your App. Each client gets a budget of requests, and anything over it is turned away with **HTTP 429 Too Many Requests** before it costs you anything. ASP.NET Core already ships a good rate limiter. It handles permits, queueing, rejections and metrics for fixed windows, sliding windows, token buckets and concurrency limits. What was missing was a simple way to apply it to ServiceStack APIs. **ServiceStack v10.3** adds that: - **Attach a policy to a Request DTO** with ServiceStack's `[RateLimiting]` or Microsoft's `[EnableRateLimiting]` attribute - **Share one budget across a group of APIs** by binding a policy to a `[Tag]` - **Enforced everywhere** an API can be called: explicit routes, `/api` URLs, format URLs and autobatch requests - **Rejected before your service runs**, so an over-limit request costs almost nothing - **Checked on startup**, so ambiguous or bypassable configurations fail when your App starts, not in production ![](/img/posts/rate-limiting/rate-limiting.webp) ## Limit an entire workflow with one policy Rate limits usually protect a resource rather than a single endpoint. When "orders" are expensive, you want to limit how much order work a client can do, whether they're listing, placing or cancelling orders. Giving each endpoint its own allowance means a client gets three times the budget you intended. With ServiceStack you define the policy once, using ASP.NET Core's `AddRateLimiter`, and bind it to a tag: ```csharp builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter("orders", limiter => { limiter.PermitLimit = 60; limiter.Window = TimeSpan.FromMinutes(1); limiter.QueueLimit = 0; }); }); var app = builder.Build(); app.UseRouting(); app.UseRateLimiter(); app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); options.RateLimitTag("orders", "orders"); }); ``` Every operation tagged `orders` now draws from the **same `orders` budget**. You can also name the policy directly on a Request DTO: ```csharp [Tag("orders")] public class ListOrders : IGet, IReturn { } [Tag("orders")] public class CancelOrder : IPost, IReturn { } [RateLimiting("orders")] public class PlaceOrder : IPost, IReturn { } ``` Try spending the budget below, then switch to a separate policy per API to see how much more a client could get away with: APIs without a policy, like `GetProfile` above, aren't affected. Rate limiting is **opt-in**, so upgrading doesn't change the behavior of any existing API. ## Keep your DTOs free of ASP.NET Core Request DTOs often live in a shared `ServiceModel` project that's referenced by .NET clients, desktop apps and other services. The `[RateLimiting]` attribute is defined in **ServiceStack.Interfaces**, so these projects can declare their policy without taking a dependency on ASP.NET Core. If you'd rather use Microsoft's own attributes, `[EnableRateLimiting]` and `[DisableRateLimiting]` work on Request DTOs as well. You can also bind a policy from your App's configuration, without touching the DTO at all: ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); options.RateLimitOperation("reports"); }); ``` ## No ambiguity, no surprises An operation can end up with more than one binding: a tag, an attribute and some configuration. ServiceStack resolves them in a simple order: 1. `options.RateLimitOperation()` in your App's configuration 2. `[RateLimiting]` or `[EnableRateLimiting]` on the Request DTO 3. A `[Tag]` bound with `options.RateLimitTag()` Bindings can agree with each other, but they can never quietly disagree. If two bindings name different policies, or an operation is tagged with two tags bound to different policies, or `[DisableRateLimiting]` is combined with a policy, ServiceStack **throws on startup** and tells you which operation to fix. You'll never find out in production that an API has been running under the wrong limit. Try it: `[DisableRateLimiting]` is also how you exempt an operation from a global limiter, e.g. to keep health checks and webhooks from being throttled. ## Enforced on every route A rate limit is only as good as its weakest route. ServiceStack APIs can be called from several URLs, and it only takes one unmetered URL for a client to ignore your limits. ServiceStack applies the policy to all of them: | Route | Example | | | --- | --- | --- | | Explicit routes | `POST /orders` | ✓ Limited | | JSON API | `POST /api/PlaceOrder` | ✓ Limited | | Format URLs | `POST /api/PlaceOrder.json` | ✓ Limited | | Autobatch requests | `POST /api/PlaceOrder[]` | ✓ Limited | | Legacy pre-defined routes | `POST /json/reply/PlaceOrder` | Not mapped | Rate limiting runs in ASP.NET Core's middleware, so this requires ServiceStack APIs to be served by [ASP.NET Core Endpoint Routing](https://docs.servicestack.net/endpoint-routing). Keep `MapEndpoints()` at its default `force: true` so legacy ServiceStack routes aren't served. If a rate-limited App is configured in a way that could leave a route unmetered, it fails on startup instead of running without its limits. Since the limiter runs before ServiceStack, an over-limit request is rejected with a **429** before its Request DTO is deserialized, its filters run or your service is called. ## Choose the limiter that fits A named policy can use any of ASP.NET Core's built-in limiters, or a custom policy of your own. They all bind to ServiceStack APIs in the same way: ## Per-user budgets A single shared budget protects your servers, but it lets one busy client use up everyone's allowance. For SaaS APIs you'll usually want **one budget per user**, which you get by partitioning a policy by the authenticated user: ```csharp builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddPolicy("per-user", context => { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); return userId != null // Each signed-in user gets their own bucket of 100 requests a minute ? RateLimitPartition.GetTokenBucketLimiter(userId, _ => new TokenBucketRateLimiterOptions { TokenLimit = 100, TokensPerPeriod = 100, ReplenishmentPeriod = TimeSpan.FromMinutes(1), QueueLimit = 0, }) // Anonymous callers share a much smaller budget per IP : RateLimitPartition.GetFixedWindowLimiter( context.Connection.RemoteIpAddress?.ToString() ?? "anonymous", _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, }); }); }); var app = builder.Build(); app.UseRouting(); app.UseAuthentication(); // must run before the limiter to partition by user app.UseRateLimiter(); ``` The limiter runs before ServiceStack does, so it partitions on the **ASP.NET Core principal** in `HttpContext.User`. Make sure `UseAuthentication()` comes before `UseRateLimiter()`, as shown above, so the user is known when the limiter runs. The same approach works for per-tenant or per-API Key budgets, using whichever claim identifies them. ## Things to know - **Limits are per App Server.** ASP.NET Core's built-in limiters keep their counters in memory, so with 3 App Servers behind a load balancer, a 60 requests/minute policy allows up to 180 in total. For a quota shared across servers, use a distributed policy, e.g. backed by Redis, or enforce it at your API gateway. - **Friendly rejections.** Use `options.OnRejected` to add a `Retry-After` header or a custom error body. ServiceStack's [generated service clients](https://docs.servicestack.net/add-servicestack-reference) surface a 429 like any other failed API call. - **Metrics included.** ASP.NET Core's limiter publishes its own metrics for leased and rejected requests, which you can collect alongside the new [ServiceStack OpenTelemetry metrics](https://docs.servicestack.net/releases/v10_03#follow-a-request-with-opentelemetry). - **Not the same as Job rate limits.** API rate limiting controls how fast clients can call your APIs. To control how fast your App calls someone else's API, use the new [Background Jobs queue rate limits](https://docs.servicestack.net/releases/v10_03#rate-limit-third-party-apis), also in this release. ## Get Started API Rate Limiting is available in **ServiceStack v10.3** for all ASP.NET Core Apps using Endpoint Routing. To add it to an existing App: 1. Define your named policies with `builder.Services.AddRateLimiter()` 2. Add `app.UseRateLimiter()` after `UseRouting()`, and after `UseAuthentication()` for per-user policies 3. Bind policies to your APIs with `[RateLimiting]`, `[EnableRateLimiting]`, `RateLimitTag()` or `RateLimitOperation()` See the [v10.3 Release Notes](https://docs.servicestack.net/releases/v10_03#api-rate-limiting) for everything else in this release, and the [Rate Limiting docs](https://docs.servicestack.net/rate-limiting) for more on configuring ASP.NET Core's rate limiter. # Software Licensing Next.js Project Template Source: https://servicestack.net/posts/next-license Selling a desktop app means building a store around it. You need a pricing page, checkout, a way to issue license keys after payment, somewhere customers can get their key again, renewals and upgrades, downloads for each platform, and a back office to handle refunds and support. Then your app needs a way to check the key. Most licensing systems handle that with an activation server, which your customers now depend on to use the software they paid for. **Next License** is a new [React Template](https://react-templates.net) that includes the store and a simpler way to check licenses. Customers buy a license with Stripe Checkout and copy a signed license key from their account. Your app verifies the key **offline**, against a public key built into it, and enables its Pro features. There's no activation server to run and no network call to make. It's built with **.NET 10**, **ServiceStack**, **ASP.NET Core Identity**, **React 19** and **Next.js 16**, and includes license verifiers for both **.NET** and **Electron** apps. Try the build coverage demo below, then take a tour of the storefront and Operations Center: Create a new Next License project with: :::sh npx create-net next-license ProjectName ::: ## What's included The template sells licenses for **Acme Studio**, an example desktop app that unlocks Pro features with a license key. It includes an example .NET console and Avalonia desktop app that verifies keys, so you can see the whole flow working before you connect your own app: ## From purchase to unlock A license goes through 4 steps from purchase to unlocking Pro in your app: 1. **Purchase** - the customer chooses an offer on `/pricing`, enters the name, organization and number of seats to register, accepts your license agreement and pays in Stripe Checkout 2. **Fulfill** - a Stripe webhook confirms the payment, and the server signs a license key and records the payment in a single transaction 3. **Deliver** - the customer opens **My licenses** and copies the key, or downloads it as a license file 4. **Unlock** - your app verifies the key with its embedded public key and enables Pro ![My licenses in the customer account](https://react-templates.net/img/next-license/customer-licenses.png) Delivery emails only link to the customer's account, they never attach the key. If an email goes missing or the customer loses their key, they can copy it again from their account at any time. ## A license is a signed JWT A license key is a standard **ES256-signed JWT**. Its claims contain everything your app needs to display who it's registered to and decide which builds it covers: ```json { "iss": "acme-studio", "aud": "acme-studio", "sub": "13a25819-3ed9-441f-8bcd-6a94ee56dc00", "name": "Alex Morgan", "organization": "Northstar Studio", "seats": 3, "edition": "Pro", "iat": 1780000000, "lifetime": false, "updatesThrough": "2027-03-21" } ``` The server signs licenses with a private P-256 key that never leaves it. Your apps only contain the matching public key, which can verify a license but can't create one. Anyone can read the claims, which is how your app shows the registered name, organization and seats, but any change to them invalidates the signature. A single command creates your key pair. It won't overwrite an existing directory, so you can't accidentally replace a key that has already signed licenses: :::sh dotnet run --project MyApp.Licensing.Tool -- jwt ./license-keys ::: Because it's a standard JWT, you aren't limited to the verifiers included in the template. Any maintained JWT library that supports ES256 can verify it, in any language. ## Licenses that never expire Notice what the claims above don't include: an `exp` expiry date. A Next License license never expires. Instead, it covers every build of your app **released** on or before its `updatesThrough` date, and keeps working with those builds forever. **Lifetime** licenses cover every future build as well. Your app embeds its own release date when it's built, and compares that to the license: ``` valid = signature valid AND issuer/product match AND paid edition AND (lifetime OR buildDate <= updatesThrough) ``` | License | App build date | Pro | |----------------------------------|----------------|------------------| | Dated through 2026-09-21 | 2026-09-21 | Enabled | | Dated through 2026-09-21 | 2026-09-22 | Renewal required | | Lifetime | Any | Enabled | | Missing, tampered or wrong product | Any | Disabled | Nothing is compared against today's date. A covered build still works years later, changing the system clock changes nothing, and customers are never locked out of software they paid for. To get Pro in a newer build, customers renew. Renewals extend from the later of the current cutoff and the payment date, so renewing early never loses time, and the license keeps the same ID. ## Unlock Pro in your app In a .NET app, reference `MyApp.Licensing` and call `LicenseJwt.Verify`. It uses the runtime's built-in cryptography and never contacts a server: ```csharp using MyApp.Licensing; // Public key, issuer, product and build date come from your app, never from user input. var result = LicenseJwt.Verify( pastedKey, embeddedPublicKeyPem, issuer: "acme-studio", product: "acme-studio", buildDate: "2026-09-21"); bool enablePro = result.Valid; if (result.License is { } license) { Console.WriteLine($"Registered to {license.Name}"); Console.WriteLine($"Organization: {license.Organization}"); Console.WriteLine($"Licensed seats: {license.Seats}"); } ``` In an Electron app, copy `license.mjs` into your app and call `verifyLicense` from the main process. It only uses Node's built-in `crypto`, with no npm dependencies, no .NET runtime and no helper process: ```js import { verifyLicense } from './license.mjs'; const result = verifyLicense(savedOrPastedKey, { publicKey: bundledPublicKeyPem, issuer: 'acme-studio', product: 'acme-studio', buildDate: '2026-09-21', }); const enablePro = result.valid; ``` Both return the same statuses, so your app knows exactly what to show: | Status | Suggested app behavior | |---------------------|---------------------------------------------------------------------| | `valid` | Enable Pro and show the registered name, organization and seats | | `missing` | Free mode with an **Enter license** action | | `invalid` | Explain that the key can't be verified and stay in Free mode | | `buildNotCovered` | Offer a renewal, or a download of a version the license covers | | `editionNotCovered` | Keep Pro disabled | An uncovered license still includes its verified registered details, so your app can show a friendly *"Registered to Alex Morgan, renew to use this version"* message instead of treating the customer like a stranger. The public key and release date are embedded at build time, and the sample app fails `dotnet publish` if no public key is supplied, so a demo key can't ship by accident: ```bash dotnet publish MyApp.SampleApp -c Release -r win-x64 --self-contained true \ -p:LicensePublicKeyFile=/absolute/path/license-public.pem \ -p:AppBuildDate=2026-09-21 ``` Apps that want renewals to arrive without the customer pasting a new key can use the optional `ActivationClient`. It fetches the latest version of a license at most once a day, only if the user opts in, and only saves the result if it verifies. The server stores nothing about the request, and a failed refresh never affects Pro. ## Payments you can trust Customers pay with one-time **Stripe Checkout** purchases. Before redirecting to Stripe, the server records the order, its price and the checkout policy, so nothing the customer changes in the browser can alter what they're charged for: ![Pricing page with Free, 12 month and Lifetime offers](https://react-templates.net/img/next-license/pricing.png) Stripe webhooks are verified and saved to a durable inbox before they're processed by a [Background Job](https://docs.servicestack.net/background-jobs). The webhook body alone is never trusted: the job retrieves the current Checkout Session and payment from Stripe and checks the order, price, quantity, currency, discount and tax before it signs a license. If a webhook is delayed or lost, background reconciliation checks Stripe again. Replayed events can't issue a second license, and a late failure event can't undo a successful payment. Stripe also handles the rest of the checkout: - **Promotion codes** - create coupons and codes in the Stripe Dashboard and enable them with `Stripe__AllowPromotionCodes=true` - **Automatic tax** - enable Stripe Tax with `Stripe__AutomaticTax=true` - **Refunds and disputes** - are recorded from Stripe events, keep partial and multiple refunds, and flag the order for review in the Operations Center - **Invoices** - customers open their Stripe invoices from their order history ## Downloads from your GitHub releases Your software is built and published by your own app's repository, not the licensing server. Point the site at the GitHub repository that holds your releases: ```dotenv Licensing__GitHubRepository=owner/repository ``` Your published releases then appear as platform download buttons on `/download`, with the visitor's platform first, and as Markdown release notes on `/changelog`. Installer file extensions decide which platform each download is for, like `.exe` and `.msi` for Windows, `.dmg` for macOS and `.AppImage` or `.deb` for Linux: ![Downloads page built from GitHub releases](https://react-templates.net/img/next-license/downloads.png) The last successful response from GitHub is saved to disk, so the download page keeps working through GitHub outages and rate limits, and across restarts. ## An Operations Center for your store Admins manage the store from the `/admin` Operations Center. Its overview shows active licenses, orders that need review, pending Stripe events and a launch checklist of what's left to configure before you can sell: ![Operations Center overview](https://react-templates.net/img/next-license/operations-center.png) - **Products & pricing** - save prices, create the matching Stripe prices and approve offerings for sale - **Licenses** - search licenses, extend their coverage, grant Lifetime updates or reissue them, with each change requiring a reason that's recorded in the audit log - **Issue a license** - search registered customers and issue complimentary or replacement licenses - **Orders** - search orders, open their Stripe payments and review refunds and disputes - **License terms** - publish new versions of your license agreement with a live Markdown preview - **Integrations** - check the status of your Stripe, webhook, signing key and GitHub configuration - **Releases** - view your published GitHub releases with their notes and installers ![Managing licenses in the Operations Center](https://react-templates.net/img/next-license/licenses.png) Customers also have what they need to look after their own licenses. From their account they can renew a dated license or upgrade it to Lifetime, transfer it to another verified account, and choose which lifecycle emails they receive, like update reminders before their cutoff and announcements of new releases. ## Honest about what it doesn't do Offline verification is a deliberate trade-off. Your customers never depend on your server to use their software, and you never have to run an availability-critical activation service. In return, a license a customer already holds can't be switched off. Refunding, revoking or transferring a license stops your store from providing its key from then on, but copies already downloaded keep working with the builds they cover. Next License is an eligibility check, not DRM. It doesn't count running machines, enforce seats, bill on a recurring schedule or remotely disable licenses, and a user who controls your app's code can patch its checks. For most desktop software sold to honest customers, that's the right trade-off: a clear, verifiable license that just works, without the support burden of activations. ## Your choice of database, one App to deploy Like the other React Templates, Next License runs as **one ASP.NET Core App**. Next.js provides the React UI as a static export served by ASP.NET Core, so there's no Node.js server to run in production. In development, `dotnet watch` proxies the Next.js dev server so you get HMR on the same origin as your APIs. It uses **SQLite** by default, or **PostgreSQL**, **MySQL/MariaDB** or **SQL Server** by setting `Database__Provider`. The included GitHub Actions deploy to any Linux server with [Kamal](https://kamal-deploy.org), with a destination for each database. ## Get Started Create a new Next License project with: :::sh npx create-net next-license ProjectName ::: Then run it locally: ```bash cd ProjectName cp .env.example .env cd MyApp.Client && npm ci cd ../MyApp && dotnet watch ``` Sign in as `admin@email.com` with `p@55wOrd` to explore the Operations Center. To see offline verification working without configuring anything, run the example app from the repository root. It generates disposable keys and prints covered, uncovered and Lifetime results, or opens a desktop window with `--desktop` where you can paste the example keys and watch Pro turn on: ```bash dotnet run --project MyApp.SampleApp dotnet run --project MyApp.SampleApp -- --desktop ``` The [Next License docs](https://react-templates.net/docs/next-license) walk you through each step, from creating your signing key and issuing your first license, to enabling Pro in your .NET or Electron app, connecting a Stripe sandbox and shipping to production. They also include reference guides for every concept, feature, operations runbook and security control in the template. # SaaS Next.js Project Template Source: https://servicestack.net/posts/next-saas Every SaaS product needs the same foundation before it can charge its first customer. You need sign up and sign in, organizations with team roles, pricing plans, Stripe subscriptions and webhooks, usage limits, API keys, audit logs, data export and deletion, and a back office to support your customers. None of it is your product, but all of it has to work before you can launch. **Next SaaS** is a new [React Template](https://react-templates.net) that includes that foundation, so you can start on the part that's unique to you. It's built with **.NET 10**, **ServiceStack**, **ASP.NET Core Identity**, **React 19** and **Next.js 16**, and it includes three complete Apps from day one: - **Public Site** - a product landing page, pricing rendered from published plan versions, ASP.NET Core Identity sign up and hosted Stripe Checkout with trials, coupons and promotion codes - **Customer App** - organization dashboards, organization-scoped file storage, real-time usage meters and quotas, plan and billing management through the Stripe Customer Portal, team roles, API keys, audit logs and data export, ownership transfer and delayed deletion - **Operations Center** - platform-wide commercial health, a Customer 360 view, an immutable plan editor, Stripe-synced coupons, platform usage analytics and recovery of failed webhooks, jobs and integrations Take a tour through each of them: Create a new Next SaaS project with: :::sh npx create-net next-saas ProjectName ::: ## What's included The template ships with **Acme**, a small example product for secure document storage and usage analytics. Acme is kept small on purpose. It shows how subscriptions, quotas, storage, analytics, teams and operations fit together, but it doesn't push its own domain onto you. You replace Acme with your product and keep the parts every SaaS needs: ## One App to run and deploy Next SaaS runs as **one ASP.NET Core App**. ASP.NET Core and ServiceStack handle authentication, APIs, persistence, billing, background work and hosting. Next.js provides the React UI as a static export, so there's **no Node.js server to run in production**: ![One-Runtime Production Architecture](https://react-templates.net/img/next-saas/production-architecture.png) In development, `dotnet watch` starts the Next.js dev server and proxies it, so you get HMR on the same origin as your Identity cookies and APIs. In production, `next build` generates static files that are published to the App's `wwwroot`. Your App has one public origin in both environments, so there's no CORS to configure and no second server to deploy. Every API is a typed ServiceStack Service. C# Request DTOs in `MyApp.ServiceModel` are the source of truth, and the React client calls them through the TypeScript DTOs generated from them. You can follow any feature from its DTO, to its Service, to the page that calls it: ![End-to-End Typed ServiceStack Client](https://react-templates.net/img/next-saas/typed-client.png) It also supports your preferred database. A single `DB_PROVIDER` setting selects **SQLite**, **PostgreSQL**, **MySQL** or **SQL Server** for both local development and production, so you develop against the same database you deploy to. SQLite is the zero-dependency default. For the other databases, `./scripts/dev-db.sh up` runs the same database image locally that your deployment uses. ## Organizations and teams Customers sign up with a **Personal** account for themselves or a **Business** organization for their team. One user can belong to many organizations and switch between them. Each organization has its own data, billing, quotas, files and API keys. Business organizations have 4 roles: | Role | Permissions | |---------|-------------------------------------------------------------------------------| | Owner | Everything, including ownership transfer and deleting the organization | | Admin | Members, settings, exports and day-to-day administration | | Billing | Billing and subscription management | | Member | Uses the product features their plan includes | ![Team Roles and Member Management](https://react-templates.net/img/next-saas/team-roles.png) Invitations use one-time tokens that expire, and can only be accepted by a signed-in user with a matching verified email. The Owner role can't be removed by mistake: an organization always has an Owner, and an Owner can only leave by transferring ownership to another member first. ### Tenant isolation, enforced on the server The browser is never trusted to decide which organization's data it can see. Every customer request goes through `IWorkspaceContextResolver`, which reloads the caller's active membership on the server. Every query for customer data is then filtered by that organization: ```csharp var widget = Db.Single(x => x.Id == request.Id && x.WorkspaceId == context.Workspace.Id) ?? throw new HttpError(404, "WidgetNotFound", "The widget was not found."); ``` The same rule applies to files, exports, analytics and background jobs. Every table is also classified as `global`, `tenant-owned` or `platform-operational` in `features.json`, and an architecture test fails the build if you add a table without classifying it. API keys are bound to the organization they were created in, not to whatever organization the browser has selected. They're shown once, listed afterwards by fingerprint, and can't be used to manage other keys. ## Stripe billing off the hot path Stripe handles payments, and your database handles access. Customers subscribe through **Stripe Checkout** and manage their payment methods, invoices and cancellations in the **Stripe Customer Portal**. You don't have to build any of that UI: ![Stripe Hosted Checkout](https://react-templates.net/img/next-saas/stripe-checkout.png) Signed Stripe webhooks keep a local `BillingSubscription` up to date. Every request checks access against this local copy, so **Stripe is never called to decide whether a request is allowed**. Stripe latency and outages don't slow down or break your App. Each webhook event is recorded once in an inbox table and processed by a [Background Job](https://docs.servicestack.net/background-jobs), so duplicate deliveries are harmless. Failed events keep their attempts and errors, and operators can retry them from the Operations Center. An hourly job also reconciles subscriptions with Stripe to repair any webhooks that were missed: ![Stripe Webhook Ingestion Pipeline](https://react-templates.net/img/next-saas/webhook-flow.png) When a payment fails, a configurable grace period gives the customer time to fix it. Afterwards the organization falls back to **Free**, **ReadOnly** or **Suspended** access, depending on how you configure it. A billing change never deletes customer data. Admins can also create Stripe **coupons and promotion codes** from the Operations Center. The codes can be percentage or fixed-amount, one-time, repeating or forever, with optional redemption limits and expiry dates. ## Plans that never change under your customers A plan is a set of prices, features, quota allowances and an optional trial. Admins manage plans in the **Plan Editor** by editing a draft and publishing it. Publishing creates a new **immutable version**, and every existing subscription stays pinned to the version the customer signed up to. Changing your prices never changes an existing customer's contract. ![Plan Editor and Version Management](https://react-templates.net/img/next-saas/plan-editor.png) The Plan Editor can also create the matching Products and Prices in Stripe for you. It uses idempotency keys, so running it again won't create duplicates. Features are declared under `Saas.Features` in `appsettings.json`, and plans can only grant features that are declared there. Protecting an API is a single attribute on its Request DTO: ```csharp [ValidateIsAuthenticated] [RequiresFeature("reports.export")] [Route("/reports/export", "POST")] public class ExportReport : IPost, IReturn { } ``` The React UI uses the same entitlements to hide controls and suggest upgrades, but the server always has the final say. For negotiated contracts or temporary access, admins can add a per-customer override, with a reason and an optional expiry. Overrides are audited and take priority over the plan. ## Usage metering and quotas Features decide *whether* an organization can do something, and quotas decide *how much*. Meters are declared in `appsettings.json` and come in two kinds: - **Counters** count activity like API requests or reports generated, and reset each billing period - **Gauges** track what an organization currently has, like stored documents, bytes or seats Each plan sets an allowance for each meter, with a **Hard limit**, **Soft limit** or **Metered overage** policy. Usage is checked in your database within a transaction, so two requests can't both use the last remaining unit. Every usage event needs an idempotency key, so a retried request never counts twice: ```csharp var usage = manager.RecordUsage(Db, context.Workspace, subscription, context.UserId, new RecordUsage { MeterKey = "reports.generated", Units = 1, IdempotencyKey = request.IdempotencyKey, MetadataJson = new { reportId }.ToJson(), }); ``` When you don't know the final cost of some work in advance, you **reserve** capacity before starting it, **settle** it to the actual amount when it completes, and **release** it if it fails. Acme's file uploads use this pattern, reserving a document and the expected bytes before streaming the upload. If the upload fails, the reservation is released and any partial file is removed: ![Capacity Reservation and Settlement Flow](https://react-templates.net/img/next-saas/reservation-flow.png) Analytics are built from the same usage events used to enforce quotas, so your dashboards and your limits always agree. Customers see allowances, daily usage, per-member breakdowns, projections and CSV exports on their **Usage** page. Operators can see plan mix, growth and quota pressure across all customers: ![Customer Usage Dashboard and Analytics](https://react-templates.net/img/next-saas/customer-usage.png) ## An Operations Center for running the business The `/admin` Operations Center is where your team runs the business. It has 3 platform roles of its own, separate from the roles inside customer organizations: **Admin**, **BillingAdmin** and **Support**: ![Operations Center Overview](https://react-templates.net/img/next-saas/operations-center.png) - **Customer 360** - search by organization, member email, Stripe ID or API key fingerprint to see a customer's subscription, entitlements, usage, members, audit history and support notes - **Plans & Coupons** - edit and publish plan versions, and create Stripe coupons and promotion codes - **Usage** - platform-wide analytics, plan mix, growth and quota pressure - **Operations** - failed Stripe events, notifications and lifecycle work, with retries that use the original idempotency key, and a check of whether each integration is configured - **Security** - retention runs, active support sessions and the platform audit log ![Customer 360 Platform Inspection](https://react-templates.net/img/next-saas/customer-360.png) Risky operations like usage adjustments or status changes require a reason. The high-risk ones also show a preview of their impact and ask the operator to type the organization's name to confirm. **Support access** is off by default. When enabled, an Admin grants a named Support operator time-limited, read-only access to a single organization. The operator has to start the session explicitly, sensitive data is hidden from them, and every step is audited: ![Support Access Four-Stage Lifecycle](https://react-templates.net/img/next-saas/support-access-lifecycle.png) ## Lifecycle, audit and notifications The template also includes the features customers and compliance reviews will ask you for: - **Audit logs** - security, billing, lifecycle and admin actions are recorded with the actor, organization and request ID, with passwords, keys and other secrets removed. Customers and operators can both search and export them - **Data export** - Owners and Admins can download a ZIP of all their organization's data and files. The export is generated by a background job and expires after a configurable number of days - **Delayed deletion** - deleting an organization makes it read-only for a recovery window, during which the Owner can cancel. Legal holds block deletion, and user accounts are kept for their other organizations - **Retention** - global defaults for analytics, audit, notification and file retention, which operators can override per organization - **Notifications** - in-app and email notifications with per-user preferences. Duplicates are prevented, and failed deliveries are retried and shown in the Operations Center ## Durable work with Background Jobs Anything that's slow, calls an external service, runs later or needs to be retried runs in [ServiceStack Background Jobs](https://docs.servicestack.net/background-jobs). This includes Stripe webhook processing, file deletion, export creation, email delivery, subscription reconciliation, retention, usage rollups and delayed organization deletion. Important work is saved to the database before a job is queued, and jobs only receive record IDs. Each job reloads the current state before acting, so a job queued before a legal hold or membership change still respects it. Recurring jobs release abandoned reservations, rebuild usage rollups, capture daily business metrics and apply retention in bounded batches. ## Built for AI-assisted development The template's clear conventions also make it a good fit for coding agents. `AGENTS.md` and the docs give an agent the context it needs. C# DTOs define the API contracts, `features.json` records which module owns each table, and `./scripts/verify.sh` checks the result. A typical request to an agent can describe a complete feature: ```text Add organization-bound widget creation for Owner and Admin members. Gate it with widgets.basic and enforce a billing-period widgets.created quota. Use reserve/settle because the provider call can fail, enqueue the provider work with ServiceStack Background Jobs, generate the TypeScript DTOs, add an AppShell page, and test cross-tenant access, idempotent replay, exact/over limit, retry, and static build. Run ./scripts/verify.sh and report the results. ``` ## Verify and ship A single command runs everything you need to check before a release: :::sh ./scripts/verify.sh ::: It builds and tests the .NET and TypeScript projects, generates the production static export, and then starts the App in Production against a **completely empty database** to check that it can create its schema and seed data from scratch. Before deploying, `./scripts/preflight.sh` checks your production configuration without printing any secrets. In Production, the App also refuses to start with unsafe settings. It lists every missing requirement at once, like HTTPS, `AllowedHosts`, a database server, SMTP or Stripe keys. ![Release Verification and Preflight Gates](https://react-templates.net/img/next-saas/release-gates.png) The included GitHub Actions deploy to any Linux server with [Kamal](https://kamal-deploy.org). The workflow builds and tests each commit, publishes a container image tagged with the commit SHA, runs database migrations and checks the `/ready` endpoint before the release is complete. You can start with a single-server SQLite deployment, then move to PostgreSQL by changing your configuration: ![Migration and Deployment Pipeline Flow](https://react-templates.net/img/next-saas/deploy-flow.png) ## Get Started Create a new Next SaaS project with: :::sh npx create-net next-saas ProjectName ::: Then run it locally: ```bash cd ProjectName/MyApp.Client && npm install cd ../MyApp && npm install dotnet watch ``` On first run it creates the database, seeds the **Free**, **Personal**, **Pro**, **Business** and **Enterprise** plans and creates development users. Sign in as `admin@email.com` to explore the Operations Center, or as `manager@email.com` to use the customer App. To fill it with realistic sample organizations, subscriptions and usage history, run: :::sh ./scripts/seed-example-data.sh ::: The [Next SaaS docs](https://react-templates.net/docs/next-saas) walk you through each step, from choosing your database and customizing the product, to adding your first metered feature, connecting a Stripe sandbox and shipping to production. They also include reference guides for every concept, feature, operations runbook and security control in the template. # Scalable Background Jobs Source: https://servicestack.net/posts/scalable-background-jobs ## Background Jobs, ready for your important work The work that matters most in most Apps doesn't happen inside the request. Charging a payment, sending an email, importing a spreadsheet, generating a report, syncing a tenant or calling a third-party API are all things you want to run in the background - and they're also the things you can least afford to lose, run twice, or let pile up behind each other. [Background Jobs](https://docs.servicestack.net/background-jobs) started as a simple way to move that work off the request thread. **ServiceStack v10.3** makes it a complete, durable job platform for the work your business depends on. It's the same `IBackgroundJobs` API you already use, on the same database you already run. There's **no broker to deploy**, no new infrastructure to operate and nothing to learn before you benefit. Existing Jobs get safer retries, bounded resource usage and graceful shutdown just by upgrading, while the new features are there the moment you need them: Most of the features in this post come with an interactive simulation, so you can see how they behave before you write any code. ## Scale out without a broker The [RDBMS provider](https://docs.servicestack.net/background-jobs-rdbms) now safely runs on **any number of App Servers** sharing the same PostgreSQL, SQL Server or MySQL database. Every Job is **claimed with a lease** that's renewed while it runs, and every write a server makes to a Job is fenced by that lease. A Job is only ever owned by one server at a time, and a slow or partitioned server can't overwrite the result of the server that took over from it. When a server crashes, is killed or loses its network, its leases simply expire and another server **recovers its Jobs automatically**. A Job that keeps taking its server down with it is counted as a failed attempt each time, and is failed with a `LeaseExpired` error once it exceeds its `RetryLimit`, instead of being retried forever. On PostgreSQL and MySQL 8+, servers claim Jobs with `SKIP LOCKED`, so adding more servers adds more throughput rather than more contention. Try taking a server down and watch its Jobs fail over: ### Deploy without losing work Background Jobs now **shuts down gracefully**. Running Jobs are given `ShutdownTimeoutSecs` (default 30s) to finish and save their progress, and any Job that was claimed but never started is handed straight back so another server picks it up immediately, rather than waiting for its lease to expire on every deploy. It's registered by the plugin, so existing Apps get it without touching their hosted service. ### Know which servers are doing what Each App Server records a heartbeat, so the Admin UI's **Nodes** view, `jobs.GetJobNodes()` and the `AdminGetJobNodes` API report which servers are alive, what version they're running, how many Jobs each is running and which ones stopped reporting. You can **drain a server** before taking it out of service so it finishes its Jobs without taking any more, or **dedicate servers to queues**, e.g. only run GPU work on the servers that have one: ```csharp services.AddPlugin(new DatabaseJobFeature { Queues = ["gpu"], // this server only processes Jobs on the gpu queue }); jobs.SetJobNodeDraining(serverId, draining: true); ``` ## Give each workload its own lane Different work has different needs: a password reset can't wait behind a bulk import, and a third-party API can't be called faster than its quota allows. Jobs can now be routed to **named queues**, each with its own Workers and concurrency, so a backlog on one queue never delays another. Within a queue, Jobs with a higher `Priority` run first: ```csharp jobs.EnqueueCommand(request, new() { Queue = "emails", Priority = 10, // ahead of the newsletter }); ``` ```csharp services.AddPlugin(new DatabaseJobFeature { // or BackgroundsJobFeature MaxConcurrentJobs = 8, // default for every queue QueueConcurrency = { ["imports"] = 2 }, // bulk imports can't take over the server }); ``` ### Pause, re-throttle and rate limit at runtime Queues are also your controls during an incident. When a downstream system is down, **pause its queue** - its Jobs stay safely queued and resume where they left off, instead of burning through their retries. Raise a queue's concurrency to clear a backlog, or lower it to protect a struggling dependency, without a redeploy. Concurrency limits how many Jobs run at once, but a quota like "10 calls per second" limits how often they *start*. **Queue rate limits** handle exactly that, and on the RDBMS provider the limit is counted across every server, so adding servers doesn't multiply your API bill: ```csharp jobs.PauseJobQueue("payments"); jobs.ResumeJobQueue("payments"); jobs.SetJobQueueConcurrency("imports", 4); jobs.SetJobQueueRateLimit("stripe-api", rateLimit: 10, window: TimeSpan.FromSeconds(1)); ``` Every change is stored in the database, so it takes effect on every server and survives restarts - and they're all available from the Admin UI's **Queues** tab: ## Never do the same work twice Networks time out, users double-click and messages get replayed. The same request arriving twice is normal - charging a customer twice isn't. Background Jobs now gives you three ways to make sure work happens once: - **Idempotent enqueue** - give a Job a meaningful `RefId` and submitting it again returns the Job that's already queued instead of creating a duplicate - **Singleton Jobs** - a `SingletonKey` allows only one Job with that key to be queued or running at a time, enforced by a unique index so it holds even when several servers submit at the same moment - **Transactional outbox** - with the RDBMS provider, queue Jobs on your own connection so they're saved in the same transaction as your data. If the transaction rolls back, so do its Jobs ```csharp jobs.EnqueueCommand(order, new() { RefId = $"charge-order-{order.Id}", DuplicateRefIdBehavior = DuplicateRefIdBehavior.ReturnExisting, }); jobs.EnqueueCommand(new() { SingletonKey = "refresh-products-cache", }); ``` ## Failures that heal themselves Most failures are temporary, so the best thing a Job can do is wait and try again - without every failed Job hammering a recovering dependency at the same moment: - **Smarter retries** back off with `Fixed`, `Linear`, `Exponential` or the new default `ExponentialJitter`, which spreads retries out after an outage - **Every failure is kept**, with its error, server and duration, so an intermittent failure shows its full history rather than only the last error - **Jobs that shouldn't run late won't** - give a Job an `ExpiresIn` or `ExpiresAt` and if it can't start in time it's cancelled with `JobExpired`, so there are no reminders for meetings that already happened - **Timeouts are enforced**, and even a Job that ignores its cancellation token releases its Worker so the Jobs behind it keep moving - **Cancellation reaches the running Job**, on whichever server is executing it ```csharp jobs.EnqueueCommand(contact, new() { RetryLimit = 5, RetryBackoff = RetryBackoff.ExponentialJitter, RetryDelay = TimeSpan.FromSeconds(5), MaxRetryDelay = TimeSpan.FromMinutes(5), ExpiresIn = TimeSpan.FromHours(1), TimeoutSecs = 120, }); ``` Plan out a retry policy and see when each attempt would run: ## From single Jobs to workflows ### Multi-step workflows Real business processes are rarely a single step. Jobs can now depend on each other, with each step able to read the result of the step before it. If a step fails, the steps after it are cancelled, while steps queued with `JobDependencyPolicy.OnFinished` still run however the previous step ended - perfect for notifications and clean-up: ```csharp var charge = jobs.EnqueueCommand(order); var reserve = jobs.EnqueueCommand(order, new() { DependsOn = charge.Id }); var ship = jobs.EnqueueCommand(order, new() { DependsOn = reserve.Id, Callback = nameof(OrderShippedCommand), }); // Tell the customer what happened, whether shipping succeeded or not jobs.EnqueueCommand(order, new() { DependsOn = ship.Id, DependsOnPolicy = JobDependencyPolicy.OnFinished, }); ``` ### Batches with live progress Fan out thousands of Jobs as a **Batch** and watch its progress in the Admin UI. When every Job has finished the batch's `callback` runs, and `onSuccess` runs only if every Job succeeded - so "email the report when the import completes" no longer needs a polling loop. A Job with `DependsOnBatch` fans back in, only running once the whole batch is done: ```csharp jobs.CreateJobBatch(batchId, total: images.Count, callback: nameof(ImportFinishedCommand), // runs once every Job has finished onSuccess: nameof(PublishGalleryCommand)); // only when every Job succeeded foreach (var image in images) jobs.EnqueueCommand(image, new() { BatchId = batchId }); jobs.EnqueueCommand(new() { DependsOnBatch = batchId }); ``` A whole batch can be cancelled or have its failed Jobs requeued in one call: ### Keep each customer's Jobs in order Jobs that share a `ConcurrencyKey` run one at a time, while Jobs with different keys still run in parallel. Each tenant's syncs, each account's ledger updates or each document's edits are applied in order, without serialising everyone else behind them: ```csharp jobs.EnqueueCommand(sync, new() { ConcurrencyKey = $"tenant:{sync.TenantId}", TenantId = sync.TenantId, // also filter and report by tenant }); ``` ## Get results back Background work doesn't have to mean fire-and-forget. **Await a Job's result** in the same request to keep a synchronous API while the heavy lifting runs on whichever server has capacity, or give it a `ReplyTo` and have its result **delivered** the moment it completes - POSTed to a webhook URL, or published to an MQ: When a `ReplyTo` can come from your users, restrict where results can be sent so your servers can't be used to reach internal addresses: ```csharp services.AddPlugin(new DatabaseJobFeature { ValidateReplyTo = JobReplyTo.AllowUrlPrefixes(["https://hooks.example.org/"]), }); ``` ## Schedules you can trust [Recurring Tasks](https://docs.servicestack.net/recurring-tasks) are now durable and safe to run on every server - each occurrence is only ever queued once, however many servers evaluate the schedule. Schedules run in the time zone you choose, can skip an occurrence while the previous one is still running, and can be limited to a date range or number of runs: ```csharp var schedule = Schedule.Cron("0 9 * * MON-FRI"); schedule.TimeZoneId = "America/New_York"; schedule.OverlapPolicy = ScheduleOverlapPolicy.Skip; schedule.EndDate = new DateTime(2026, 12, 31); jobs.RecurringCommand("Daily Digest", schedule); ``` ## See what your Jobs are doing A job platform is only as good as your ability to see into it. The **Background Jobs Admin UI** now shows per-queue statistics and wait times on its dashboard, so a backlog shows up before Jobs start timing out: It also gains queue controls, the Nodes view, each Job's failed attempts, batch progress, pause and run-now controls for Scheduled Tasks, replaying a completed Job, and bulk actions to cancel or requeue Jobs by queue, tag or batch. Running Jobs stream their progress and logs live: Outside the Admin UI, Background Jobs plugs into the tools your team already uses: A `JobsHealthCheck` reports a growing backlog, a long wait or servers that stopped reporting to [ASP.NET Core health checks](https://docs.servicestack.net/background-jobs-monitoring#health-checks), and Jobs publish OpenTelemetry traces and metrics under `ServiceStack.Jobs`. Each Job continues the trace of the request that queued it - even after a retry, or when another server recovers it - so an API call and the work it started appear in the same trace: ```csharp services.AddHealthChecks() .AddCheck("background-jobs"); services.AddOpenTelemetry() .WithTracing(x => x.AddSource(JobsDiagnostics.Name)) .WithMetrics(x => x.AddMeter(JobsDiagnostics.Name)); ``` ## Keeping your database lean Your jobs database is a work queue, not a blob store, so v10.3 bounds what goes into it. Requests larger than `MaxRequestBodyChars` (1M chars) are rejected when queued, oversized Responses aren't stored, Job logs are capped at `MaxJobLogChars`, and you can opt in to `JobSummaryRetention` and `ArchiveRetention` to delete old history automatically: ```csharp services.AddPlugin(new DatabaseJobFeature { JobSummaryRetention = TimeSpan.FromDays(90), ArchiveRetention = TimeSpan.FromDays(365), }); ``` ## Upgrading Background Jobs upgrades its database schema automatically on startup. The changes are additive, so your history and Scheduled Tasks are preserved, but there are a few things to prepare for: - **Let your queue drain first** - Jobs that are still queued, delayed, retrying or running when the upgrade is applied are cleared, and kept in your history as Cancelled with `QueueClearedOnUpgrade` - **Upgrade every server at once** - when running the RDBMS provider on multiple servers, stop them all before starting the new version rather than doing a rolling deploy - **Check your concurrency** - Jobs without a named Worker now run at most `MaxConcurrentJobs` at a time per queue (default: the number of CPU cores) - **`ReplyTo` is now delivered** - if you used it to store your own data, move it to `Args` or `Meta` See [Upgrading to v10.3](https://docs.servicestack.net/releases/v10_03#upgrading-to-v10.3) in the Release Notes for the full checklist, schema changes and behaviour changes to review. ## Get Started Background Jobs is available in every ServiceStack App on **.NET 8+**. Existing Apps get the reliability improvements with the upgrade, while new features are opt-in per Job or per queue. Use [DatabaseJobFeature](https://docs.servicestack.net/background-jobs-rdbms) on PostgreSQL, SQL Server or MySQL to scale out across servers, or [SQLite Background Jobs](https://docs.servicestack.net/background-jobs-sqlite) for a single server. To add RDBMS Background Jobs to an existing App, run: :::sh npx add-in db-jobs ::: Or for SQLite Background Jobs: :::sh npx add-in jobs ::: Then open **Background Jobs** in the [Admin UI](https://docs.servicestack.net/admin-ui) to see your queues, batches and schedules. To dive deeper, the Background Jobs docs have been reorganized into focused guides: # Startup Tasks - Generate code from your running App Source: https://servicestack.net/posts/startup-tasks ## A Source Generator that runs inside your App C# [Source Generators](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) changed how .NET developers think about boilerplate: describe something once, and the compiler keeps the derived code up to date for you. Nobody has to remember to run anything. But source generators only run at compile time. They only see your source code, they can only emit C#, and the generated code can only go into the assembly being compiled. Much of the code a ServiceStack App depends on is derived from the **running** App instead: its registered Services, its plugins, its configuration and even the URLs it's listening on. That's where the new **Startup Tasks** come in. They're a place to register dev tasks that run automatically **once your App has fully started**, with access to the fully initialized `AppHost` and all its plugins: ```csharp StartupTasks.Register("dtos", () => appHost.GetPlugin().GenerateDtos()); ``` Startup Tasks: - **Run automatically** after ASP.NET Core reports the App has started - **Only run in development** when `DebugMode` is enabled, so they're disabled in `Production` - **Can do anything** a fully running App can do - generate code in any language, rebuild an index, seed content - **Are isolated** - failures are logged without stopping the App or any of the other tasks Think of them as source generators for everything a compiler can't see, without being limited to generating C#. ## No more `npx get-dtos` The main use-case that motivated the feature is keeping [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) client DTOs in sync with your server. Previously after adding or changing a Request DTO, Response DTO or Service, you would also need to regenerate your client DTOs with: :::sh npx get-dtos ::: It's a small step, but one that's easy to forget. When it's missed, the client keeps compiling against a stale contract and the mismatch only shows up at runtime, or gets committed to source control. With the `dtos` Startup Task registered, **restarting your App is enough**. It finds every existing `dtos.*` reference in your project - `dtos.ts`, `dtos.mjs`, `dtos.py`, `dtos.dart`, `admin.dtos.ts`, etc. - and regenerates it from the App's own metadata, honoring the options in each file's header. The generation runs entirely in-process. It doesn't need Node.js or make an HTTP request, and it only updates files that belong to the current App, so a DTO pointing to another API, staging or production server is never overwritten. Files are also left alone when only the generated `Date` header would change, so file-watching tools like `dotnet watch` or Vite don't end up in a rebuild loop. This makes for a much tighter feedback loop during full-stack development: 1. Change a Service or DTO 2. Restart the App (or let `dotnet watch` do it) 3. Your TypeScript client immediately reports any code broken by the new contract ### Add it to your project All ServiceStack project templates which use TypeScript `.ts` or JavaScript `.mjs` DTOs already include the `dtos` Startup Task. To add it to an existing project, run: :::sh npx add-in startup-dtos ::: Which adds the registration in its own `Configure.StartupTasks.GeneratedDtos.cs`: ```csharp public class ConfigureGeneratedDtos : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(afterAppHostInit: appHost => { StartupTasks.Register("dtos", () => appHost.GetPlugin().GenerateDtos()); }); } ``` ## One less thing for AI Assistants to get wrong Removing a manual build step doesn't just help developers, it's even more valuable for AI Assistants. An AI agent adding a new API needs to know that client DTOs are generated, which command regenerates them, and that it must be run against the right server URL. When it doesn't, it tends to do the worst possible thing: hand-write or "fix up" the client types itself, spending tokens on code it shouldn't be writing and quietly letting the client drift from the server contract. With Startup Tasks there's nothing for the agent to know. It only has to change the C# DTOs and Services, which are the source of truth, and the client contracts follow on the next restart: - **No client contracts to generate** - agents never write or maintain `dtos.ts` by hand - **No project-specific commands to discover** - no instructions needed in `AGENTS.md` or `CLAUDE.md` - **Fast, typed feedback** - the TypeScript compiler flags client code affected by the change, which agents can act on straight away - **Smaller, cleaner diffs** - generated files only change when the API contract actually changes The same workflow works whether a change was made by a developer, an AI Assistant or both. ## Beyond DTOs Startup Tasks can run any parameterless dev task that requires a fully started App. For example, Apps using `PdfFeature` can keep the typed C# models of their PDF templates in sync with their `.ui.json` schemas: ```csharp StartupTasks.Register("pdf", () => appHost.GetPlugin().GeneratePdfs()); ``` Or use them for your own development conveniences, like keeping a local search index up to date: ```csharp StartupTasks.Register("search-index", () => appHost.Resolve().Update()); ``` We recommend keeping each registration in its own `Configure.StartupTasks.*.cs` file, e.g: ```text Configure.StartupTasks.GeneratedDtos.cs Configure.StartupTasks.GeneratedPdfs.cs Configure.StartupTasks.SearchIndex.cs ``` Use [App Tasks](https://docs.servicestack.net/app-tasks) instead for tasks that should be explicitly run from the command-line, accept arguments and exit when complete, e.g. database migrations. ## Learn more See the [Startup Tasks docs](https://docs.servicestack.net/startup-tasks) for more on configuring `GenerateDtos()` with `GenerateDtosOptions`, how it determines which DTO files belong to your App, and how to inspect its `GenerateDtosResult`. # OrmLite gets type-safe JSON access and Upserts Source: https://servicestack.net/posts/omlite-json-upsert ## Two everyday data problems, solved properly Two tasks come up in almost every data-driven application, and both have traditionally forced an awkward compromise. **Querying JSON columns.** Semi-structured data - order payloads, event bodies, settings, integration responses - fits naturally in a JSON column. But the moment you need to filter on something inside it, you either give up typed queries and hand-write provider-specific JSON SQL, or flatten the document into columns it doesn't naturally fit. **Insert-or-update.** Imports, synchronization jobs, event consumers and retryable workers all want the same thing: *create this row, or bring the existing one up to date*. Doing that safely has meant querying whether the row exists first - an extra round trip, with a race window between the check and the write. **ServiceStack v10.2** makes both first-class in OrmLite, with the same typed APIs across **SQLite**, **PostgreSQL**, **SQL Server** and **MySQL**: ```csharp // Filter on values inside a JSON document with ordinary C# var priorityWaOrders = db.Select(db.From() .Where(x => Sql.Json(x.Data).Customer.Address.State == "WA" && Sql.Json(x.Data).Tags.Contains("priority"))); // Insert the row if it's new, update it if it isn't - in one statement db.Upsert(customer); ``` ## Portable, type-safe JSON queries `Sql.Json()` marks a column as a JSON document with C# Data Model `T`, then translates ordinary member access, collection membership and array indexes into the configured database's **native JSON functions**: ```csharp var q = db.From() .Where(x => Sql.Json(x.Data).Customer.Address.State == "WA" && Sql.Json(x.Data).Tags.Contains("priority") && Sql.Json(x.Data).Lines[0].Quantity > 1); var orders = db.Select(q); ``` You keep the flexibility of document-shaped data without giving up what makes OrmLite queries valuable: they're compiled, refactor-safe, and committed to C# rather than to one database's dialect. Rename a property on `OrderDocument` and the query refactors with it. Move from SQLite in development to PostgreSQL in production and the same query compiles and runs unchanged. ### The example documents The examples use an `OrderEvent` table with a JSON column, described by an ordinary C# Data Model: ```csharp public class OrderEvent { public long Id { get; set; } public string Data { get; set; } // JSON stored and queried as text public OrderDocument Document { get; set; } // Complex property serialized as JSON } public class OrderDocument { public Customer Customer { get; set; } public List Lines { get; set; } public List Tags { get; set; } public decimal Total { get; set; } } public class Customer { public Address Address { get; set; } } public class Address { [DataMember(Name = "shipping_state")] public string State { get; set; } } public class OrderLine { public string Sku { get; set; } public int Quantity { get; set; } } ``` With the current fluent configuration, complex properties like `Document` are stored as JSON by default on all four databases - so typed documents can be inserted like any other Data Model: ```csharp services.AddOrmLite(options => options.UseSqlite(connectionString)); // or options.UsePostgres(...) / UseSqlServer(...) / UseMySql(...) ``` ### Everything you'd expect C# to do Nested member access becomes a nested JSON path, and scalar results are converted to the member's C# type. `[DataMember(Name=...)]` is honored, so `State` above maps to `$.Customer.Address.shipping_state`: ```csharp var q = db.From() .Where(x => Sql.Json(x.Data).Customer.Address.State == "WA" && Sql.Json(x.Data).Total >= 100m); ``` Arrays and lists work the way they read - zero-based indexes (constants or captured variables), `Contains()` for membership and `Count` or `Length` for size: ```csharp var index = 0; var q = db.From() .Where(x => Sql.Json(x.Data).Lines[index].Sku == "A-1" && Sql.Json(x.Data).Lines.Count > 0 && Sql.Json(x.Data).Tags.Contains("priority")); ``` Once a scalar is extracted, further operations go through OrmLite's normal expression translation - so `Sql.Json(x.Data).Customer.Address.State.Length == 2` extracts the string and then applies the database's own string-length function. When the column is already a JSON-serialized complex property, its document type is inferred: ```csharp var q = db.From() .Where(x => Sql.Json(x.Document).Customer.Address.State == "WA"); ``` ### Project straight into typed results Selecting a scalar member extracts its C# value. Selecting an object or collection returns a JSON fragment that OrmLite **deserializes back into its C# Data Model** - so a single query can mix relational columns, JSON scalars and whole nested objects: ```csharp var q = db.From() .Select(x => new { x.Id, State = Sql.Json(x.Data).Customer.Address.State, Total = Sql.Json(x.Data).Total, Address = Sql.Json(x.Data).Customer.Address, }); // OrderSummary { long Id; string State; decimal Total; Address Address; } List summaries = db.Select(q); Address address = db.Scalar
(db.From() .Where(x => x.Id == id) .Select(x => Sql.Json(x.Data).Customer.Address)); ``` ## Dynamic documents and runtime paths Not every document has a C# Data Model, and not every path is known at compile time. An explicit SQL/JSON path API covers those cases - and the operations that have no member-access equivalent: ```csharp var totalPath = "$.Total"; var q = db.From() .Where(x => Sql.JsonValue(x.Data, "$.Customer.Address.shipping_state") == "WA" && Sql.JsonValue(x.Data, totalPath) >= 100m) .OrderByDescending(x => Sql.JsonValue(x.Data, totalPath)); ``` The complete API includes validation, path existence, type inspection, arrays and containment: | Task | Preferred typed expression | Explicit path API | | --- | --- | --- | | Read a scalar | `Sql.Json(json).Member` | `Sql.JsonValue(json, path)` | | Read an object or array | `Sql.Json(json).Member` | `Sql.JsonQuery(json[, path])` | | Array length | `Sql.Json(json).Items.Count` | `Sql.JsonArrayLength(json[, path])` | | Scalar array membership | `Sql.Json(json).Items.Contains(value)` | `Sql.JsonArrayContains(json[, path], value)` | | Array indexing | `Sql.Json(json).Items[index]` | Include `[index]` in the path | | Validate JSON | - | `Sql.IsJson(json)` | | Test whether a path exists | - | `Sql.JsonExists(json, path)` | | Read a JSON value's type | - | `Sql.JsonType(json[, path])` | | Document containment | - | `Sql.JsonContains(json, candidate[, path])` | A few of these are worth calling out: ```csharp // Count rows holding valid JSON var validRows = db.Count(x => Sql.IsJson(x.Data) == true); // Rows where a path exists - a present JSON null still counts var withTags = db.Select(db.From() .Where(x => Sql.JsonExists(x.Data, "$.Tags"))); // A normalized JsonValueType, whatever the database calls it var type = db.Scalar(db.From() .Where(x => x.Id == id) .Select(x => Sql.JsonType(x.Data, "$.Customer.Address"))); // JsonValueType.Object // PostgreSQL & MySQL: does the document contain this partial document? var candidate = new { Customer = new { Address = new { shipping_state = "WA" } } }; var matching = db.Select(db.From() .Where(x => Sql.JsonContains(x.Data, candidate))); ``` ### `null` and missing aren't the same thing Most JSON wrappers blur JSON `null`, a missing property and SQL `NULL` together. OrmLite keeps them distinguishable wherever the databases allow it: `Sql.JsonExists()` is `true` for a present JSON `null` and `false` for a missing path, while `Sql.JsonType()` returns `JsonValueType.Null` for JSON `null` and C# `null` for a missing path. Together they let an application tell "explicitly cleared" apart from "never set". ## Safe and predictable by construction - **Values are parameters.** Query values and containment candidates are sent as database parameters, never concatenated into SQL. JSON paths are emitted as escaped string literals for the selected dialect. - **Unsupported means an exception, not a surprise.** Operations a database can't express faithfully throw `NotSupportedException` while the expression is being built, rather than emitting SQL that quietly means something else. - **It's still just a `SqlExpression`.** JSON expressions compose with every other OrmLite filter, projection and ordering, work with the async APIs, and expose their SQL for inspection: ```csharp var sql = q.ToSelectStatement(); var parameters = q.Params; var results = await db.SelectAsync(q); ``` Inspecting the generated SQL is also the best way to decide when a frequently queried JSON property deserves a generated column or JSON index in your database. ## Native Upsert APIs OrmLite's new `Upsert` APIs express the insert-or-update workflow directly. When the Primary Key is new the row is inserted; when it already exists the row is updated: ```csharp var customer = new Customer { Id = 1, Name = "Initial Name", Email = "initial@example.org", }; db.Upsert(customer); // Inserts Id=1 customer.Name = "Updated Name"; db.Upsert(customer); // Updates Id=1 ``` On SQLite, PostgreSQL, SQL Server and MySQL/MariaDB this compiles to each database's **native single-statement conflict handling**. There's no separate existence query as with `Save()`, and no race window between checking for a row and writing it. Whether a message is delivered once or three times, or a job succeeds on its first attempt or its fifth, the table converges on the same state. | Database | Generated operation | | --- | --- | | SQLite | `INSERT ... ON CONFLICT (PrimaryKey) DO UPDATE` | | PostgreSQL | `INSERT ... ON CONFLICT (PrimaryKey) DO UPDATE` | | SQL Server | `MERGE ... WITH (HOLDLOCK)` matching the Primary Key | | MySQL / MariaDB | `INSERT ... ON DUPLICATE KEY UPDATE` | Other providers fall back to `Save()`-style behavior with the same semantics, using an existence check followed by an `INSERT` or `UPDATE`. ### Update only the fields you own Real rows often have fields owned by different parts of an application. A typed `updateOnly` expression restricts which fields change when the row already exists - while a new row still gets every insertable field: ```csharp public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } [IgnoreOnUpdate] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; } db.Upsert(customer, updateOnly: x => new { x.Name, x.Email }); ``` That makes it safe to submit a complete Data Model from a sync job without clobbering columns maintained elsewhere. Primary Key and RowVersion fields can never be updated, and `[IgnoreOnUpdate]` properties like `CreatedDate` stay excluded from updates - so the original creation time survives every subsequent upsert. When the field set is chosen at runtime, use the string field-name overload: ```csharp var fields = includeEmail ? new[] { nameof(Customer.Name), nameof(Customer.Email) } : new[] { nameof(Customer.Name) }; db.Upsert(customer, updateOnly: fields); ``` ### Batches, async and auto-increment keys `UpsertAll` inserts new rows and updates existing ones together **in a transaction**, and accepts the same `updateOnly` options: ```csharp db.UpsertAll(customers); db.UpsertAll(customers, updateOnly: x => new { x.Name, x.Email }); ``` Every single-row, batch, typed-field and runtime-field API has an async equivalent with optional `CancellationToken` support: ```csharp await db.UpsertAsync(customer, token: cancellationToken); await db.UpsertAllAsync(customers, updateOnly: x => new { x.Name, x.Email }, token: cancellationToken); ``` Auto-increment models work too. An `[AutoIncrement]` Primary Key with its default value inserts a new row and populates the generated ID, and subsequent calls use that ID as the conflict key: ```csharp var customer = new Customer { Name = "New Customer", Email = "new@example.org" }; db.Upsert(customer); // Inserts and populates customer.Id customer.Name = "Updated Customer"; db.Upsert(customer); // Updates the same row ``` :::info MySQL and MariaDB's `ON DUPLICATE KEY UPDATE` can also be triggered by a secondary `UNIQUE` constraint. Apps that require strict Primary-Key-only matching can set `MySqlDialect.Instance.UseNativeUpsert = false` to use OrmLite's existence-check fallback instead. ::: ## Better together The two features fit naturally in the kind of code that motivated them. An event consumer can upsert document-shaped payloads idempotently, and the rest of the application can query inside them with typed, portable expressions: ```csharp // Idempotent: replaying the same events converges on the same rows await db.UpsertAllAsync(events.Map(e => new OrderEvent { Id = e.Id, Document = e.Order, }), token: token); // Typed queries inside the stored documents, on any supported database var toShip = await db.SelectAsync(db.From() .Where(x => Sql.Json(x.Document).Tags.Contains("paid") && Sql.Json(x.Document).Lines.Count > 0)); ``` ## Get Started JSON queries and Upsert are available in **ServiceStack v10.2** - upgrade your `ServiceStack.OrmLite.*` packages and both are ready to use on SQLite, PostgreSQL, SQL Server and MySQL/MariaDB, with nothing new to install or configure. See the docs for the complete reference: - [OrmLite JSON Support](https://docs.servicestack.net/ormlite/json) - supported operations, typed projections, dynamic paths, null semantics and database-specific capabilities - [OrmLite Upsert](https://docs.servicestack.net/ormlite/upsert) - batch and async APIs, auto-increment keys, selective updates and choosing between `Upsert`, `Save`, `Insert` and `Update` - [v10.2 Release Notes](https://docs.servicestack.net/releases/v10_02) - everything else in this release # Hardened ServiceStack - A Codebase-Wide Security & Reliability Audit Source: https://servicestack.net/posts/hardened-servicestack ## Security that arrives with the upgrade Most of the code that decides whether your App is secure isn't code you wrote. It's the request pipeline that parses every body, the serializer that turns hostile JSON into objects, the auth providers that compare password hashes, the virtual file system that resolves paths and the background workers that keep running long after deployment. When that layer has a weakness, every App built on it inherits it - and no amount of careful application code can compensate. For **ServiceStack v10.2** we put that entire layer through a codebase-wide, AI-assisted security and reliability audit. The review covered ServiceStack's core runtime, authentication, data access, messaging, cloud integrations, UIs, serialization, image processing and client libraries - **36 packages** in all - and resolved the issues it found. The value for existing Apps is that **this work happens beneath your code**. The trust boundaries, parsers and background workers your App already relies on now handle malformed input, hostile input and concurrent load more defensively. There's nothing new to install or learn: upgrade your packages and the hardening comes with them. Every change is published in a per-package `SECURITY_CHANGES.md` report that your security team can review line by line. ## Hostile input can no longer become code The most serious class of framework vulnerability is the one where data becomes behavior. Several paths where an attacker-controlled payload could influence which types get created have been closed: - **Untrusted runtime types are restricted.** `[Serializable]` has been removed from the attributes that permit polymorphic type creation in ServiceStack.Text. Many built-in .NET types carry that attribute, which made it far too broad an allowlist. Runtime types now require `[RuntimeSerializable]` or `[DataContract]`. - **A process-wide deserialization bypass is gone.** Constructing the Azure Service Bus MQ server previously set `JsConfig.AllowRuntimeType = _ => true` - silently disabling runtime type validation for the *entire host process*. That global assignment has been removed. - **`BinaryFormatter` paths are deprecated.** The legacy Redis `ObjectSerializer`, `OptimizedObjectSerializer` and `SerializingRedisClient` relied on `BinaryFormatter` and are now marked `[Obsolete]` with a clear warning. The primary `RedisClient` APIs never used them. ## Hostile documents can't take down your process A server that can be crashed by one request isn't secure, whatever else it gets right. The parsers that read untrusted input now bound the work any single document can cause: - **Stack exhaustion is prevented.** Deeply nested or cyclic JSON and JSV payloads could previously recurse until an *uncatchable* `StackOverflowException` terminated the host. Every JSON, JSV, collection and dictionary parser now tracks depth and throws a normal `SerializationException` past `JsConfig.MaxDepth` (default `50`). - **XML is parsed safely.** `XmlSerializer.DeserializeFromStream` now reads through an `XmlReader` that prohibits DTD processing and limits document size - closing **XXE**, **SSRF** and "billion laughs" entity-expansion attacks. - **Regex backtracking is bounded.** Patterns vulnerable to catastrophic backtracking (ReDoS) in markup stripping, SQL fragment validation, cache key matching, user-agent parsing and OpenAPI filters were rewritten or given explicit match timeouts. - **Caches can't be flooded.** Random `__type` names could previously grow the type-lookup cache without limit; negative lookups are now held in a separate cache capped at 1,000 entries. ## Queries, pages and exports mean what you wrote Injection bugs are about attacker-controlled text changing the meaning of something - a SQL statement, an HTML page or a spreadsheet: - **SQL fragment validation can't be bypassed with an unclosed quote.** OrmLite verifies user-supplied fragments such as `q.OrderBy(userInput)`. An input with an unmatched quote like `'; DROP TABLE Users; --` previously caused everything after the quote to be stripped before verification, so the empty remainder passed as safe. Unclosed quotes are now treated as unsafe and rejected, and schema names are quoted in generated DDL. - **CSV exports are safe to open in Excel.** Cells beginning with `=`, `+`, `-` or `@` can execute formulas when a user opens an exported file. `CsvConfig.EscapeFormulas` is now **enabled by default**, prefixing dangerous non-numeric cells so spreadsheets treat them as text - and unescaping them transparently when read back. - **Dynamic HTML and JavaScript are encoded.** Values embedded in HTML format pages, Swagger UI and Blazor HTML helpers are now properly encoded, including escaping `<`, `>` and `&` in JavaScript strings so a value can't break out of a ` ``` The self-contained widget streams Markdown answers with citations, suggested questions and a welcome message, supports maximized reading and Ctrl/⌘+K launch, and keeps each visitor's session across page loads. It renders inside a **Shadow DOM**, so the host site's CSS can't break it and its keyboard handling can't trigger the site's shortcuts. Float it in a corner, or mount its launcher inside your own nav bar. ### The host page can style it. It can't weaken it. Retrieval scope, the private system prompt, model selection, allowed origins and rate limits stay on the server - they never appear in the embed code. The host page may override safe presentation choices like theme, accent color, launcher icon and position, and nothing else. Answers can be held to an evidence standard that's enforced on the server, not merely requested in a prompt. With **Require retrieved evidence** enabled, a response that comes back with fewer than your **minimum citations** is replaced by your fallback message - and streaming is buffered until the evidence has been checked, so unsupported text is never shown to a visitor. ### Purpose-built assistants from one knowledge base Seven editable **behavior templates** - documentation guide, technical troubleshooter, customer support, developer/API assistant, product advisor, onboarding guide and policy & procedures - provide strong starting points. Combined with response styles, custom prompts and document scopes, the same knowledge base can power several focused experiences without duplicating its content. Every Assistant is fully brandable with **Auto**, **Light**, **Dark**, **Nord**, **Matrix** and **Soft Pink** themes, each a preset you can override color by color, plus a customizable launcher: Assistants follow a complete draft, publish, unpublish, archive and restore lifecycle. Regenerating a deployment ID invalidates old embeds immediately, and **Run diagnostics** checks publication, store access, indexed documents, model selection, allowed origins and the widget URL before customers ever see it. ## Publish site search that costs nothing per query The same imported documents also build a **full-text index inside the database your App already uses**, so one content pipeline powers two public experiences. **Website Search** is completely independent of Gemini: queries never reach a model, never incur usage costs and never leave your App. ```html ``` The widget gives visitors the interaction they already expect from a documentation site: Ctrl/⌘+K and / shortcuts, keyboard navigation, grouped results with match highlighting, infinite scrolling and recently opened links. Results open the document's canonical page and anchor, or a rendered in-place preview when it has none. ### Tune relevance against real results One consistent ranking model runs across every database, with tunable weights for titles, headings, body text, exact phrases, exact titles, content freshness and preferred document types. Every adjustment re-queries immediately against the same term, so relevance is tuned against actual results instead of guessed at - and across a multi-site store you decide which sources and document types should win. ## Learn what your visitors are looking for ### Search analytics Every published Search reports its own demand and quality signals without adding any model usage: total searches, click-through rates, popular documents and the latest queries with their originating page. Related queries are grouped by normalized wording so near-identical phrasings count as one intent, and **no-result searches point directly at the documentation you haven't written yet**. ### Optional first-party website analytics Because the Search script is already on every page, it can optionally double as a lightweight website analytics system - page views, visitors, sessions, bounce rate, load times, referrers, campaigns, devices and platforms, charted over 24 hours, 7, 30 or 90 days. It's **off by default** and privacy-respecting by design: no cookies, no precise location, IPs anonymized to an IPv4 `/24` or IPv6 `/48`, Do Not Track honored, known bots and your own IPs excluded, retention enforced automatically, and an optional consent callback to gate collection entirely. IP geography is only resolved if you explicitly register a resolver. The data stays in your database rather than a third party's. ### Close the loop with every conversation Website Assistant conversations, messages and citations are retained server-side for your team to review, with each conversation showing the page it started on. Discover what visitors actually need, find weak or missing coverage and improve the source material - then the next synchronized import improves every Assistant and search result grounded in it. ## Maintainable long after the first demo The hard part of a knowledge base isn't the first import - it's knowing months later what's indexed, what drifted and what quietly failed. Gemini RAG is built for that second year: - **Restart-safe workers** - both the Gemini upload worker and the local Search indexer track desired and completed hashes in the database, so interrupted work resumes instead of disappearing. - **Failures stay visible** - a failed document keeps its provider error for inspection and one-click retry rather than silently vanishing from the corpus. - **Reconciliation** - **Sync Store** compares your catalogue against Gemini and reports missing documents, metadata drift and duplicate indexed copies, with links straight to the affected filter. - **Index health** - total, indexed, pending, stale and failed counts, the active database provider, last successful index time and recent errors, at a glance. - **Pre-flight diagnostics** - validate every Search and Assistant deployment before customers hit it. - **Deletion you can reason about** - destructive operations summarize exactly what they cascade through and require typed-name confirmation. ## No vector database bill Managed RAG architectures commonly carry a recurring charge just to keep embeddings stored. With Gemini File Search, Google currently charges for embeddings when documents are **indexed**, while **file storage and query-time embeddings are free**: An organization can therefore maintain durable knowledge bases and pay primarily when content is indexed and when users actually ask - while previewed imports, content hashing and deduplication make sure the same document is never paid for twice. Website Search adds no model cost at all. :::info Pricing and limits are Google's and can change - check the current [Gemini File Search pricing and limits](https://ai.google.dev/gemini-api/docs/file-search) before deploying. ::: ## Get Started Gemini RAG is a built-in AI Chat extension for **.NET 8+** ServiceStack Apps. It enables itself automatically once the App has a Gemini API key and an `IDbConnectionFactory` for its local catalogue. **1. Add AI Chat** to your App: npx add-in chat **2. Add a Gemini API key** from [Google AI Studio](https://aistudio.google.com/) to the App's environment, and make sure at least one Google Gemini chat model is enabled in AI Chat: ```bash GOOGLE_API_KEY=your_api_key # or GEMINI_API_KEY=your_api_key ``` **3. Create a File Store** from the **Gemini** icon in the `/chat` toolbar, then import a few documents. The quickest start is a folder import pointed at the docs folder of a repo you already have. **4. Ask your first grounded question** from **New Chat** and expand **Sources** to inspect the evidence. **5. Publish** a **Search** widget for instant navigation and an **Assistant** for grounded answers - two script tags, one set of curated documents. See the [Gemini RAG](https://docs.servicestack.net/rag) overview, or dive into the docs: - [Overview & Setup](https://docs.servicestack.net/chat/gemini-rag) - [Importing Documents](https://docs.servicestack.net/chat/gemini-imports) - [Crawling Websites](https://docs.servicestack.net/chat/gemini-crawling) - [Metadata & Source URLs](https://docs.servicestack.net/chat/gemini-metadata) - [Explore & Ask](https://docs.servicestack.net/chat/gemini-explore) - [AI Assistants](https://docs.servicestack.net/chat/gemini-assistants) - [Website Search](https://docs.servicestack.net/chat/gemini-search) - [Search Analytics & Privacy](https://docs.servicestack.net/chat/gemini-analytics) - [Operations & Troubleshooting](https://docs.servicestack.net/chat/gemini-operations) # ServiceStack PDF Studio — From AI Design to Production PDFs Source: https://servicestack.net/posts/servicestack-pdf ## The complete PDF lifecycle for .NET Apps :::youtube g7gDCveT2GY ServiceStack PDF Overview ::: Here is what we want at the end: an ordinary ServiceStack API that returns a real invoice, with no LLM anywhere near it. ```csharp public class InvoiceServices(IPdfRenderer pdf) : Service { public async Task Any(GetOrderInvoice request) { var order = await Db.LoadSingleByIdAsync(request.Id); return await pdf.PdfResultAsync(MapToInvoice(order), $"Invoice-{order.InvoiceNo}.pdf"); } } ``` And here is the part that normally takes a week: designing the document, agreeing its data contract, testing the awkward payloads, and getting a typed C# model that matches. In PDF Studio you describe the document you want — or hand a vision model a screenshot of the one you're replacing — and iterate on a live preview until it's right. [![](/img/pages/pdf/designer-overview.webp)](/img/pages/pdf/designer-overview.webp) Generating a PDF is easy. Maintaining a production document system is not. Invoices, statements, certificates, reports, shipping labels and contracts need precise layouts, realistic test data, reusable styles, typed application integration, predictable deployment and a safe way to evolve without breaking documents customers depend on. ServiceStack's new PDF support addresses the whole lifecycle: ```text AI Chat PDF Studio → Publish → Admin PDF → Generate C# → Render in your App ``` PDF Studio at `/chat/pdf` is an AI-assisted development environment for creating [Typst](https://typst.app) templates. Publishing promotes a validated, flattened and versioned artifact into `App_Data/pdf`. Admin PDF at `/admin-ui/pdf` lets administrators test the exact production template, inspect its contract and generate strongly typed C# models. `IPdfRenderer` then renders PDFs from ordinary application code without calling an LLM. The result combines the speed of AI-assisted design with the deterministic runtime needed by business applications. ### Two plugins, so AI never reaches production | Capability | Plugin | Runtime dependencies | | --- | --- | --- | | AI-assisted authoring and live preview | `ChatFeature` PDF extension | Typst and an AI provider | | Published template management and rendering | `PdfFeature` | Typst | An organization can design documents in a development environment with AI Chat, then deploy only `PdfFeature`, Typst and the published artifacts. **Production rendering does not require ChatFeature, an AI provider or an API key.** That clean boundary is fundamental: AI accelerates authoring, but does not sit in the path of every invoice your application generates. ## Why Typst ServiceStack uses Typst because it brings modern, code-first authoring to high-quality document layout. Templates are plain text, compile quickly and can express precise typography, tables, page structure, reusable functions and conditional content. A document named `invoice` normally contains: ```text invoice.typ invoice.json invoice.ui.json invoice.fixture.empty.json invoice.fixture.long.json lib/v1.typ ``` Each file has one clear responsibility: - `invoice.typ` owns layout and rendering. - `invoice.json` supplies realistic preview data. - `invoice.ui.json` describes the data contract as JSON Schema. - `invoice.fixture.*.json` exercises important payload shapes. - `lib/v1.typ` supplies versioned shared styles and helpers. This separation makes templates easier to design, test, review and integrate than documents where layout and business data are inseparable. ## Design in AI Chat PDF Studio PDF Studio opens with a Typst editor and a real rendered PDF preview. Edit the template or its data and the document recompiles immediately. The workspace is per user under `App_Data/chat/user/{user}/pdf`, so developers and designers can experiment independently. Nothing becomes a shared runtime template until an administrator explicitly publishes it. PDF Studio inherits [AI Chat's](/posts/ai-chat-v4) Integrated Auth, allowing existing application users to enter with their current ServiceStack identity whilst keeping drafts, assets and experiments isolated in their own workspace. Publishing is a separate Admin-authorized boundary, so access to the designer does not imply permission to change the templates used by production. The designer separates layout and content into focused panes: [![](/img/pages/pdf/designer-panes.webp)](/img/pages/pdf/designer-panes.webp) ### Form and Code views for document data The JSON data can be edited directly in Code View or through a form generated from `invoice.ui.json`. Form View provides labels, date inputs, enum selections and add/remove controls for line items without requiring users to edit JSON. [![](/img/pages/pdf/data-form.webp)](/img/pages/pdf/data-form.webp) [![](/img/pages/pdf/data-code.webp)](/img/pages/pdf/data-code.webp) Every edit uses the same data contract that will later generate the application's C# model. The preview is not a disconnected design mock-up; it is an executable example of the production template. ### Edit layouts with natural language The Edit with AI panel gives the model the current template, its data and referenced partials. Describe the change you want: > Add a Paid watermark when the outstanding balance is zero. > Move the totals into a bordered box and show tax on a separate row. > Reformat this as a 4×6 shipping label with no page margins. The model returns complete updated files and Studio compiles them immediately. If the first edit fails to compile, the model gets a repair pass with the compiler error. [![](/img/pages/pdf/edit-with-ai-before.webp)](/img/pages/pdf/edit-with-ai-before.webp) [![](/img/pages/pdf/edit-with-ai-after.webp)](/img/pages/pdf/edit-with-ai-after.webp) AI edits update editor buffers rather than silently committing files. Unsaved changes remain visible and the previous contents can be restored, making experimentation fast and reversible. ### Recreate an existing document with vision Starting from a blank page is rarely necessary. Attach screenshots, photos or rasterized pages from an existing PDF and ask a vision-capable model to reproduce the design as a reusable Typst template. [![](/img/pages/pdf/ai-attachment-new.webp)](/img/pages/pdf/ai-attachment-new.webp) [![](/img/pages/pdf/ai-attachment-edit.webp)](/img/pages/pdf/ai-attachment-edit.webp) The model separates visual layout from sample content and creates a new template and JSON data model. This is visual reconstruction—not structural PDF conversion—but it is an extraordinarily fast way to turn a legacy document into maintainable source. ### Design visually without memorizing Typst PDF Studio includes formatting controls for common design operations: - Font discovery and preview - Font size, weight and line height - Page sizes from A3–A6, Letter, Legal and presentation formats - Portrait and landscape orientation - Margins - Image and asset selection - Shared library previews [![](/img/pages/pdf/font-picker.webp)](/img/pages/pdf/font-picker.webp) [![](/img/pages/pdf/page-setup.webp)](/img/pages/pdf/page-setup.webp) The controls modify source rather than hiding it. Developers retain a readable Typst template whilst less experienced authors can make common visual changes confidently. ## Versioned design systems Shared document design belongs in a library, but changing that library should not unexpectedly reflow every historical template. New workspaces use explicit imports such as `lib/v1.typ`. Shared fonts, colors, formatters, headers and footers live in that version. An incompatible redesign becomes `lib/v2.typ`, allowing templates to migrate one at a time. `lib/v1.preview.typ` renders the design system itself so typography and components can be checked independently: [![](/img/pages/pdf/lib-preview.webp)](/img/pages/pdf/lib-preview.webp) PDF Studio tracks direct and transitive dependencies. A referenced library cannot be renamed or deleted until its dependants are moved, and publishing captures the exact imported library with the document's artifact set. This protects production documents from accidental global changes whilst still giving teams a coherent visual system. ## Publish is a controlled promotion boundary Design work remains private until an administrator chooses **Publish**. Publishing copies the template from the user's Studio workspace into the App's shared `App_Data/pdf` runtime directory. It performs substantially more work than a file copy: - Follows JSON data, Typst includes, images, assets and versioned libraries. - Flattens folder-based authoring into a self-contained runtime artifact set. - Rewrites local references for the flattened result. - Validates example data and named fixtures against `.ui.json`. - Exercises C# model generation. - Checks statically visible data paths. - Compiles every fixture through the actual flattened Typst template. - Generates the gallery preview. - Records publisher and source metadata. - Prevents silent takeover of a name published by another user. - Rolls back the publish if validation or compilation fails. - Saves every successful publish as an immutable revision. This changes publishing from “copy whatever currently works on my machine” into a repeatable release step. ## Contract fixtures catch the failures that matter One sample invoice cannot represent every production invoice. Add named fixtures beside the template: ```text invoice.fixture.empty.json invoice.fixture.long.json invoice.fixture.international.json invoice.fixture.maximum-items.json ``` At publish time, every fixture is checked against the JSON Schema and rendered with the flattened template. A payload can therefore be structurally valid yet still block publishing if long text, an empty collection or international characters trigger a Typst failure. The built-in validator covers the schema features generated by PDF Studio, including types, required members, object properties, arrays, enums, numeric and string bounds, patterns, date formats, local references and schema composition. This is practical contract testing for documents: validate both the data and its ability to render. ## Immutable history and reversible rollback Every successful publish creates a revision under: ```text App_Data/pdf/.versions/{template}/{revision}/ ``` The revision contains the complete published artifact set, preview and publishing metadata. Admin PDF's History view shows who published each version and when. Restoring a revision never edits or deletes history. It creates a new revision that records which version was restored, making rollback itself auditable and reversible. Unpublishing removes the live template but retains its history. This gives teams a lightweight, database-free release history that can be backed up with the rest of `App_Data/pdf`. ## Admin PDF manages what production can render Open `/admin-ui/pdf` to browse the templates currently available to the application. The Admin-only gallery shows publish-time thumbnails with search and sorting. Selecting a template opens a two-pane workspace containing editable data and the real PDF rendered through the production renderer. [![](/img/pages/pdf/saved-pdfs.webp)](/img/pages/pdf/saved-pdfs.webp) The data pane provides: - **Form** — schema-generated controls for testing without writing JSON. - **Data** — raw editable JSON for pasting real payloads. - **Code** — generated models and usage examples. The preview uses pdf.js and supports live rendering, zoom, fit, page count and download. [![](/img/pages/pdf/data-form-edited.webp)](/img/pages/pdf/data-form-edited.webp) [![](/img/pages/pdf/download-pdf.webp)](/img/pages/pdf/download-pdf.webp) **Edit** opens the published document back in PDF Studio. If another user created it, the published files are first copied into the current administrator's workspace. **Unpublish** removes the live runtime files whilst preserving revision history. Admin PDF is the handoff point between document authors and application developers. It answers three important questions with the real deployed artifact: 1. What templates can production render? 2. Does this template render with this data? 3. What typed code should the App use? ## Generate strongly typed C# contracts The `.ui.json` schema can generate strongly typed C# models directly from the Admin PDF Code tab: [![](/img/pages/pdf/generated-types-csharp.webp)](/img/pages/pdf/generated-types-csharp.webp) ```csharp public class LineItem { [JsonPropertyName("description")] public string Description { get; set; } = null!; [JsonPropertyName("qty")] public int Qty { get; set; } [JsonPropertyName("rate")] public decimal Rate { get; set; } } [Pdf("invoice")] public class Invoice { [JsonPropertyName("items")] public List Items { get; set; } = new(); } ``` `[Pdf("invoice")]` binds the root model to its published template. Dates, UUIDs, decimals, enums, required members and documentation are inferred from the schema instead of guessed from sample JSON. For all templates, configure `PdfCodeGen` and register an AppTask: ```csharp services.AddPlugin(new PdfFeature { PdfCodeGen = new() { Namespace = "MyApp.ServiceModel.Pdf", OutputPath = Path.Combine(contentRootPath, "../MyApp.ServiceModel/Pdf"), } }); AppTasks.Register("pdf", _ => appHost.GetPlugin().GeneratePdfs()); ``` Then regenerate after publishing template changes: ```bash dotnet run --AppTasks=pdf ``` Generated files contain content hashes. If a developer adopts and edits one, later generation skips it instead of overwriting their work. Include, exclude, naming and filtering options support larger applications with many templates. The Admin UI preview and AppTask use the same generator, so the code copied from the browser matches the code written into the project. ## Render from an ordinary ServiceStack API Inject `IPdfRenderer`, map application data into the generated model and return the PDF: ```csharp [Route("/orders/{Id}/invoice")] public class GetOrderInvoice : IGet, IReturn { public int Id { get; set; } } public class InvoiceServices(IPdfRenderer pdf) : Service { public async Task Any(GetOrderInvoice request) { var order = await Db.LoadSingleByIdAsync(request.Id); var invoice = new Invoice { InvoiceValue = new InvoiceDetails { Number = order.InvoiceNo, Date = order.OrderDate.ToString("d MMMM yyyy"), Due = order.DueDate, Currency = "$", }, Items = order.Details.Map(x => new LineItem { Description = x.ProductName, Qty = x.Quantity, Rate = x.UnitPrice, }), TaxRate = 0.10m, }; return await pdf.PdfResultAsync( invoice, $"Invoice-{order.InvoiceNo}.pdf"); } } ``` `PdfResultAsync` returns an `HttpResult` with `application/pdf` and the correct content-disposition filename. Pass `inline: true` to display it in the browser instead of downloading it. For other workflows the renderer can return bytes, write directly to a stream or rasterize a selected page to PNG. Runtime rendering is deterministic: - The `[Pdf]` model selects the template. - The model serializes to its JSON contract. - `IPdfRenderer` invokes the published Typst template. - No LLM is called. - No personal Studio workspace is read. - Only the live files in `App_Data/pdf` are used. ## Attach PDFs from background jobs The same renderer can generate an attachment inside a ServiceStack Command: ```csharp [Worker("smtp")] public class SendInvoiceEmailCommand( IPdfRenderer pdf, SmtpConfig config, IDbConnectionFactory dbFactory) : AsyncCommand { protected override async Task RunAsync( SendInvoiceEmail request, CancellationToken token) { using var db = await dbFactory.OpenAsync(token: token); var order = await db.LoadSingleByIdAsync( request.OrderId, token: token); var invoice = MapToInvoice(order); var bytes = await pdf.RenderPdfAsync(invoice, token); // Attach bytes to your email message and send it... } } ``` Queue an identifier, not rendered PDF bytes. The worker loads current data and renders inside the job, keeping persisted job messages small and retryable. This pattern works for scheduled statements, order confirmations, certificates, reports and any other document produced outside an interactive HTTP request. ## Production controls are built in PDF rendering starts external Typst processes, so `PdfFeature` includes practical operational limits: - Render and preview timeouts - Maximum concurrent renders - Maximum data payload size - Restricted Typst root directory - Flat validated template names - Admin role requirements for every Admin PDF API - Per-user, path-checked Studio workspaces - Publish-time validation enabled by default Production environments should pin the Typst version and deploy the same fonts used during validation. Put application fonts in `App_Data/pdf/fonts` and back up live artifacts, `.published.json` and `.versions` together. Typst's root restriction limits document file access but is not an operating-system sandbox. Organizations compiling untrusted templates should isolate compilation in an appropriate container or worker. ## Clear scope, extensible renderer The built-in feature is focused on generating PDFs from Typst templates. It supports authoring, AI editing, compilation, publishing, revisions, schema validation, code generation, PDF/PNG rendering and delivery. It does not claim to be a general PDF manipulation library. Merging, splitting, encryption, digital signatures, PDF/A, AcroForm filling, OCR and structural import belong in a dedicated PDF library or a custom `IPdfRenderer` pipeline. Likewise, importing a PDF into Studio gives a vision model rasterized reference pages from which to create a new Typst approximation. It does not preserve the original document's text objects, fonts, forms, annotations or metadata. These boundaries keep the built-in workflow dependable whilst allowing specialized processing before or after rendering. ## AI where it accelerates, typed code where it matters ServiceStack PDF support uses AI at the point where it provides the most leverage: turning a visual idea or natural-language request into editable document source. Everything after that becomes a controlled software artifact: 1. Typst source defines the layout. 2. JSON Schema defines the data contract. 3. Fixtures validate important payloads. 4. Publish creates a tested immutable revision. 5. Admin PDF verifies the production artifact. 6. Generated C# provides a typed integration. 7. `IPdfRenderer` produces the document deterministically. This is the missing bridge between “AI made me a nice document” and “our application can safely generate this document for every customer.” Open `/chat/pdf`, describe the document you need and publish it when it is ready. Your ServiceStack App can take it from there. ## Get Started **1. Install the Typst CLI** and make it available on `PATH`, or configure `TYPST_PATH`: ```bash # macOS brew install typst # Cargo cargo install --locked typst-cli ``` **2. Add the plugins** to a .NET 8+ ServiceStack App: npx add-in chat This configures both `ChatFeature` and `PdfFeature`, so you get the PDF Studio designer and production rendering together. To deploy production rendering *without* AI Chat, register `PdfFeature` on its own: ```csharp services.AddPlugin(new PdfFeature()); ``` **3. Design a document** at `/chat/pdf`. Start from the example invoice or describe what you need and let a model draft it. **4. Publish it**, then open `/admin-ui/pdf` to test the production template with real data and copy the generated C# model. **5. Wire up code generation** so the models live in your project: ```csharp services.AddPlugin(new PdfFeature { PdfCodeGen = new() { Namespace = "MyApp.ServiceModel.Pdf", OutputPath = Path.Combine(contentRootPath, "../MyApp.ServiceModel/Pdf"), } }); AppTasks.Register("pdf", _ => appHost.GetPlugin().GeneratePdfs()); ``` dotnet run --AppTasks=pdf **6. Inject `IPdfRenderer`** and return the document from any API, Command or background job. Before deploying, pin the Typst version and deploy the same fonts used during validation — put application fonts in `App_Data/pdf/fonts` and back up `App_Data/pdf` including `.published.json` and `.versions`. # Instant CRUD Apps with AutoQuery Schemas Source: https://servicestack.net/posts/autoquery-schema :::youtube M6KYkZFIM-8 AutoQuery Schema: Instant CRUD Apps from JSON Schemas ::: ## Every AutoQuery model is now a complete App If your App has AutoQuery APIs, open `/auto` and you already have an admin application: a searchable list of your data models, and behind each one a working CRUD app with a results grid, paging, sorting, filters, saved preferences, Create and Edit forms, reference lookups and guarded Delete actions. No frontend project, no generated source files, no scaffolding step. Every action it offers is one the signed-in user is authorized to perform, because the page is assembled at runtime from your APIs and the current session. That changes what a data UI costs. A back-office screen that would have been a sprint of grid, form, validation, lookup and permission work is now the thing you get *before* deciding whether a bespoke UI is worth building. ServiceStack's [AutoQuery](https://docs.servicestack.net/autoquery/rdbms) has long been one of the fastest ways to servicify an existing database or add production-ready data APIs to a new application. Where an [API Schema](/posts/api-schema) describes how one `/api/{RequestDto}` endpoint can be rendered and executed, an AutoQuery Schema describes the whole data capability: its Query API, returned model and every authorized Create, Update, Patch, Delete or Save API. Available in .NET 8+ ServiceStack Apps, the new schema routes and UIs are registered automatically with ServiceStack's Metadata feature: ```text /auto /auto/{ModelName} ``` Just as with ServiceStack's new [API Schemas](/posts/api-schema), add `.json` to retrieve the portable schema powering the UI: ```text /auto/{ModelName}.json ``` The integration is deliberately small: fetch that one document and give it to the generic `AutoQuerySchema` component. It uses the embedded `/api/{RequestDto}` definitions to query and mutate the data, rendering the grid, filters, paging, preferences, forms, lookups and guarded actions itself. ```html ``` Those embedded operation schemas are reusable beyond the CRUD page. Because each write operation carries its own [API Schema](/posts/api-schema), AI Chat can render an editable preview and approval form whenever a Model proposes an AutoQuery Create, Update, Patch, Delete or Save — so an assistant that can change your data still puts the exact record in front of a person first, for every model, with no per-API Chat component to write. ## Where this fits alongside Locode ServiceStack already ships [Locode](https://docs.servicestack.net/locode/), an Auto UI over the same AutoQuery APIs — so the obvious question is which to reach for. | | `/auto` | Locode | Custom UI | | --- | --- | --- | --- | | **Best for** | Embedding data UIs in your own App | A complete standalone admin App | Product surfaces users live in | | **Loads** | One model's schema at a time | The App's full metadata | Whatever you build | | **Customization** | Compose the Vue/React components yourself | Locode's customization model | Total | | **Runs where** | Built-in page *or* inside your App | Built-in page | Your App | They aren't competing and neither is going away. Locode remains the fuller standalone back-office experience. `/auto` is the schema-driven equivalent that scales to very large API surfaces and — the part that matters most — can be *taken apart*: the grid, the forms, the lookups and the field inputs are components you can drop into your own application's navigation and design system. If Locode already does what you need, keep using it. ## Browse your data APIs at `/auto` The `/auto` home page groups AutoQuery CRUD APIs by the data model they operate on. Instead of showing five disconnected Request DTOs for Query, Create, Update, Delete and Save, it presents one coherent application capability. Each model card identifies its available actions and their underlying Request DTOs. Models can be searched by name, title, tags and API names, making the browser equally useful for developers who think in code and users who think in business concepts. The catalog only includes models with a Query API the current user can access. Within each model, it only advertises CRUD operations authorized for that session. This makes `/auto` an instant launchpad for: - Internal administration - Back-office data management - Database exploration - Customer support tools - Content and configuration management - Rapid application prototyping - AutoQuery API testing ## The same elegant pattern, at CRUD scale An ordinary API Schema pairs one `/schema/{RequestDto}.json` description with one executable `/api/{RequestDto}` endpoint. An AutoQuery Schema applies the same idea at model scale, describing a model and all the APIs available for working with it in one envelope. `/auto/Booking.json` can include: ```json { "name": "Booking", "title": "Booking", "primaryKey": "Id", "model": { "type": "object", "properties": {} }, "query": { "$id": "/api/QueryBookings", "method": "GET", "operation": "Query", "properties": {} }, "create": { "$id": "/api/CreateBooking", "method": "POST", "operation": "Create", "properties": {} }, "update": { "$id": "/api/UpdateBooking", "method": "PATCH", "operation": "Patch", "properties": {} }, "delete": { "$id": "/api/DeleteBooking", "method": "DELETE", "operation": "Delete", "properties": {} } } ``` The actual schema contains the complete properties, constraints, UI metadata and authorization requirements for every included operation. It also distinguishes between the model used for writes and the view model returned by `IQueryDb`. This matters when a query joins, projects or enriches stored data: grids should display the returned view, whilst Create and Edit forms must still use the writable model and Request DTOs. The result is a self-contained description of an application's data capability, not merely a database table schema. Like the per-API `/schema/{RequestDto}.json` endpoints, this model-level envelope avoids requiring the complete `MetadataApp` graph used by the traditional `/ui` API Explorer. Even applications with thousands of APIs only load the schemas for the selected model and any referenced models opened through a lookup. Browser transfer, parsing, memory and rendering costs therefore scale with the current data UI instead of the application's total API surface. ## A full AutoQuery grid with no frontend code The built-in UI is powered by the `AutoQuerySchema` and `SchemaResults` components in `@servicestack/vue`. The results grid calls the schema's Query API and provides: - Server-side paging with first, previous, next and last navigation. - Per-column sorting. - AutoQuery's typed filter conventions. - Multiple filters and sort expressions. - Selectable visible columns. - Configurable page sizes. - Formatted values from `[Intl]` and `[Format]` metadata. - Persistent preferences stored per model. - Responsive light and dark modes. Query state is kept in the URL, so a filtered view can be bookmarked, refreshed or shared: ```text /auto/Booking?RoomType=Queen&orderBy=-StartDate&skip=20 ``` That URL is not a screenshot of a transient client state. It is a durable link back to the same server-side query. ## Schema-driven Create, Edit and Delete When a model has an authorized Create API, `/auto/{Model}` displays a New action. The form is generated from the Create Request DTO rather than from assumptions about the database table. Selecting a row opens the Edit form when an Update or Patch API is available. Delete is shown only when the caller can access the selected Delete API. Each form preserves the exact behavior of its API: - Required values and validation constraints are shown before submission. - Server validation errors bind back to their fields. - Enums and allowable values become selections. - Nested objects and collections remain editable. - File properties use multipart uploads when required. - Reference properties open searchable record pickers. - HTTP methods and payloads come from the schema. Patch APIs receive only changed values. When a user clears an existing field, the UI adds ServiceStack's `reset` instruction so the server can distinguish “set this to empty” from “leave this field unchanged.” This is the kind of edge case custom CRUD UIs frequently get wrong. A shared schema renderer solves it once for every model. ## References become live lookup UIs Foreign keys are often the point where generated CRUD tools stop feeling like real applications. Asking a user to remember that “Customer 1042” is “Acme Inc.” is technically accurate and practically unusable. AutoQuery Schemas preserve reference metadata from `[Ref]`, `[References]`, `[ForeignKey]` and related attributes. The Vue renderer uses it to display a lookup control that can resolve the current label and open a full searchable `SchemaResults` picker for the referenced model. The picker has the same paging, sorting, filters and column preferences as the main grid. Referenced model schemas are loaded on demand from `/auto/{ReferencedModel}.json`, so the parent schema remains compact. This creates a composable graph of data UIs: each model describes itself, and relationships link to other independently discoverable model schemas. ## Authorization shapes the App Generated UIs are only useful when they preserve the application's security model. The AutoQuery catalog, model schema and CRUD UI are all generated for the current authenticated session: - A model is only listed when its Query API can be accessed. - Create appears only when the user can call the Create API. - Rows become editable only when Update or Patch is available. - Delete appears only when the selected Delete API is authorized. - Each action can carry different roles, permissions, claims, scopes or API-key requirements. The schema's auth metadata allows the UI to explain an unavailable operation, whilst the API continues to enforce authorization on every request. This is more precise than a single “admin page” permission. A support user may have read access, an operator may create and edit, and an administrator may also delete—all from the same generated UI. ## Smart conventions for real AutoQuery APIs ServiceStack derives the most useful CRUD surface when several API shapes are available: - Query access is required because a data UI needs a readable model. - `IPatchDb` is preferred over `IUpdateDb` when both exist. - A single-row Delete API is preferred over a bulk delete operation. - Create, Update, Delete and Save are included only when available and authorized. - The primary key is discovered from `[PrimaryKey]`, `[AutoIncrement]`, `Id` or `{Model}Id` conventions. - Query view models are represented separately when they differ from write models. These conventions let AutoQuery Schema work with generated CRUD APIs, hand-written AutoQuery Request DTOs and mixed applications where only selected operations are exposed. ## Use `AutoQuerySchema` in your own Vue Apps The built-in `/auto/{Model}` page is a reference host around reusable `@servicestack/vue` components. Applications can fetch the same schema and embed the full CRUD experience in their own navigation and design system: ```html ``` `AutoQuerySchema` deliberately renders no surrounding page chrome, leaving the host application free to provide its own title, navigation and layout. It composes `SchemaResults`, `SchemaInput` and the standard ServiceStack inputs, dialogs, error summaries and confirmation controls. As in ServiceStack's Vue templates, the host installs the `ServiceStackVue` plugin and a Vue Router instance so filters, sorting, paging and the selected row can remain synchronized with the URL. ### The same App in React `@servicestack/react` ships the same components with the same names, so a React App embeds the identical CRUD experience: ```tsx import { useEffect, useState } from 'react' import { AutoQuerySchema } from '@servicestack/react' export default function Bookings() { const [schema, setSchema] = useState(null) useEffect(() => { fetch('/auto/Booking.json').then(r => r.json()).then(setSchema) }, []) return schema ? : null } ``` This is the payoff of publishing a portable contract instead of generating a frontend: the server didn't need to know which framework was asking. Teams on Vue and teams on React consume the same `/auto/{Model}.json` and get native components in their own ecosystem, with no second UI schema to keep in step. For more specialized experiences, both libraries expose the lower-level components independently: - `SchemaResults` for a schema-powered query grid. - `SchemaInput` for individual generated fields. - `JsonSchemaForm` for arbitrary nested JSON Schema forms. - `SchemaLookup` for reference pickers. - `SchemaGrid` and `SortableColumn` for building a custom results view. The same schema can therefore power the built-in page, a complete embedded CRUD tool or a custom workflow that only reuses selected pieces. ## A portable UI architecture Traditional generated admin pages couple server metadata to one generated frontend. AutoQuery Schemas instead define a portable capability document delivered at runtime. That architecture has several advantages: - **Always current** — schemas are generated from the APIs running in the deployed application. - **No generated source drift** — there is no second frontend model to regenerate and commit. - **Incrementally customizable** — use the built-in page first, then embed or replace only the pieces that need a tailored experience. - **Framework reusable** — the JSON contract can be rendered outside Vue by any client that understands its schema and UI hints. - **Security aware** — the available operations reflect the current user's authorization. - **Automation friendly** — tests, scripts and AI tools can consume the same `.json` representation. The server publishes what can be done. Clients decide how it should look. ## From database to App in minutes AutoQuery already makes it possible to expose an existing database with very little code. For example: ```csharp [Tag("Bookings")] [Route("/bookings", "GET")] public class QueryBookings : QueryDb { } [ValidateHasRole("Employee")] [AutoApply(Behavior.AuditCreate)] public class CreateBooking : ICreateDb, IReturn { [ValidateNotEmpty] public string Name { get; set; } = ""; public RoomType RoomType { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } } ``` Once the AutoQuery CRUD APIs exist, `/auto/Booking` is immediately useful. Add descriptions, validation, references and input metadata to refine both the APIs and their generated experience. This changes the economics of internal application development. Teams can begin with a secure, working data application, validate the workflow with users and invest in bespoke UI only where it creates real value. ## The structured foundation for AI-operated data AutoQuery Schemas are also a natural complement to ServiceStack's [API Tools](/posts/api-tools). AI Assistants can search for AutoQuery APIs, inspect their schemas and call them as the authenticated user. The model schema explains the records returned by a query, whilst the action schemas explain how records can be created or changed. This supports experiences such as: - “Show overdue invoices over $5,000, ordered by customer.” - “Find tomorrow's bookings and move this one to the available conference room.” - “Create a follow-up task for every high-priority support case assigned to me.” - “Find products below their reorder threshold and prepare updates for approval.” For writes, AI.Chat can render the proposed Request DTO with the same schema-driven inputs used by `/auto`. Since the renderer consumes the operation's API Schema, this works automatically for every discoverable AutoQuery write API. The user reviews and can edit the exact fields before approving the operation, whilst AutoQuery validation, authorization and audit behaviors remain server-side. The grid UI and AI Assistant are two clients over the same typed capability layer: one starts with visual exploration, the other starts with natural language. ## AutoQuery APIs that build their own Apps With `/auto` and `/auto/{Model}`, ServiceStack's AutoQuery APIs no longer stop at delivering data. They describe the model, advertise its authorized operations and provide everything needed to render a complete CRUD application. The built-in UI is immediately useful, the underlying Vue components are reusable and the JSON schema remains open to any future client—including AI Assistants. Start with your data model. Add focused AutoQuery APIs. Open `/auto`. The App is already there. ## Get Started Nothing to install. `/auto` is registered with the Metadata feature, so any .NET 8+ ServiceStack App with AutoQuery APIs already serves it. Run your App and open: Every model with a Query API the signed-in user can access appears immediately. To get more out of the generated App, improve the APIs rather than the UI — `[Description]`, validation attributes, `[Input]`, `[Ref]` and `[Intl]`/`[Format]` all show up in the grid and forms. To embed the components in your own App: npm install @servicestack/vue npm install @servicestack/react And to withhold the built-in pages whilst keeping the JSON schemas your own components and AI Chat consume: ```csharp services.ConfigurePlugin(feature => { feature.DisableAutoQuerySchema = true; }); ``` # Richer APIs and Auto UIs with API Schemas Source: https://servicestack.net/posts/api-schema ## Every API now comes with its own UI :::youtube ZmEeHErBNe0 API Schema: Auto Runtime UIs and Executable Contracts ::: Open `/schema` in any .NET 8+ ServiceStack App and you'll find a searchable index of every API the signed-in user can call. Open one and you get a working UI for it — form, validation, request preview, `curl` command, execution and response — with no code written and nothing installed. The page isn't special. It fetches one small JSON document and hands it to a generic component: ```html ``` That's the whole integration — for that API, and every other one. The component never learns anything about `CreateCoffeeShopOrder`; the fetched schema supplies the fields, controls, validation, HTTP method and execution URL. It is also what lets **AI Chat render a trustworthy approval form for any API a Model proposes calling**, without anyone building a Chat component per Request DTO. ServiceStack has always treated your typed Request and Response DTOs as the source of truth for your APIs. From those types it can generate routes, metadata, validation, native client DTOs, API Explorer forms and OpenAPI specifications without requiring developers to maintain parallel descriptions of the same service. The new built-in **API Schema** support takes that philosophy further: the description is now portable, per-API, and enough on its own to build a UI from. These routes are registered with the existing Metadata feature and require no separate schema plugin or frontend project. :::{.not-prose .my-8 .overflow-hidden .rounded-2xl .border .border-slate-200 .bg-white .shadow-sm .dark:border-slate-700 .dark:bg-slate-900}
One API, two complementary endpoints
Execute the API and describe how to use it
Execute
{HTTP Method} /api/{RequestDto}
Send the typed Request DTO and receive its response.
Describe
GET /schema/{RequestDto}.json
Fetch the portable contract a generic UI or tool needs to use it.
::: ### One contract connects execution and presentation These endpoints are natural complements. `/api/{RequestDto}` **executes** the API; `/schema/{RequestDto}.json` explains how to use it. The schema carries the API's fields, nested types, validation, authorization requirements, HTTP method and an `$id` that points back to its `/api/{RequestDto}` execution URL. That relationship is what makes the approach so elegant: fetch one small schema and pass it to the generic [`ApiFormSchema`](#powered-by-reusable-vue-and-react-components) component. The component can render the inputs, preview the HTTP request and `curl` command, invoke the API and display its response. The same component works for any Request DTO, so adding another API does not require adding another hand-built screen. ### From machine-readable schema to executable UI Remove `.json` and the same endpoint becomes a complete, executable UI: It renders a responsive form for the API, selects suitable controls for every property, validates user input, shows the exact HTTP request it will send, generates a copyable `curl` command, invokes the API and displays its response. No application UI needs to be written. No OpenAPI client needs to be installed. No separate schema needs to be maintained. Your existing ServiceStack metadata contains everything needed to describe both the API contract and a high-quality UI for using it. ## Browse every API at `/schema` Open `/schema` in a ServiceStack App to see every API available to the current user. The built-in API browser provides a fast searchable launcher designed for large applications. APIs can be filtered by name, description, tag and HTTP verb, with fuzzy matching that understands the PascalCase names used by Request DTOs. Each entry shows the API's name, purpose, route, verb and tags. Authorization requirements are surfaced without exposing APIs the current session is not allowed to discover. The browser is useful throughout an application's lifecycle: - Developers can explore an unfamiliar codebase without searching for Service classes. - Frontend developers can inspect and invoke APIs before their production UI exists. - Testers can reproduce requests and validation errors without writing a client. - Support teams can use approved operational APIs directly. - API designers can immediately see whether descriptions and validation produce a clear user experience. - AI developers can inspect the exact schemas made available to Models and approval forms. Because the page is generated at runtime, it always reflects the application that is actually deployed. ## JSON when you need a contract, HTML when you need an App The schema routes use ordinary HTTP content negotiation, with convenient `.json` URLs for clients and tools: The collection endpoint returns a compact catalog of APIs available to the caller. The detail endpoint returns the complete schema for one Request DTO. Open the same routes without `.json` in a browser: and ServiceStack serves their full interactive UI. This pairing is deliberately simple. The machine-readable contract and the human-readable application are two views over the same metadata. A developer, automation tool or AI Model can consume JSON; a person can open the adjacent URL and use it immediately. ## More than a type definition The schema is based on JSON Schema Draft-07, but it describes more than the structural shape of a DTO. It carries the API's route and HTTP method, its titles and descriptions, required fields, validation constraints, allowable values, authorization requirements and UI hints for controls, help text, lookups, uploads and formatting — enough to construct and invoke the API without loading ServiceStack's full application metadata document. For example, a generated schema can look like: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/api/CreateCoffeeShopOrder", "request": "CreateCoffeeShopOrder", "method": "POST", "title": "Submit a Coffee Shop Order", "description": "Submits a validated coffee shop order", "type": "object", "required": ["CustomerName", "Items"], "properties": { "CustomerName": { "type": "string", "title": "Name to put on the order" }, "Items": { "type": "array", "title": "Final order items", "items": { "type": "object", "properties": { "ProductId": { "type": "integer", "minimum": 1 }, "Quantity": { "type": "integer", "minimum": 1 } } } } }, "ui": { "submitLabel": "Create Coffee Shop Order" } } ``` Compact enough to retrieve on demand, expressive enough to power an independent client. The [API Schema docs](https://docs.servicestack.net/api-schema) document every key in full. ## Built to scale beyond thousands of APIs ### The cost of loading everything up front ServiceStack's API Explorer at `/ui` loads the full `MetadataApp` document from `/metadata/app.json` — every operation, DTO, data model and related type — before it can show you one API. That is convenient at smaller scales and increasingly expensive at larger ones: the browser downloads, parses and retains metadata for the entire application to render a single form. For customers with the largest API surfaces this eventually became a hard ceiling. Applications with 1,000+ APIs could hit JavaScript engine limits during initialization, preventing API Explorer from loading at all — the teams who most needed a way to explore their APIs were the ones who couldn't. **Those Apps now get a working, searchable UI for every one of their APIs**, because nothing has to load the whole application to render one form. ### Load one focused schema at a time API Schemas invert that dependency: The catalog contains enough information to search and select an API. Opening an API loads only the schema required to render and invoke that operation. Nested DTOs needed by the form are encapsulated within that schema, whilst unrelated APIs and models never enter the page. This provides several compounding scalability benefits: - **Smaller payloads** — clients transfer one focused contract instead of the application's entire metadata graph. - **Lower parsing and memory costs** — the browser only materializes types visible in the current UI. - **No giant JavaScript arguments** — schemas can be fetched and parsed as ordinary JSON without passing a monolithic metadata string through initialization functions. - **Faster first use** — users can search a compact catalog and open one API without waiting for every API definition. - **Independent caching** — individual schemas can be cached and invalidated separately. - **Bounded UI complexity** — rendering cost is determined by the selected API, not the total size of the application. An application can grow from ten APIs to ten thousand without making a single API form ten thousand times heavier. ## Encapsulated contracts for independent UIs A per-API schema is also a stronger unit of encapsulation. ### Self-contained by design The renderer does not need global knowledge of the application or a privileged metadata singleton. Everything needed for the interaction travels with the selected API: its URL, HTTP method, properties, nested types, constraints, authorization requirements and UI hints. That makes schema-powered UI components easier to embed and compose. A page can render `CreateBooking` without also learning about payroll, reporting and administration APIs. A plugin can own its APIs and forms without coupling itself to the host application's complete metadata shape. A micro-frontend can request only the capabilities within its own boundary. It also improves isolation. APIs excluded from metadata or unavailable to the current user do not need to be shipped to the client merely to render an unrelated form. Each UI receives the minimum description needed for its job. ### Progressive disclosure for people and AI This is the same progressive-disclosure model used by ServiceStack API Tools. An AI Model first searches a compact API index, then describes only the APIs relevant to the user's request. Human UIs and AI Assistants therefore share the same scalable principle: For traditional UIs this protects browser resources. For AI it protects the Model's finite context window. For both it creates smaller, clearer and more dependable integrations. ## Existing attributes become a richer experience API Schemas reward the metadata already present in well-designed ServiceStack APIs. ### Turn existing API metadata into UI ```csharp [Tag("CoffeeShop")] [Description("Submits a validated coffee shop order")] [Route("/coffee-shop/orders", "POST")] public class CreateCoffeeShopOrder : IPost, IReturn { [Description("Name to put on the order")] [ValidateNotEmpty] public string CustomerName { get; set; } = ""; [Description("Optional instructions applying to the whole order")] [Input(Type = "textarea", Placeholder = "e.g. call when ready")] public string? Notes { get; set; } [Description("Final order items")] [ValidateNotEmpty] public List Items { get; set; } = []; } ``` The generated form uses the descriptions as labels and help text, marks required values, renders `Notes` as a textarea and turns the `Items` collection into an editable nested form. ### Rich controls without a parallel schema Other metadata unlocks richer controls: - Enums and `[ApiAllowableValues]` become constrained selections. - `[ValidateGreaterThan]`, `[ValidateLength]`, `[Range]` and related validators become client constraints. - `[Input]` and `[Field]` select widgets, placeholders, help text, steps and layout. - `[Ref]`, `[References]` and foreign keys become searchable lookup UIs. - `[UploadTo]` becomes a file input with accepted file types. - `[Intl]` and `[Format]` describe how values should be displayed. - `[FieldCss]` and API form layouts control responsive presentation. - `[Authenticate]`, roles, permissions, claims and scopes describe who can invoke the API. None of this metadata is specific to the schema page. It continues to improve ServiceStack's other Auto UIs, API Explorer and generated clients. ## A complete API workbench The UI at `/schema/{RequestDto}` is more than a generated form. It is a focused workbench for one API. ### Inspect and reproduce every request As values are entered, the page shows the exact request that will be sent. Developers can switch to a copyable `curl` command to reproduce it from a terminal or share it with another team member. Submitting the form uses the method and `$id` from the schema. `GET` and `DELETE` values are encoded in the query string, write requests use JSON and APIs containing file inputs are sent as multipart form data. ### Three ways to read every response Responses aren't dumped as raw text. Each one can be inspected from tabs in the response header: - **Data** — results rendered in a formatted grid with nested complex types expanded inline. - **JSON** — the syntax-highlighted raw JSON response. - **Headers** — the HTTP response headers returned by the server. Any response can also be maximized over the request form to inspect larger result sets, keeping the same three views. It turns a generated form into something you can actually debug with — inspect a real payload, confirm a cache header, then copy the `curl` command that produced it. ServiceStack validation errors are bound back to their corresponding fields, whilst non-field errors remain visible in the form summary. The page stays an ordinary client of your API — authentication, authorization, request filters and validation all still run on the server. ### Shareable, executable queries Query-string values can pre-populate the form, making API examples shareable as ordinary links: For `GET` APIs, opening a populated link can execute the request immediately. After submission the URL is updated with non-empty values, producing a durable, reloadable API query — a bug report, a support ticket or a runbook step can now be a link that reproduces itself. ## Powered by reusable Vue and React components :::{.not-prose .my-8}
Vue
Schema-powered components
@servicestack/vue + @servicestack/react

The same production components that power ServiceStack's built-in API Schema pages are available for any Vue or React application.

::: The built-in pages are not a separate UI framework. They are composed from the same reusable components published in `@servicestack/vue` and `@servicestack/react`. ### Generic components, operation-specific schemas `ApiFormSchema` is the generic executable API UI. Give it any `/schema/{RequestDto}.json` document and it renders the request form, selects a control for every property, builds the request from the schema's method and `$id`, and invokes `/api/{RequestDto}`. It renders only the form, leaving the surrounding workbench to the host page. Everything else it derives — the HTTP request preview, its `curl` equivalent, the request itself, the result and any error — is passed to its **default slot**, and completed calls also emit `success` and `error` events. That's how the built-in page composes its Request, Schema and Response panels around the same component you can use. ### Embed the renderer in your own Vue App Applications can use the components directly: ```html ``` This is the entire operation-specific integration. `ApiFormSchema` stays generic whilst the fetched schema supplies everything unique to `CreateCoffeeShopOrder`. The same component can render `Hello`, `QueryBookings` or any future API without importing its generated DTO or writing a new form. ### Add the request preview and response you want That renders the form and its submit button. To show the request, `curl` command or response alongside it, read them from the slot and lay them out however the App prefers: ```html ``` Nothing here is API-specific either. A page can render the full workbench, only the `curl` command, or just the form — the component computes the same values regardless of which the host chooses to display. `ApiFormSchema` also accepts a `client` for authenticated calls, `auto-execute` to run `GET` APIs on load, and `sync-url` to keep the current values in the address bar. If you want the entire workbench rather than the form, `ApiExplorerSchema` is the component the built-in `/schema/{RequestDto}` page is built from. ### The same component in React React Apps aren't a second-class translation of the Vue experience — they consume the identical schema with the identical component API, where the default slot becomes a render prop: ```tsx import { ApiFormSchema, JsonView } from '@servicestack/react' export default function HelloForm({ schema, client }) { return ( {({ result, requestText, curl }) => (
{requestText}
curl
{curl}
{result && }
)}
) } ``` The server publishes one portable contract; Vue and React each render it with their native components. Teams can choose their preferred framework without changing the ServiceStack APIs or maintaining a separate UI schema. :::{.not-prose .my-8} ::: ### Not just APIs — any JSON Schema becomes a form Underneath `ApiFormSchema` is `JsonSchemaForm`, and it isn't limited to ServiceStack Request DTOs. Hand it any supported JSON Schema and it recursively renders a complete form — nested objects, editable arrays, enums, dates, validation constraints and arbitrary dictionary properties — keeping the whole JSON value synchronized through `v-model`. That makes it useful well beyond executable API forms. The same renderer can power configuration editors, workflow inputs, structured content tools, agent tool arguments and any UI that needs to safely capture a complex JSON value without building a bespoke form for every shape. There's a [live recursive JSON Schema demo](https://docs.servicestack.net/vue/json-schema#recursive-jsonschemaform) you can edit in the docs. The schema you edit can also become code: `generateTypes` turns a schema and an example value into C#, Python, TypeScript or JavaScript models. ## One schema can describe any UI The most valuable property of this design is that the schema is not tied to the built-in page. It is a portable description of an interaction. Any renderer can map the same structure and UI hints into its own native controls: - A web form can use text fields, selectors, date pickers and nested panels. - A mobile App can render platform-native inputs. - A desktop administration tool can build property editors. - A terminal client can ask interactive questions. - A workflow engine can generate configuration steps. - A test tool can synthesize valid requests and boundary cases. - An AI Assistant can render a human approval form for a proposed tool call. The schema describes intent and constraints without dictating one visual implementation. ServiceStack's built-in Vue UI is a complete reference renderer, not the only possible renderer. ## Approval and Request UIs for any API in AI.Chat This is where API Schemas become especially powerful. In our [API Tools announcement](/posts/api-tools), we showed how AI Models can use `api_search`, `api_describe` and `api_call` to discover and invoke existing ServiceStack APIs. The schema returned by `api_describe` is the same rich API Schema used by `/schema/{RequestDto}`. ### Editable approval forms for every API Because the renderer is generic, AI.Chat can create a Request preview and Approval UI on demand for **any API it can discover**. When an AI proposes a write operation, it renders the arguments with the same schema components used by the standalone API UI. The user sees an editable form instead of an opaque JSON blob. They can correct a name, adjust a quantity, select an allowed value or add a nested item before approving the request. This gives every API a consistent human-in-the-loop experience without designing and maintaining a separate Chat UI for each Request DTO. New APIs automatically gain the same preview and approval capability as soon as their schema is available. ### Natural language in, structured review out For the CoffeeShop example, the model can translate: > Order two grande hot oat milk lattes with light vanilla syrup for Sam. into a typed `CreateCoffeeShopOrder` request. AI.Chat then renders the nested order with the labels, validation and collection controls defined by the API Schema. The user remains in control of the final operation. The same architecture supports many consequential workflows: - Review a refund before issuing it. - Edit recipients and content before sending a message. - Confirm dates and attendees before creating a booking. - Inspect deployment options before starting a release. - Review changed fields before updating a customer record. Natural language is excellent for expressing intent. Forms remain excellent for reviewing exact structured data. API Schemas let AI experiences use both. ## And every AutoQuery model gets a whole App An API Schema describes one API. Apply the same idea to a data model and you get something larger. `/auto/{DataModel}.json` returns an **AutoQuery Schema** — the model plus every authorized Query, Create, Update, Patch, Delete and Save API around it — and `/auto/{DataModel}` renders it as a complete CRUD application with a results grid, paging, sorting, filters, lookups and guarded Delete actions. Same Metadata feature, same runtime authorization, nothing to install. We covered it in [Instant CRUD Apps with AutoQuery Schemas](/posts/autoquery-schema). ## Authorization remains server-owned Schema discovery respects the current request and authenticated session. APIs excluded from metadata or inaccessible to the caller are not presented as available capabilities. Schemas also carry the API's authentication requirements, including authentication, API keys, required or any-of roles and permissions, required claims and scopes, and authorization policies and schemes. These details help UIs explain why an operation is unavailable, but they do not replace enforcement. The API remains the final authorization boundary when invoked. This is essential for generated UIs and AI Assistants: presentation can adapt to the caller, whilst security remains deterministic and server-side. ## Better metadata now has compounding returns Clear API descriptions and declarative validation have always made ServiceStack applications easier to use. API Schemas multiply their value. One well-described Request DTO can now improve: - API documentation - API Explorer - Generated client DTOs - Built-in schema forms - Custom Vue and React forms - Automated testing tools - AI tool discovery - AI-generated approval UIs - MCP clients consuming API Tools There is no synchronization problem because every experience is generated from the same application contract. ## APIs that explain and demonstrate themselves An API should not require a developer to read its implementation before they can use it. With the new schema endpoints, every ServiceStack API can explain its structure in JSON and demonstrate itself through a complete executable UI. The contract is available to code, the UI is available to people and the same schema can safely bridge AI intent into user-approved actions. Your existing APIs already contain the application knowledge. API Schemas make that knowledge portable. ## Get Started There is nothing to install. API Schemas are part of the Metadata feature, so any .NET 8+ ServiceStack App already serves them. Run your App and open: To embed the components in your own App, add the client library for your framework: npm install @servicestack/vue npm install @servicestack/react Both export `ApiFormSchema` and `JsonSchemaForm` with the same behavior, so the snippets above translate directly. If you'd rather not expose the pages in production, they can be disabled without affecting the JSON contract used by AI Chat and your own components: ```csharp services.ConfigurePlugin(feature => { feature.DisableApiSchema = true; }); ``` Schemas can also be customized before they're returned with `OnApiSchema` — see the [API Schema docs](https://docs.servicestack.net/api-schema) for the complete reference. The fastest way to see the value is to open `/schema` on an App you already have and search for an API you wrote months ago. Whatever descriptions and validation you gave it then are the UI you get now. # Instant AI Integration: Expose ServiceStack APIs to LLMs & MCP Source: https://servicestack.net/posts/api-tools ## Your APIs are now AI Tools In 2023 we set out to build a natural language ordering experience for a CoffeeShop App. The goal sounded simple: > Order two grande hot oat milk lattes with light vanilla syrup for Sam. Turning that sentence into a valid, priced and persisted order was anything but simple. Our [original solution](https://github.com/NetCoreApps/CoffeeShop) used Microsoft's TypeChat to constrain an LLM with a custom TypeScript schema. We modelled every product, size, temperature and option in TypeScript, generated the schema and prompt from our .NET data model, invoked a Node.js process from .NET, handled schema validation and corrective retries, translated the result into our application's types, then built custom application logic and UI around the result. It worked, and at the time it was a useful demonstration of how schemas could make probabilistic LLM output safe enough for application code. But it also exposed the cost of the approach: we had created a second, AI-specific description of our application that needed to remain synchronized with the real APIs, validation rules, database and authorization model. ### Let the AI decide :::youtube pClPDAtpqz8 Your existing C# DTO is the AI Contract - enable API + MCP tools for your APIs ::: The biggest lesson was that every piece of hand-coded logic we added to constrain or steer AI behavior was eventually made redundant. Schema validation, corrective retry loops, prompt scaffolding to force a particular output shape - all of it was discarded as models grew capable enough to make those decisions themselves. The smarter approach turned out to be the open-ended one: expose your real APIs with rich metadata as the single source of truth, let models progressively discover what is available and decide what to call. That kind of architecture can accomplish far more than any number of hard-coded use-cases, and it improves automatically as models advance. ### Introducing API Tools Today the same end-to-end CoffeeShop experience can be added to a ServiceStack App in minutes. ServiceStack's new **API Tools** let AI Models discover, learn and call your existing ServiceStack APIs. The built-in [AI Chat](/posts/ai-chat-v4) UI gives users a complete conversational interface with editable approval forms, whilst the built-in **MCP Server** makes the same APIs available to external AI Assistants like OpenCode, Claude Code, Cursor and VS Code. There is no parallel TypeScript schema, no bespoke function-calling gateway and no AI-specific application backend. Your typed ServiceStack Request DTOs, metadata, validation, authorization and Services remain the single source of truth. [![API Tools](/img/posts/api-tools/coffeeshop-screenshot.webp)](https://github.com/NetCoreApps/CoffeeShopChat) :::{.text-center} [NetCoreApps/CoffeeShopChat](https://github.com/NetCoreApps/CoffeeShopChat) ::: ## From weeks of integration to minutes of configuration The [old CoffeeShop](https://github.com/NetCoreApps/CoffeeShop) required us to build and maintain an AI translation pipeline: 1. Model the menu and ordering language in a TypeScript schema. 2. Generate a prompt containing the schema and application-specific instructions. 3. Install and invoke Node.js and TypeChat from the .NET App. 4. Validate the model's JSON and retry with corrective prompts. 5. Translate the TypeChat result into the application's .NET types. 6. Resolve generated values against live products and prices. 7. Build custom APIs and UI for reviewing and submitting the order. 8. Keep the AI schema synchronized as the application evolved. The [new CoffeeShop](https://github.com/NetCoreApps/CoffeeShopChat) uses ordinary ServiceStack APIs for the menu, previewing an order, creating an order and retrieving it. Enabling them for AI.Chat only requires registering the feature: ```csharp services.AddPlugin(new ChatFeature { // Require authentication to access /chat RequireAuth = true, // RequiredRole = "Admin", Tools = { // README: Give AI Models access to Filesystem or Code Execution tools // EnableCodeExecution = true, // EnableFilesystemTools = true, }, // Expose APIs with these tags to API & MCP Tools // ApiTools = { // IncludeTags = ["todos"] // }, // Add this App's own tools to the built-in extensions // Expose these tools to external AI Agents over MCP at /chat/mcp Mcp = { // Default uses Two-Phase Confirmation Tokens for write/destructive operations // ApprovalMode = McpApprovalMode.DelegateToClient, }, Setup = (ctx => { // Advanced setup }), }); ``` The APIs themselves are the same typed APIs used by every other client. A small amount of optional `[Tool]` metadata helps AI Models select the right API and understand its place in a workflow: ```csharp [Tag("CoffeeShop")] [Description("Returns the complete coffee shop menu with product IDs, prices, " + "valid sizes, temperatures and customization options")] [Tool( "the user wants to browse the coffee shop menu, learn what can be ordered, " + "check prices, or build an order", Safety = ToolSafety.ReadOnly, Keywords = ["coffee", "drink", "food", "bakery", "customizations"])] [Route("/coffee-shop/menu", "GET")] public class GetCoffeeShopMenu : IGet, IReturn { } ``` For the final write API we can tell the model when to use it, describe its safety, require approval and provide MCP-specific instructions. The `[Mcp]` attribute lets you add imperative wording for MCP Agents - such as prerequisites and confirmation steps - without polluting the `[Description]` used by OpenAPI generators and admin UIs: ```csharp [Tag("CoffeeShop")] [Description("Submits and charges a coffee shop order. Product names and " + "prices are always resolved from the database.")] [Mcp(Description = "Submits and charges a coffee shop order. " + "IMPORTANT: Before calling this API you MUST first call " + "PreviewCoffeeShopOrder, present the itemized summary and " + "total price to the customer, and WAIT for their explicit " + "confirmation.")] [Tool( "the user has finished choosing an order and wants to place or submit it", Safety = ToolSafety.Write, RequiresApproval = true, Keywords = ["buy", "checkout", "place order"])] [Route("/coffee-shop/orders", "POST")] public class CreateCoffeeShopOrder : IPost, IReturn { [Description("Name to put on the order")] [ValidateNotEmpty] public string CustomerName { get; set; } = ""; [Description("Final order items. The approval form lets the user edit " + "these before submission")] [ValidateNotEmpty] public List Items { get; set; } = []; } ``` That's the integration. The Request DTO already describes the data contract, its validation attributes already define valid input, its response type already describes the result and its Service already contains the trusted business logic. API Tools make all of it usable by AI without creating a competing application model. ## Three tools unlock your entire API surface Sending every API schema to an LLM on every request would be expensive, slow and confusing. A mature application can contain hundreds of operations, most of which are irrelevant to the user's current request. Instead, ServiceStack exposes three stable tools: - `api_search` finds APIs relevant to the user's intent. - `api_describe` returns complete schemas and workflow metadata for selected APIs. - `api_call` invokes an API using its typed Request DTO as the current user. This gives Models progressive access to application knowledge. They start with a compact searchable index, load detailed schemas only when needed, then call the chosen APIs with structured arguments. For our CoffeeShop request the model can: 1. Search for APIs related to ordering coffee. 2. Describe the menu, preview and create-order APIs. 3. Call the menu API to resolve the current product ID, supported sizes and available options. 4. Preview the order to apply defaults, validate customizations and calculate the current price. 5. Present the final create-order request for user approval. 6. Submit the approved request and report the persisted order number. This is an important change in how AI features are built. The model is not asked to memorize a snapshot of the menu embedded in a prompt. It is taught how to find and use the application's live capabilities. ## Your ServiceStack metadata becomes AI context ServiceStack Apps already contain rich machine-readable information about their APIs: - Request and Response DTOs - Routes and HTTP methods - Property and API descriptions - Required fields and declarative validation - Enums and allowable values - Authentication, roles, permissions, claims and scopes - AutoQuery conventions and result types - UI metadata such as `[Input]` and `[Ref]` API Tools reuse this metadata to generate JSON Schemas the model can understand and schema-driven forms users can edit. Existing investments in well-described, well-validated APIs immediately improve AI reliability. The optional `[Tool]` attribute adds the information most useful to an Agent: - `WhenToUse` explains the user situation in which it should select the API. - `Keywords` and `Aliases` improve discovery using natural user vocabulary. - `Examples` demonstrate realistic request payloads. - `Prerequisites` identify APIs that should be called first. - `Preview` identifies a read-only validation or pricing API. - `FollowUps` suggest useful next operations. - `Safety` classifies the operation as read-only, write or destructive. - `RequiresApproval` guarantees a human decision before execution in AI.Chat. - `Fields` and `Take` keep large query responses within useful context limits. These are hints layered on your real API contract, not a new contract that must be kept synchronized. ## Human approval is part of the workflow Giving an LLM access to an API should not mean giving it permission to perform every operation unattended. AI.Chat infers safe defaults from HTTP semantics: reads can execute immediately, whilst writes and destructive operations require approval. APIs can override their safety classification or explicitly set `RequiresApproval = true` where every call deserves review. When our CoffeeShop assistant is ready to place the order, AI.Chat does not immediately write to the database. It renders the proposed `CreateCoffeeShopOrder` Request DTO as an editable form. The user can inspect the customer name, quantities, sizes, temperatures and options, make any final changes, then approve or reject it. Only an approved request is sent to the Service. Only the successful Service response allows the assistant to report that the order was created. The valuable part is that **no API-specific Chat component had to be written**. The approval form is generated from the API's own schema, so complex nested DTOs, collections, allowable values, descriptions and validation all remain visible and editable - and every API you add later gets the same treatment for free. [API Schemas](/posts/api-schema), next in this series, covers how that rendering works and how to use the same components in your own Apps. This gives users the convenience of natural language without surrendering control of consequential actions. After the request is submitted, the AI model is informed whether the user approved the proposed order as-is or made changes, allowing it to acknowledge modifications and adjust its understanding of the user's preferences. ## The AI acts as the authenticated user API Tools are not a privileged backdoor into your application. Search, description and execution all operate within the current HTTP request and authenticated identity. APIs the user cannot access are omitted from search, cannot be described and cannot be called. Existing authentication requirements, API-key rules, roles, permissions, claims and scopes remain enforced. Calls are deserialized into the real Request DTO and executed through ServiceStack's in-process Service Gateway. Existing DTO validation, Service filters, business rules and database behavior remain authoritative. That means the safest way to make your application AI-accessible is also the familiar way: design focused APIs, validate at the boundary, authorize on the server and expose only the capabilities each user needs. ## Built-in AI.Chat for every ServiceStack App With `ChatFeature` enabled, ServiceStack Apps gain a complete AI Chat interface at `/chat`. Developers do not need to build a chat shell, tool-call renderer, schema viewer, approval experience or conversation UI before testing an AI workflow. AI.Chat can use API Tools alongside its other enabled tools, allowing applications to combine their business APIs with capabilities such as search, files, images, audio or custom commands. Because the tool registry is shared, custom AI.Chat tools and ServiceStack Commands can participate in the same experience. This makes AI.Chat useful beyond a demo page. It can become a natural language operations console for internal users, a guided assistant for customers, an administration interface for support teams or a rapid way to test whether existing APIs provide enough context for autonomous workflows. ## Connect any MCP-compatible AI Assistant AI experiences should not be confined to a single chat UI. ServiceStack's built-in MCP Server exposes selected AI.Chat tools over the open Model Context Protocol using stateless Streamable HTTP. Enable the `api_tools` group and external Assistants gain the same `api_search`, `api_describe` and `api_call` capabilities: ```csharp Mcp = { ToolGroups = ["api_tools"], } ``` The default endpoint is: https://your-app.example.com/chat/mcp MCP clients authenticate with a ServiceStack API key in the Bearer token, so tools execute as the API key's user with their roles and scopes. This works with ServiceStack's [ASP.NET Core API Key Feature](https://docs.servicestack.net/auth/apikeys) which lets you create and manage API keys for your users, each scoped with specific roles, permissions and expiry dates. The MCP Server publishes input and output schemas, structured results and safety annotations. Images and audio can be returned inline whilst larger files are represented as resource links. Tool groups and explicit tool names let applications expose only the capabilities intended for external clients. The screenshots below all run the **same** CoffeeShop workflow - browse the menu, preview a priced order, then place it - from different external Assistants against the same unmodified ServiceStack App. ### Claude Code Claude Code registers the remote MCP Server with a single command: ```bash claude mcp add --transport http coffeeshop https://your-app.example.com/chat/mcp \ --header "Authorization: Bearer ak-xxxx" ``` The entire ordering workflow then happens in the terminal. It discovers the menu with `api_search` and `api_describe`, prices the order with a preview call, pauses for confirmation because the ordering API is annotated as a write operation, then submits it with the short-lived confirmation token: ### Claude Desktop Claude Desktop connects to remote MCP Servers with `mcp-remote`, configured in `claude_desktop_config.json`: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "coffeeshop": { "command": "npx", "args": [ "mcp-remote@latest", "https://your-app.example.com/chat/mcp", "--header", "Authorization: Bearer ${AUTH_TOKEN}" ], "env": { "AUTH_TOKEN": "ak-xxxx" } } } } ``` Same APIs, same validation rules, same authorization - this time driven from a desktop chat UI where the confirmation step is rendered as an inline approval the user clicks through before the order is submitted: ### Codex OpenAI's Codex registers the same endpoint from its CLI: ```bash codex mcp add coffeeshop https://your-app.example.com/chat/mcp \ --header "Authorization: Bearer ak-xxxx" ``` Or directly in `~/.codex/config.toml`: ```toml [mcp_servers.coffeeshop] url = "https://your-app.example.com/chat/mcp" http_headers = { "Authorization" = "Bearer ak-xxxx" } ``` The Codex App then runs the identical workflow from ChatGPT's models, arriving at the same $10.00 total for the same 2 × Hot Grande Oat Milk Lattes before placing the order: ### OpenCode OpenCode registers remote MCP Servers in its JSON config: ```json { "type": "remote", "url": "https://your-app.example.com/chat/mcp", "oauth": false, "headers": { "Authorization": "Bearer {env:MY_APP_API_KEY}" } } ``` Running the same workflow in OpenCode's TUI makes each step of the protocol visible - `api_search` narrowing to the right APIs, `api_describe` loading the contract, `api_call` invoking `GetCoffeeShopMenu` then `PreviewCoffeeShopOrder`. The preview comes back needing approval, which OpenCode surfaces as its own native prompt, and the confirmed call replays the signed `confirmationToken` alongside `CreateCoffeeShopOrder`: Because MCP is model-agnostic, the same ServiceStack endpoint works regardless of which AI model powers the assistant. Here the CoffeeShop workflow completes successfully under two very different models - **Luna Pro** and **DeepSeek Flash** - each discovering the menu, previewing the order and placing it through the same `api_search`, `api_describe` and `api_call` tools: ### Oh My Pi Oh My Pi is another MCP-compatible assistant that can connect to the same ServiceStack endpoint. Registering the server is a single command: /mcp add coffeeshop --url https://your-app.example.com/chat/mcp --token ak-xxxx Once registered, Oh My Pi discovers the available tools and can list them alongside any other MCP servers it is connected to: Here it runs the full CoffeeShop ordering workflow using **GLM 5.2** - yet another model driving the same typed APIs, validation rules and authorization boundaries: ## Approval across MCP boundaries The built-in AI.Chat UI can pause execution and render ServiceStack's editable approval form. A generic MCP client cannot render or resume that same server UI workflow, so MCP uses a configurable `ApprovalMode` policy. ### Default: Two-Phase Confirmation Token By default, `ApprovalMode = McpApprovalMode.ConfirmationToken`. When a write or destructive API is called without a token: 1. The server returns a `requires_confirmation` status containing a summary, argument parameters, and a signed, short-lived `confirmationToken`. 2. The AI assistant presents the summary to the user in chat for confirmation. 3. Upon approval, the assistant re-invokes `api_call` with the same arguments and the `confirmationToken`. 4. The server cryptographically validates the token (user identity, target API, payload argument hash, expiry, and single-use replay check) before executing. Read-only operations execute immediately without requiring a token. This is the flow visible in the screenshots above: browsing the menu returns immediately, whilst placing the order comes back as a priced summary the assistant has to relay and the user has to approve before it is submitted. The OpenCode "Order Confirmed" screenshot shows the mechanism in the raw - the second `api_call` carries the signed `mcp_cf_…` token issued by the preview. ### Fail-closed: Reject Applications that want strictly read-only MCP exposure can refuse all tools requiring approval: ```csharp Mcp = { ToolGroups = ["api_tools"], ApprovalMode = McpApprovalMode.Reject, } ``` ### Delegate to the MCP client Applications using a trusted MCP client with its own native confirmation dialog can delegate the approval decision: ```csharp Mcp = { ToolGroups = ["api_tools"], ApprovalMode = McpApprovalMode.DelegateToClient, } ``` In this mode ServiceStack publishes the tool's read-only, write or destructive safety annotations and the MCP client is expected to ask the user before allowing the call. In all modes, API authorization and DTO validation are never disabled. Only responsibility for the interactive approval decision changes. This lets the same CoffeeShop workflow run from Claude Code, Claude Desktop, Codex, OpenCode or any other MCP Assistant whilst keeping the trust boundary explicit. ## More than Coffee CoffeeShop is deliberately easy to understand, but API Tools are designed for real ServiceStack applications. Any workflow that can be expressed through well-designed APIs can become available through natural language. ### Customer service Let support staff ask for a customer's recent orders, inspect delivery status, issue an approved refund or add an account note. The assistant can discover the required APIs and the server continues to enforce staff permissions. ### Bookings and scheduling Search availability, resolve customers and resources, preview a booking, then ask for approval before committing it. Follow-up APIs can retrieve confirmation details or send notifications. ### Commerce and procurement Find products from live inventory, calculate current prices, validate quantities and submit an approved purchase. Models no longer need catalog snapshots embedded in prompts. ### Business intelligence Expose focused read-only reporting and AutoQuery APIs so users can ask questions in natural language. Default fields, row limits and truncation protect the model's context from unbounded datasets. ### Internal operations Create tickets, update CRM records, run reports, manage content or initiate deployment workflows from AI.Chat or an MCP-enabled development assistant. Destructive actions can be explicitly classified and guarded. ### Vertical AI assistants ServiceStack developers can package the domain knowledge already present in their APIs into specialized assistants for healthcare, finance, logistics, education, property management and other industries. The application remains responsible for deterministic validation and authorization; the model provides natural language understanding and orchestration. ## Better APIs produce better Agents API Tools remove most of the integration work, but they do not remove the value of thoughtful API design. Models are most reliable when APIs are focused, names are clear, descriptions explain intent and write workflows provide a read-only preview. The best pattern for consequential workflows is: This pattern works because each participant does what it is best at: - The LLM understands the user's language and chooses a workflow. - ServiceStack supplies authoritative schemas and live application capabilities. - Read APIs resolve current IDs, allowed values, prices and state. - Preview APIs normalize and validate without side effects. - The user approves the exact operation that will be performed. - Write APIs enforce business rules and persist the result. The result is more dependable than asking a model to emit a large domain object from a static prompt and hoping every duplicated rule is still current. ## One application, one source of truth, every AI client Our original CoffeeShop demonstrated that LLMs could translate natural language into structured application data. It also required weeks of work to build the schemas, prompts, providers, translation pipeline, validation loop and custom UX around one domain. ServiceStack API Tools turn that architecture inside out. Instead of building a parallel AI application, they let the model progressively discover and use the application you already have. The same CoffeeShop request now works through the built-in AI.Chat UI and external MCP Assistants. It uses live menu data, shares the application's DTO validation and business logic, respects the current user's authorization and places the final write behind an explicit approval boundary. What took weeks for one purpose-built demo can now be enabled in minutes for any well-designed ServiceStack App. Your APIs are no longer just endpoints for code. They are a safe, typed and discoverable capability layer for AI. ## Get Started API Tools ship with AI Chat, so a .NET 8+ ServiceStack App gets both from: npx add-in chat API Tools are **on by default** once `ChatFeature` is registered. Only APIs you opt in are exposed - decorate them with `[Tool]`, or include their tags: ```csharp services.AddPlugin(new ChatFeature { RequireAuth = true, ApiTools = { // APIs with [Tool] are always included; add whole tags here IncludeTags = ["CoffeeShop"], }, }); ``` Then open `/chat` and ask for something your APIs can do. To let external Assistants use the same tools, name the group over MCP: ```csharp Mcp = { ToolGroups = ["api_tools"], } ``` and point your MCP client at `https://your-app.example.com/chat/mcp` with a ServiceStack [API key](https://docs.servicestack.net/auth/apikeys) as the Bearer token. A good first step on an existing App: pick one read-only API, add a `[Tool]` attribute describing when to use it, and ask AI Chat a question that should reach it. Once discovery works for one API, adding the rest is just metadata. # AI Chat v4: A full-featured AI Assistant integrated with your APIs and Auth Source: https://servicestack.net/posts/ai-chat-v4 ## Add one plugin, get a complete AI Assistant Register `ChatFeature` in an existing ServiceStack App and your users get this at `/chat`: They sign in with the account they already have. They can talk to OpenAI, Anthropic, Google, Groq, xAI, Mistral, a local Ollama model or any other configured provider from one model selector. They can attach documents, dictate with their voice, generate images and audio, ask questions grounded in your organization's documents, design a PDF, and — most usefully — ask the assistant to do things with your application's own APIs, with an editable approval form in front of anything consequential. None of it leaves your application boundary. There is no second user directory, no separate AI product to buy and administer, no per-seat contract, and no vendor holding your conversation history. ## See AI Chat v4 in action Watch how AI Chat v4 turns a standard .NET application into a complete AI operating environment with multi-provider models, enterprise RAG, PDF Studio, authenticated API tools, human approval and real-time analytics: :::youtube aRAHzHYGjKQ AI Chat v4 - Complete Self-Hosted AI Operating Environment ::: /chat AI Chat v4 is a complete rewrite of ServiceStack's built-in AI experience. Previous versions proved the value of giving every ServiceStack App a capable, self-hosted interface for working with Large Language Models. v4 expands that idea into a modular AI application platform designed for both individual creativity and enterprise workflows. ### What this replaces Assembling the same capability from separate products usually means running — and paying for — several at once: | Capability | Typically bought as | In AI Chat v4 | | --- | --- | --- | | Chat UI for staff | Per-seat AI assistant subscription | Built in at `/chat` | | Multi-provider routing | Model gateway / LLM proxy service | Built-in provider config | | Document Q&A (RAG) | Managed vector DB + indexing pipeline | Gemini File Search extension | | Usage, token and cost reporting | Separate observability tooling | Analytics extension | | Document/PDF generation | Reporting or document service | PDF Studio + `PdfFeature` | | Agent access to business systems | Custom function-calling backend | API Tools over your existing APIs | | Assistant access for developer tools | Bespoke MCP server | Built-in MCP Server at `/chat/mcp` | | User accounts and permissions | A second identity silo to administer | Your App's existing auth | The point isn't that each piece is individually novel. It's that they share one identity model, one datastore, one deployment and one security boundary — so the integration work that normally sits between them doesn't exist. ## Rewritten around modularity Modern AI applications evolve too quickly for a fixed collection of hard-coded screens. New model providers, tools, modalities and workflows arrive continuously. Enterprises also need to disable capabilities, replace UI, add policy and integrate their own systems without maintaining a fork. AI Chat v4 solves this with a shared extension architecture across the server and browser. Each extension can contribute only what it needs: - ServiceStack routes and APIs - Model tools and tool groups - UI components and pages - Sidebar and toolbar actions - Chat request and response filters - Import maps and client dependencies - Per-user files and preferences - Database tables and background workers - Startup and shutdown lifecycle hooks The entire UI is assembled from registered Vue components. Extensions can add new components or deliberately replace an existing building block by registering the same component name. The host App can add its own final customization extension after the built-ins have loaded, giving it a clean place to brand, rearrange or specialize the experience. On the server, features are equally isolated. Gemini File Stores owns its document catalog and upload worker. Analytics owns reporting APIs. Projects owns workspace boundaries. PDF Studio owns template authoring. API Tools owns ServiceStack API discovery and approval. Each feature can evolve independently whilst sharing authentication, persistence, providers and the chat pipeline. This is what makes v4 a platform instead of a page. Everything below ships as a separately installable extension: | Extension | What it adds | | --- | --- | | `app` | Threads, history, avatars and the core conversation UI | | `agents` | Agent Profiles and the Profile Manager | | `analytics` | Cost, token and activity reporting for admins | | `api_tools` | Discovery and invocation of the App's own ServiceStack APIs | | `computer` | Filesystem tools and `run_bash` — **off by default** | | `core_tools` | Utilities, math, and code execution tools | | `credentials` | Username/password sign-in for the Chat UI | | `gallery` | Browsable catalog of generated images and audio | | `gemini` | Gemini File Search stores for RAG | | `identity` | Sign-in using the host App's ASP.NET Identity users | | `katex` | Mathematical typesetting | | `mcp` | The built-in MCP Server at `/chat/mcp` | | `pdf` | PDF Studio at `/chat/pdf` | | `projects` | Per-user workspaces and directory boundaries | | `publish` | Sharing threads, projects and media — **off by default** | | `skills` | Skill management, search, install and authoring | | `system_prompts` | The system prompt library | | `tools` | The shared Tool Registry and tools panel | | `voice` | Voice input and transcription | Extensions can be enabled, disabled and configured from one `ChatFeature`: ```csharp services.AddPlugin(new ChatFeature { RequireAuth = true, AuthType = ChatAuthType.Credentials, // Remove an extension entirely - here the filesystem and run_bash tools DisableExtensions = ["computer"], Tools = { EnableApiTools = true, // on by default EnableFilesystemTools = false, // off by default EnableCodeExecution = false, // off by default }, }); ``` Higher-risk capabilities are opt-in rather than opt-out. Filesystem access and code execution are disabled unless the host explicitly turns them on, and `DisableExtensions` removes a capability from the server and the UI together — a disabled extension registers no routes, no tools and no components. ## The llms.py v4 experience, native to ServiceStack AI Chat shares its UI architecture with [llms.py](https://llmspy.org), our open, multi-provider AI application and enhances it with: - ASP.NET Core hosting - ServiceStack routing and Services - ServiceStack Identity Auth or AuthFeature credentials - OrmLite persistence using the App's database - Per-user storage under `App_Data/chat` - ServiceStack Admin UI - Native access to the App's APIs through API Tools - A built-in MCP Server for external AI Assistants The result is the familiarity and pace of llms.py v4 inside the security and operational boundary of an existing ServiceStack application. ## Every model behind one consistent experience AI Chat normalizes leading commercial, open and locally hosted providers behind one interface. Built-in provider support includes OpenAI, Anthropic, Google, OpenRouter, Groq, xAI, Cerebras, Mistral, Fireworks, Ollama, LM Studio and other OpenAI-compatible services. The model selector makes large catalogs manageable with search, provider filters, capability filters, context sizes, pricing and favorites. Users can move between fast inexpensive models, frontier reasoning models, private local models and specialized generation models without learning a new application for each one. Streaming responses are consistent across providers, so every model begins returning useful output immediately. Conversation history, attachments, system prompts, tool calls and usage records remain in the same UI when users switch providers. This is valuable to individuals, but essential to organizations. It avoids locking the entire workforce into one model vendor and makes it practical to route different jobs to the model with the best combination of capability, latency, privacy and price. ## Consumer creativity and enterprise intelligence Many AI products are designed primarily as consumer chat applications. AI Chat v4 includes the features users expect from those products: - Rich Markdown, syntax highlighting and KaTeX mathematics - Image and document attachments - Voice input and transcription - Image, audio and speech generation - Persistent conversations - User and Agent avatars - Themes - Media galleries - Compact tool-call rendering - Projects for AI-built applications - Publishing of selected creations when configured But the architecture is equally focused on enterprise requirements: - Self-hosting inside the application boundary - Existing user authentication and role-based access - Per-user data isolation - Central provider and credential configuration - Cost, token and activity analytics - Controlled tool availability - Sandboxed project directories - Reusable organizational skills and Agent Profiles - RAG over enterprise documents - Direct access to authorized ServiceStack APIs - Human approval for write and destructive operations - MCP access for trusted external Assistants - Admin-managed PDF templates and production rendering AI Chat is therefore useful before and after the novelty of a general chat box wears off. It becomes a governed interface for how an organization applies AI to real work. ## Integrated Auth: your existing users, permissions and data boundaries AI Chat is installed inside your ServiceStack App, so it does not need a separate user directory, a second login experience or a new identity silo. Existing application users can access `/chat` with the authentication system the host already trusts. AI Chat supports: - ServiceStack Identity Auth cookies and the App's existing login flow. - AuthFeature credentials for a built-in username and password sign-in experience. - ServiceStack API keys for programmatic clients and MCP Assistants. - A required role that can limit the entire AI Chat capability to an approved user group. This is a major advantage over deploying a separate AI product beside the application. User lifecycle, password policy, roles, lockouts and account administration remain owned by the existing App. Authentication is also the boundary for AI Chat's state. User-facing features are scoped to the current identity, including: - Conversation threads and history - Generated images, audio and media - Agent Profile customizations - Projects and their allowed directories - Personal Skills and preferences - Gemini File Stores and document catalogs - PDF Studio workspaces - Provider and model preferences One user's working context does not become another user's prompt history or project filesystem. Shared and administrator-managed capabilities remain explicit rather than emerging from a common global workspace. Integrated Auth becomes even more valuable when AI Chat acts on the application. API Tools search and invoke ServiceStack APIs as the current user, preserving authentication, API-key requirements, roles, permissions, claims and scopes. An Assistant cannot discover or call an API merely because the server process itself could access it. Administrators gain system-wide analytics and operational visibility through role-protected Admin UIs, whilst ordinary users retain their own scoped experience. This combination—existing identity, per-user isolation and controlled administration—is what makes AI Chat suitable for adding to a real multi-user application instead of remaining a single-user AI demo. ### The boundaries, stated plainly Security questions tend to arrive early when a team proposes adding AI to a production application, so the answers are worth stating directly: | Question | Answer | | --- | --- | | Where does conversation data live? | Your App's database via OrmLite, and `App_Data/chat` on your server | | Who can reach `/chat`? | Whoever `RequireAuth` and `RequiredRole` allow — enforced by your existing auth | | Can one user see another's threads, media or projects? | No. State is scoped to the authenticated identity; admin cross-user access is explicit and role-gated | | What can an Agent call? | Only APIs the *signed-in user* is authorized to call — not what the server process could reach | | Can it write to the filesystem or run code? | Only if you enable those tools, and only within configured project directories | | What is exposed over MCP? | Nothing until you name tool groups; tools needing interactive approval are rejected by default | | Does any of it require an outbound AI provider? | Only the features you configure. Nothing calls a model you haven't set up | Which providers see your prompts remains entirely your decision — including "none outside this network" if you point AI Chat at a local Ollama or LM Studio endpoint. ## Enterprise RAG with Gemini File Search The Gemini extension is one of AI Chat v4's most compelling enterprise features. It provides a complete UI for managing Google Gemini File Search Stores and using them as managed Retrieval Augmented Generation knowledge bases. Teams can create separate stores for departments, customers, products or projects, then drag and drop PDFs, Markdown, text and supported business documents into categories. Uploads are content-addressed, deduplicated and processed asynchronously by a background worker so indexing does not block the UI. Once indexed, users can start grounded conversations at three useful scopes: - **Ask File Store** searches the complete knowledge base. - **Ask Category** restricts retrieval to a department, subject or folder. - **Ask Document** grounds the conversation in one selected document. Grounding sources are retained with responses so users can verify where an answer came from instead of treating fluent output as evidence. ### Market-leading managed storage economics Gemini File Search is particularly attractive for enterprise document collections because Google currently charges for embeddings when documents are indexed, whilst **file storage and query-time embeddings are free**. Retrieved document tokens are billed as normal model context. Stores persist until deleted, with project capacity scaling by usage tier. See Google's current [Gemini File Search pricing and limits](https://ai.google.dev/gemini-api/docs/file-search) before deployment. That pricing model removes the recurring vector-storage charge common in managed RAG architectures. Organizations can maintain durable knowledge bases and pay primarily when content is indexed and when users actually query it. AI Chat adds the operational experience needed around Google's service: - Local document catalog and status - Store and category organization - Background upload queue - SHA-256 deduplication - Remote/local synchronization reports - Re-upload and delete controls - User-scoped management - One-click grounded chat creation This makes managed RAG approachable without asking every ServiceStack team to design a chunking pipeline, vector database, background indexer, retrieval tool and administration UI from scratch. ## Specialized Agent Profiles One system prompt cannot be ideal for every task. Agent Profiles package a model, system prompt, theme, avatar, allowed tools, allowed skills and workflow actions into a named assistant. AI Chat includes general Chat, Planner and Coder profiles. The Profile Manager can inspect built-in profiles, override their default model and theme, or create new profiles entirely from the UI. Custom profiles can compose `SYSTEM.template` from separate Markdown files for organizational policy, user context, procedures and domain knowledge. Tools and skills can be restricted per profile, reducing both risk and model confusion. The built-in Planner → Coder workflow demonstrates how profiles cooperate. A Planner decomposes a goal and writes `PLAN.md`; the Coder receives the approved plan and implements it with the tools allowed in the selected Project. For enterprises, profiles become governed job descriptions: Support Assistant, Policy Analyst, Release Planner, Sales Researcher or Finance Reviewer—each with only the context and capabilities needed for its role. ## Projects create safe working boundaries Projects give each user a persistent workspace and define the directories filesystem tools may access. Switching Projects changes the active boundary for read, write, edit, search and listing operations. This allows capable coding and document Agents without granting them ambient access to the whole server. Project directories are normalized and path checked, whilst code execution and filesystem tools remain disabled unless explicitly enabled by the host. Projects can hold generated websites, games, reports, plans and source code. They are also natural collaboration units for Agent Profiles and, when publishing is configured, can be deployed as static projects. ## Skills turn procedures into reusable capabilities Skills package specialized instructions, references and supporting files that Models load only when relevant. Instead of adding every procedure to every system prompt, an organization can maintain focused skills for incident response, customer onboarding, code review, compliance checks or internal systems. AI Chat includes management, search, installation, creation and editing experiences for Skills. Built-in and administrator-provided skills can be shared, whilst authenticated users maintain their own scoped additions. Profiles can restrict which skills are available, and Models load the selected skill's detailed instructions only when triggered. This progressive disclosure keeps prompts smaller and helps specialists remain specialists. ## Tools for action, not just answers AI Chat v4 has a shared Tool Registry used by chat models, custom extensions, ServiceStack Commands, API Tools and the MCP Server. Tools are described with JSON Schema, grouped for selection and rendered as compact expandable calls in conversation history. Users can inspect arguments and results without allowing verbose machine output to overwhelm the thread. The host controls whether higher-risk tools are installed: ```csharp Tools = { EnableApiTools = true, EnableFilesystemTools = true, EnableCodeExecution = false, } ``` Provider-hosted Server Tools can also expose capabilities such as web search, web fetch and code execution where supported by the selected provider. Their JSON Schemas dynamically generate the configuration UI shown to users and the definitions supplied to Models. ## Turn your ServiceStack APIs into AI capabilities API Tools are where AI Chat becomes uniquely valuable to ServiceStack developers. Instead of building AI-specific functions, Models can progressively discover and invoke existing ServiceStack APIs through: - `api_search` - `api_describe` - `api_call` The same Request DTOs, validation, roles, permissions, claims, scopes and Service implementations used by every client remain authoritative. Read APIs can execute immediately; writes and destructive calls pause in AI Chat and render an editable schema-generated approval form. This enables Assistants for ordering, bookings, support, reporting, operations and administration without creating a second application backend — and it does so without a parallel AI-specific model of your domain that has to be kept in sync. This is significant enough that it gets its own post: [API Tools](/posts/api-tools), next in this series, walks through the complete CoffeeShop workflow end to end. ## Make those capabilities available over MCP AI Chat can expose selected tool groups through its built-in stateless MCP Server at `/chat/mcp`. Trusted external Assistants such as OpenCode, Claude Code, Cursor and VS Code can then discover and use the same ServiceStack API Tools under an API key user's identity. ```csharp Mcp = { ToolGroups = ["api_tools"], } ``` Nothing is exposed until the host selects tool groups or explicit tools. The server publishes input schemas, output schemas, structured results and safety annotations. AI Chat's interactive approval form cannot be transported through a generic MCP client, so the MCP Server uses a configurable `ApprovalMode`. The default mode uses a Two-Phase Confirmation Token: write and destructive calls return a `requires_confirmation` response with a signed token, the assistant presents the summary for user confirmation, and then re-invokes with the token. Applications can also set `ApprovalMode = McpApprovalMode.Reject` for fail-closed read-only exposure, or `McpApprovalMode.DelegateToClient` when the MCP client has its own native confirmation dialog. API authorization and DTO validation remain enforced in all modes. AI Chat therefore works both as the organization's first-party interface and as a secure capability server for the AI clients its users already prefer. ## Multi-user administration and analytics Enterprise AI needs observability. AI Chat records requests, provider, model, tokens, duration and cost so administrators can understand usage instead of discovering it only on a provider invoice. The Analytics extension provides cost analysis, token usage and activity views. Admin users can filter metrics by user, compare request and token totals, sort users by activity and inspect conversation transcripts when operational review is required. Authentication can use ServiceStack Identity cookies, the App's credentials provider or API keys. Threads, media, profiles, projects and preferences remain scoped per user, whilst Admin UI integrations provide controlled operational visibility. This is a different product posture from a personal desktop chat client: AI Chat is designed to be operated as part of a multi-user application. ## Voice, media and multimodal creation Text is only one AI modality. AI Chat supports voice-to-text input, image generation, audio generation and speech synthesis through configured providers. Generated media is stored in the App's media catalog with optimized gallery browsing. Users can revisit, download and reuse creations instead of losing them inside a transient provider response. The same modular provider system allows an organization to enable only approved vendors and models. A creative team can expose image and speech models, whilst a regulated workflow can restrict users to text models hosted within its approved boundary. ## PDF Studio: from a prompt to a production document PDF Studio brings code-first document design into AI Chat at `/chat/pdf`. It combines Typst templates, live PDF preview, schema-generated data forms, visual formatting tools and AI-assisted editing. Users can describe changes in plain English or attach a screenshot or sample PDF for a vision model to recreate as a reusable template and JSON data contract. Templates remain plain-text `.typ` files, so they are versionable, reviewable and independent of the model that helped create them. PDF Studio is only the authoring half. Admins can publish validated templates into `App_Data/pdf`, generate typed C# models and render production PDFs through `IPdfRenderer` without an LLM or ChatFeature at runtime. That separation is the whole point: AI accelerates authoring, but never sits in the path of the invoices your application sends to customers. [ServiceStack PDF Studio](/posts/servicestack-pdf), later in this series, covers the complete AI Chat → Publish → Admin PDF → application rendering workflow. ## Publish selected creations When configured, the Publish extension can share selected chat threads, projects, images and audio through a publishing service. Publishing is deliberately separate and disabled by default in ServiceStack AI Chat, allowing each host to decide whether content may leave its application boundary. This opt-in distinction matters. Consumer users may value instant sharing; enterprises often require retention policy, review and an approved destination. The extension boundary supports both without weakening the secure default. ## One platform, shaped for each App AI Chat v4 brings together capabilities that would otherwise require separate chat, RAG, model gateway, tool, analytics, media, document and administration products. Its greatest feature is not the number of screens. It is that every screen and capability is an encapsulated module over shared infrastructure: - Providers supply intelligence. - Extensions supply capabilities. - Profiles and Skills supply specialization. - Projects supply boundaries. - Identity supplies users and authorization. - Analytics supplies governance. - API Tools supply access to the application. - MCP supplies access to external Assistants. - PDF Studio turns AI-assisted design into production artifacts. Start with the built-in experience. Disable what your users do not need. Configure the providers you trust. Add your APIs, documents, profiles and skills. Replace individual UI building blocks where your product needs its own voice. AI Chat v4 is no longer just a place to talk to a Model. It is the modular AI layer for your ServiceStack App. ## Get Started Add AI Chat and PDF support to an existing .NET 8+ ServiceStack App with: npx add-in chat This configures both `ChatFeature` and `PdfFeature`, adds the `ServiceStack.AI.Chat` package and writes a starting configuration you can edit. Set an API key for at least one provider — `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY` or a local Ollama endpoint — then run your App and open: /chat Your existing users can sign in immediately. From there: - Choose which providers and models to expose in the model selector - Set `RequiredRole` if AI Chat should be limited to an approved group - Turn on API Tools for the tags you want Models to reach - Leave `EnableFilesystemTools` and `EnableCodeExecution` off unless you need them **Already running AI Chat?** v4 is a rewrite, so review the migration notes before upgrading — provider configuration moved to the models.dev format and the extension system replaces several v2-era configuration points. # Typed ServiceStack APIs for Go, Rust, Ruby and Zig Source: https://servicestack.net/posts/go-rust-ruby-zig-typed-apis ## 15 languages. One API. ServiceStack has always been built around a simple idea: define your APIs once, then make them feel native everywhere they are used. With first-class support for **Go, Rust, Ruby and Zig**, Add ServiceStack Reference now generates end-to-end typed API integrations for **15 of the world's most popular programming languages**: - **.NET:** C#, F# and VB.NET - **Web and scripting:** TypeScript, JavaScript, Python, PHP and Ruby - **Mobile:** Swift, Java, Kotlin and Dart - **Cloud and systems:** Go, Rust and Zig This is more than generating classes from JSON. Each language gets clean native DTOs and an idiomatic, feature-rich Service Client that understands the API contract: request and response types, HTTP method, routes, authentication, structured errors, validation failures, AutoQuery conventions and more. Your ServiceStack Request DTO remains the source of truth. Developers add a reference to the running API and receive the types and client integration needed to call it naturally from their chosen platform. One backend can now serve a TypeScript website, a Swift or Kotlin mobile App, a Python automation, a Ruby business system, a Go cloud service, a Rust application and Zig systems software-without maintaining a separate hand-written SDK for each one. ## One API. 15 Languages. Zero Hand-Written SDKs. Define your contract once. Every developer gets clean native DTOs, an idiomatic feature-rich Service Client, editor autocomplete, and compile-time confidence - generated directly from a running API, not hand-written and left to drift. In this video, see how Add ServiceStack Reference replaces brittle JSON parsing, manual route wiring, and the ongoing maintenance of bespoke SDKs with a single, end-to-end typed source of truth across 15 modern programming languages (now including Go, Rust, Ruby, and Zig).
## The world's most important API, in every language The OpenAI-compatible Chat Completions API may be the most consequential API introduced in modern software development. It has become a common interface between applications and increasingly capable AI models, powering assistants, agents, search, document intelligence, automation and entirely new product experiences. It also makes an ideal demonstration of what end-to-end typed APIs provide. `ChatCompletion` is not a trivial request: it contains nested messages, polymorphic content, model settings and a rich response graph. With Add ServiceStack Reference, that entire contract becomes native code in every supported language. Choose a language below to see the same typed `ChatCompletion` API called from all 15 languages: :::{.-ml-60 .w-auto .not-prose} ::: The important part is not that each example can send HTTP. Every language can send HTTP. The difference is that developers work with the API as native types, with editor completion and compile-time feedback, whilst the Service Client handles serialization, routes, headers, authentication and response deserialization. There are no hand-written URLs, anonymous JSON objects or duplicated response models. The request DTO knows which response it returns and the generated integration tells the client how it should be sent. ## Add a reference, not another SDK project Traditional SDK development multiplies work. Every API change must be reflected in documentation, models and client code for each platform. Generated SDKs can reduce that work, but often introduce large toolchains, verbose output and clients that feel foreign to the language using them. Add ServiceStack Reference takes a lighter approach. It reads the rich metadata already published by your ServiceStack App and generates a single source file containing native DTOs tailored to the target language. Two steps in any language - add the client, then generate the DTOs: Run the same command whenever the API changes. New APIs and fields appear in the generated DTOs; removed or changed members become visible to the language's own compiler, type checker or development tools. This keeps the integration coupled to the API contract instead of to a manually maintained document. API descriptions and validation metadata travel with the generated types, so the code developers consume is also the documentation they see in their editor. The result is a development loop that scales cleanly: 1. Define or update a typed ServiceStack Request DTO. 2. Implement it once on the server. 3. Refresh the ServiceStack Reference in any client. 4. Call the API with native types. No schema translation layer. No parallel SDK release train. No drift between a wiki and the API that is actually deployed. ## Go: simple, typed APIs for cloud software Go has become a natural home for cloud services, infrastructure, networking tools and operational software. Its appeal comes from simplicity, fast builds, straightforward deployment and excellent concurrency support. The new [`servicestack-go`](https://github.com/ServiceStack/servicestack-go) client preserves those qualities. Generated request DTOs carry their response type and HTTP method, allowing Go's generic ServiceStack client to infer the complete API call: ```go res, err := ss.Send(client, dtos.Hello{Name: "World"}) if err != nil { log.Fatal(err) } fmt.Println(res.Result) ``` The library uses Go's standard library with no external runtime dependencies. It provides `context.Context` variants for cancellation and deadlines, structured `ResponseStatus` errors, field validation errors, typed AutoQuery responses, multipart uploads, batched and one-way requests, and authentication using Basic Auth, API Keys, JWTs, refresh tokens or session cookies. That makes it suitable for everything from a small command-line tool to a long-running cloud service. Go developers retain familiar `error` handling and context propagation whilst gaining a client that understands ServiceStack APIs. ## Rust: correctness from the wire to application code Rust is chosen when performance, memory safety and correctness matter together: secure services, developer tooling, cross-platform applications and high-performance systems. The [`servicestack`](https://crates.io/crates/servicestack) crate brings the same priorities to API integration. Generated DTOs implement the traits that associate each request with its response, route and HTTP method, so calls remain concise and strongly typed: ```rust let response = client.send(&Hello { name: "World".to_string(), }).await?; println!("{}", response.result); ``` The async client is the default, with an optional blocking client for applications that prefer synchronous execution. It supports structured ServiceStack errors, authentication and automatic token refresh, typed AutoQuery, multipart uploads, batch and one-way calls, and access to the underlying `reqwest` configuration when an application needs finer control. Most importantly, the generated contract turns changes at the API boundary into ordinary Rust compiler feedback. Developers do not need to scatter `serde_json::Value` through business logic or manually specify response types at every call site. The API becomes part of the program's type system. ## Ruby: ServiceStack productivity in a dynamic language Ruby remains one of the most productive languages for building web products, internal systems, automation and integrations. Its expressiveness lets teams move quickly, but remote APIs can still become a source of runtime surprises when their contracts live only in documentation. The [`servicestack`](https://rubygems.org/gems/servicestack) Ruby gem provides generated DTOs with explicit properties and API metadata, giving editors and developers a discoverable model of every request and response: ```ruby client = ServiceStack::JsonServiceClient.new(base_url) response = client.send(Hello.new(name: 'World')) puts response.result ``` It is implemented with Ruby's standard library and has no external runtime dependencies. The client includes structured `WebServiceException` errors, field validation details, authentication, typed AutoQuery conventions, batch calls, one-way requests, custom URLs and file uploads. Ruby keeps its natural named-argument syntax and fast feedback loop. ServiceStack adds a consistent contract, rich metadata and a client that removes repetitive integration code-bringing more confidence without making Ruby feel like another language. ## Zig: explicit, efficient APIs for systems software Zig gives systems developers precise control over memory, allocation and platform behavior with a small language and toolchain. It is increasingly attractive for native utilities, embedded-adjacent software, games, libraries and performance-sensitive applications. [`servicestack-zig`](https://github.com/ServiceStack/servicestack-zig) is built in the same spirit. It uses Zig's standard library with no third-party dependencies, infers the response type at compile time and makes ownership explicit: ```zig var client = try ss.JsonServiceClient.init(allocator, base_url); defer client.deinit(); var response = try client.send(dtos.Hello{ .name = "World", }); defer response.deinit(); ``` The caller supplies the allocator and owns the parsed response lifecycle. At the same time, the client still provides the higher-level behavior expected from ServiceStack: structured errors, validation details, authentication, session cookies, typed AutoQuery, batch and one-way requests, custom URLs and multipart uploads. Zig applications can therefore consume rich business APIs without giving up the explicit resource management and minimal dependency surface that brought them to Zig in the first place. ## One consistent capability set, expressed natively The four clients do not try to force a single language's style everywhere. Go returns values and errors. Rust supports async results and optional blocking calls. Ruby uses natural constructors and exceptions. Zig makes allocation and cleanup visible. What remains consistent is the capability of the integration: - **Typed request and response DTOs** generated from the live ServiceStack API - **Response and HTTP method inference** from request metadata - **Structured errors** including `ResponseStatus` and field validation failures - **Authentication** using the mechanisms supported by each platform, including API Keys, Bearer tokens, JWTs and sessions - **Typed AutoQuery responses** for querying rich data APIs without hand-building query plumbing - **Batch and one-way APIs** for efficient workflows and messaging patterns - **Multipart and file uploads** for APIs that go beyond JSON-only requests - **Custom routes and URLs** when an integration needs direct HTTP control This balance is what makes a generated integration feel like a real client library instead of a dump of data classes. ServiceStack standardizes the behavior and contract whilst each implementation stays idiomatic to its ecosystem. ## Typed APIs are an architectural advantage End-to-end typing is often described as developer convenience, but its larger value is architectural. An API contract is a boundary between teams, applications and release schedules. If that boundary is represented by copied JSON examples, every consumer has to interpret and maintain it independently. Small server changes become latent runtime failures spread across a fleet of clients. With Add ServiceStack Reference, the deployed API publishes the machine-readable contract used to create each native integration. Consumers can refresh it on demand and let their existing language tools identify affected code. Validation errors return in a common structured format. Authentication and transport behavior live in the reusable client instead of being reimplemented by every team. This is especially valuable for organizations with a polyglot architecture. A mobile team and a systems team do not need to agree on one programming language to share the same precise API contract. Each can use the platform best suited to its product whilst the ServiceStack backend remains consistent. ## From websites to systems software The addition of Go, Rust, Ruby and Zig completes an unusually broad client story. ServiceStack APIs can now be consumed natively from the dominant .NET languages, every major browser and server scripting ecosystem, the leading mobile platforms, and the languages increasingly used to build modern cloud infrastructure and systems software. That opens up practical combinations which previously required custom client work: - A TypeScript web App and Swift/Kotlin mobile Apps sharing the same backend - Go workers and command-line tools automating operational APIs - Rust desktop software consuming authenticated business services - Ruby applications integrating with an organization's ServiceStack platform - Zig utilities calling the same APIs from a minimal native binary - AI features exposed once through `ChatCompletion` and used from all of them The backend team continues to design ordinary typed ServiceStack APIs. Every client team gets native models, native tooling and a capable Service Client in the language they already use. ## Build the API once. Use it everywhere. Supporting another platform should not require inventing another integration architecture. Add ServiceStack Reference turns your existing API metadata into the native contract each client needs. The new Go, Rust, Ruby and Zig libraries extend that experience to 15 languages spanning websites, mobile Apps, cloud services, automation and systems software. Your APIs are already typed. Now virtually every application your organization wants to build can be typed end to end as well. ## Get Started Nothing needs to be installed or configured on the server. Every ServiceStack App already publishes the metadata `get-dtos` reads, so you can point it at an API you run - or one of ours - right now: npx get-dtos go https://vue-spa.web-templates.io Then add the client library for your language and make the first call: - **Go** - [servicestack-go](https://github.com/ServiceStack/servicestack-go) · [docs](https://docs.servicestack.net/go-add-servicestack-reference) - **Rust** - [servicestack crate](https://crates.io/crates/servicestack) · [docs](https://docs.servicestack.net/rust-add-servicestack-reference) - **Ruby** - [servicestack gem](https://rubygems.org/gems/servicestack) · [docs](https://docs.servicestack.net/ruby-add-servicestack-reference) - **Zig** - [servicestack-zig](https://github.com/ServiceStack/servicestack-zig) · [docs](https://docs.servicestack.net/zig-add-servicestack-reference) Run `npx get-dtos` with no arguments to list all 15 supported languages. --- This is the first post in our latest release series. Next we look at the AI capabilities added to ServiceStack Apps in this release, starting with [AI Chat v4](/posts/ai-chat-v4). # Next.js React Server Components - Secure Design Source: https://servicestack.net/posts/nextjs-rsc-secure-design In the face of recent back-to-back critical vulnerabilities that have plagued Next.js React Server Components - including three CVEs with severity scores reaching the maximum CVSS 10.0 - we felt it essential to explain how our only template that utilizes React Server Components, [Next.js RSC](https://react-templates.net/docs/templates/next-rsc) was architected to be **secure by design** by employing a **defense-in-depth architecture** that makes entire classes of attacks like these CVEs physically impossible. :::info All other [React and Next.js templates](https://react-templates.net) utilize static rendering and doesn't use a Node runtime in production at all ::: The [Next.js RSC](https://react-templates.net/docs/templates/next-rsc) template leverages Next.js React Server Components as a **pure rendering layer**, gaining the benefits of server-side rendering: improved performance, SEO, and reduced client bundle sizes - without the security risks inherent to fullstack Next.js applications. Unlike traditional fullstack Next.js apps that use (`'use server'`) Server Functions to directly handle data operations, the [next-rsc](https://next-rsc.react-templates.net) template routes all data access through type-safe **ServiceStack .NET APIs**. Next.js serves exclusively as the presentation layer, never touching databases, secrets, or business logic directly. This architectural decision makes the template **immune to React Server Components vulnerabilities**. Since it doesn't use the vulnerable Server Functions feature, the primary attack vector doesn't exist. Combined with process-level isolation between the Node.js and .NET layers, even future vulnerabilities in Next.js cannot compromise your application's data or infrastructure. ## Security by Design This document details how the [Next.js RSC](https://react-templates.net/docs/templates/next-rsc) project template is architected to be **immune to React Server Components vulnerabilities** through: 1. **No Server Functions** - The primary attack vector doesn't exist 2. **Process Isolation** - Node.js runs as unprivileged user with no access to .NET resources 3. **File System Permissions** - .NET configuration and code cannot be read or modified 4. **Environment Separation** - Secrets never reach the Node.js process 5. **Automated Testing** - Security boundaries are continuously verified Even if you accidentally introduce Server Functions or a new vulnerability is discovered, the infrastructure limits the damage. An attacker with full code execution in Node.js still cannot: - Read database credentials - Access configuration files - Steal API keys or licenses - Modify .NET code - Persist backdoors This **security by design** architecture physically prevents entire classes of attacks, making your application resilient against both known and future vulnerabilities. ## Critical Vulnerabilities in React Server Components In December 2025, React disclosed three critical vulnerabilities in React Server Components with severity scores reaching CVSS 10.0 - the highest possible rating. These vulnerabilities enable remote code execution, denial of service, and source code exposure in applications using React Server Functions. - [Denial of Service and Source Code Exposure in React Server Components](https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components) - [Critical Security Vulnerability in React Server Components](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components) This Next.js + .NET template is **architected to be immune to these vulnerabilities** through a combination of secure design patterns and infrastructure-level isolation. Even if similar vulnerabilities are discovered in the future, the blast radius is minimized by design. ## Understanding the Vulnerabilities The React Server Components vulnerabilities exploit a fundamental weakness: **Server Functions** (`'use server'`) that deserialize untrusted client input can be manipulated to: - [CVE-2025-55182 (CVSS 10.0)](https://www.cve.org/CVERecord?id=CVE-2025-55182): Execute arbitrary code on the server - [CVE-2025-55184/67779 (CVSS 7.5)](https://www.cve.org/CVERecord?id=CVE-2025-55184): Cause infinite loops that hang the server - [CVE-2025-55183 (CVSS 5.3)](https://www.cve.org/CVERecord?id=CVE-2025-55183): Leak source code and potentially hardcoded secrets The attack requires no authentication and can be triggered by a single malicious HTTP request. ## Two Layers of Protection This template employs a **defense-in-depth approach** with two independent security layers: ### Primary Defense: No Server Functions The template **does not use React Server Functions** at all, eliminating the primary attack vector. Instead, all data operations flow through type-safe ServiceStack APIs: ```typescript // ❌ Vulnerable pattern (not used in this template) 'use server' export async function createBooking(data: Booking) { const db = await getDatabase() // Direct database access from React return db.bookings.create(data) } ``` ```typescript // ✅ Secure pattern (how this template works) import { client } from '@/lib/gateway' import { CreateBooking } from '@/lib/dtos' export async function createBooking(data: Booking) { // Type-safe API call to .NET backend return await client.api(new CreateBooking(data)) } ``` All mutations and data access happen in .NET ServiceStack APIs with proper authentication, validation, and authorization - never directly in React components. ### Secondary Defense: Process Isolation Even if Server Functions were accidentally introduced or a new vulnerability discovered, the template's infrastructure limits the damage through **container-level isolation**: ![](https://docs.servicestack.net/img/pages/react/info/next-rsc-security.svg) The Node.js process runs as an unprivileged user (`nextjs`) with no access to .NET resources. Even with full code execution in Node.js, an attacker **cannot**: | Resource | Node.js Access | Result | |----------|----------------|------------| | Database Connection String | ❌ No access | ✅ Protected | | `appsettings.json` | ❌ Permission denied | ✅ Protected | | .NET Assembly Files | ❌ Permission denied | ✅ Protected | | `App_Data` Directory | ❌ Permission denied | ✅ Protected | | Confidential Environment Variables | ❌ Not in environment | ✅ Protected | | .NET Process Memory | ❌ Separate process | ✅ Protected | | API Endpoints | ✅ Can Call via HTTP | ⚠️ By Design | | RSC Render Data | ✅ Can Access | ⚠️ By Design | ## React Server Components Vulnerability Analysis ### Recent Critical Vulnerabilities 1. **CVE-2025-55182** - Remote Code Execution (CVSS 10.0) 2. **CVE-2025-55184 / CVE-2025-67779** - Denial of Service (CVSS 7.5) 3. **CVE-2025-55183** - Source Code Exposure (CVSS 5.3) ### Primary Protection: No Server Functions This template **does NOT use React Server Functions** (`'use server'`), eliminating the primary attack vector. ```typescript // ❌ Don't use server functions - Creates a potential vulnerability surface 'use server' export async function createUser(name: string) { const db = await getDatabase() // Vulnerable! return db.users.create({ name }) } ``` ```typescript // ✅ Instead use .NET APIs for any access to data or resources import { client } from '@/lib/gateway' import { CreateUser } from '@/lib/dtos' export async function createUser(name: string) { return await client.api(new CreateUser({ name })) } ``` ### Secondary Protection: Process Isolation (Defense in Depth) Even if Server Functions were accidentally added or a new vulnerability discovered: **CVE-2025-55182 (RCE) Impact:** - ✅ Attacker cannot read database credentials - ✅ Attacker cannot access .NET configuration - ✅ Attacker cannot modify .NET code - ⚠️ Attacker can make API calls (must secure APIs with authentication) **CVE-2025-55183 (Source Exposure) Impact:** - ✅ No database credentials in source code to leak - ✅ No ServiceStack license in environment to expose - ✅ No .NET secrets accessible to Node.js **CVE-2025-55184/67779 (DoS) Impact:** - ⚠️ Node.js process can hang (UI unavailable) - ✅ .NET process unaffected (APIs remain functional) - ✅ Cannot consume .NET CPU/memory ## Implementation Details ### 1. File System Isolation **Dockerfile Configuration:** ```dockerfile # Create unprivileged user for Next.js RUN groupadd -r nextjs && useradd -r -g nextjs -s /bin/bash nextjs # .NET files: owned by root, mode 700 (rwx------) COPY --from=dotnet-build /src/MyApp/bin/Release/net10.0/publish ./dotnet RUN chmod -R 700 ./dotnet && chown -R root:root ./dotnet # Node.js files: owned by nextjs user, mode 500 (r-x------) COPY --from=dotnet-build /src/MyApp.Client ./nextjs RUN chown -R nextjs:nextjs ./nextjs && chmod -R 500 ./nextjs # Temporary directory for Next.js cache RUN mkdir -p /tmp && chmod 1777 /tmp ``` **Verification:** ```bash # Inside container as nextjs user cat /app/dotnet/appsettings.json # Permission denied cat /app/nextjs/package.json # Success ``` ### 2. Environment Variable Isolation **Entrypoint Configuration:** ```bash # Start Node.js with minimal environment using su cd /app/nextjs && su nextjs -s /bin/bash -c " export HOME=/tmp export NODE_ENV=production export NEXT_PORT=${NEXT_PORT} export INTERNAL_API_URL=${INTERNAL_API_URL:-http://127.0.0.1:8080} export KAMAL_DEPLOY_HOST=${KAMAL_DEPLOY_HOST} export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin cd /app/nextjs npm run start -- --port ${NEXT_PORT} " & ``` **Environment Variables Available to Node.js:** | Variable | Value | Safe? | Purpose | |----------|-------|-------|---------| | `NODE_ENV` | `production` | ✅ Yes | Next.js mode | | `NEXT_PORT` | `3000` | ✅ Yes | Port number | | `INTERNAL_API_URL` | `http://127.0.0.1:8080` | ✅ Yes | API endpoint | | `KAMAL_DEPLOY_HOST` | Public hostname | ✅ Yes | Public info | | `PATH` | Standard paths | ✅ Yes | Binary paths | | `HOME` | `/tmp` | ✅ Yes | Temp directory | Only the allowlisted variables above are exported; **no other confidential environment variables are available to Next.js**. **Verification:** ```bash # Check Node.js process environment NODE_PID=$(pgrep -f "npm run start") cat /proc/$NODE_PID/environ | tr '\0' '\n' # Expected output: # HOME=/tmp # NODE_ENV=production # NEXT_PORT=3000 # INTERNAL_API_URL=http://127.0.0.1:8080 # ... # (NO SERVICESTACK_LICENSE) ``` ### 3. Process User Isolation ```bash # .NET runs as root with full privileges ASPNETCORE_CONTENTROOT="/app/dotnet" dotnet /app/dotnet/MyApp.dll & # Node.js runs as nextjs user with restricted privileges su -s /bin/bash nextjs -c "npm run start ..." & ``` **Process Verification:** ```bash ps aux | grep -E "dotnet|node" # root 123 ... dotnet /app/dotnet/MyApp.dll # nextjs 456 ... npm run start ``` ### 4. Read-Only Application Files The `/app/nextjs` directory is mounted with `r-x` (500) permissions, preventing: - Modification of application code - Writing malicious payloads to disk - Persisting backdoors across restarts ```bash # As nextjs user echo "malicious" > /app/nextjs/backdoor.js # Permission denied ``` ## Verified Security The template includes an [automated security test suite](https://github.com/NetCoreTemplates/next-rsc/blob/main/test-isolation.sh) that verifies all isolation guarantees: ```bash # Build and start container docker build -t next-rsc . docker run -d -p 5001:8080 --name next-rsc-test next-rsc # Run security tests ./test-isolation.sh ``` **Test Coverage:** 1. File system isolation (**nextjs** user cannot read `/app/dotnet`) 2. Environment variable isolation (sensitive vars not exposed) 3. Process user isolation (different UIDs) 4. Write protection (cannot modify files) ### Manual Testing ```bash CONTAINER=$(docker ps -q -f "name=next-rsc-test") # Test 1: File access docker exec -u nextjs $CONTAINER cat /app/dotnet/appsettings.json # Expected: Permission denied # Test 2: Directory listing docker exec -u nextjs $CONTAINER ls /app/dotnet # Expected: Permission denied # Test 3: Environment variables NODE_PID=$(docker exec $CONTAINER pgrep -f "npm run start" | head -1) docker exec $CONTAINER cat /proc/$NODE_PID/environ | tr '\0' '\n' | grep SERVICESTACK # Expected: No output (not present) # Test 4: Write attempt docker exec -u nextjs $CONTAINER sh -c 'echo "test" > /app/dotnet/malicious.txt' # Expected: Permission denied # Test 5: Own files accessible docker exec -u nextjs $CONTAINER cat /app/nextjs/package.json # Expected: Success ``` ## Best Practices ### Development Guidelines #### DO 1. **Keep all data operations in ServiceStack APIs** ```typescript // Good: API call from React component const api = await client.api(new CreateBooking({ ... })) ``` 2. **Use typed ServiceStack client** ```typescript import { client } from '@/lib/gateway' import { QueryBookings } from '@/lib/dtos' ``` 3. **Protect APIs with authentication** ```csharp [ValidateIsAuthenticated] [ValidateHasRole("Employee")] public class CreateBooking : ICreateDb { } ``` 4. **Store secrets in .NET configuration** ```csharp // In Configure.Db.cs - NOT accessible to Node.js var connStr = Configuration.GetConnectionString("DefaultConnection"); ``` 5. **Use environment variables for deployment secrets** ```yaml # config/deploy.yml env: secret: - SERVICESTACK_LICENSE ``` #### ❌ DON'T 1. **Never use React Server Functions** ```typescript // NEVER DO THIS 'use server' export async function serverAction() { } ``` 2. **Never commit secrets to git** ```json // ❌ Bad: Committed to repo in git // MyApp/appsettings.json { "ConnectionStrings": { "DefaultConnection": "Server=prod;Password=secret123" } } ``` Use GitHub Secrets, environment variables, or secret managers instead, see [Secret Management](#secret-management). 3. **Never expose sensitive data via environment variables to Node.js** ```bash # ❌ Bad: Would expose to Node.js env -i ... DATABASE_PASSWORD="${DATABASE_PASSWORD}" ... ``` 4. **Never disable isolation "for convenience"** ```dockerfile # ❌ Bad: Removes security # RUN chmod 777 /app/dotnet ``` ### Code Review Checklist Before merging code: - [x] No `'use server'` directives anywhere in codebase - [x] All data mutations use ServiceStack APIs - [x] **No secrets committed to git** (use GitHub Secrets or environment variables) - [x] No secrets passed to Node.js environment - [x] API endpoints have proper authentication - [x] Tests verify isolation is working - [x] Production secrets managed via GitHub Secrets, environment variables, or secret manager ## Secret Management The template includes built-in support for managing production App settings whilst maintaining the Node.js isolation guarantees: ### Workflow: Development to Production Run the `secret:prod` npm script to securely store your production configuration in GitHub Actions: ```bash npm run secret:prod ``` This uses the GitHub CLI to add your `appsettings.Production.json` to your GitHub repository's Action secrets: ```bash gh secret set APPSETTINGS_JSON < appsettings.Production.json ``` **How It Works:** 1. **Development** - Create `appsettings.Production.json` locally with your production configuration 2. **Upload** - Run `npm run secret:prod` to store it as a GitHub Action secret (never committed to git) 3. **Deployment** - GitHub Actions injects the secret as the `APPSETTINGS_JSON_BASE64` environment variable 4. **Runtime** - The container startup script decodes and writes it to `/app/dotnet/appsettings.Production.json` 5. **Isolation** - The file is written with root-only permissions, preventing Node.js access Configuration in [config/deploy.yml](https://github.com/NetCoreTemplates/next-rsc/blob/main/config/deploy.yml): ```yaml # config/deploy.yml env: secret: - APPSETTINGS_JSON_BASE64 # Base64-encoded production config ``` **Benefits:** - Secrets never committed to git repository - Secrets never baked into Docker image layers - Same Docker image can be used across all environments - Production configuration remains isolated from Node.js process --- ### Learn More - Run the [Automated Security Test Suite](https://github.com/NetCoreTemplates/next-rsc/blob/main/test-isolation.sh) - Explore the [next-rsc GitHub Repository](https://github.com/NetCoreTemplates/next-rsc) # Replacing Legacy UIs in an AI-First world Source: https://servicestack.net/posts/replacing-legacy-uis Software Development has reached an inflection point where AI Models and tools are now good enough to rebuild complete application UIs in hours, not months. This fundamentally changes the economics of modernizing legacy applications where for many legacy frontends, it’s finally cheaper to rewrite than to keep refactoring legacy code bases. ## The Legacy UI Problem Like old buildings, legacy UIs accumulate years of technical debt that makes renovation increasingly expensive: - **Layers of workarounds** - Code built on top of framework limitations that no longer exist - **Outdated patterns** - Solutions to problems that modern frameworks solve elegantly out-of-the-box - **Dependency hell** - Ancient package versions with security vulnerabilities and incompatible upgrades - **Framework quirks** - Intimate knowledge required of deprecated APIs and edge cases - **Integration friction** - Every new feature must navigate the minefield of existing code Traditional renovation means dealing with all these nuances. Each new feature requires understanding why things were done a certain way, working around old limitations, and maintaining compatibility with outdated patterns. It's like trying to add a second floor to your house whilst still living in it, technically possible, but an expensive, delicate, and compromised endeavor that's rarely done. ## AI-Powered UI Transformation Modern AI models have transformed UI development from a time-intensive rewrite into a rapid transformation process. By utilizing an AI-first development approach (aka Vibe Coding) we can now leverage existing codebases as detailed specifications they can use as a blueprint, telling them exactly what to build. Something AI Models excel at is code transformations, they are remarkable at understanding the intent of code and transforming code from one framework to another, a perfect fit for rewriting legacy UIs. #### Context is King With AI code generation, the more details and context you provide, the closer the output matches your intent. With existing code-bases there is no ambiguity, the AI gets to know exactly what features it needs to build and how they work. ### What Makes This Possible The key insight is that **your existing codebase is the perfect specification** as Legacy UIs already define: - All features and functionality - User interactions and workflows - Data structures and API contracts - Edge cases and business logic - Visual layouts and component hierarchy With just an existing code-base and a detailed migration plan, AI models can translate it to modern frameworks with remarkable accuracy to 90% completion in minutes, whilst you can Vibe Code the rest to get it over the line. ### What Framework to Choose? Up until now, the framework to choose was mostly up to developers personal preferences; in our case we preferred Vue, given its readability and progressive enhancement capabilities. But with an AI-first development model, you're no longer writing the code directly, you're task becomes feeding AI Models text prompts and context on what features to implement, so it's more important to choose a framework that AI Models understand well. Currently, that's: ### The Optimal Stack for AI Development Through this process, we've identified the most effective technology stack for AI-assisted development: - **Next.js 16** - Modern React framework with excellent AI model familiarity - **React 19** - Component patterns that AI models understand deeply - **TypeScript** - Type safety that helps AI generate correct code - **Tailwind CSS v4** - Utility-first styling that AI excels at composing This stack represents the sweet spot where AI models have the most training data, the clearest patterns, and the best ability to generate cohesive, loosely coupled, high-quality code, and what was used for the new [techstacks.io](https://techstacks.io). ## A Real-World Example: TechStacks Whilst the TechStacks C# ServiceStack backend is over a decade old, its UI has undergone multiple migrations, with the last version rewritten 7 years ago. - **v1**: [Angular 1 + Bootstrap](https://github.com/ServiceStackApps/TechStacks) - **v2**: [Nuxt.js 1.4 + Vuetify 1](https://github.com/NetCoreApps/TechStacks) - **v3**: [Next.js 16 + React 19 + Tailwindcss v4](https://github.com/NetCoreApps/techstacks.io) (Vibe Coded UI / Preserved backend .NET APIs) The previous migration from **Angular 1 / Bootstrap** to **Nuxt.js / Vuetify** was done over **several weeks** whilst the last AI completed migration to **React / Tailwindcss** was done within a couple of days. The actual migration and Vibe coded walkthrough itself **only took a few hours**, as the majority of the time was spent moving the existing deployment from an AWS ECS / RDS setup to a much less expensive Hetzner + PostgreSQL setup, [deployed using GitHub Actions](https://github.com/NetCoreApps/techstacks.io/tree/main/.github/workflows) and [Kamal](https://kamal-deploy.org). ### Migration Scope This wasn't a trivial update. The migration involved: - **20 pages** with complex routing and dynamic content - **23 components** including complex forms and interactive elements - Complete conversion from Vuetify/Bootstrap to React 19/Tailwindcss - Migration from JavaScript to strict TypeScript - Replaced Vuetify with Tailwind CSS + [@servicestack/react](https://react.servicestack.net) components - Implementation of modern patterns (Server Components, App Router) ### Where to start: 1. **Create a detailed Migration Plan** - It's vital for big migrations (and other large code generation tasks) to have a detailed plan of what needs to be done, how it will be done, and what the end result will be. ### The Migration Prompt As it's a vital part of AI Assisted development, most AI Tools have planning tools built-in, Since Anthropic gave out free credits to their [Claude Code on the web](https://www.claude.com/blog/claude-code-on-the-web) we used it to create the Migration plan: ```txt Create a detailed plan for completely rewriting this old Nuxt.js Vuetify website into a new modern beautiful Next.js 16 Web App utilizing the existing C# ServiceStack back-end. The entire UI can be erased to make way for a modern, visually stunning React 19, TypeScript and Tailwindcss v4 App. Use the existing Nuxt Vuetify pages to learn how to call its C# ServiceStack APIs with the TypeScript JsonServiceClient and Typed DTOs in ./TechStacks/src/shared/dtos.ts. Do not generate code, only generate a comprehensive detailed plan for how to rewrite the UI layer for the existing C# back-end APIs. All Data is already available in the existing C# APIs. ``` The result of which was the [NEXTJS_MIGRATION_PLAN.md](https://github.com/NetCoreApps/techstacks.io/blob/main/NEXTJS_MIGRATION_PLAN.md). After reviewing the plan and making the necessary changes to match what you want to build it's time to execute the migration. ### Executing the Migration We took a copy of the existing **Nuxt.js / Vuetify code-base** with the **migration plan** and instructed Claude Code to execute the migration with the prompt: ``` Implement the NEXTJS_MIGRATION_PLAN.md ``` With access to both the Migration Plan and existing code-base, Claude Code was able to generate the entire new Next.js UI within 10-20 minutes, for less than $10 (in free credits). The initial AI-generated code wasn't perfect, but it generated an excellent starting point that converted most of the existing Nuxt/Vuetify implementation into a modern Next.js App, with its preferred project structure. ### Vibe Code the rest The most time consuming part of the migration is walking through the entire Application, in tandem with your existing App to test that everything functions as it did before. Fortunately you never need to take the reins to get it over the line, as now that we have a modern AI-friendly Next.js/React/Tailwind UI we can just use Vibe Coding to prompt the AI Models to implement any missing features or fix any issues that you find along the way. If this is your first time using AI Models for all development, it can seem like unrealistic magic from the future. But not only is it possible, it's the most productive development model we've ever experienced, and is all likely to be the future of software development. ### Old vs New UI Here's a sample set of screenshots of the old vs new UIs: Whilst we keep the old UI around for reference, you can view both UIs side-by-side at: - Old (AWS + ECS + RDS): https://vuetify.techstacks.io - New (Hetzner + PostgreSQL): https://techstacks.io ### Why UIs Are Perfect Candidates for Replacement Unlike backend systems where "tear down and rebuild" is far riskier and requires a more methodical approach, UIs are uniquely suited for complete replacement: **1. WYSIWYG Validation** The end result is immediately visible. You can see if it works correctly just by using it. No hidden business logic, no subtle data corruption bugs - it just needs to look and behave right. **2. Clear API Boundaries** When your UI integrates with existing, battle-tested backend APIs, you're building on proven business logic. The API contract is your safety boundary — preventing you from accidentally introducing server-side vulnerabilities or data corruption. **3. No Legacy Baggage** Start fresh without inheriting: - Workarounds for bugs in older framework versions but never updated - CSS hacks for IE11 compatibility that we no longer need - State management patterns designed before modern solutions existed - Build configurations accumulated over years of framework updates **4. Better Modern Frameworks** Today's frameworks solve problems that required custom code in legacy stacks: - Server Components eliminate entire categories of client-side state management - Modern CSS (Grid, Flexbox, Container Queries) replaces brittle layout hacks - TypeScript catches errors that required runtime checks and defensive coding - Built-in optimizations (code splitting, lazy loading) that were manual before **5. Separation of Concerns** The UI layer is just the presentation layer. All the critical business logic, validation, authorization, and data integrity remains safely in the backend where it's been tested and proven over years of production use. ### The AI-First Advantage The real return on migrating isn’t the one-time rewrite, it’s the velocity that's gained afterward. After migrating to an AI‑native stack (Next.js, React, TypeScript, Tailwind), AI agents are better able to reliably implement new features asynchronously, i.e. without human intervention. Changes like **"Add dark mode support"**, **"Implement infinite scrolling"** or **"Add export to CSV"** become natural‑language prompts instead of full developer-assigned tickets. Iteration cycles of most UI features get compressed down to seconds, where it now takes less time to get AI Agents to implement features than it would take to describe the feature to a developer. Code becomes a disposable resource that's become cheap to write, where complete UI rewrites, prototypes, and experiments are now feasible. Most importantly, a high quality product is the result of multiple dev iterations, i.e. continuously improving on features until they work exactly as intended and AI Agents enables far greater iteration velocity than hand coded implementations, at a fraction of the cost – that's able to supercharge the productivity of existing developers. This is the primary benefit of rewriting to an AI‑native stack: creating a new foundation that enables **"Vibe Coding"** - where UI changes are described by text prompts and implemented by AI Agents. Work that once took hours of human effort can now be done using AI Agents in minutes, with the migration typically paying for itself within the first few features. ## Looking Forward We're at the beginning of a fundamental shift in how we approach software development. AI Assistance has become a mandatory tool for developers, amplifying their capabilities and automating away tedious, repetitive work. AI models are only going to get better at understanding codebases and generating accurate implementations. The frameworks and patterns that work best with AI will become the new standard. For organizations with legacy applications, it means **modernization is now economically viable**. Barriers that made UI rewrites prohibitively expensive have eroded. What was once a multi-month project requiring dedicated teams is now achievable in days with AI assistance. ## From .NET APIs to AI First UIs The unprecedented productivity of AI Assisted development has transformed our roadmap which is now firmly centered on developing the ideal .NET AI‑first development stack, powered by a growing suite of React / TypeScript / Tailwind CSS templates and components, the ultimate UX of hot-reloading npm UIs and built on our highly capable & performant .NET backend APIs. # New .NET 10 + Angular 21 ASP.NET Identity Auth Tailwind SPA Template Source: https://servicestack.net/posts/angular-template We're excited to announce the release of our new **Angular 21 SPA Template** - a modern, full-stack template combining the latest Angular 21 frontend with a powerful .NET 10 backend powered by ServiceStack. ## What's New ### Angular with Modern Features - **Standalone Components** - No NgModules, cleaner component architecture - **Signal-based State Management** - Reactive state with Angular's new signals API - **TypeScript 5.9** - Latest TypeScript features and improved type safety - **Tailwind CSS 4** - Utility-first styling with dark mode support ### .NET 10 Backend - **ServiceStack v10** - High-performance .NET APIs with AutoQuery CRUD - **Entity Framework Core 10** - For ASP.NET Core Identity - **OrmLite** - Fast, typed POCO ORM for application data - **SQLite** - Zero-configuration database (easily swap for PostgreSQL, SQL Server, etc.) ### Upgrading to an production RDBMS To switch from SQLite to PostgreSQL/SQL Server/MySQL: 1. Install preferred RDBMS (`ef-postgres`, `ef-mysql`, `ef-sqlserver`), e.g: :::sh {.mb-8} npx add-in ef-postgres ::: 2. Install `db-identity` to also switch to use this RDBMS for [Background Jobs](https://docs.servicestack.net/rdbms-background-jobs) and [Request Logs Analytics](https://docs.servicestack.net/admin-ui-rdbms-analytics): :::sh {.mb-8} npx add-in db-identity ::: ## Simplified .NET + Angular Development Workflow - Single endpoint `https://localhost:5001` for both .NET and Angular UI (no dev certs required) - ASP.NET Core proxies requests to Angular dev server (port 4200) - Hot Module Replacement (HMR) support for instant UI updates - WebSocket proxying for Angular HMR functionality ![](https://docs.servicestack.net/img/pages/templates/angular-dev.svg) ## .NET Angular App with Static Export **Angular SPA** uses **static export**, where a production build of the Angular App is generated at deployment and published together with the .NET App in its `/wwwroot` folder, utilizing static file serving to render its UI. This minimal `angular-spa` starting template is perfect for your next AI Assisted project, offering a streamlined foundation for building modern web applications with **Angular 21** and **.NET 10**: ![](https://docs.servicestack.net/img/pages/templates/static-prod.svg) ## Key Features ### 🔐 ASP.NET Core Identity Authentication Full authentication system with beautifully styled Tailwind CSS pages: - User registration and login - Email confirmation - Password reset - Profile management - Role-based authorization ### ⚡ Rapid AutoQuery CRUD dev workflow Quickly generate complete C# [CRUD APIs](https://docs.servicestack.net/autoquery/crud) and [DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) from simple [TypeScript data models](https://localhost:5002/autoquery/okai-models): 1. Create a new feature :::sh npx okai init MyFeature ::: 2. Define your TypeScript data models in `MyFeature.d.ts`, e.g: :::sh code MyApp.ServiceModel/MyFeature.d.ts ::: 3. When ready, generate C# APIs and migrations :::sh npx okai MyFeature.d.ts ::: 4. Apply database migrations :::sh npm run migrate ::: ### Use AI for quick scaffolding To help quickly scaffold your data models and features, use ServiceStack's AI assistant. Example of creating AutoQuery CRUD APIs for managing products: :::sh npx okai "Manage products price and inventory" ::: ### 📊 Background Jobs Durable background job processing with: - Command-based job execution - Recurring job scheduling - SMTP email sending via background workers ### 📝 Request Logging SQLite-backed request logging for: - API request tracking - Error monitoring - Performance analysis ### 🔍 Built-in Admin UIs - **/ui** - ServiceStack API Explorer - **/admin-ui** - Database management, user administration - **/swagger** - OpenAPI documentation (development mode) ## Architecture Highlights ### Hybrid Development Model During development, `dotnet watch` starts both the .NET backend and Angular dev server with Hot Module Replacement. In production, Angular builds to static files served directly by ASP.NET Core. ### Modular Configuration Clean separation of concerns with `IHostingStartup` pattern: - [Configure.AppHost.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.AppHost.cs) - Main ServiceStack AppHost registration - [Configure.Auth.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.Auth.cs) - ServiceStack AuthFeature with ASP.NET Core Identity integration - [Configure.AutoQuery.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.AutoQuery.cs) - AutoQuery features and audit events - [Configure.Db.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.Db.cs) - Database setup (OrmLite for app data, EF Core for Identity) - [Configure.Db.Migrations.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.Db.Migrations.cs) - Runs OrmLite and EF DB Migrations and creates initial users - [Configure.BackgroundJobs.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.BackgroundJobs.cs) - Background job processing - [Configure.HealthChecks.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Configure.HealthChecks.cs) - Health monitoring endpoint This pattern keeps [Program.cs](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp/Program.cs) clean and separates concerns. ### Type-Safe API Client Auto-generated TypeScript DTOs ensure type safety across the stack: ```typescript import { QueryBookings } from '@/dtos' const response = await client.api(new QueryBookings({ minCost: 100 })) if (response.succeeded) { console.log(response.response!.results) } ``` ## Deployment Ready GitHub Actions workflows included for: - **CI/CD** - Automated build and test - **Container Builds** - Docker image creation - **Kamal Deployment** - One-command production deployment with SSL ### Kamal Deployments All deployments include the GitHub Action workflows to deploy your App to [any Linux Server with Kamal](https://docs.servicestack.net/kamal-deploy) using Docker, SSH and GitHub Container Registry (ghcr). Where you can host it on a [Hetzner US Cloud](https://www.hetzner.com/cloud) VM for as low as **$5 per month** or if you have multiple Apps you can deploy them all to a single VM which we're doing for our .NET Template Live Demos which runs **30 Docker Apps** on a **8GB RAM/80GB SSD** dedicated VM for **$15 /month**. ## AI-Assisted Development with CLAUDE.md As part of our objectives of improving developer experience and embracing modern AI-assisted development workflows - all new .NET React templates include a comprehensive `AGENTS.md` file designed to optimize AI-assisted development workflows. ### What is CLAUDE.md? `CLAUDE.md` and [AGENTS.md](https://agents.md) onboards Claude (and other AI assistants) to your codebase by using a structured documentation file that provides it with complete context about your project's architecture, conventions, and technology choices. This enables more accurate code generation, better suggestions, and faster problem-solving. ### What's Included Each template's `AGENTS.md` contains: - **Project Architecture Overview** - Technology stack, design patterns, and key architectural decisions - **Project Structure** - Gives Claude a map of the codebase - **ServiceStack Conventions** - DTO patterns, Service implementation, AutoQuery, Authentication, and Validation - **React Integration** - TypeScript DTO generation, API client usage, component patterns, and form handling - **Database Patterns** - OrmLite setup, migrations, and data access patterns - **Common Development Tasks** - Step-by-step guides for adding APIs, implementing features, and extending functionality - **Testing & Deployment** - Test patterns and deployment workflows ### Extending with Project-Specific Details The existing `CLAUDE.md` serves as a solid foundation, but for best results, you should extend it with project-specific details like the purpose of the project, key parts and features of the project and any unique conventions you've adopted. ### Benefits - **Faster Onboarding** - New developers (and AI assistants) understand project conventions immediately - **Consistent Code Generation** - AI tools generate code following your project's patterns - **Better Context** - AI assistants can reference specific ServiceStack patterns and conventions - **Reduced Errors** - Clear documentation of framework-specific conventions - **Living Documentation** - Keep it updated as your project evolves ### How to Use Claude Code and most AI Assistants already support automatically referencing `CLAUDE.md` and `AGENTS.md` files, for others you can just include it in your prompt context when asking for help, e.g: > Using my project's AGENTS.md, can you help me add a new AutoQuery API for managing Products? The AI will understand your App's ServiceStack conventions, React setup, and project structure, providing more accurate and contextual assistance. ### Getting Started All new [angular-spa.web-templates.io](https://angular-spa.web-templates.io) include [AGENTS.md](https://github.com/NetCoreTemplates/angular-spa/blob/main/AGENTS.md) by default. For existing projects, you can adapt the template to document your App's conventions, patterns and technology choices. ## Feature Tour Angular's structured approach to modern web development is ideal for large complex Applications that stitches together various technologies, handling authentication, designing responsive UIs, and managing complex state which the new Angular SPA template embraces to provide a productive starting point with a robust foundation packed with essential features right out of the box. 1. **Built-in Identity Authentication:** Secured out-of-the-box, this template integrates seamlessly with ASP.NET Core Identity, providing ready-to-use registration, login, and User Admin management features. 2. **Tailwind v4 CSS:** Rewritten to use Tailwind v4 CSS, allowing you to rapidly build beautiful, responsive designs directly in your markup. 3. **Dark Mode Support:** Cater to user preferences with built-in, easily toggleable dark mode support, styled elegantly with Tailwind. 4. **Customizable DataGrid Component:** Effortlessly display tabular data with the included customizable DataGrid. Easily adapt it for sorting, filtering and displaying your specific data structures. 5. **Reusable Input Components with Validation:** The template includes reusable, pre-styled input components (e.g., text input, selects) with built-in support for validation bound forms and contextual displaying of validation errors. 6. **RxJS & Signals Support:** Modern Angular reactivity: whether you prefer the established power of **RxJS Observables** or the new granular reactivity of **Angular Signals**, our template is structured to support *both* programming models. We'll take a quick tour to explore the templates features: ### Home Page The home page sports a responsive Tailwind design where all its components are encapsulated within its [/app/home](https://github.com/NetCoreTemplates/angular-spa/tree/main/MyApp.Client/src/app/home) with its logic maintained in `*.ts` files and its presentation UI optionally maintained in a separate `*.html` file. ### Dark Mode The [dark-mode-toggle.component.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/components/dark-mode-toggle.component.ts) and [theme.service.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/components/services/theme.service.ts) handles switching between Light and Dark Mode which is initially populated from the Users OS preference. ### Weather The Weather page maintained in [/app/weather](https://github.com/NetCoreTemplates/angular-spa/tree/main/MyApp.Client/src/app/weather) provides a good example of utilizing an RxJS Observable programming model with the [api-http-client.service.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/components/services/api-http-client.service.ts) that extends Angular's Observable HttpClient with an additional `api` method that lets you use your Services typed `dtos.ts` TypeScript DTOs to enable type-safe integration with your back-end services: ```ts import { Forecast, GetWeatherForecast, ResponseStatus } from 'src/dtos' import { ApiHttpClient } from 'src/components/services/api-http-client.service' export class WeatherComponent { http = inject(ApiHttpClient); public error: ResponseStatus | null = null; public forecasts: Forecast[] = []; getForecasts() { this.http.api(new GetWeatherForecast({ date:'2025-04-01' })).subscribe({ next:(result) => { this.error = null; this.forecasts = result; }, error:(error) => { this.error = error; } }); } } ``` Whilst its [weather.component.html](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/app/weather/weather.component.html) template showcases the new [data-grid.component.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/components/data-grid.component.ts) to display a beautiful tailwind DataGrid with just: ```html ``` :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-datagrid-default.webp)](https://angular-spa.web-templates.io/weather) ::: It's a direct port of our [Vue DataGrid](https://docs.servicestack.net/vue/datagrid) that also supports the same customizations allowing for custom Headers and Column fields, e.g: ```html
Date
{{ x | date:'MMMM d, yyyy' }} {{ x }}° {{ x }}° {{ x }}
``` Which renders the expected: :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-datagrid-custom.webp)](https://angular-spa.web-templates.io/weather) ::: ## Todos MVC The Todos MVC App maintained in [/app/todomvc](https://github.com/NetCoreTemplates/angular-spa/tree/main/MyApp.Client/src/app/todomvc) demonstrates how to create the popular [todomvc.com](https://todomvc.com) App in Angular 19. :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-todos.webp)](https://angular-spa.web-templates.io/todomvc) ::: It's another example of building a simple CRUD Application with Angular RxJS Observables and your APIs TypeScript DTOs. This snippet shows how to query and create Todos with the `ApiHttpClient`: ```ts import { Todo, QueryTodos, CreateTodo, ResponseStatus } from 'src/dtos' import { ApiHttpClient } from 'src/components/services/api-http-client.service' export class TodoMvcComponent implements OnInit { client = inject(ApiHttpClient); error: ResponseStatus | null = null; todos: Todo[] = []; newTodoText = ''; loadTodos(): void { this.client.api(new QueryTodos()).subscribe({ next: (todos) => { this.todos = todos.results; }, error: (err) => { this.error = err; } }); } addTodo(): void { if (!this.newTodoText.trim()) return; this.client.api(new CreateTodo({ text: this.newTodoText.trim() })).subscribe({ next: (todo) => { this.todos.push(todo); this.newTodoText = ''; }, error: (err) => { this.error = err; console.error('Error adding todo:', err); } }); } //... } ``` ## Bookings All other examples in the template uses Angular's newer Signal for reactivity and the standard ServiceStack `JsonServiceClient` used in all other TypeScript/JS Apps. The Bookings Pages are maintained in [/app/bookings](https://github.com/NetCoreTemplates/angular-spa/tree/main/MyApp.Client/src/app/bookings) and showcases a more complete example of developing a CRUD UI in Angular starting with an example of how to encapsulate route information for a feature in an isolated [booking.routes.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/app/bookings/booking.routes.ts): ```ts import { Routes } from '@angular/router'; import { BookingListComponent } from './booking-list.component'; import { BookingCreateComponent } from './booking-create.component'; import { BookingEditComponent } from './booking-edit.component'; import { authGuard } from 'src/guards'; export const BOOKING_ROUTES: Routes = [ { path: 'bookings', component: BookingListComponent, canActivate: [authGuard] }, { path: 'bookings/create', component: BookingCreateComponent, canActivate: [authGuard] }, { path: 'bookings/edit/:id', component: BookingEditComponent, canActivate: [authGuard] } ]; ``` The use of the Route `authGuard` ensures only Authenticated Users can access these routes, as well as redirecting non-authenticated users to the Sign In page. ### Bookings List :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-bookings-list.webp)](https://angular-spa.web-templates.io/bookings) ::: The bookings list component shows an example of using Angular's Signals with the `JsonServiceClient` together with an `ApiState` context to enable data bound forms and validation errors: ```ts @Component({ templateUrl: './booking-list.component.html', providers: [ ...provideApiState() ], //... }) export class BookingListComponent implements OnInit { private router = inject(Router); private client = inject(JsonServiceClient); api = inject(ApiState); // Signals for state allBookings = signal([]); ngOnInit(): void { this.loadBookings(); } async loadBookings(): Promise { this.api.begin(); const api = await this.client.api(new QueryBookings({ orderByDesc: 'BookingStartDate', })); if (api.succeeded) { this.allBookings.set(api.response!.results); } this.api.complete(api.error); } } ``` Using `provideApiState()` implicitly injects the populated API context containing both the APIs Loading and Error state into child components saving you from having to explicitly inject it into each component. E.g. the `` component will display when API Requests are in-flight whilst API Error Responses are displayed after receiving failed API Responses: ```html @if (allBookings().length > 0) { ... } @else {

No bookings found

}
``` ### Create Booking The [booking-create.component.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/app/bookings/booking-create.component.ts) shows the standard pattern of calling ServiceStack Typed APIs to save forms whilst saving any validation errors to the `ApiState` context: ```ts async save(): Promise { this.api.begin(); const request = new CreateBooking(this.booking()); const api = await this.client.api(request); if (api.succeeded) { // Navigate back to bookings list after successful save this.router.navigate(['/bookings']); } this.api.complete(api.error); } ``` Where any contextual validation will be displayed next to the input field: :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-booking-create-validation.webp)](https://angular-spa.web-templates.io/bookings/create) ::: ### Edit Booking The [booking-edit.component.ts](https://github.com/NetCoreTemplates/angular-spa/blob/main/MyApp.Client/src/app/bookings/booking-edit.component.ts) shows an example of using the `JsonServiceClient` with Signals to get and modify bookings: ```ts export class BookingEditComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private client = inject(JsonServiceClient); meta = inject(MetadataService); api = inject(ApiState); // Signals booking = signal(new Booking()); ngOnInit(): void { // Get booking ID from route params const id = this.route.snapshot.paramMap.get('id'); if (id) { this.fetchBooking(parseInt(id, 10)); } else { this.api.setErrorMessage('Booking ID is required'); } } async fetchBooking(id: number): Promise { this.api.begin(); const api = await this.client.api(new QueryBookings({id})); if (api.succeeded) { this.booking.set(api.response!.results[0]); } this.api.complete(api.error); } async save(): Promise { this.api.begin(); const api = await this.client.api(new UpdateBooking(this.booking())); if (api.succeeded) { this.router.navigate(['/bookings']); } this.api.complete(api.error); } } ``` :::{.not-prose .p-4 .mx-auto .max-w-3xl .shadow .rounded-lg} [![](https://docs.servicestack.net/img/pages/templates/angular-spa/angular-booking-edit.webp)](https://angular-spa.web-templates.io/bookings/edit/1) ::: It shows an example of a validation bound form bounded to a signal instance of a `Booking` DTO with summary and contextual validation and utilization of your API's metadata with `meta.enumOptions('RoomType')` which populates the `
Close @if (booking().cancelled) { Restore Booking } @else { Cancel Booking } {{ api.loading() ? 'Saving...' : 'Save Booking' }}
} @else { } ``` # New Vibe Codable .NET 10 React Templates Source: https://servicestack.net/posts/vibecode-react-templates Over the last few months our primary focus has been on enabling first-class support for React, this is a contrast from our own decade-long personal preference for Vue which has better affinity with HTML and its support for progressive enhancement enabling a [Simple, Modern JavaScript](https://servicestack.net/posts/javascript) development workflow without requiring npm or any build tools and why it was chosen for all of ServiceStack's [built-in UIs](https://servicestack.net/auto-ui). Whilst our [focus for Blazor](https://servicestack.net/blazor) was driven by .NET's preference for Blazor given it's primary positioning by the .NET team. ### Software Development has changed forever Software Development has reached an inflection point where AI Models and tools are now good enough to build features in minutes, not hours and rewrite mid-sized application UIs in hours, not months. This fundamentally changes the economics of Software Development. Whatever our developer preferences were have become significantly less important in the age of AI where the most important factor is now which frameworks AI Models are most proficient in. ### The Optimal Stack for AI Development With an AI-first development model, you're no longer writing the code directly, your task becomes feeding AI Models text prompts and context on what features to implement, so it's more important to choose a framework that AI Models understand well. Currently, that's: - **Next.js 16** - Modern React framework with excellent AI model familiarity - **React 19** - Component patterns that AI models understand deeply - **TypeScript** - Type safety that helps AI generate correct code - **Tailwind CSS v4** - Utility-first styling that AI excels at composing This stack represents the sweet spot where AI models have the most training data, the clearest patterns, and the best ability to generate cohesive, loosely coupled, high-quality code - where it's the de facto standard for instant AI-generated Apps from [Replit](https://blog.replit.com/react), [Lovable](https://lovable.dev/blog/best-tailwind-css-component), [Google's AI Studio](https://aistudio.google.com), [Vercel's v0](https://v0.app) and [Claude Code Web](https://claude.ai/code). ### react-templates.net The culmination of our work on React support is being poured into the new [react-templates.net](https://react-templates.net) website: [![](https://docs.servicestack.net/img/pages/react/react-templates.net.webp)](https://react-templates.net) ### Ultimate Developer Experience As we expect this to be the future of software development, we've focused on creating the best possible developer experience for all React templates, starting with removing the complexity of needing to manage 2 independent dev servers and local self-signed dev SSL certificates. Instead you're able to run `dotnet watch` or `dotnet run` to run your React App like every other .NET App, where it's accessible at `https://localhost:5001`: ![](https://docs.servicestack.net/img/pages/react/info/react-static-dev.svg) During development the new `NodeProxy` takes care of routing all non-matching routes to the underlying Node server, it also takes care of proxying the HotModuleReload (HMR) WebSocket connections of Next.js or Vite React Apps, where we finally get to experience the benefits that Vite/Next.js developers have been enjoying for years, with fast, stateful, iterative feedback loops. ### Seamless fusion of .NET APIs, Razor Pages and React UIs Another benefit of this architecture of the .NET App handling all Requests and only proxying unknown requests to the Node server is that it enables a seamless fusion of .NET Razor Pages and React UIs. As many customers have customized Identity Auth flows we've included the [Tailwind Identity Auth Razor Pages](https://github.com/NetCoreTemplates/react-static/tree/main/MyApp/Areas/Identity/Pages) from the [razor](https://github.com/NetCoreTemplates/razor) template into all new .NET React Templates. This ability to seamlessly integrate React components within Razor Pages enables a gradual migration strategy, allowing teams to incrementally modernize legacy ASP.NET websites by progressively replacing individual pages or sections with React UIs without requiring a complete rewrite or disrupting existing functionality. ### .NET React Templates with Static Exports All existing SPA Templates have only used **static exports**, where at deployment a production build of the Node App is generated and published together with the .NET App in its `/wwwroot` folder, utilizing static file serving to render its UI: ![](https://docs.servicestack.net/img/pages/react/info/react-static-prod.svg) This continues to be the case for **4/5 of the .NET React Templates**, starting with 2x new `react-static` and `next-static` minimal starting templates - perfect base for your next Vibe Coding project, starting with the simplest template: ## Vibe Codable .NET React Templates When your App needs the features from a full-featured Web Framework like Next.js with file-based routing and SEO you choose from the Next.js templates starting with: Like React Static, Next.js Static is a static export of a Next.js App, but what about when you need the full power of Next.js? For that you can use: ### Next.js in Production Using full Next.js does mean we also need to have a Next.js runtime at production, which looks like: ![](https://docs.servicestack.net/img/pages/react/info/next-rsc-prod.svg) Fortunately Docker simplifies managing both .NET and Node servers as a single deployable unit, with the next-rsc custom [Dockerfile](https://github.com/NetCoreTemplates/next-rsc/blob/main/Dockerfile) handling the orchestration. ### Full-Featured React Templates In addition to the minimal starting templates above, we've also created 2 full-featured React Templates providing a good reference implementation for integrating several React features including Blog, MDX, Todos and shadcn/ui components alongside .NET features like API Keys, AI Chat & Swagger UI. ### Kamal Deployments All deployments include the GitHub Action workflows to deploy your App to [any Linux Server with Kamal](https://react-templates.net/docs/deployments) using Docker, SSH and GitHub Container Registry (ghcr). Where you can host it on a [Hetzner US Cloud](https://www.hetzner.com/cloud) VM for as low as **$5 per month** or if you have multiple Apps you can deploy them all to a single VM which we're doing for our .NET Template Live Demos which runs **30 Docker Apps** on a **8GB RAM/80GB SSD** dedicated VM for **$15 /month**. ## AI-Assisted Development with CLAUDE.md As part of our objectives of improving developer experience and embracing modern AI-assisted development workflows - all new .NET React templates include a comprehensive `AGENTS.md` file designed to optimize AI-assisted development workflows. ### What is CLAUDE.md? `CLAUDE.md` and [AGENTS.md](https://agents.md) onboards Claude (and other AI assistants) to your codebase by using a structured documentation file that provides it with complete context about your project's architecture, conventions, and technology choices. This enables more accurate code generation, better suggestions, and faster problem-solving. ### What's Included Each template's `AGENTS.md` contains: - **Project Architecture Overview** - Technology stack, design patterns, and key architectural decisions - **Project Structure** - Gives Claude a map of the codebase - **ServiceStack Conventions** - DTO patterns, Service implementation, AutoQuery, Authentication, and Validation - **React Integration** - TypeScript DTO generation, API client usage, component patterns, and form handling - **Database Patterns** - OrmLite setup, migrations, and data access patterns - **Common Development Tasks** - Step-by-step guides for adding APIs, implementing features, and extending functionality - **Testing & Deployment** - Test patterns and deployment workflows ### Extending with Project-Specific Details The existing `CLAUDE.md` serves as a solid foundation, but for best results, you should extend it with project-specific details like the purpose of the project, key parts and features of the project and any unique conventions you've adopted. ### Benefits - **Faster Onboarding** - New developers (and AI assistants) understand project conventions immediately - **Consistent Code Generation** - AI tools generate code following your project's patterns - **Better Context** - AI assistants can reference specific ServiceStack patterns and conventions - **Reduced Errors** - Clear documentation of framework-specific conventions - **Living Documentation** - Keep it updated as your project evolves ### How to Use Claude Code and most AI Assistants already support automatically referencing `CLAUDE.md` and `AGENTS.md` files, for others you can just include it in your prompt context when asking for help, e.g: > Using my project's AGENTS.md, can you help me add a new AutoQuery API for managing Products? The AI will understand your App's ServiceStack conventions, React setup, and project structure, providing more accurate and contextual assistance. ### Getting Started All new [react-templates.net](https://react-templates.net) include [AGENTS.md](https://github.com/NetCoreTemplates/react-static/blob/main/AGENTS.md) by default. For existing projects, you can adapt the template to document your App's conventions, patterns and technology choices. # .NET 10's new OpenAPI Scalar + Swagger UIs Source: https://servicestack.net/posts/openapi-net10 # .NET 10 LTS We're excited to announce **ServiceStack v10** - a major release aligned with Microsoft's newly released .NET 10! This milestone release brings first-class .NET 10 support across the entire ServiceStack ecosystem, with all packages now shipping native .NET 10 builds optimized for the latest runtime. As part of this major version upgrade, we've modernized our tooling and streamlined our package offerings: - **All project templates upgraded to .NET 10** - Start new projects on the latest LTS framework - **Adopted the new `.slnx` solution format** - Embracing .NET's modern, simplified solution file format - **Enhanced Kamal GitHub Action deployments** - Streamlined CI/CD that intelligently derives configuration from your repository name and GitHub Action context ## .NET 10 OpenAPI Support .NET 10 has added support for generating OpenAPI schemas and API Explorer UIs with there now being multiple ways to generate OpenAPI schemas and multiple ways to view them. Assuming the trend of Microsoft's defaults having a determinant influence on the wider .NET ecosystem, we expect Microsoft's new [Microsoft.AspNetCore.OpenApi](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) to become the defacto default and Swashbuckle to suffer a slow death as a result, until then it's still the simpler and more popular combination that we've added .NET 10 support for in the new NuGet package: ### ServiceStack.OpenApi.Swashbuckle Depends on: - Microsoft.OpenApi v2.x - Swashbuckle.AspNetCore v10.x
Which can be added to .NET 10 Project with: :::sh npx add-in openapi-swagger ::: Which uses `Swashbuckle.AspNetCore` for generating OpenAPI schemas and displaying the Swagger UI. ## Microsoft.AspNetCore.OpenApi This was a more frustrating package to support starting with trying to use the latest **Microsoft.OpenApi v3.x** results in failures at runtime since its latest version is limited to **Microsoft.OpenApi v2.x**. In addition **Microsoft.OpenApi v2.x** use of Analyzers/Interceptors made it impossible to add support for the **Microsoft.OpenApi v2.x** in .NET 10 builds whilst preserving **Microsoft.OpenApi v1.x** for NET 8.0 builds. As a result we've had to publish .NET 10 support in the new NuGet package: ### ServiceStack.OpenApi.Microsoft Depends on: - Microsoft.OpenApi v2.x - Microsoft.AspNetCore.OpenApi v10.x
Which can be added to .NET 10 Project with: :::sh npx add-in openapi-scalar ::: Which uses `Microsoft.AspNetCore.OpenApi` to generate OpenAPI schemas and is configured to use `Scalar.AspNetCore` to display the newer Scalar UI from the VC-backed [scalar.com](https://scalar.com). Unfortunately `Microsoft.AspNetCore.OpenApi` has issues that we're surprised to find its complexity leaking into end-user projects which requires adding an `` configuration to your **MyApp.csproj**: ```xml $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated ``` Another alternative we've discovered to avoid build issues is to disable its XML Comment source generator: ```xml ``` ## npx scripts A new **v10.0.0** version of the **x** dotnet tool is now available with.NET 10 support: ```bash dotnet tool install --global x # install dotnet tool update -g x # update ``` Although this is the last .NET runtime that the `x` tool will support as it's being phased out in favor of use-case specific `npx` scripts as it doesn't require a separate install or needs a .NET 10 SDK to be pre-installed. The npx tools have the same behavior as the different x sub-features where you can just replace the command prefix with the npx script equivalent, e.g: | x command | npx script | description | | ------------ | ----------------------- | ----------- | | `x new` | `npx create-net` | Create a new App from a .NET 10 project template | | `x mix` | `npx add-in` | Register and configure a Plugin with your App | | `x ts` | `npx get-dtos ts` | Regenerate latest TypeScript DTOs | | `x ts ` | `npx get-dtos ts ` | Generate DTOs for a remote ServiceStack API | # New React SPA Template Source: https://servicestack.net/posts/net8-react-spa-template ::include react-templates.md:: ## ServiceStack React SPA Template Just as we've enhanced the built-in ASP.NET Core React SPA template with the new [ServiceStack Vue SPA template](/posts/net8-vue-spa-template) we've also enhanced the built-in ASP.NET Core React SPA template with the new TypeScript [Vite React SPA template](https://react-spa.web-templates.io) with many new value-added and high-productivity features.

Vite React SPA Template

Explore the high productivity features in the new ServiceStack React SPA template

:::{.text-center} ## Live Demo ::: :::{.shadow .pb-1} [![](https://raw.githubusercontent.com/ServiceStack/Assets/master/csharp-templates/react-spa.png)](https://react-spa.web-templates.io) ::: ## ASP.NET Core React SPA Template The [React and ASP.NET Core](https://learn.microsoft.com/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react) template provides a seamless starting solution which runs both the .NET API backend and Vite React frontend during development. It's a modern template enabling an excellent developer workflow for .NET React Apps, configured with Vite's fast HMR (Hot Module Reload), TypeScript support with TSX enabling development of concise and expressive type-safe components. ### Minimal API integration Whilst a great starting point, it's still only a basic template configured with a bare-bones React Vite App that's modified to show an example of calling a Minimal API. And the need for hand maintained Types to describe the shape of the API responses with [Stringly Typed](https://wiki.c2.com/?StringlyTyped) fetch API calls referencing **string** routes: ```ts interface Forecast { date: string; temperatureC: number; temperatureF: number; summary: string; } function App() { const [forecasts, setForecasts] = useState(); useEffect(() => { populateWeatherData(); }, []); //... } async function populateWeatherData() { const response = await fetch('weatherforecast'); const data = await response.json(); setForecasts(data); } ``` Which is used to render the API response in a hand rolled table: ```tsx function App() { //... const contents = forecasts === undefined ?

Loading... Please refresh once the ASP.NET backend has started. See jsps for more details.

: {forecasts.map(forecast => )}
Date Temp. (C) Temp. (F) Summary
{forecast.date} {forecast.temperatureC} {forecast.temperatureF} {forecast.summary}
; } ``` ### ServiceStack API Integration Fortunately ServiceStack can significantly improve this development experience with the [/api pre-defined route](https://docs.servicestack.net/endpoint-routing#api-pre-defined-route) where only a single proxy rule is needed to proxy all APIs: ```ts export default defineConfig({ //... server: { proxy: { '^/api': { target, secure: false } }, } }) ``` ### End-to-end Typed APIs Instead of hand-rolled types and Stringly Typed API calls, it utilizes server [generated TypeScript DTOs](https://docs.servicestack.net/typescript-add-servicestack-reference) with a generic JsonServiceClient to enable end-to-end Typed APIs: ```ts import { useState, useEffect } from "react" import { useClient } from "@/gateway" import { GetWeatherForecast } from "@/dtos" const client = useClient() const [forecasts, setForecasts] = useState([]) useEffect(() => { (async () => { const api = await client.api(new GetWeatherForecast()) if (api.succeeded) { setForecasts(api.response!) } })() }, []) ``` This benefits in less code to maintain, immediate static typing analysis to ensure correct usage of APIs and valuable feedback when APIs are changed, that's easily updated with a single command: :::sh npm run dtos ::: ### React Component Ecosystem Given it's popularity, React has arguably the richest ecosystem of freely available libraries and components, a good example are the popular [shadcn/ui](https://ui.shadcn.com) Tailwind components. Unlike most libraries they're source copied piecemeal into your project where they're locally modifiable, i.e. instead of an immutable package reference. As they're just blueprints, they're not dependent on a single library and will utilize the best library to implement each component if needed. E.g. the [Data Table](https://ui.shadcn.com/docs/components/data-table) component documents how to implement your own Data Table utilizing the headless [TanStack Table](https://tanstack.com/table/latest) - a version of which we've built into [DataTable.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/components/DataTable.tsx) which is used in the template to implement both complex CRUD UIs and [weather.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/weather.tsx) simple table results: ```tsx import { columnDefs, DataTable, getCoreRowModel } from "@/components/DataTable.tsx" const columns = columnDefs(['date', 'temperatureC', 'temperatureF', 'summary'], ({ temperatureC, temperatureF}) => { temperatureC.header = "Temp. (C)" temperatureF.header = "Temp. (F)" temperatureC.cell = temperatureF.cell = ({ getValue }) => <>{getValue()}° }) return ( ) ``` To render the [/weather](https://react-spa.web-templates.io/weather) customized Data Table: :::{.mx-auto .max-w-lg .shadow .rounded} [![](/img/posts/net8-react-spa-template/data-table.png)](https://react-spa.web-templates.io/weather) ::: The template also includes customizable [Form.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/components/Form.tsx) Input components which can be used to create beautiful validation-bound forms which effortlessly integrates with ServiceStack's [Error Handling](https://docs.servicestack.net/error-handling) and [Declarative Validation](https://docs.servicestack.net/declarative-validation) attributes. ## ServiceStack React SPA Features Other high-productivity features available in the ServiceStack React SPA template include: ### Integrated Identity Auth Pre-configured with ASP.NET Core Identity Auth, including Sign In and Custom Registration APIs and UI Pages which can be customized as needed, examples of Role-based security as well as a turn key solution for Integrating Identity Auth Registration workflow with your [SMTP Provider](https://docs.servicestack.net/auth/identity-auth#smtp-iemailsender) with all emails sent from a managed non-blocking [Background MQ](https://docs.servicestack.net/background-mq) for optimal responsiveness and execution. ### tailwindcss [Tailwind](https://tailwindcss.com) has quickly become the best modern CSS framework for creating scalable, [mobile-first](https://tailwindcss.com/#mobile-first) responsive websites built upon a beautiful expert-crafted constraint-based [Design System](https://tailwindcss.com/#constraint-based) that enables effortless reuse of a growing suite of [Free Community](https://tailwindcomponents.com) and professionally-designed [Tailwind UI Component](https://tailwindui.com) Libraries, invaluable for quickly creating beautiful websites. [![](/img/pages/blazor/tailwindui.png)](https://tailwindcss.com) In addition to revolutionizing how we style mobile-first responsive Apps, Tailwind's [Dark Mode](https://tailwindcss.com/#dark-mode) does the same for enabling Dark Mode a feature supported throughout the template and its Tailwind UI Components. [![](/img/posts/net8-react-spa-template/dark-mode.png)](https://tailwindcss.com/#dark-mode) ### Built for Productivity So that you're immediately productive out-of-the-box, the template includes a rich set of high-productivity features, including: | | | |---------------------------------------------------------------------|--------------------------------------------------------------| | [tailwind/typography](https://tailwindcss-typography.vercel.app) | Beautiful css typography for markdown articles & blog posts | | [tailwind/forms](https://github.com/tailwindlabs/tailwindcss-forms) | Beautiful css form & input styles that's easily overridable | | [Markdown](https://mdxjs.com/docs/getting-started/) | Native [mdx](https://mdxjs.com) Markdown integration | | [React Router](https://reactrouter.com) | Full featured routing library for React | | [plugin/press](https://github.com/ServiceStack/vite-plugin-press) | Static markdown for creating blogs, videos and other content | | [plugin/pages](https://github.com/hannoeru/vite-plugin-pages) | Conventional file system based routing for Vite | | [plugin/svg](https://github.com/pd4d10/vite-plugin-svgr) | Load SVG files as React components | | [Iconify](https://iconify.design) | Unified registry to access 100k+ high quality SVG icons | ### Bookings CRUD Pages The [Bookings CRUD example](https://react-spa.web-templates.io/bookings-crud) shows how you can utilize a customized Data Table and templates Form components to create a beautifully styled CRUD UI with minimal effort. ## Vite Press Plugin [![](https://images.unsplash.com/photo-1524668951403-d44b28200ce0?crop=entropy&fit=crop&h=384&w=768)](https://vue-spa.web-templates.io/posts/vite-press-plugin) Most Apps typically have a mix of dynamic functionality and static content which in our experience is best maintained in Markdown, which is why excited about the new [Vite Press Plugin](https://vue-spa.web-templates.io/posts/vite-press-plugin) which brings the same Markdown features in our [razor-ssg](https://razor-ssg.web-templates.io), [razor-press](https://razor-press.web-templates.io) and our [blazor-vue](https://blazor-vue.web-templates.io) templates, and re-implements them in Vite where they can be used to add the same rich content features to Vite Vue and Vite React Apps. A goal for vite-press-plugin is to implement a suite of universal markdown-powered features that can be reused across all our Vue, React and .NET Razor and Blazor project templates, allowing you to freely copy and incorporate same set of markdown feature folders to power markdown content features across a range of websites built with different technologies. All of Razor SSG's features are available in Vite Press Plugin, including: - [Blog](https://vue-spa.web-templates.io/blog) - Full Featured, beautiful Tailwind Blog with multiple discoverable views - [What's New](https://vue-spa.web-templates.io/whatsnew) - Build Product and Feature Release pages - [Videos](https://vue-spa.web-templates.io/videos) - Maintain Video Libraries and Playlists - [Metadata APIs](https://vue-spa.web-templates.io/posts/vite-press-plugin#metadata-apis-feature) - Generate queryable static .json metadata APIs for all content - [Includes](https://vue-spa.web-templates.io/posts/vite-press-plugin#includes-feature) - Create and reuse Markdown fragments It also supports an enhanced version of markdown for embedding richer UI markup in markdown content where most of [VitePress Containers](https://vitepress.dev/guide/markdown#custom-containers) are supported, including: - [Custom Markdown Containers](https://vue-spa.web-templates.io/posts/vite-press-plugin#markdown-containers) - **Alerts** - `info` - `tip` - `warning` - `danger` - `copy` - `sh` - `youtube` - [Markdown Fenced Code Blocks](https://vue-spa.web-templates.io/posts/vite-press-plugin#markdown-fenced-code-blocks) - Convert fenced code blocks into Richer UIs ### React Components In Markdown At the cost of reduced portability, you’re also able to embed richer Interactive Vue components directly in markdown: - [React Components in Markdown](https://react-spa.web-templates.io/posts/markdown-components-in-react) # Creating a custom Explorer UI for OpenAIs Chat API Source: https://servicestack.net/posts/ai-chat-explorer Anyone who's used ServiceStack's built-in [API Explorer](https://docs.servicestack.net/api-explorer) or [Auto HTML API](https://docs.servicestack.net/auto-html-api) UIs know that not all API Explorer UIs are created equal. The differences are more pronounced as APIs get larger and more complex which we can see by comparing it with Swagger UI for rendering [AI Chat's](/posts/ai-chat) `ChatCompletion` API: [![](/img/posts/ai-chat-explorer/ai-chat-swagger-form.webp)](/img/posts/ai-chat-explorer/ai-chat-swagger-form.webp) This is just the tip of the iceberg, the [full-length Swagger UI Screenshot](/img/posts/ai-chat-explorer/ai-chat-swagger-long.webp) is absurdly long, past the point of being usable. As expected from a generic UI we get very little assistance from the UI on what values are allowed, the numeric fields aren't number inputs and the only dropdowns we see are for `bool` properties to select from their `true` and `false` values. There's not going to be any chance for it to be able to show App-specific options like which models are currently enabled. ## API Explorer UI By contrast here is the same API rendered with ServiceStack's [API Explorer](https://docs.servicestack.net/api-explorer): [![](/img/posts/ai-chat-explorer/ai-chat-form.webp)](/img/posts/ai-chat-explorer/ai-chat-form.webp) This is much closer to what you'd expect from a hand-crafted Application UI and far more usable. #### Properties use optimized UI Components It renders an optimized UI for each property, with the **Model**, **Reasoning Effort**, **Service Tier** and **Verbosity** properties all using a [Combobox](https://docs.servicestack.net/vue/combobox) component for quickly searching through a list of supported options, or they can choose to enter a custom value. **Bool** properties use Checkboxes whilst Numeric fields use **number** inputs, with integer properties only allowing integer values and floating point properties being able to step through fractional values. #### UI-specific text hints Each property also contains **placeholder** text and **help** text hints that's more focused and concise than the verbose API documentation. #### HTML client-side validation Client-side HTML validation ensure properties are valid and within any configured min/max values before any request is sent. [![](/img/posts/ai-chat-explorer/ai-chat-form-completed.webp)](/img/posts/ai-chat-explorer/ai-chat-form-completed.webp) ### Custom Components for Complex Properties The only property that doesn't use a built-in component is `Messages` which is rendered with a custom `ChatMessages` component purpose-built to populate the `List Messages` property. It uses a **Markdown Editor** for the UserPrompt, a collapsible Textarea for any System Prompt and the ability to attach **image**, **audio** & **file** document attachments to the API request. ## How is it done? The entire UI is driven by these [declarative annotations](https://docs.servicestack.net/locode/declarative) added on the [ChatCompletion](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/ChatCompletion.cs) Request DTO: ```csharp [Description("Chat Completions API (OpenAI-Compatible)")] [Notes("The industry-standard, message-based interface for interfacing with Large Language Models.")] public class ChatCompletion : IPost, IReturn { [DataMember(Name = "messages")] [Input(Type = "ChatMessages", Label=""), FieldCss(Field = "col-span-12")] public List Messages { get; set; } = []; [DataMember(Name = "model")] [Input(Type = "combobox", EvalAllowableValues = "Chat.Models", Placeholder = "e.g. glm-4.6", Help = "ID of the model to use")] public string Model { get; set; } [DataMember(Name = "reasoning_effort")] [Input(Type="combobox", EvalAllowableValues = "['low','medium','high','none','default']", Help = "Constrains effort on reasoning for reasoning models")] public string? ReasoningEffort { get; set; } [DataMember(Name = "service_tier")] [Input(Type = "combobox", EvalAllowableValues = "['auto','default']", Help = "Processing type for serving the request")] public string? ServiceTier { get; set; } [DataMember(Name = "safety_identifier")] [Input(Type = "text", Placeholder = "e.g. user-id", Help = "Stable identifier to help detect policy violations")] public string? SafetyIdentifier { get; set; } [DataMember(Name = "stop")] [Input(Type = "tag", Max = "4", Help = "Up to 4 sequences for the API to stop generating tokens")] public List? Stop { get; set; } [DataMember(Name = "modalities")] [Input(Type = "tag", Max = "3", Help = "The output types you would like the model to generate")] public List? Modalities { get; set; } [DataMember(Name = "prompt_cache_key")] [Input(Type = "text", Placeholder = "e.g. my-cache-key", Help = "Used by OpenAI to cache responses for similar requests")] public string? PromptCacheKey { get; set; } [DataMember(Name = "tools")] public List? Tools { get; set; } [DataMember(Name = "verbosity")] [Input(Type = "combobox", EvalAllowableValues = "['low','medium','high']", Placeholder = "e.g. low", Help = "Constrains verbosity of model's response")] public string? Verbosity { get; set; } [DataMember(Name = "temperature")] [Input(Type = "number", Step = "0.1", Min = "0", Max = "2", Placeholder = "e.g. 0.7", Help = "Higher values more random, lower for more focus")] public double? Temperature { get; set; } [DataMember(Name = "max_completion_tokens")] [Input(Type = "number", Value = "2048", Step = "1", Min = "1", Placeholder = "e.g. 2048", Help = "Max tokens for completion (inc. reasoning tokens)")] public int? MaxCompletionTokens { get; set; } [DataMember(Name = "top_logprobs")] [Input(Type = "number", Step = "1", Min = "0", Max = "20", Placeholder = "e.g. 5", Help = "Number of most likely tokens to return with log probs")] public int? TopLogprobs { get; set; } [DataMember(Name = "top_p")] [Input(Type = "number", Step = "0.1", Min = "0", Max = "1", Placeholder = "e.g. 0.5", Help = "Nucleus sampling - alternative to temperature")] public double? TopP { get; set; } [DataMember(Name = "frequency_penalty")] [Input(Type = "number", Step = "0.1", Min = "0", Max = "2", Placeholder = "e.g. 0.5", Help = "Penalize tokens based on frequency in text")] public double? FrequencyPenalty { get; set; } [DataMember(Name = "presence_penalty")] [Input(Type = "number", Step = "0.1", Min = "0", Max = "2", Placeholder = "e.g. 0.5", Help = "Penalize tokens based on presence in text")] public double? PresencePenalty { get; set; } [DataMember(Name = "seed")] [Input(Type = "number", Placeholder = "e.g. 42", Help = "For deterministic sampling")] public int? Seed { get; set; } [DataMember(Name = "n")] [Input(Type = "number", Placeholder = "e.g. 1", Help = "How many chat choices to generate for each input message")] public int? N { get; set; } [Input(Type = "checkbox", Help = "Whether or not to store the output of this chat request")] [DataMember(Name = "store")] public bool? Store { get; set; } [DataMember(Name = "logprobs")] [Input(Type = "checkbox", Help = "Whether to return log probabilities of the output tokens")] public bool? Logprobs { get; set; } [DataMember(Name = "parallel_tool_calls")] [Input(Type = "checkbox", Help = "Enable parallel function calling during tool use")] public bool? ParallelToolCalls { get; set; } [DataMember(Name = "enable_thinking")] [Input(Type = "checkbox", Help = "Enable thinking mode for some Qwen providers")] public bool? EnableThinking { get; set; } [DataMember(Name = "stream")] [Input(Type = "hidden")] public bool? Stream { get; set; } } ``` Which uses the [[Input] attribute](https://docs.servicestack.net/locode/declarative#custom-fields-and-inputs) to control the HTML Input rendered for each property whose `Type` can reference any HTML Input or any [ServiceStack Vue Component](https://docs.servicestack.net/vue/form-inputs) that's either built-in or registered with the Component library. In addition, you also have control to the css of the containing **Field**, **Input** and **Label** elements with the [[FieldCss] attribute](https://docs.servicestack.net/locode/declarative#field) which uses `[FieldCss(Field="col-span-12")]` to render the field to span the full width of the form. The `[Input(Type="hidden")]` is used to hide the `Stream` property from the UI since it is invalid from an API Explorer UI. ### Combobox Values The Combobox `EvalAllowableValues` can reference any JavaScript expression which is evaluated with [#Script](https://sharpscript.net) with the results embedded in the API Metadata that API Explorer uses to render its UI. All combo boxes references a static JS Array except for `Model` which uses `EvalAllowableValues = "Chat.Models"` to invoke the registered `Chat` instance `Models` property which returns an ordered list of all available models from all enabled providers: ```csharp appHost.ScriptContext.Args[nameof(Chat)] = new Chat(this); public class Chat(ChatFeature feature) { public List Models => feature.Providers.Values .SelectMany(x => x.Models.Keys) .Distinct() .OrderBy(x => x) .ToList(); } ``` ### Custom ChatMessages Component The only property that doesn't use a built-in component is: ```csharp [Input(Type = "ChatMessages", Label=""), FieldCss(Field = "col-span-12")] public List Messages { get; set; } = []; ``` Which makes use of a custom `ChatMessages` component in [/modules/ui/components/ChatMessages.mjs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/modules/ui/components/ChatMessages.mjs). Custom Components can be added to API Explorer in the same way as [overriding any built-in API Explorer](https://docs.servicestack.net/locode/custom-overview#ui) component by adding it to your local `/wwwroot` folder: ```files /modules /ui /components ChatMessages.mjs ``` All components added to the `/components` folder will be automatically registered and available for use. That's all that's needed to customize the `ChatCompletion` Form UI in API Explorer, for more features and customizations see the [API Explorer Docs](https://docs.servicestack.net/api-explorer). ## Install To experience [AI Chat's UI](/posts/ai-chat-ui) and its `ChatCompletion` API Explorer UI for yourself, you can add AI Chat to any .NET 8+ project by installing the **ServiceStack.AI.Chat** NuGet package and configuration with: :::sh npx add-in chat ::: Prerequisites: Which drops this simple [Modular Startup](https://docs.servicestack.net/modular-startup) that adds the `ChatFeature` and registers a link to its UI on the [Metadata Page](https://docs.servicestack.net/metadata-page) if you want it: ```csharp public class ConfigureAiChat : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { // Docs: https://docs.servicestack.net/ai-chat-api services.AddPlugin(new ChatFeature { EnableProviders = [ "servicestack", // "groq", // "google_free", // "openrouter_free", // "ollama", // "google", // "anthropic", // "openai", // "grok", // "qwen", // "z.ai", // "mistral", // "openrouter", ] }); // Persist AI Chat History, enables analytics at /admin-ui/chat services.AddSingleton(); // Or store history in monthly partitioned tables in PostgreSQL: // services.AddSingleton(); services.ConfigurePlugin(feature => { feature.AddPluginLink("/chat", "AI Chat"); }); }); } ``` ## Learn more about AI Chat To dive deeper into what AI Chat can do: - Read the [AI Chat API docs](https://docs.servicestack.net/ai-chat-api) to integrate AI into your own services and apps. - Explore the [AI Chat UI guide](https://docs.servicestack.net/ai-chat-ui) to customize the built-in experience. - Use [Admin UI](https://docs.servicestack.net/ai-chat-analytics) to inspect analytics, monitor usage, and review audit history. # FREE Gemini, Minimax M2, GLM 4.6, Kimi K2 Source: https://servicestack.net/posts/ai-chat-servicestack To give AI Chat instant utility, we're making available a free `servicestack` OpenAI Chat provider that can be enabled with: ```csharp services.AddPlugin(new ChatFeature { EnableProviders = [ "servicestack", // "groq", // "google_free", // "openrouter_free", // "ollama", // "google", // "anthropic", // "openai", // "grok", // "qwen", // "z.ai", // "mistral", // "openrouter", ] }); ``` The `servicestack` provider is configured with a default `llms.json` which enables access to Gemini and the best value OSS models for FREE: ```json { "providers": { "servicestack": { "enabled": false, "type": "OpenAiProvider", "base_url": "http://okai.servicestack.com", "api_key": "$SERVICESTACK_LICENSE", "models": { "gemini-flash-latest": "gemini-flash-latest", "gemini-flash-lite-latest": "gemini-flash-lite-latest", "kimi-k2": "kimi-k2", "kimi-k2-thinking": "kimi-k2-thinking", "minimax-m2": "minimax-m2", "glm-4.6": "glm-4.6", "gpt-oss:20b": "gpt-oss:20b", "gpt-oss:120b": "gpt-oss:120b", "llama4:400b": "llama4:400b", "mistral-small3.2:24b": "mistral-small3.2:24b" } } } } ``` ## Clean, Lightweight & Flexible AI Integration ServiceStack's AI Chat delivers a production-ready solution for integrating AI capabilities into your applications with minimal overhead and maximum flexibility. The [llms.json](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/chat/llms.json) configuration approach provides several key advantages: ### Unified Provider Abstraction Define the exact models you want your application to use through a single, declarative configuration file. This thin abstraction layer eliminates vendor lock-in and allows seamless switching between providers without code changes, enabling you to: - **Optimize for cost** - Route requests to the most economical provider for each use case - **Maximize performance** - Leverage faster models for latency-sensitive operations while using more capable models for complex tasks - **Ensure reliability** - Configure automatic failover between providers to maintain service availability - **Control access** - Specify which models are available to users in your preferred priority order ### Hybrid Deployment Flexibility Mix and match local and cloud providers to meet your specific requirements. Deploy privacy-sensitive workloads on local models while leveraging cloud providers for scale, or combine premium models for critical features with cost-effective alternatives for routine tasks. ### Zero-Dependency Architecture The lightweight implementation adds minimal footprint to your application while providing enterprise-grade AI capabilities. No heavy SDKs or framework dependencies required—just clean, direct performant integrations. The `servicestack` provider requires the `SERVICESTACK_LICENSE` Environment Variable, although any ServiceStack License Key can be used, including expired and Free ones. Learn more about [AI Chat's UI](https://docs.servicestack.net/ai-chat-ui): [![](/img/posts/ai-chat/llms-syntax.webp)](https://docs.servicestack.net/ai-chat-ui) ### FREE for Personal Usage To be able to maintain this as a free service we're limiting usage for development or personal assistance and research by limiting usage to **60 requests /hour** which should be more than enough for most personal usage and research whilst deterring usage in automated tools or usage in production. :::tip info Rate limiting is implemented with a sliding [Token Bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) that replenishes 1 additional request every 60s ::: ## Effortless AI Integration In addition of providing UI and ChatGPT-like features, it also makes it trivially simple to access AI Features from within your own App that's as simple as sending a populated `ChatCompletion` Request DTO with the `IChatClient` dependency: ```csharp class MyService(IChatClient client) { public async Task Any(DefaultChat request) { return await client.ChatAsync(new ChatCompletion { Model = "glm-4.6", Messages = [ Message.Text(request.UserPrompt) ], }); } } ``` It's also makes it easy to send Image, Audio & Document inputs to AI Models that support it, e.g: ```csharp var image = new ChatCompletion { Model = "qwen2.5vl", Messages = [ Message.Image(imageUrl:"https://example.org/image.webp", text:"Describe the key features of the input image"), ] } var audio = new ChatCompletion { Model = "gpt-4o-audio-preview", Messages = [ Message.Audio(data:"https://example.org/speaker.mp3", text:"Please transcribe and summarize this audio file"), ] }; var file = new ChatCompletion { Model = "gemini-flash-latest", Messages = [ Message.File( fileData:"https://example.org/order.pdf", text:"Please summarize this document"), ] }; ``` ## Learn more about AI Chat To dive deeper into what AI Chat can do: - Read the [AI Chat API docs](https://docs.servicestack.net/ai-chat-api) to integrate AI into your own services and apps. - Explore the [AI Chat UI guide](https://docs.servicestack.net/ai-chat-ui) to customize the built-in experience. - Use [Admin UI](https://docs.servicestack.net/ai-chat-analytics) to inspect analytics, monitor usage, and review audit history. # AI Chat history persistence and Admin Analytics UI Source: https://servicestack.net/posts/ai-chat-analytics ServiceStack's [AI Chat](https://docs.servicestack.net/ai-chat-api) feature provides a unified API for integrating multiple AI providers into your applications. To gain visibility into usage patterns, costs, and performance across your AI infrastructure, the platform includes comprehensive chat history persistence and analytics capabilities. :::sh npx add-in chat ::: Or by referencing the **ServiceStack.AI.Chat** NuGet package and adding the `ChatFeature` plugin: ```csharp services.AddPlugin(new ChatFeature { EnableProviders = [ "servicestack", ] }); ``` ## AI Chat History Persistence Enabling chat history persistence allows you to maintain a complete audit trail of all AI interactions, track token consumption, monitor costs across providers and models, and analyze usage patterns over time that captures every request and response flowing through AI Chat's UI, external OpenAI endpoints and internal `IChatStore` requests. ### Database Storage Options ServiceStack provides two storage implementations to suit different deployment scenarios: `DbChatStore` - A universal solution that stores chat history in a single table compatible with any RDBMS [supported by OrmLite](https://docs.servicestack.net/ormlite/getting-started): ```csharp services.AddSingleton(); ``` `PostgresChatStore` - An optimized implementation for PostgreSQL that leverages monthly table partitioning for improved query performance and data management: ```csharp services.AddSingleton(); ``` Both implementations utilize indexed queries with result limits to ensure consistent performance even as your chat history grows. The partitioned approach in PostgreSQL offers additional benefits for long-term data retention and archival strategies. ## Admin UI Analytics Once chat history persistence is enabled, the Admin UI provides comprehensive analytics dashboards that deliver actionable insights into your AI infrastructure. The analytics interface offers multiple views to help you understand costs, optimize token usage, and monitor activity patterns across all configured AI providers and models. The analytics dashboard includes three primary tabs: - **Cost Analysis** - Track spending across providers and models with daily and monthly breakdowns - **Token Usage** - Monitor input and output token consumption to identify optimization opportunities - **Activity** - Review detailed request logs with full conversation history and metadata These visualizations enable data-driven decisions about provider selection, model usage, and cost optimization strategies. ### Cost Analysis The Cost Analysis tab provides financial visibility into your AI operations with interactive visualizations showing spending distribution across providers and models. Daily cost trends help identify usage spikes, while monthly aggregations reveal long-term patterns. Pie charts break down costs by individual models and providers, making it easy to identify your most expensive AI resources and opportunities for cost optimization. :::{.wideshot} ![](/img/posts/ai-chat-analytics/admin-chat-costs.webp) ::: ### Token Usage The Token Usage tab tracks both input (prompt) and output (completion) tokens across all requests. Daily usage charts display token consumption trends over time, while model and provider breakdowns show which AI resources consume the most tokens. This granular visibility helps optimize prompt engineering, identify inefficient usage patterns, and forecast capacity requirements. :::{.wideshot} ![](/img/posts/ai-chat-analytics/admin-chat-tokens.webp) ::: ### Activity Log The Activity tab maintains a searchable log of all AI chat requests, displaying timestamps, models, providers, and associated costs. Clicking any request opens a detailed view showing the complete conversation including user prompts, AI responses, token counts, duration, and the full request payload. This audit trail is invaluable for debugging, quality assurance, and understanding how your AI features are being used in production. :::{.wideshot} ![](/img/posts/ai-chat-analytics/admin-chat-activity.webp) ::: # AI Chat - A Simple OpenAI Chat Completions API, UI & Client LLM Gateway Source: https://servicestack.net/posts/ai-chat We're excited to introduce **AI Chat** — a refreshingly simple solution for integrating AI into your applications by unlocking the full value of the OpenAI Chat API. Unlike most other OpenAI SDKs and Frameworks, all of AI Chat's features are centered around arguably the most important API in our time - OpenAI's simple [Chat Completion API](https://platform.openai.com/docs/api-reference/chat) i.e. the primary API used to access Large Language Models (LLMs). ## AI Chat UI [AI Chat](/posts/ai-chat) also allows you to offer a curated ChatGPT-like UI to your users where you're able to change the branding to suit your App, control the API Keys, billing, and sanctioned providers your users can access to maintain your own **Fast, Local, and Private** access to AI from within your own organization. ## Install AI Chat can be added to any .NET 8+ project by installing the **ServiceStack.AI.Chat** NuGet package and configuration with: :::sh npx add-in chat ::: Which drops this simple [Modular Startup](https://docs.servicestack.net/modular-startup) that adds the `ChatFeature` and registers a link to its UI on the [Metadata Page](https://docs.servicestack.net/metadata-page) if you want it: ```csharp public class ConfigureAiChat : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { // Docs: https://docs.servicestack.net/ai-chat-api services.AddPlugin(new ChatFeature { EnableProviders = [ "servicestack", // "groq", // "google_free", // "openrouter_free", // "ollama", // "google", // "anthropic",e // "openai", // "grok", // "qwen", // "z.ai", // "mistral", // "openrouter", ] }); // Persist AI Chat History, enables analytics at /admin-ui/chat services.AddSingleton(); // Or store history in monthly partitioned tables in PostgreSQL: // services.AddSingleton(); services.ConfigurePlugin(feature => { feature.AddPluginLink("/chat", "AI Chat"); }); }); } ``` ### Identity Auth or Valid API Key AI Chat makes of ServiceStack's new [API Keys or Identity Auth APIs](/posts/apikey_auth_apis) which allows usage for both Authenticated Identity Auth users otherwise unauthenticated users will need to provide a valid API Key: :::{.shadow} [![](/img/posts/ai-chat/ai-chat-ui-apikey.webp)](/img/posts/ai-chat/ai-chat-ui-apikey.webp) ::: If needed `ValidateRequest` can be used to further restrict access to AI Chat's UI and APIs, e.g. you can restrict access to API Keys with the `Admin` scope with: ```csharp services.AddPlugin(new ChatFeature { ValidateRequest = async req => req.GetApiKey()?.HasScope(RoleNames.Admin) == true ? null : HttpResult.Redirect("/admin-ui"), }); ``` ### Import / Export All data is stored locally in the users local browser's IndexedDB. When needed you can backup and transfer your entire chat history between different browsers using the **Export** and **Import** features on the home page. :::{.wideshot} [![llms-home.webp](/img/posts/ai-chat/llms-home.webp)](/img/posts/ai-chat/llms-home.webp) ::: ## Simple and Flexible UI Like all of [ServiceStack's built-in UIs](https://servicestack.net/auto-ui), AI Chat is also [naturally customizable](https://docs.servicestack.net/locode/custom-overview) where you can override any of [AI Chat's Vue Components](https://github.com/ServiceStack/ServiceStack/tree/main/ServiceStack/src/ServiceStack.AI.Chat/chat) and override them with your own by placing them in your [/wwwroot/chat](https://github.com/ServiceStack/ServiceStack/tree/main/ServiceStack/tests/AdhocNew/wwwroot/chat) folder: ```files /wwwroot /chat Brand.mjs Welcome.mjs ``` Where you'll be able to customize the appearance and behavior of AI Chat's UI to match your App's branding and needs. :::{.wideshot} [![](/img/posts/ai-chat/ai-chat-custom-ui.webp)](/img/posts/ai-chat/ai-chat-custom-ui.webp) ::: ## Customize The built-in [ui.json](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/chat/ui.json) configuration can be overridden with your own to use your preferred system prompts and other defaults by adding them to your local folder: ```files /wwwroot /chat llms.json ui.json ``` Alternatively `ConfigJson` and `UiConfigJson` can be used to load custom JSON configuration from a different source, e.g: ```csharp services.AddPlugin(new ChatFeature { // Use custom llms.json configuration ConfigJson = vfs.GetFile("App_Data/llms.json").ReadAllText(), // Use custom ui.json configuration UiConfigJson = vfs.GetFile("App_Data/ui.json").ReadAllText(), }); ``` ## Rich Markdown & Syntax Highlighting To maximize readability there's full support for Markdown and Syntax highlighting for the most popular programming languages. :::{.wideshot} [![llms-syntax.webp](/img/posts/ai-chat/llms-syntax.webp)](/img/posts/ai-chat/llms-syntax.webp) ::: To quickly and easily make use of AI Responses, **Copy Code** icons are readily available on hover of all messages and code blocks. ## Rich, Multimodal Inputs The Chat UI goes beyond just text and can take advantage of the multimodal capabilities of modern LLMs with support for Image, Audio, and File inputs. ### 🖼️ 1. Image Inputs & Analysis Images can be uploaded directly into your conversations with vision-capable models for comprehensive image analysis. Visual AI Responses are highly dependent on the model used. This is a typical example of the visual analysis provided by the latest Gemini Flash of our [ServiceStack Logo](/img/logo.png): :::{.wideshot} [![llms-image.webp](/img/posts/ai-chat/llms-image.webp)](/img/posts/ai-chat/llms-image.webp) ::: ### 🎤 2. Audio Input & Transcription Likewise you can upload Audio files and have them transcribed and analyzed by multi-modal models with audio capabilities. :::{.wideshot} [![llms-audio.webp](/img/posts/ai-chat/llms-audio.webp)](/img/posts/ai-chat/llms-audio.webp) ::: Example of processing audio input. Audio files can be uploaded with system and user prompts to instruct the model to transcribe and summarize its content where its multi-modal capabilities are integrated right within the chat interface. ### 📎 3. File and PDF Attachments In addition to images and audio, you can also upload documents, PDFs, and other files to capable models to extract insights, summarize content or analyze. **Document Processing Use Cases:** - **PDF Analysis**: Upload PDF documents for content extraction and analysis - **Data Extraction**: Extract specific information from structured documents - **Document Summarization**: Get concise summaries of lengthy documents - **Query Content**: Ask questions about specific content in documents - **Batch Processing**: Upload multiple files for comparative analysis Perfect for research, document review, data analysis, and content extractions. :::{.wideshot} [![llms-files.webp](/img/posts/ai-chat/llms-files.webp)](/img/posts/ai-chat/llms-files.webp) ::: ## Custom AI Chat Requests Send Custom Chat Completion requests through the settings dialog, allowing Users to fine-tune their AI requests with advanced options including: - **Temperature** `(0-2)` for controlling response randomness - **Max Completion Tokens** to limit response length - **Seed** values for deterministic sampling - **Top P** `(0-1)` for nucleus sampling - **Frequency** & **Presence Penalty** `(-2.0 to 2.0)` for reducing repetition - **Stop** Sequences to control where the API stops generating - **Reasoning Effort** constraints for reasoning models - **Top Logprobs** `(0-20)` for token probability analysis - **Verbosity** settings :::{.wideshot} [![llms-settings.webp](/img/posts/ai-chat/llms-settings.webp)](/img/posts/ai-chat/llms-settings.webp) ::: ## Enable / Disable Providers **Admin** Users can manage which providers they want enabled or disabled at runtime. Providers are invoked in the order they're defined in `llms.json` that supports the requested model. If a provider fails, it tries the next available one. By default Providers with Free tiers are enabled first, followed by local providers and then premium cloud providers which can all be enabled or disabled in the UI: :::{.wideshot} [![llms-providers.webp](/img/posts/ai-chat/llms-providers.webp)](/img/posts/ai-chat/llms-providers.webp) ::: ## Search History Quickly find past conversations with built-in search: :::{.wideshot} [![llms-search-python.webp](/img/posts/ai-chat/llms-search-python.webp)](/img/posts/ai-chat/llms-search-python.webp) ::: ## Smart Autocomplete for Models & System Prompts Autocomplete components are used to quickly find and select the preferred model and system prompt. Only models from enabled providers will appear in the drop down, which will be available immediately after providers are enabled. :::{.wideshot} [![llms-autocomplete.webp](/img/posts/ai-chat/llms-autocomplete.webp)](/img/posts/ai-chat/llms-autocomplete.webp) ::: ## Comprehensive System Prompt Library Access a curated collection of 200+ professional system prompts designed for various use cases, from technical assistance to creative writing. :::{.wideshot} [![llms-system-prompt.webp](/img/posts/ai-chat/llms-system-prompt.webp)](/img/posts/ai-chat/llms-system-prompt.webp) ::: System Prompts be can added, removed & sorted in your `ui.json` ```json { "prompts": [ { "id": "it-expert", "name": "Act as an IT Expert", "value": "I want you to act as an IT expert. You will be responsible..." }, ... ] } ``` ### Reasoning Access the thinking process of advanced AI models with specialized rendering for reasoning and chain-of-thought responses: :::{.wideshot} [![llms-reasoning.webp](/img/posts/ai-chat/llms-reasoning.webp)](/img/posts/ai-chat/llms-reasoning.webp) ::: ## AI Solutions get outdated quickly We've had several attempts at adding a valuable layer of functionality for harnessing AI into our Apps, including: - [GptAgentFeature](https://servicestack.net/posts/chat-gpt-agents) - Use Semantic Kernel to implement our own Chain-of-Thought functionality to develop Autonomous agents - [TypeScript TypeChat](https://servicestack.net/posts/typescript-typechat-examples) - Use Semantic Kernel to implement all of TypeScript's TypeChat examples in .NET - [ServiceStack.AI](https://servicestack.net/posts/servicestack-ai) - TypeChat providers and unified Abstractions over AWS, Azure and Google Cloud AI Providers The problem being that we wouldn't consider any of these solutions to be relevant today, any "smarts" or opinionated logic added look to become irrelevant as AI models get more capable and intelligent. ## The Problem with Complex Abstractions Over the years, we've seen AI integration libraries grow in complexity. Take [Microsoft Semantic Kernel](https://github.com/microsoft/semantic-kernel) - a sprawling codebase that maintains its own opinionated abstractions that aren't serializable and has endured several breaking changes over the years. After investing development effort in catching up with their breaking changes we're now told to [Migrate to Agent Framework](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel/). The fundamental issue? These complex abstractions didn't prove to be reusable. Microsoft's own next competing solution [Agent Framework](https://github.com/microsoft/agent-framework) - doesn't even use Semantic Kernel Abstractions. Instead, it maintains its own non-serializable complex abstractions, repeating the same architectural issues. This pattern of building heavyweight, non-portable abstractions creates vendor lock-in, adds friction, hinders reuse, and limits how and where it can be used. After getting very little value from Semantic Kernel, we don't plan for any rewrites to follow adoption of their next over-engineered framework. ## Back to OpenAI Chat The only AI Abstraction we feel confident that has any longevity in this space, that wont be subject to breaking changes and rewrites is the underlying OpenAI Chat Completion API itself. The API with the most utility, with all the hard work of having AI Providers adopt this common API already done for us, we just have to facilitate calling it. Something so simple that it can be easily called from a shell script: ```bash RESPONSE=$(curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5", "messages": [{"role": "user", "content": "Capital of France?"}] }') echo "$RESPONSE" | jq -r '.choices[0].message.content' ``` Shouldn't require complex libraries over several NuGet packages to make use of. The simplest and obvious solution is design around the core `ChatCompletion` DTO itself - a simple, serializable, implementation-free data structure that maps directly to the OpenAI Chat API request body maintained in [ChatCompletion.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/ChatCompletion.cs) with all its functionality encapsulated (no third-party dependencies) within the new **ServiceStack.AI.Chat** NuGet package. Using DTOs gives us all the natural [advantages of message-based APIs](https://docs.servicestack.net/advantages-of-message-based-web-services) whose clean POCO models helps us [fight against complexity](https://docs.servicestack.net/service-complexity-and-dto-roles). ### Why This Matters Because `ChatCompletion` is a plain serializable DTO, you can: - **Store it in a database** - Save conversation history, audit AI requests, or implement retry logic - **Use it in client workflows** - Pass the same DTO between frontend and backend without transformations - **Send it through message queues** - Build asynchronous AI processing pipelines with RabbitMQ and others - **Debug easily** - Inspect the exact JSON being sent to OpenAI - **Test easily** - Mock AI responses with simple DTOs or JSON payloads - **Use it outside the library** - The DTO works independently of any specific client implementation More importantly, because it's a **Request DTO**, we unlock a wealth of ServiceStack features for free, since most of ServiceStack's functionality is designed around Request DTOs — which we'll explore later. ## Install AI Chat can be added to any .NET 8+ project by installing the **ServiceStack.AI.Chat** NuGet package and configuration with: :::sh npx add-in chat ::: Which drops this simple [Modular Startup](https://docs.servicestack.net/modular-startup) that adds the `ChatFeature` and registers a link to its UI on the [Metadata Page](https://docs.servicestack.net/metadata-page) if you want it: ```csharp public class ConfigureAiChat : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { // Docs: https://docs.servicestack.net/ai-chat-api services.AddPlugin(new ChatFeature { EnableProviders = [ "servicestack", // "groq", // "google_free", // "openrouter_free", // "ollama", // "google", // "anthropic", // "openai", // "grok", // "qwen", // "z.ai", // "mistral", // "openrouter", ] }); // Persist AI Chat History, enables analytics at /admin-ui/chat services.AddSingleton(); // Or store history in monthly partitioned tables in PostgreSQL: // services.AddSingleton(); services.ConfigurePlugin(feature => { feature.AddPluginLink("/chat", "AI Chat"); }); }); } ``` ## Simple, Not Simplistic How simple is it to use? It's just as you'd expect, your App logic need only bind to a simple `IChatClient` interface that accepts a Typed `ChatCompletion` Request DTO and returns a Typed `ChatResponse` DTO: ```csharp public interface IChatClient { Task ChatAsync( ChatCompletion request, CancellationToken token=default); } ``` An impl-free easily substitutable interface for calling any OpenAI-compatible Chat API, using clean Typed `ChatCompletion` and `ChatResponse` DTOs. Unfortunately since the API needs to be typed and .NET Serializers don't have support for de/serializing union types yet, the DTO adopts OpenAI's more verbose and flexible multi-part Content Type which looks like: ```csharp IChatClient client = CreateClient(); var request = new ChatCompletion { Model = "gpt-5", Messages = [ new() { Role = "user", Content = [ new AiTextContent { Type = "text", Text = "Capital of France?" } ], } ] }; var response = await client.ChatAsync(request); ``` To improve the UX we've added a [Message.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/Message.cs) helper which encapsulates the boilerplate of sending **Text**, **Image**, **Audio** and **Files** into more succinct and readable code where you'd typically only need to write: ```csharp var request = new ChatCompletion { Model = "gpt-5", Messages = [ Message.SystemPrompt("You are a helpful assistant"), Message.Text("Capital of France?"), ] }; var response = await client.ChatAsync(request); string? answer = response.GetAnswer(); ``` ### Same ChatCompletion DTO, Used Everywhere That's all that's required for your internal App Logic to access your App's configured AI Models. However, as AI Chat also makes its own OpenAI Compatible API available, your external .NET Clients can use the **same exact DTO** to get the **same Response** by calling your API with a [C# Service Client](https://docs.servicestack.net/csharp-client): ```csharp var client = new JsonApiClient(BaseUrl) { BearerToken = apiKey }; var response = await client.SendAsync(request); ``` ### Support for Text, Images, Audio & Files For Multi-modal LLMs which support it, you can also send Images, Audio & File attachments with your AI Request using **URLs**, e.g: ```csharp var image = new ChatCompletion { Model = "qwen2.5vl", Messages = [ Message.Image(imageUrl:"https://example.org/image.webp", text:"Describe the key features of the input image"), ] } var audio = new ChatCompletion { Model = "gpt-4o-audio-preview", Messages = [ Message.Audio(data:"https://example.org/speaker.mp3", text:"Please transcribe and summarize this audio file"), ] }; var file = new ChatCompletion { Model = "gemini-flash-latest", Messages = [ Message.File( fileData:"https://example.org/order.pdf", text:"Please summarize this document"), ] }; ``` #### Relative File Path If a [VirtualFiles Provider](https://docs.servicestack.net/virtual-file-system) was configured, you can specify a relative path instead: ```csharp var image = new ChatCompletion { Model = "qwen2.5vl", Messages = [ Message.Image(imageUrl:"/path/to/image.webp", text:"Describe the key features of the input image"), ] }; ``` #### Manual Download & Embedding Alternatively you can embed and send the raw Base64 Data or Data URI yourself: ```csharp var bytes = await "https://example.org/image.webp".GetBytesFromUrlAsync(); var dataUri = $"data:image/webp;base64,{Convert.ToBase64String(bytes)}"; var image = new ChatCompletion { Model = "qwen2.5vl", Messages = [ Message.Image(imageUrl:dataUri, text:"Describe the key features of the input image"), ] }; ``` Although sending references to external resources allows keeping AI Requests payloads small, making them easier to store in Databases, send in MQs and client workflows, etc. This illustrates some of the "value-added" features of AI Chat where it will automatically download any URL Resources and embed it as Base64 Data in the `ChatCompletion` Request DTO. ### Configure Downloads Relative paths can be enabled by configuring a `VirtualFiles` Provider to refer to a safe path that you want to allow access to. Whilst URLs are downloaded by default, but its behavior can be customized with `ValidateUrl` or replaced entirely with `DownloadUrlAsBase64Async`: ```csharp services.AddPlugin(new ChatFeature { // Enable Relative Path Downloads VirtualFiles = new FileSystemVirtualFiles(assetDir), // Validate URLs before download ValidateUrl = url => { if (!IsAllowedUrl(url)) throw HttpError.Forbidden("URL not allowed"); }, // Use Custom URL Downloader // DownloadUrlAsBase64Async = async (provider, url) => { // var (base64, mimeType) = await MyDownloadAsync(url); // return (base64, mimeType); // }, }); ``` ## Configure AI Providers By default AI Chat is configured with a list of providers in its `llms.json` which is pre-configured with the best models from the leading LLM providers. The easiest way to use a custom `llms.json` is to add a local modified copy of [llms.json](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/chat/llms.json) to your App's `/wwwroot/chat` folder: ```files /wwwroot /chat llms.json ``` If you just need to change which providers are enabled you can specify them in `EnableProviders`: ```csharp services.AddPlugin(new ChatFeature { // Specify which providers you want to enable EnableProviders = [ "openrouter_free", "groq", "google_free", "codestral", "ollama", "openrouter", "google", "anthropic", "openai", "grok", "qwen", "z.ai", "mistral", ], // Use custom llms.json configuration ConfigJson = vfs.GetFile("App_Data/llms.json").ReadAllText(), }); ``` Alternatively you can use `ConfigJson` to load a custom JSON provider configuration from a different source, which you'll want to use if you prefer to keep your provider configuration and API Keys all in `llms.json`. ### llms.json - OpenAI Provider Configuration [llms.json](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.AI.Chat/chat/llms.json) contains a list of OpenAI Compatible Providers you want to make available along with a user-defined **model alias** you want to use for model routing along with the provider-specific model name it maps to when the model is used with that provider, e.g: ```json { "providers": { "openrouter": { "enabled": false, "type": "OpenAiProvider", "base_url": "https://openrouter.ai/api", "api_key": "$OPENROUTER_API_KEY", "models": { "grok-4": "x-ai/grok-4", "glm-4.5-air": "z-ai/glm-4.5-air", "kimi-k2": "moonshotai/kimi-k2", "deepseek-v3.1:671b": "deepseek/deepseek-chat", "llama4:400b": "meta-llama/llama-4-maverick" } }, "anthropic": { "enabled": false, "type": "OpenAiProvider", "base_url": "https://api.anthropic.com", "api_key": "$ANTHROPIC_API_KEY", "models": { "claude-sonnet-4-0": "claude-sonnet-4-0" } }, "ollama": { "enabled": false, "type": "OllamaProvider", "base_url": "http://localhost:11434", "models": {}, "all_models": true }, "google": { "enabled": false, "type": "GoogleProvider", "api_key": "$GOOGLE_API_KEY", "models": { "gemini-flash-latest": "gemini-flash-latest", "gemini-flash-lite-latest": "gemini-flash-lite-latest", "gemini-2.5-pro": "gemini-2.5-pro", "gemini-2.5-flash": "gemini-2.5-flash", "gemini-2.5-flash-lite": "gemini-2.5-flash-lite" }, "safety_settings": [ { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_ONLY_HIGH" } ], "thinking_config": { "thinkingBudget": 1024, "includeThoughts": true } }, //... } } ``` The only non-OpenAI Chat Provider AI Chat supports is `GoogleProvider`, where an exception was made to add explicit support for Gemini's Models given its low cost and generous free quotas. ### Provider API Keys API Keys can be either be specified within the `llms.json` itself, alternatively API Keys starting with `$` like `$GOOGLE_API_KEY` will first try to resolve it from `Variables` before falling back to checking Environment Variables. ```csharp services.AddPlugin(new ChatFeature { EnableProviders = [ "openrouter", "anthropic", "google", ], Variables = { ["OPENROUTER_API_KEY"] = secrets.OPENROUTER_API_KEY, ["ANTHROPIC_API_KEY"] = secrets.ANTHROPIC_API_KEY, ["GOOGLE_API_KEY"] = secrets.GOOGLE_API_KEY, } }); ``` ### Model Routing and Failover Providers are invoked in the order they're defined in `llms.json` that supports the requested model. If a provider fails, it tries the next available provider. This enables scenarios like: - Routing different request types to different providers - Optimize by Cost, Performance, Reliability, or Privacy - A/B testing different models - Added resilience with fallback when a provider is unavailable The model aliases don't need to identify a model directly, e.g. you could use your own artificial names for use-cases you need like `image-captioner`, `audio-transcriber`, `pdf-extractor` then map them to different models different providers should use to achieve the desired task. #### Use Model Routing with Fallback To make use of the model routing and fallback you would call `ChatAsync` on `IChatClient` directly: ```csharp class MyService(IChatClient client) { public async Task Any(DefaultChat request) { return await client.ChatAsync(new ChatCompletion { Model = "glm-4.6", Messages = [ Message.Text(request.UserPrompt) ], }); } } ``` #### Use Specific Provider Alternatively to use a specific provider, you can use `IChatClients` dependency `GetClient(providerId)` method to resolve the provider then calling `ChatAsync` will only use that provider: ```csharp class MyService(IChatClients clients) { public async Task Any(ProviderChat request) { var groq = clients.GetClient("groq"); return await groq.ChatAsync(new ChatCompletion { Model = "kimi-k2", Messages = [ Message.Text(request.UserPrompt) ], }); } } ``` ### Compatible with llms.py The other benefit of simple configuration and simple solutions, is that they're easy to implement. A perfect example of this being that this is the 2nd implementation done using this configuration. The same configuration, UI, APIs and functionality is also available in our [llms.py](https://github.com/ServiceStack/llms) Python CLI and server gateway we've developed in order to have a dependency-free LLM Gateway solution needed in our ComfyUI Agents. :::sh pip install llms-py ::: This also means you can use and test your own custom `llms.json` configuration on the command-line or in shell automation scripts: ```sh # Simple question llms "Explain quantum computing" # With specific model llms -m gemini-2.5-pro "Write a Python function to sort a list" # With system prompt llms -s "You are a helpful coding assistant" "Reverse a string in Python?" # With image (vision models) llms --image image.jpg "What's in this image?" llms --image https://example.com/photo.png "Describe this photo" # Display full JSON Response llms "Explain quantum computing" --raw # Start the UI and an OpenAI compatible API on port 8000: llms --serve 8000 ``` Incidentally, as [llms.py UI](https://servicestack.net/posts/llms-py-ui) and AI Chat utilize the same UI you can use its **import/export** features to transfer your AI Chat History between them. Check out the [llms.py GitHub repo](https://github.com/ServiceStack/llms) for even more features. # React + Tailwind + TypeScript for AI-First Development Source: https://servicestack.net/posts/react We're witnessing a fundamental shift in how applications are built. AI code generation has evolved from a novelty to a productivity multiplier that's become too significant to ignore. While AI models still require oversight for production backend systems, they excel at generating frontend UIs—compressing development timelines that once took months into days. ## The Rise of Vibe Coding AI can now generate complete, production-ready UI code. This enables an entirely new development workflow that [Andrej Karpathy](https://en.wikipedia.org/wiki/Andrej_Karpathy) has termed ["Vibe Coding"](https://en.wikipedia.org/wiki/Vibe_coding)—where developers iteratively guide AI agents to implement features through natural language instructions, where features can be iteratively prototyped, refined and improved within seconds instead of hours. This AI-first approach is rapidly maturing, with tools like [Cursor](https://cursor.com), [Claude Code](https://www.claude.com/product/claude-code), and [Codex](https://chatgpt.com/features/codex/) becoming the preferred platforms for this new paradigm with new tools designed to get maximum effectiveness of AI models with sophisticated planning tools, focused models optimized for code generation and edits and agentic workflows that's able to solidifying each new feature iteration with tests, along with detailed documentation, planning, migrations and usage guides. ## React & Tailwind: The AI Development Standard React and Tailwind have emerged as the de facto standards for AI-generated UIs. Every major platform for generating applications from prompts has converged on this stack including [Replit](https://blog.replit.com/react), [Lovable](https://lovable.dev/blog/best-tailwind-css-component), [Google's AI Studio](https://aistudio.google.com), [Vercel's v0](https://v0.app) and [Claude Code Web](https://claude.ai/code). ### TypeScript Whilst TypeScript is often excluded in one-prompt solutions catering to non-developers, it's still a critical part of the AI development workflow. It provides a type system that helps AI models generate more accurate and maintainable code and TypeScript's static analysis also helps identify errors in the generated code which AI Models have have become really good at correcting—as such it's an integral part in all our React templates. ## How ServiceStack Excels in AI-First Development Context is king when developing with AI models. The better the context, the higher the quality of generated code and ServiceStack's architecture is uniquely suited for AI-assisted development: ### Declarative Typed APIs All ServiceStack APIs follow a flat, declarative structure—The contract is explicit and consistent and LLMs don't need to guess what APIs accept or return. ### End-to-End Type Safety Context quality directly impacts generated code quality. ServiceStack's TypeScript integration provides complete static analysis of what APIs accept, return, and how to bind responses—giving AI models the full context they need. The static analysis feedback also directs models to identify and correct any errors in the generated code. ### Zero-Ambiguity Integration AI models thrive on consistency. ServiceStack removes guesswork with a single pattern for all API calls: - One generic `JsonServiceClient` for all APIs - Consistent methods used to send all requests - Consistent Typed Request DTO → Response DTO flow - Uniform error handling ### Intuitive Project Structure ServiceStack's [physical project structure](https://docs.servicestack.net/physical-project-structure) provides clear separation of concerns, with the entire API surface area contained in [the ServiceModel project](https://docs.servicestack.net/physical-project-structure#servicemodel-project)—making codebases easy for AI models to navigate and understand. ### Minimal Code Surface Less code means fewer opportunities for errors. ServiceStack's high-productivity features minimize the code AI needs to generate: - **[AutoQuery APIs](https://docs.servicestack.net/autoquery/)** - Flexible, queryable APIs defined with just a Request DTO - **[AutoQueryGrid Component](https://react.servicestack.net/gallery/autoquerygrid)** - Complete CRUD UIs in 1 line of code - **[Auto Form Components](https://react.servicestack.net/gallery/autoform)** - Beautiful, validation-bound forms in 1 line of code These components are ideal for rapidly building backend management interfaces, freeing developers to focus on differentiating customer-facing features. ## Modern React Project Templates We're introducing three production-ready React templates, each optimized for different use cases: ## Comprehensive React Component Library All three templates leverage our new [React Component Gallery](https://react.servicestack.net)—a high-fidelity port of our proven [Vue Component Library](https://docs.servicestack.net/vue/) and [Blazor Component Library](https://blazor.servicestack.net). This comprehensive collection provides everything needed to build highly productive, modern and responsive web applications. :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/react/react-gallery.webp)](https://react.servicestack.net) ::: ::: Switch to Dark Mode to see how all components looks in Dark Mode: :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/react/react-gallery-dark.webp)](https://react.servicestack.net) ::: ::: ServiceStack's first-class React support positions your applications at the forefront of AI-assisted development. With declarative APIs, complete type safety, and minimal boilerplate, you can leverage AI code generation with confidence while maintaining the quality and maintainability your production systems demand. ## Admin Analytics UI & Persistence ServiceStack's AI Chat now includes comprehensive chat history persistence and analytics capabilities, providing deep visibility into AI usage patterns, costs, and performance across your infrastructure. Choose between `DbChatStore` for universal RDBMS compatibility or `PostgresChatStore` for optimized PostgreSQL performance with monthly table partitioning—both ensuring consistent performance as history grows. The Admin UI Analytics dashboard delivers actionable insights through three key views: **Cost Analysis** tracks spending across providers and models with daily and monthly breakdowns, **Token Usage** monitors input and output token consumption to identify optimization opportunities, and **Activity Log** maintains a searchable audit trail with full conversation details. These visualizations enable data-driven decisions about provider selection, cost optimization, and help debug AI features in production by capturing every request and response flowing through AI Chat's UI, external OpenAI endpoints, and internal `IChatStore` requests. ## TypeScript Data Models As AI Models are not as adept at generating C# APIs or Migrations yet, they excel at generating TypeScript code, which our [TypeScript Data Models](https://docs.servicestack.net/autoquery/okai-models) feature can take advantage of by generating all the C# AutoQuery CRUD APIs and DB Migrations needing to support it. With just a TypeScript Definition: - [Bookings.d.ts](https://github.com/NetCoreTemplates/react-static/blob/main/MyApp.ServiceModel/Bookings.d.ts) We can generate all the AutoQuery CRUD APIs and DB Migrations needed to enable a CRUD UI with: :::copy npx okai Bookings.d.ts ::: This is enough to generate a complete CRUD UI to manage Bookings in your React App with the [React AutoQueryGrid Component](https://react.servicestack.net/gallery/autoquerygrid). or with ServiceStack's built-in [Locode UI](https://docs.servicestack.net/locode/): :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/react/bookings-locode.webp)](https://docs.servicestack.net/locode/) ::: ::: ### Cheat Sheet We'll quickly cover the common dev workflow for this feature. To create a new Table use `init `, e.g: :::copy npx okai init Transaction ::: This will generate an empty `MyApp.ServiceModel/
.d.ts` file along with stub AutoQuery APIs and DB Migration implementations. #### Regenerate AutoQuery APIs and DB Migrations After modifying the TypeScript Data Model to include the desired fields, you can re-run the `okai` tool to generate the AutoQuery APIs and DB Migrations (which can be run anywhere within your Solution): :::copy npx okai Transaction.d.ts ::: After you're happy with your Data Model you can run DB Migrations to run the DB Migration and create your RDBMS Table: :::copy npm run migrate ::: #### Making changes after first migration If you want to make further changes to your Data Model, you can re-run the `okai` tool to update the AutoQuery APIs and DB Migrations, then run the `rerun:last` npm script to drop and re-run the last migration: :::copy npm run rerun:last ::: #### Removing a Data Model and all generated code If you changed your mind and want to get rid of the RDBMS Table you can revert the last migration: :::copy npm run revert:last ::: Which will drop the table and then you can get rid of the AutoQuery APIs, DB Migrations and TypeScript Data model with: :::copy npx okai rm Transaction.d.ts ::: ## AI-First Example There are a number of options for starting with an AI generated Application, with all the Instant AI App Generators like [Google's App Studio](https://aistudio.google.com/apps) able to provide a great starting point. Although currently Professional Developers tend to use [Cursor](https://cursor.com/), [Claude Code](https://www.claude.com/product/claude-code) or [Codex](https://openai.com/codex/) as their day-to-day tools of choice. ### Use GitHub Copilot when creating a new Repository If you're using [GitHub Copilot](https://copilot.github.com/) you can also use it to generate a new App [from the Vite React template ](https://github.com/new?template_name=react-static&template_owner=NetCoreTemplates): :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/react/react-new-repo.webp)](https://github.com/new?template_name=react-static&template_owner=NetCoreTemplates) ::: ::: For the example, I've started with a useful App that I've never created before, a Budget Planner App, using the prompt: ### Budget Planner Prompt ``` - React 19, TypeScript, TailwindCSS v4 - Persistence in IndexedDB/localStorage - Recharts - Vitest with React Testing Library ## Features Dashboard - Overview of total income, expenses, and remaining budget - Monthly summary chart (line graph) - Expense categories (pie chart) Transactions - Add/Edit/Delete income or expenses - Date filtering/sorting Budgets - Set monthly budget goals per category - Progress bars for spending vs. budget Reports - View past months - Export ``` The generated source code for the App was uploaded to: [github.com/mythz/budgets.apps.cafe](https://github.com/mythz/budgets.apps.cafe) ### Budget Planner App After a few minutes Copilot creates a PR with what we asked for, even things that we didn't specify in the prompt but could be inferred from the Project Template like **Dark Mode** support where it made use of the existing ``. ### Prompt AI to add new Features AI Assistance doesn't end after the initial implementation as AI Models and tools are more than capable to create 100% of the React UI now, including new features, fixes and other improvements. For this example I used Claude Code to Implement Category Auto-Tagging with this prompt: Implement Category Auto-Tagging Allow specifying tags when creating a new transaction. When users add a transaction, try to predict the tag from the Description, e.g: Input: “Starbucks latte” → Suggests category: Food & Drinks Input: “Uber to work” → Suggests category: Transport Implementation: Maintain a small local list of common keywords + categories. Pre-fill category in the transaction form as the user types in the Description. Which resulted in [this commit](https://github.com/mythz/budgets.apps.cafe/commit/e45a17b8dfd2b5983971554ced3e52ded6fa050e) which sees the feature available in the UI: :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} ![](/img/posts/react/budget/expense-tags-dark.webp) ::: ::: Along with different seed data, tailored for Income and Expenses: - [categoryAutoTag.ts](https://github.com/mythz/budgets.apps.cafe/blob/main/MyApp.Client/src/lib/categoryAutoTag.ts) And 19 passing tests to verify a working implementation: - [categoryAutoTag.test.ts](https://github.com/mythz/budgets.apps.cafe/blob/main/MyApp.Client/src/lib/categoryAutoTag.test.ts) Combined with Vite's instant hot-reload, this creates a remarkably fluid development experience where we get to watch our prompts materialize into working features in real-time. All this to say that this new development model exists today, and given its significant productivity gains, it's very likely to become the future of software development, especially for UIs. Since developers are no longer the primary authors of code, our UI choices swing from Developer preferences to UI technologies that AI models excel at. So whilst we have a preference for Vue given it's more readable syntax and progressive enhancement capabilities, and despite the .NET ecosystem having a strong bias towards Blazor, we're even more excited for the future of React and are committed to providing the best possible support for it. ::include react-templates.md:: # Ask ServiceStack Docs - Introducing AI Search Source: https://servicestack.net/posts/typesense-ai-search We're excited to announce the new Typesense-powered **AI Search**, a powerful new feature bringing conversational AI capabilities to [ServiceStack Docs](https://docs.servicestack.net). :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/typesense-ai-search/ai-search-default.webp)](https://docs.servicestack.net) ::: ::: ### Comprehensive Docs As ServiceStack has grown over the years, so have our docs - now spanning hundreds of pages covering everything from core features to advanced integrations. While comprehensive documentation is invaluable, finding the right information quickly can be challenging. Traditional search works well when you know what you're looking for, but what about when you need to understand concepts, explore solutions, or learn how different features work together? That's where **AI Search** comes in. :::{.not-prose} :::{.my-8 .max-w-2xl .mx-auto} [![](/img/posts/typesense-ai-search/ai-search-button.webp)](https://docs.servicestack.net) ::: ::: **AI Search** leverages Typesense's advanced [Conversational Search API](https://typesense.org/docs/29.0/api/conversational-search-rag.html) that uses Retrieval-Augmented Generation (RAG) of our docs combined with an LLM to provide intelligent, context-aware answers directly from our documentation. :::{.not-prose} :::{.my-8 .max-w-3xl .mx-auto .rounded-lg .overflow-hidden .shadow .hover:shadow-xl} [![](/img/posts/typesense-ai-search/ai_search_add_servicestack_reference.webp)](https://docs.servicestack.net) ::: ::: #### AI Search vs Instant Typesense Search **AI Search** is ideal for when you need conversational answers, explanations of concepts, or help understanding how different features work together. The AI excels at synthesizing information across multiple documentation pages to answer complex `how do I...` questions. Otherwise the existing instant Typesense Search is still the best option when you know exactly what you're looking for - like a specific API name, configuration option, or documentation page. ## What is Typesense AI Search? Typesense AI Search is a conversational interface that allows you to ask natural language questions about ServiceStack and receive: - **AI-Generated Answers** - Intelligent responses powered by Typesense's conversational model - **Relevant Documentation Links** - Direct links to the most relevant documentation pages - **Multi-turn Conversations** - Ask follow-up questions within the same conversation context ## Key Features [![](/img/posts/typesense-ai-search/ai-search-button.webp)](https://docs.servicestack.net) ### 🤖 Conversational Interface Click the AI Search button (chat icon) in the header to open an intuitive modal dialog. Type your question and get instant answers without leaving the documentation. ### 📚 Retrieval-Augmented Generation (RAG) The AI doesn't just generate responses - it grounds its answers in actual ServiceStack documentation. Each response includes: - **AI-Generated Answer** - Contextual explanation based on your question - **Search Results** - Up to 10 relevant documentation snippets with direct links - **Snippets** - Quick previews of relevant content to help find what you need ### 💬 Multi-turn Conversations Maintain context across multiple questions in a single conversation: - Ask initial questions about ServiceStack features - Follow up with clarifications or related topics - The conversation ID is automatically maintained for coherent context - Start a new conversation anytime by clicking on **clear** links or refreshing ### Asking Questions - Type your question naturally (e.g., "How do I set up authentication?") - Review the AI answer and explore the suggested documentation links ### Following Up 1. Ask related questions in the same conversation 2. The AI maintains context from previous messages 3. Click any documentation link to navigate to the full page 4. Start a new conversation anytime by refreshing ## Technical Implementation The AI Search feature was built with: - [TypesenseConversation Component](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/wwwroot/mjs/components/TypesenseConversation.mjs) - AI Search UI Vue component - **Indexing** - Uses [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper) to index content and generate embeddings using custom field definitions defined in [typesense-scraper-config.json](https://github.com/ServiceStack/docs.servicestack.net/blob/main/search-server/typesense-scraper/typesense-scraper-config.json) - **Setup** - Conversational Model and Conversation History collection created in [setup-search-index.yml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/.github/workflows/setup-search-index.yml) GitHub Action - **LLM** - Typesense sends the query and relevant context to Gemini Flash 2.5 as the Conversational Model - **Backend**: Uses [Typesense Conversational Search (RAG)](https://typesense.org/docs/29.0/api/conversational-search-rag.html) `multi_search` API ## Use Cases ### For Developers - **Quick Answers** - Get instant answers without searching through docs - **Learning** - Understand ServiceStack concepts through conversational explanations - **Troubleshooting** - Ask about common issues and get relevant solutions - **Discovery** - Find features you didn't know existed ### For Teams - **Onboarding** - New team members can quickly learn ServiceStack - **Documentation** - Reduces support burden by providing instant answers - **Knowledge Base** - Conversational access to your documentation ## Feedback & Support We'd love to hear your feedback! If you encounter any issues or have suggestions for improvements, please [let us know!](https://forums.servicestack.net/). # Protect same APIs with API Keys or Identity Auth Source: https://servicestack.net/posts/apikey_auth_apis Modern APIs need to serve different types of clients, each with distinct authentication requirements. Understanding when to use **Identity Auth** versus **API Keys** is crucial to optimize for security, performance, and user experience. ## Two Auth Paradigms for Different Use Cases ### Identity Auth: User → API **Identity Auth** is designed for scenarios where a **human user** is interacting with your API, typically through a web or mobile application which: - Requires user credentials (username/password, OAuth, etc.) - Establishes a user session with roles and permissions - For interactive workflows like logins, password resets & email confirmation - Enables user-specific features like profile management and personalized UX - Provides full access to user context, claims, and role-based authorization ### API Keys: Machine → API / User Agent → API **API Keys** are purpose-built for **machine-to-machine** communication or **user agents** accessing your API programmatically, without interactive user authentication. This authentication model: - Provides simple, token-based authentication without user sessions - Enables fine-grained access control through scopes and features - Supports non-interactive scenarios like scripts, services, and integrations - Can optionally be associated with a user but doesn't run in their context - Offers superior performance by avoiding the auth workflow overhead - Supports project based billing and usage metrics by API Key **Common scenarios:** - Microservices communicating with each other - Third-party integrations accessing your API - CLI tools and scripts that need API access - Mobile apps or SPAs making direct API calls without user context - Webhooks and automated processes - Providing API access to partners or customers with controlled permissions Despite serving 2 different use-cases there are a few times when you may want to serve the same API with both Identity Auth and API Keys. ### Supporting both Auth Models with 2 APIs Previously you would've needed to maintain two separate APIs, one protected with Identity Auth and another with API Keys. Thanks to ServiceStack's message-based APIs and [built-in Auto Mapping](https://docs.servicestack.net/auto-mapping) this is fairly easy to do: ```csharp // For authenticated users [ValidateIsAuthenticated] public class QueryOrders : QueryDb { } // For API key access [ValidateApiKey] public class QueryOrdersApiKey : QueryDb { } public class OrderService : Service { public List Get(GetOrders request) { var userId = Request.GetRequiredUserId(); // Shared business logic } public List Get(GetOrdersViaApiKey request) => Get(request.ConvertTo()); } public static class MyExtensions { public static string GetRequiredUserId(this IRequest? req) => req.GetApiKey()?.UserAuthId ?? req.GetClaimsPrincipal().GetUserId() ?? throw HttpError.Unauthorized("API Key must be associated with a user"); } ``` Whilst easy to implement, the biggest draw back with this approach is that it requires maintaining 2x APIs, 2x API endpoints, and 2x API docs. ## The Best of Both Worlds ServiceStack's flexible [API Keys feature](https://docs.servicestack.net/auth/apikeys) now allows you to protect the same APIs with **both** Identity Auth and API Keys, enabling you to: - Maintain a single API surface for all clients - Serve the same interactive UIs protected with Identity Auth or API Keys - Provide programmatic access via API Keys - Maintain all the benefits of API Keys To achieve this, Users will need to have a valid API Key generated for them which would then need to be added to the `apikey` Claim in the `UserClaimsPrincipalFactory` to be included in their Identity Auth Cookie: ```csharp // Program.cs services.AddScoped, AdditionalUserClaimsPrincipalFactory>(); // AdditionalUserClaimsPrincipalFactory.cs /// /// Add additional claims to the Identity Auth Cookie /// public class AdditionalUserClaimsPrincipalFactory( UserManager userManager, RoleManager roleManager, IApiKeySource apiKeySource, IOptions optionsAccessor) : UserClaimsPrincipalFactory( userManager, roleManager, optionsAccessor) { public override async Task CreateAsync(ApplicationUser user) { var principal = await base.CreateAsync(user); var identity = (ClaimsIdentity)principal.Identity!; var claims = new List(); if (user.ProfileUrl != null) { claims.Add(new Claim(JwtClaimTypes.Picture, user.ProfileUrl)); } // Add Users latest valid API Key to their Auth Cookie's 'apikey' claim var latestKey = (await apiKeySource.GetApiKeysByUserIdAsync(user.Id)) .OrderByDescending(x => x.CreatedDate) .FirstOrDefault(); if (latestKey != null) { claims.Add(new Claim(JwtClaimTypes.ApiKey, latestKey.Key)); } identity.AddClaims(claims); return principal; } } ``` After which Authenticated Users will be able to access `[ValidateApiKey]` protected APIs where it attaches the API Key in the `apikey` Claim to the request - resulting in the same behavior had they sent their API Key with the request. ```csharp // For authenticated users or API Keys [ValidateApiKey] public class QueryOrders : QueryDb { } ``` # RDBMS Background Jobs Source: https://servicestack.net/posts/rdbms_jobs We're excited to announce that we've ported our much loved [Background Jobs](https://docs.servicestack.net/background-jobs) feature to the popular **PostgreSQL**, **SQL Server** and **MySQL** RDBMS's! Since launching [Background Jobs](https://servicestack.net/posts/background-jobs) in September 2024, it's become one of our most popular features - providing a simple, infrastructure-free solution for managing background jobs and scheduled tasks in .NET 8+ Apps. The original implementation used SQLite for its durability, which worked beautifully for many use cases thanks to SQLite's low latency, fast disk persistence, and zero infrastructure requirements. However, we recognize that many of our customers need the features and scalability of industrial-strength RDBMS systems. Whether it's for leveraging existing database infrastructure, meeting enterprise requirements, or utilizing advanced database features like native table partitioning - we wanted to ensure Background Jobs could work seamlessly with your preferred database platform. ## Introducing DatabaseJobsFeature The new **DatabaseJobsFeature** is a purpose-built implementation for PostgreSQL, SQL Server, and MySQL that's a drop-in replacement for SQLite's **BackgroundsJobFeature**. It maintains the same simple API, data models, and service contracts - making migration from SQLite straightforward while unlocking the power of enterprise RDBMS platforms. Best of all, it can be added to an existing .NET 8+ project with a single command using our [mix tool](https://docs.servicestack.net/mix-tool): ## Quick Start ### For Identity Auth Projects If you're using [ServiceStack ASP.NET Identity Auth](https://servicestack.net/start) templates, simply run: :::sh x mix db-identity ::: This replaces both `Configure.BackgroundJobs.cs` and `Configure.RequestLogs.cs` with RDBMS-compatible versions that use `DatabaseJobsFeature` for background jobs and `DbRequestLogger` for API request logging. ### For Other .NET 8+ Apps For all other ServiceStack applications, use: :::sh x mix db-jobs ::: This replaces `Configure.BackgroundJobs.cs` to use the new `DatabaseJobsFeature`: ```csharp public class ConfigureBackgroundJobs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new CommandsFeature()); services.AddPlugin(new DatabaseJobsFeature { // Optional: Use a separate named connection // NamedConnection = "jobs" }); services.AddHostedService(); }).ConfigureAppHost(afterAppHostInit: appHost => { var services = appHost.GetApplicationServices(); var jobs = services.GetRequiredService(); // Example: Register recurring jobs to run on a schedule // jobs.RecurringCommand(Schedule.Hourly); }); } public class JobsHostedService(ILogger log, IBackgroundJobs jobs) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await jobs.StartAsync(stoppingToken); using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) { await jobs.TickAsync(); } } } ``` ## Seamless Migration from SQLite We've maintained the same `IBackgroundJobs` interface, data models, and API service contracts, which means: - **Zero code changes** to your existing job enqueueing logic - **Same Admin UI** for monitoring and managing jobs - **Compatible APIs** - all your existing commands and job configurations work as-is The only change needed is swapping `BackgroundsJobFeature` for `DatabaseJobsFeature` in your configuration! Watch our video introduction to Background Jobs to see it in action: :::youtube 2Cza_a_rrjA Durable C# Background Jobs and Scheduled Tasks for .NET ::: ## Smart RDBMS Optimizations One of the key benefits of SQLite Background Jobs was the ability to maintain completed and failed job history in separate **monthly databases** (e.g., `jobs_2025-01.db`, `jobs_2025-02.db`). This prevented unbounded database growth and made it easy to archive or delete old job history. For `DatabaseJobsFeature`, we've replicated this monthly partitioning strategy using **monthly partitioned tables** for the `CompletedJob` and `FailedJob` archive tables - but the implementation varies by database platform to leverage each RDBMS's strengths. ### PostgreSQL - Native Table Partitioning PostgreSQL provides native support for table partitioning, allowing us to automatically create monthly partitions using `PARTITION BY RANGE` on the `CreatedDate` column. The `DatabaseJobFeature` automatically creates new monthly partitions as needed, maintaining the same logical separation as SQLite's monthly .db's while keeping everything within a single Postgres DB: ```sql CREATE TABLE CompletedJob ( -- columns... CreatedDate TIMESTAMP NOT NULL, PRIMARY KEY ("Id","CreatedDate") ) PARTITION BY RANGE ("CreatedDate"); -- Monthly partitions are automatically created, e.g.: CREATE TABLE CompletedJob_2025_01 PARTITION OF CompletedJob FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); ``` This provides excellent query performance since PostgreSQL can use partition pruning to only scan relevant monthly partitions when filtering by `CreatedDate`. ### SQLServer / MySQL - Manual Partition Management For **SQL Server** and **MySQL**, monthly partitioned tables need to be created **out-of-band** (either manually or via cronjob scripts) since they don't support the same level of automatic partition management as PostgreSQL. However, this still works well in practice as it uses: 1. **Write-Only Tables** - The `CompletedJob` and `FailedJob` tables are write-only append tables. Jobs are never updated after completion or failure, only inserted. 2. **CreatedDate Index** - All queries against these tables use the `CreatedDate` indexed column for filtering and sorting, ensuring efficient access patterns even as the tables grow. The indexed `CreatedDate` column ensures that queries remain performant regardless of table size, and the write-only nature means there's no complex update logic to manage across partitions. This approach maintains the same benefits as SQLite's monthly databases - easy archival, manageable table sizes, and efficient queries - while leveraging the scalability and features of enterprise RDBMS systems. ### Separate Jobs Database Or if preferred, you can maintain background jobs in a **separate database** from your main application database. This separation keeps the write-heavy job processing load off your primary database, allowing you to optimize each database independently for its specific workload patterns like maintaining different backup strategies for your critical application data vs. job history. ```csharp // Configure.Db.cs services.AddOrmLite(options => options.UsePostgres(connectionString)) .AddPostgres("jobs", jobsConnectionString); // Configure.BackgroundJobs.cs services.AddPlugin(new DatabaseJobFeature { NamedConnection = "jobs" }); ``` ### Real Time Admin UI The Jobs Admin UI provides a real time view into the status of all background jobs including their progress, completion times, Executed, Failed, and Cancelled Jobs, etc. which is useful for monitoring and debugging purposes. [![](/img/posts/background-jobs/jobs-dashboard.webp)](/img/posts/background-jobs/jobs-dashboard.webp) View Real-time progress of queued Jobs [![](/img/posts/background-jobs/jobs-queue.webp)](/img/posts/background-jobs/jobs-queue.webp) View real-time progress logs of executing Jobs [![](/img/posts/background-jobs/jobs-logs.webp)](/img/posts/background-jobs/jobs-logs.webp) View Job Summary and Monthly Databases of Completed and Failed Jobs [![](/img/posts/background-jobs/jobs-completed.webp)](/img/posts/background-jobs/jobs-completed.webp) View full state and execution history of each Job [![](/img/posts/background-jobs/jobs-failed.webp)](/img/posts/background-jobs/jobs-failed.webp) Cancel Running jobs and Requeue failed jobs ## Usage For even greater reuse of your APIs you're able to queue your existing ServiceStack Request DTOs as a Background Job in addition to [Commands](https://docs.servicestack.net/commands) for encapsulating units of logic into internal invocable, inspectable and auto-retryable building blocks. ### Queue Commands Any API, Controller or Minimal API can execute jobs with the `IBackgroundJobs` dependency, e.g. here's how you can run a background job to send a new email when an API is called in any new Identity Auth template: ```csharp class MyService(IBackgroundJobs jobs) : Service { public object Any(MyOrder request) { var jobRef = jobs.EnqueueCommand(new SendEmail { To = "my@email.com", Subject = $"Received New Order {request.Id}", BodyText = $""" Order Details: {request.OrderDetails.DumpTable()} """, }); //... } } ``` Which records and immediately executes a worker to execute the `SendEmailCommand` with the specified `SendEmail` Request argument. It also returns a reference to a Job which can be used later to query and track the execution of a job. ### Queue APIs Alternatively a `SendEmail` API could be executed with just the Request DTO: ```csharp var jobRef = jobs.EnqueueApi(new SendEmail { To = "my@email.com", Subject = $"Received New Order {request.Id}", BodyText = $""" Order Details: {request.OrderDetails.DumpTable()} """, }); ``` Although Sending Emails is typically not an API you want to make externally available and would want to [Restrict access](https://docs.servicestack.net/auth/restricting-services) or [limit usage to specified users](https://docs.servicestack.net/auth/identity-auth#declarative-validation-attributes). In both cases the `SendEmail` Request is persisted into the Jobs SQLite database for durability that gets updated as it progresses through the queue. For execution the API or command is resolved from the IOC before being invoked with the Request. APIs are executed via the [MQ Request Pipeline](https://docs.servicestack.net/order-of-operations) and commands executed using the [Commands Feature](https://docs.servicestack.net/commands) where they'll also be visible in the [Commands Admin UI](https://docs.servicestack.net/commands#command-admin-ui). ### Background Job Options The behavior for each `Enqueue*` method for executing background jobs can be customized with the following options: - `Worker` - Serially process job using a named worker thread - `Callback` - Invoke another command with the result of a successful job - `DependsOn` - Execute jobs after successful completion of a dependent job - If parent job fails all dependent jobs are cancelled - `UserId` - Execute within an Authenticated User Context - `RunAfter` - Queue jobs that are only run after a specified date - `RetryLimit` - Override default retry limit for how many attempts should be made to execute a job - `TimeoutSecs` - Override default timeout for how long a job should run before being cancelled - `RefId` - Allow clients to specify a unique Id (e.g Guid) to track job - `Tag` - Group related jobs under a user specified tag - `CreatedBy` - Optional field for capturing the owner of a job - `BatchId` - Group multiple jobs with the same Id - `ReplyTo` - Optional field for capturing where to send notification for completion of a Job - `Args` - Optional String Dictionary of Arguments that can be attached to a Job ### Feature Overview It packs most features needed in a Background Jobs solution including: - Use your App's existing RDBMS (no other infrastructure dependencies) - Execute existing APIs or versatile Commands - Commands auto registered in IOC - Scheduled Reoccurring Tasks - Track Last Job Run - Serially execute jobs with the same named Worker - Queue Jobs dependent on successful completion of parent Job - Queue Jobs to be executed after a specified Date - Execute Jobs within the context of an Authenticated User - Auto retry failed jobs on a default or per-job limit - Timeout Jobs on a default or per-job limit - Cancellable Jobs - Requeue Failed Jobs - Execute custom callbacks on successful execution of Job - Maintain Status, Logs, and Progress of Executing Jobs - Execute transitive (i.e. non-durable) jobs using named workers - Attach optional `Tag`, `BatchId`, `CreatedBy`, `ReplyTo` and `Args` with Jobs Please [let us know](https://servicestack.net/ideas) of any other missing features you'd love to see implemented. ## Schedule Recurring Tasks In addition to queueing jobs to run in the background, it also supports scheduling recurring tasks to execute APIs or Commands at fixed intervals. :::youtube DtB8KaXXMCM Schedule your Reoccurring Tasks with Background Jobs! ::: APIs and Commands can be scheduled to run at either a `TimeSpan` or [CRON Expression](https://github.com/HangfireIO/Cronos?tab=readme-ov-file#cron-format) interval, e.g: ### CRON Expression Examples ```csharp // Every Minute Expression jobs.RecurringCommand(Schedule.Cron("* * * * *")); // Every Minute Constant jobs.RecurringCommand(Schedule.EveryMinute, new CheckUrls { Urls = urls }); ``` ### CRON Format You can use any **unix-cron format** expression supported by the [HangfireIO/Cronos](https://github.com/HangfireIO/Cronos) library: ```txt |------------------------------- Minute (0-59) | |------------------------- Hour (0-23) | | |------------------- Day of the month (1-31) | | | |------------- Month (1-12; or JAN to DEC) | | | | |------- Day of the week (0-6; or SUN to SAT) | | | | | | | | | | * * * * * ``` The allowed formats for each field include: | Field | Format of valid values | |------------------|--------------------------------------------| | Minute | 0-59 | | Hour | 0-23 | | Day of the month | 1-31 | | Month | 1-12 (or JAN to DEC) | | Day of the week | 0-6 (or SUN to SAT; or 7 for Sunday) | #### Matching all values To match all values for a field, use the asterisk: `*`, e.g here are two examples in which the minute field is left unrestricted: - `* 0 1 1 1` - the job runs every minute of the midnight hour on January 1st and Mondays. - `* * * * *` - the job runs every minute (of every hour, of every day of the month, of every month, every day of the week, because each of these fields is unrestricted too). #### Matching a range To match a range of values, specify your start and stop values, separated by a hyphen (-). Do not include spaces in the range. Ranges are inclusive. The first value must be less than the second. The following equivalent examples run at midnight on Mondays, Tuesdays, Wednesdays, Thursdays, and Fridays (for all months): - `0 0 * * 1-5` - `0 0 * * MON-FRI` #### Matching a list Lists can contain any valid value for the field, including ranges. Specify your values, separated by a comma (,). Do not include spaces in the list, e.g: - `0 0,12 * * *` - the job runs at midnight and noon. - `0-5,30-35 * * * *` - the job runs in each of the first five minutes of every half hour (at the top of the hour and at half past the hour). ### TimeSpan Interval Examples ```csharp jobs.RecurringCommand( Schedule.Interval(TimeSpan.FromMinutes(1))); // With Example jobs.RecurringApi(Schedule.Interval(TimeSpan.FromMinutes(1)), new CheckUrls { Urls = urls }); ``` That can be registered with an optional **Task Name** and **Background Options**, e.g: ```csharp jobs.RecurringCommand("Check URLs", Schedule.EveryMinute, new() { RunCommand = true // don't persist job }); ``` :::info If no name is provided, the Command's Name or APIs Request DTO will be used ::: ## Interned Cronos A major source of friction in .NET Libraries and most Frameworks from all platforms in general is dependency conflicts. E.g. Conflicting versions of JSON.NET have plagued many a .NET library and framework for several years, something that never impacted ServiceStack Apps since we maintain our own fast/flexible JSON Serializer and have never had a dependency to JSON.NET. As supply chain attacks from external OSS libraries have become more common, it's even more important to avoid taking dependencies on external libraries where possible. As we now have multiple packages that referenced [Hangfire's Cronos](https://github.com/HangfireIO/Cronos) library we've decided to intern it in ServiceStack, removing the previous dependency **ServiceStack.Jobs** had to Cronos. The only issue was that [CronParser.cs](https://github.com/HangfireIO/Cronos/blob/main/src/Cronos/CronParser.cs) uses unsafe parsing and we don't allow `` in any ServiceStack package, so it was rewritten to use Spans in our interned [CronParser.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.Common/Cronos/CronParser.cs) implementation. It's released under the same MIT License as Cronos so anyone else is welcome to use it, as is our port of their [CronExpressionTests.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/tests/ServiceStack.Common.Tests/CronExpressionTests.cs) to NUnit. ### Idempotent Registration Scheduled Tasks are idempotent where the same registration with the same name will either create or update the scheduled task registration without losing track of the last time the Recurring Task, as such it's recommended to always define your App's Scheduled Tasks on Startup: ```csharp public class ConfigureBackgroundJobs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context,services) => { //... }).ConfigureAppHost(afterAppHostInit: appHost => { var services = appHost.GetApplicationServices(); var jobs = services.GetRequiredService(); // App's Scheduled Tasks Registrations: jobs.RecurringCommand(Schedule.Hourly); }); } ``` ### Background Jobs Admin UI The last job the Recurring Task ran is also viewable in the Jobs Admin UI: [![](/img/posts/background-jobs/jobs-scheduled-tasks-last-job.webp)](/img/posts/background-jobs/jobs-scheduled-tasks-last-job.webp) ### Executing non-durable jobs `IBackgroundJobs` also supports `RunCommand*` methods for executing background jobs transiently (i.e. non-durable), which is useful for commands that want to be serially executed by a named worker but don't need to be persisted. #### Execute in Background and return immediately You could use this to queue system emails to be sent by the same **smtp** worker and are happy to not have its state and execution history tracked in the Jobs database. ```csharp var job = jobs.RunCommand(new SendEmail { ... }, new() { Worker = "smtp" }); ``` In this case `RunCommand` returns the actual `BackgroundJob` instance that will be updated by the worker. #### Execute in Background and wait for completion You can also use `RunCommandAsync` if you prefer to wait until the job has been executed. Instead of a Job it returns the **Result** of the command if it returned one. ```csharp var result = await jobs.RunCommandAsync(new SendEmail {...}, new() { Worker = "smtp" }); ``` ### Serially Execute Jobs with named Workers By default jobs are executed immediately in a new Task, we can also change the behavior to instead execute jobs one-by-one in a serial queue by specifying them to use the same named worker as seen in the example above. Alternatively you can annotate the command with the `[Worker]` attribute if you **always** want all jobs executing the command to use the same worker: ```csharp [Worker("smtp")] public class SendEmailCommand(IBackgroundJobs jobs) : SyncCommand { //... } ``` ### Use Callbacks to process the results of Commands Callbacks can be used to extend the lifetime of a job to include processing a callback to process its results. This is useful where you would like to reuse the the same command but handle the results differently, e.g. the same command can email results or invoke a webhook by using a callback: ```csharp jobs.EnqueueCommand(new CheckUrls { Urls = allUrls }, new() { Callback = nameof(EmailUrlResultsCommand), }); jobs.EnqueueCommand(new CheckUrls { Urls = criticalUrls }, new() { Callback = nameof(WebhookUrlResultsCommand), ReplyTo = callbackUrl }); ``` Callbacks that fail are auto-retried the same number of times as their jobs, which if they all fail then the entire job is also marked as failed. ### Run Job dependent on successful completion of parent Jobs can be queued to only run after the successful completion of another job, this is useful for when you need to kick off multiple jobs after a long running task has finished like generating monthly reports after monthly data has been aggregated, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }); jobs.EnqueueCommand(new () { DependsOn = jobRef.Id, }); jobs.EnqueueCommand(new () { DependsOn = jobRef.Id, }); ``` Inside your command you can get a reference to your current job with `Request.GetBackgroundJob()` which will have its `ParentId` populated with the parent job Id and `job.ParentJob` containing a reference to the completed Parent Job where you can access its Request, Results, and other job information: ```csharp public class GenerateSalesReportCommand(ILogger log) : SyncCommand { protected override void Run() { var job = Request.GetBackgroundJob(); var parentJob = job.ParentJob; } } ``` ### Atomic Batching Behavior We can also use `DependsOn` to implement atomic batching behavior where from inside our executing command we can queue new jobs that are dependent on the successful execution of the current job, e.g: ```csharp public class CheckUrlsCommand(IHttpClientFactory factory, IBackgroundJobs jobs) : AsyncCommand { protected override async Task RunAsync(CheckUrls req, CancellationToken ct) { var job = Request.GetBackgroundJob(); var batchId = Guid.NewGuid().ToString("N"); using var client = factory.CreateClient(); foreach (var url in req.Urls) { var msg = new HttpRequestMessage(HttpMethod.Get, url); var response = await client.SendAsync(msg, ct); response.EnsureSuccessStatusCode(); jobs.EnqueueCommand(new SendEmail { To = "my@email.com", Subject = $"{new Uri(url).Host} status", BodyText = $"{url} is up", }, new() { DependsOn = job.Id, BatchId = batchId, }); } } } ``` Where any dependent jobs are only executed if the job was successfully completed. If instead an exception was thrown during execution, the job will be failed and all its dependent jobs cancelled and removed from the queue. ### Executing jobs with an Authenticated User Context If you have existing logic dependent on a Authenticated `ClaimsPrincipal` or ServiceStack `IAuthSession` you can have your APIs and Commands also be executed with that user context by specifying the `UserId` the job should be executed as: ```csharp var openAiRequest = new CreateOpenAiChat { Request = new() { Model = "gpt-4", Messages = [ new() { Content = request.Question } ] }, }; // Example executing API Job with User Context jobs.EnqueueApi(openAiRequest, new() { UserId = Request.GetClaimsPrincipal().GetUserId(), CreatedBy = Request.GetClaimsPrincipal().GetUserName(), }); // Example executing Command Job with User Context jobs.EnqueueCommand(openAiRequest, new() { UserId = Request.GetClaimsPrincipal().GetUserId(), CreatedBy = Request.GetClaimsPrincipal().GetUserName(), }); ``` Inside your API or Command you access the populated User `ClaimsPrincipal` or ServiceStack `IAuthSession` using the same APIs that you'd use inside your ServiceStack APIs, e.g: ```csharp public class CreateOpenAiChatCommand(IBackgroundJobs jobs) : AsyncCommand { protected override async Task RunAsync( CreateOpenAiChat request, CancellationToken token) { var user = Request.GetClaimsPrincipal(); var session = Request.GetSession(); //... } } ``` ### Queue Job to run after a specified date Using `RunAfter` lets you queue jobs that are only executed after a specified `DateTime`, useful for executing resource intensive tasks at low traffic times, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }, new() { RunAfter = DateTime.UtcNow.Date.AddDays(1) }); ``` ### Attach Metadata to Jobs All above Background Job Options have an effect on when and how Jobs are executed. There are also a number of properties that can be attached to a Job that can be useful in background job processing despite not having any effect on how jobs are executed. These properties can be accessed by commands or APIs executing the Job and are visible and can be filtered in the Jobs Admin UI to help find and analyze executed jobs. ```csharp var jobRef = jobs.EnqueueCommand(openAiRequest, new() { // Group related jobs under a common tag Tag = "ai", // A User-specified or system generated unique Id to track the job RefId = request.RefId, // Capture who created the job CreatedBy = Request.GetClaimsPrincipal().GetUserName(), // Link jobs together that are sent together in a batch BatchId = batchId, // Capture where to notify the completion of the job to ReplyTo = "https:example.org/callback", // Additional properties about the job that aren't in the Request Args = new() { ["Additional"] = "Metadata" } }); ``` ### Querying a Job A job can be queried by either it's auto-incrementing `Id` Primary Key or by a unique `RefId` that can be user-specified. ```csharp var jobResult = jobs.GetJob(jobRef.Id); var jobResult = jobs.GetJobByRefId(jobRef.RefId); ``` At a minimum a `JobResult` will contain the Summary Information about a Job as well as the full information about a job depending on where it's located: ```csharp class JobResult { // Summary Metadata about a Job in the JobSummary Table JobSummary Summary // Job that's still in the BackgroundJob Queue BackgroundJob? Queued // Full Job information in Monthly DB CompletedJob Table CompletedJob? Completed // Full Job information in Monthly DB FailedJob Table FailedJob? Failed // Helper to access full Job Information BackgroundJobBase? Job => Queued ?? Completed ?? Failed } ``` ### Job Execution Limits Default Retry and Timeout Limits can be configured on the `DatabaseJobFeature`: ```csharp services.AddPlugin(new DatabaseJobFeature { DefaultRetryLimit = 2, DefaultTimeout = TimeSpan.FromMinutes(10), }); ``` These limits are also overridable on a per-job basis, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }, new() { RetryLimit = 3, Timeout = TimeSpan.FromMinutes(30), }); ``` ### Logging, Cancellation an Status Updates We'll use the command for checking multiple URLs to demonstrate some recommended patterns and how to enlist different job processing features. ```csharp public class CheckUrlsCommand( ILogger logger, IBackgroundJobs jobs, IHttpClientFactory clientFactory) : AsyncCommand { protected override async Task RunAsync(CheckUrls req, CancellationToken ct) { // 1. Create Logger that Logs and maintains logging in Jobs DB var log = Request.CreateJobLogger(jobs,logger); // 2. Get Current Executing Job var job = Request.GetBackgroundJob(); var result = new CheckUrlsResult { Statuses = new() }; using var client = clientFactory.CreateClient(); for (var i = 0; i < req.Urls.Count; i++) { // 3. Stop processing Job if it's been cancelled ct.ThrowIfCancellationRequested(); var url = req.Urls[i]; try { var msg = new HttpRequestMessage(HttpMethod.Get,url); var response = await client.SendAsync(msg, ct); result.Statuses[url] = response.IsSuccessStatusCode; log.LogInformation("{Url} is {Status}", url, response.IsSuccessStatusCode ? "up" : "down"); // 4. Optional: Maintain explicit progress and status updates log.UpdateStatus(i/(double)req.Urls.Count,$"Checked {i} URLs"); } catch (Exception e) { log.LogError(e, "Error checking {Url}", url); result.Statuses[url] = false; } } // 5. Send Results to WebHook Callback if specified if (job.ReplyTo != null) { jobs.EnqueueCommand(result, new() { ParentId = job.Id, ReplyTo = job.ReplyTo, }); } } } ``` We'll cover some of the notable parts useful when executing Jobs: #### 1. Job Logger We can use a Job logger to enable database logging that can be monitored in real-time in the Admin Jobs UI. Creating it with both `BackgroundJobs` and `ILogger` will return a combined logger that both Logs to standard output and to the Jobs database: ```csharp var log = Request.CreateJobLogger(jobs,logger); ``` Or just use `Request.CreateJobLogger(jobs)` to only save logs to the database. #### 2. Resolve Executing Job If needed the currently executing job can be accessed with: ```csharp var job = Request.GetBackgroundJob(); ``` Where you'll be able to access all the metadata the jobs were created with including `ReplyTo` and `Args`. #### 3. Check if Job has been cancelled To be able to cancel a long running job you'll need to periodically check if a Cancellation has been requested and throw a `TaskCanceledException` if it has to short-circuit the command which can be done with: ```csharp ct.ThrowIfCancellationRequested(); ``` You'll typically want to call this at the start of any loops to prevent it from doing any more work. #### 4. Optionally record progress and status updates By default Background Jobs looks at the last API or Command run and worker used to estimate the duration and progress for how long a running job will take. If preferred your command can explicitly set a more precise progress and optional status update that should be used instead, e.g: ```csharp log.UpdateStatus(progress:i/(double)req.Urls.Count, $"Checked {i} URLs"); ``` Although generally the estimated duration and live logs provide a good indication for the progress of a job. #### 5. Notify completion of Job Calling a Web Hook is a good way to notify externally initiated job requests of the completion of a job. You could invoke the callback within the command itself but there are a few benefits to initiating another job to handle the callback: - Frees up the named worker immediately to process the next task - Callbacks are durable, auto-retried and their success recorded like any job - If a callback fails the entire command doesn't need to be re-run again We can queue a callback with the result by passing through the `ReplyTo` and link it to the existing job with: ```csharp if (job.ReplyTo != null) { jobs.EnqueueCommand(result, new() { ParentId = job.Id, ReplyTo = job.ReplyTo, }); } ``` Which we can implement by calling the `SendJsonCallbackAsync` extension method with the Callback URL and the Result DTO it should be called with: ```csharp public class NotifyCheckUrlsCommand(IHttpClientFactory clientFactory) : AsyncCommand { protected override async Task RunAsync( CheckUrlsResult request, CancellationToken token) { await clientFactory.SendJsonCallbackAsync( Request.GetBackgroundJob().ReplyTo, request, token); } } ``` #### Callback URLs `ReplyTo` can be any URL which by default will have the result POST'ed back to the URL with a JSON Content-Type. Typically URLs will contain a reference Id so external clients can correlate a callback with the internal process that initiated the job. If the callback API is publicly available you'll want to use an internal Id that can't be guessed so the callback can't be spoofed, like a Guid, e.g: :::copy `https://api.example.com?refId={RefId}` ::: If needed the callback URL can be customized on how the HTTP Request callback is sent. You can change the HTTP Method used by including it before the URL: :::copy `PUT https://api.example.com` ::: If the auth part contains a colon `:` it's treated as Basic Auth: :::copy `username:password@https://api.example.com` ::: If name starts with `http.` sends a HTTP Header :::copy `http.X-API-Key:myApiKey@https://api.example.com` ::: Otherwise it's sent as a Bearer Token: :::copy `myToken123@https://api.example.com` ::: Bearer Token or HTTP Headers starting with `$` is substituted with Environment Variable if exists: :::copy `$API_TOKEN@https://api.example.com` ::: When needed headers, passwords, and tokens can be URL encoded if they contain any delimiter characters. ## Implementing Commands At a minimum a command need only implement the [IAsyncCommand interface](https://docs.servicestack.net/commands#commands-feature): ```csharp public interface IAsyncCommand { Task ExecuteAsync(T request); } ``` Which is the singular interface that can execute any command. However commands executed via Background Jobs have additional context your commands may need to access during execution, including the `BackgroundJob` itself, the `CancellationToken` and an Authenticated User Context. To reduce the effort in creating commands with a `IRequest` context we've added a number ergonomic base classes to better capture the different call-styles a unit of logic can have including **Sync** or **Async** execution, whether they require **Input Arguments** or have **Result Outputs**. Choosing the appropriate Abstract base class benefits from IDE tooling in generating the method signature that needs to be implemented whilst Async commands with Cancellation Tokens in its method signature highlights any missing async methods that are called without the token. ### Sync Commands - `SyncCommand` - Requires No Arguments - `SyncCommand` - Requires TRequest Argument - `SyncCommandWithResult` - Requires No Args and returns Result - `SyncCommandWithResult` - Requires Arg and returns Result ```csharp public record MyArgs(int Id); public record MyResult(string Message); public class MyCommandNoArgs(ILogger log) : SyncCommand { protected override void Run() { log.LogInformation("Called with No Args"); } } public class MyCommandArgs(ILogger log) : SyncCommand { protected override void Run(MyArgs request) { log.LogInformation("Called with {Id}", request.Id); } } public class MyCommandWithResult(ILogger log) : SyncCommandWithResult { protected override MyResult Run() { log.LogInformation("Called with No Args and returns Result"); return new MyResult("Hello World"); } } public class MyCommandWithArgsAndResult(ILogger log) : SyncCommandWithResult { protected override MyResult Run(MyArgs request) { log.LogInformation("Called with {Id} and returns Result", request.Id); return new MyResult("Hello World"); } } ``` ### Async Commands - `AsyncCommand` - Requires No Arguments - `AsyncCommand` - Requires TRequest Argument - `AsyncCommandWithResult` - Requires No Args and returns Result - `AsyncCommandWithResult` - Requires Arg and returns Result ```csharp public class MyAsyncCommandNoArgs(ILogger log) : AsyncCommand { protected override async Task RunAsync(CancellationToken token) { log.LogInformation("Async called with No Args"); } } public class MyAsyncCommandArgs(ILogger log) : AsyncCommand { protected override async Task RunAsync(MyArgs request, CancellationToken t) { log.LogInformation("Async called with {Id}", request.Id); } } public class MyAsyncCommandWithResult(ILogger log) : AsyncCommandWithResult { protected override async Task RunAsync(CancellationToken token) { log.LogInformation("Async called with No Args and returns Result"); return new MyResult("Hello World"); } } public class MyAsyncCommandWithArgsAndResult(ILogger log) : AsyncCommandWithResult { protected override async Task RunAsync( MyArgs request, CancellationToken token) { log.LogInformation("Called with {Id} and returns Result", request.Id); return new MyResult("Hello World"); } } ``` # In Depth Interactive API Analytics for PostgreSQL, SQL Server & MySQL Source: https://servicestack.net/posts/rdbms_analytics ServiceStack v8.9 restores parity to **PostgreSQL**, **SQL Server** & **MySQL** RDBMS's for our previous SQLite-only features with the new `DbRequestLogger` which is a drop-in replacement for [SQLite Request Logging](https://docs.servicestack.net/sqlite-request-logs) for persisting API Request Logs to a RDBMS. Whilst maintaining an archive of API Requests is nice, the real value of DB Request Logging is that it unlocks the comprehensive API Analytics and querying Logging available that was previously limited to SQLite Request Logs. :::youtube kjLcm1llC5Y In Depth and Interactive API Analytics available to all ASP .NET Core ServiceStack Apps! ::: ### Benefits of API Analytics They provide deep and invaluable insight into your System API Usage, device distribution, its Users, API Keys and the IPs where most traffic generates: - **Visibility:** Provides a clear, visual summary of complex log data, making it easier to understand API usage and performance at a glance. - **Performance Monitoring:** Helps track key metrics like request volume and response times to ensure APIs are meeting performance expectations. - **User Understanding:** Offers insights into how users (and bots) are interacting with the APIs (devices, browsers). - **Troubleshooting:** Aids in quickly identifying trends, anomalies, or specific endpoints related to issues. - **Resource Planning:** Understanding usage patterns helps in scaling infrastructure appropriately. - **Security Insight:** Identifying bot traffic and unusual request patterns can be an early indicator of security concerns. ### Interactive Analytics Analytics are also interactive where you're able to drill down to monitor the activity of individual APIs, Users, API Keys and IPs which have further links back to the request logs which the summary analytics are derived from. As they offer significant and valuable insights the `SqliteRequestLogger` is built into all ASP.NET Core IdentityAuth templates, to switch it over to use a RDBMS we recommend installing `db-identity` mix gist to also replace SQLite BackgroundJobs with the RDBMS `DatabaseJobFeature`: :::sh x mix db-identity ::: Or if you just want to replace SQLite Request Logs with a RDBMS use: :::sh x mix db-requestlogs ::: Or you can copy the [Modular Startup](https://docs.servicestack.net/modular-startup) script below: ```csharp [assembly: HostingStartup(typeof(MyApp.ConfigureRequestLogs))] namespace MyApp; public class ConfigureRequestLogs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddPlugin(new RequestLogsFeature { RequestLogger = new DbRequestLogger { // NamedConnection = "" }, EnableResponseTracking = true, EnableRequestBodyTracking = true, EnableErrorTracking = true }); services.AddHostedService(); if (context.HostingEnvironment.IsDevelopment()) { services.AddPlugin(new ProfilingFeature()); } }); } public class RequestLogsHostedService(ILogger log, IRequestLogger requestLogger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); if (requestLogger is IRequireAnalytics logger) { while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) { await logger.TickAsync(log, stoppingToken); } } } } ``` ### RDBMS Provider When using a remote RDBMS, network latency becomes a primary concern that any solution needs to be designed around, as such the API Request Logs are initially maintained in an in memory collection before being flushed to the database **every 3 seconds** — configurable in the `PeriodicTimer` interval above. To reduce the number of round-trips to the database, the `DbRequestLogger` batches all pending logs into a single request using [OrmLite's Bulk Inserts](https://docs.servicestack.net/ormlite/bulk-inserts) which is supported by all major RDBMS's. ### PostgreSQL Table Partitioning PostgreSQL provides native support for table partitioning, allowing us to automatically create monthly partitions using `PARTITION BY RANGE` on the `CreatedDate` column. The `DbRequestLogger` automatically creates new monthly partitions as needed, maintaining the same logical separation as SQLite's monthly .db's while keeping everything within a single Postgres DB: ```sql CREATE TABLE "RequestLog" ( -- columns... "CreatedDate" TIMESTAMP NOT NULL, PRIMARY KEY ("Id","CreatedDate") ) PARTITION BY RANGE ("CreatedDate"); -- Monthly partitions are automatically created, e.g.: CREATE TABLE "RequestLog_2025_01" PARTITION OF "RequestLog" FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); ``` ### SQLServer / MySQL - Manual Partition Management For **SQL Server** and **MySQL**, monthly partitioned tables need to be created **out-of-band** (either manually or via cronjob scripts) since they don't support the same level of automatic partition management as PostgreSQL. However, this still works well in practice as because `RequestLog` is an **Append Only** table with all querying from the Admin UIs being filtered by its indexed `CreatedDate` in monthly viewable snapshots like it was with SQLite. ### Separate RequestLog Database Or if preferred, you can maintain request logs in a **separate database** from your main application database. This separation keeps the write-heavy logging load off your primary database, allowing you to optimize each database independently for its specific workload patterns like maintaining different backup strategies for your critical application data vs. log history. ```csharp // Configure.Db.cs services.AddOrmLite(options => options.UsePostgres(connectionString)) .AddPostgres("logs", logsConnectionString); // Configure.RequestLogs.cs services.AddPlugin(new RequestLogsFeature { RequestLogger = new DbRequestLogger { NamedConnection = "logs" }, //... }); ``` ## Queryable Admin Logging UI This will enable a more feature rich Request Logging Admin UI which utilizes the full queryability of the [AutoQueryGrid](https://docs.servicestack.net/vue/autoquerygrid) component to filter, sort and export Request Logs. [![](/img/posts/analytics/sqlitelogs.webp)](/img/posts/analytics/sqlitelogs.webp) ## Analytics Overview Utilizing an `DbRequestLogger` also enables the **Analytics** Admin UI in the sidebar which initially displays the API Analytics Dashboard: :::{.wideshot} [![](/img/posts/analytics/analytics-apis1.webp)](/img/posts/analytics/analytics-apis1.webp) ::: ### Distribution Pie Charts Lets you quickly understand the composition of your user base and traffic sources and the distribution of users across different web browsers, device types, and to identify the proportion of traffic coming from automated bots. ### Requests per day Line Chart Lets you monitor API usage trends and performance over time. It tracks the total number of API requests and the average response time day-by-day. You can easily spot trends like peak usage hours/days, identify sudden spikes or drops in traffic, and correlate request volume with API performance which is crucial for capacity planning and performance troubleshooting. ### API tag groups Pie Chart Lets you understand the usage patterns across different functional categories of your APIs. By grouping API requests based on assigned tags (like Security, Authentication, User Management, Tech, etc.), you get a high-level view of which *types* of functionalities are most frequently used or are generating the most load. ### API Requests Bar Chart Lets you identify the most and least frequently used specific API endpoints which ranks individual API endpoints by the number of requests they receive. This helps pinpoint: - **Critical Endpoints:** The most heavily used APIs that require robust performance and monitoring. - **Optimization Targets:** High-traffic endpoints that could benefit from performance optimization. - **Underutilized Endpoints:** APIs that might be candidates for deprecation or require promotion. - **Troubleshooting:** If performance issues arise (seen in the line chart), this helps narrow down which specific endpoint might be responsible. :::{.wideshot} [![](/img/posts/analytics/analytics-apis2.webp)](/img/posts/analytics/analytics-apis2.webp) ::: ### Total Duration Bar Chart Identifies which API endpoints consume the most *cumulative processing time* over the selected period. Even if an API endpoint is relatively fast per call, if it's called extremely frequently, it can contribute significantly to overall server load. Optimizing these can lead to significant savings in server resources (CPU, memory). ### Average Duration Bar Chart Pinpoints which API endpoints are the slowest on a *per-request* basis. APIs at the top of this list are prime candidates for performance investigation and optimization, as they represent potential user-facing slowness or system bottlenecks. ### Requests by Duration Ranges Histogram Provides an overview of the performance distribution for *all* API requests. This chart shows how many requests fall into different speed buckets and helps you understand the overall responsiveness of your API system at a glance. ## Individual API Analytics Clicking on an API's bar chart displays a dedicated, detailed view of a single API endpoint's behavior, isolating its performance and usage patterns from the overall system metrics offering immediate insight into the endpoint's traffic volume and reliability. :::{.wideshot} [![](/img/posts/analytics/analytics-api.webp)](/img/posts/analytics/analytics-api.webp) ::: ### Total Requests Displays the total requests for an API during the selected month. It includes HTTP Status Breakdown which provide **direct access to the filtered request logs**. This is a major benefit for **rapid troubleshooting**, allowing you to instantly view the specific log entries corresponding to successful requests or particular error codes for this API. ### Last Request Information Provides immediate context on the most recent activity for this endpoint with *when* the last request occurred, the source **IP address** and device information to help understand recent usage and check if the endpoint is still active, or quickly investigate the very last interaction if needed. ### Duration Summary Table (Total, Min, Max) Quantifies the performance characteristics specifically for this endpoint with the cumulative (Total) processing load, the best-case performance (Min), and the worst-case performance (Max) which is useful for identifying performance outliers. ### Duration Requests Histogram Visualizes the performance distribution for this API. ### Top Users Bar Chart Identifies which authenticated users are most frequently calling this API and relies on this endpoint the most. This can be useful for identifying power users, potential API abuse by a specific user account, or understanding the impact of changes to this API on key users. ### Top IP Addresses Bar Chart Shows which source IP addresses are generating the most traffic for this API. Useful for identifying high-volume clients, specific servers interacting with this endpoint, or potentially malicious IPs. ## Users The **Users** tab will display the top 100 Users who make the most API Requests and lets you click on a Users bar chart to view their individual User analytics. :::{.wideshot} [![](/img/posts/analytics/analytics-users.webp)](/img/posts/analytics/analytics-users.webp) ::: ### Individual User Analytics Provides a comprehensive view of a single user's complete interaction history and behavior across all APIs they've accessed, shifting the focus from API performance to user experience and activity. :::{.wideshot} [![](/img/posts/analytics/analytics-user.webp)](/img/posts/analytics/analytics-user.webp) ::: ### User Info & Total Requests Identifies the user and quantifies their overall activity level. Clicking on their ID or Name will navigate to the Users Admin UI. It also shows their success/error rate via the clickable status code links. This helps gauge user engagement and baseline activity. ### Last Request Information Offers a snapshot of the user's most recent interaction for immediate context. Knowing **when**, **what** API they called, from which **IP address**, using which **client** & **device** is valuable for support, identifying their last action or checking recent activity. ### HTTP Status Pie Chart Visualizes the overall success and error rate specifically for this user's API requests. ### Performance & Request Body Summary Table Quantifies the performance experienced by this user and the data they typically send. ### Duration Requests Histogram Shows the distribution of response times for requests made by this user to help understand the typical performance this user experiences. ### Top APIs Bar Chart Reveals which API endpoints this user interacts with most frequently and help understanding user behavior and which features they use most. ### Top IP Addresses Bar Chart Identifies the primary network locations or devices the user connects from. ### User Admin UI Analytics To assist in discoverability a snapshot of a Users Analytics is also visible in the Users Admin UI: [![](/img/posts/analytics/analytics-user-adminui.webp)](/img/posts/analytics/analytics-user-adminui.webp) Clicking on **View User Analytics** takes you to the Users Analytics page to access to the full Analytics features and navigation. ## API Keys The **API Keys** tab will display the top 100 API Keys who make the most API Requests and lets you click on an API Key bar chart to view its individual API Key analytics. :::{.wideshot} [![](/img/posts/analytics/analytics-apikeys.webp)](/img/posts/analytics/analytics-apikeys.webp) ::: ### Individual API Key Analytics Provides comprehensive API Key analytics Similar to User Analytics but limited to the API Usage of a single API Key: :::{.wideshot} [![](/img/posts/analytics/analytics-apikey.webp)](/img/posts/analytics/analytics-apikey.webp) ::: ## IPs The **IP Addresses** tab will display the top 100 IPs that make the most API Requests. Click on an IP's bar chart to view its individual analytics made from that IP Address. :::{.wideshot} [![](/img/posts/analytics/analytics-ips.webp)](/img/posts/analytics/analytics-ips.webp) ::: ### Individual IP Analytics Provides comprehensive IP Address analytics Similar to User Analytics but limited to the API Usage from a single IP Address: :::{.wideshot} [![](/img/posts/analytics/analytics-ip.webp)](/img/posts/analytics/analytics-ip.webp) ::: # RDBMS Async Tasks Builder Source: https://servicestack.net/posts/ormlite-async-task-builder ### Sequential Async DB Access Async improves I/O thread utilization in multi-threaded apps like Web Servers. However, it doesn't improve the performance of individual API Requests that need to execute multiple independent DB Requests. These are often written to run async db access sequentially like this: ```csharp var rockstars = await Db.SelectAsync(); var albums = await Db.SelectAsync(); var departments = await Db.SelectAsync(); var employees = await Db.SelectAsync(); ``` The issue being that it's not running them in parallel as each DB Request is executed sequentially with the Request for Albums not starting until the Request for Rockstars has completed. To run them in parallel you would need to open multiple scoped DB Connections, await them concurrently then do the syntax boilerplate gymnastics required to extract the generic typed results, e.g: ```csharp var connections = await Task.WhenAll( DbFactory.OpenDbConnectionAsync(), DbFactory.OpenDbConnectionAsync(), DbFactory.OpenDbConnectionAsync(), DbFactory.OpenDbConnectionAsync() ); using var dbRockstars = connections[0]; using var dbAlbums = connections[1]; using var dbDepartments = connections[2]; using var dbEmployees = connections[3]; var tasks = new List { dbRockstars.SelectAsync(), dbAlbums.SelectAsync(), dbDepartments.SelectAsync(), dbEmployees.SelectAsync() }; await Task.WhenAll(tasks); var rockstars = ((Task>)tasks[0]).Result; var albums = ((Task>)tasks[1]).Result; var departments = ((Task>)tasks[2]).Result; var employees = ((Task>)tasks[3]).Result; ``` Even without Error handling, writing coding like this can quickly become tedious, less readable and error prone that as a result is rarely done. ### Parallel DB Requests in TypeScript This is easier to achieve in languages like TypeScript where typed ORMs like [litdb.dev](https://litdb.dev) can run multiple DB Requests in parallel with just: ```csharp const [rockstars, albums, departments, employees] = await Promise.all([ db.all($.from(Rockstar)), //= Rockstar[] db.all($.from(Album)), //= Album[] db.all($.from(Department)), //= Department[] db.all($.from(Employee)), //= Employee[] ]) ``` Which benefits from TypeScript's powerful type system that allows destructuring arrays whilst preserving their positional types, whilst its single threaded event loop lets you reuse the same DB Connection to run DB Requests in parallel without multi-threading issues. ## OrmLite's new Async Tasks Builder OrmLite's new `AsyncDbTasksBuilder` provides a similar benefit of making it effortless to run multiple async DB Requests in parallel, which looks like: ```csharp var results = await DbFactory.AsyncDbTasksBuilder() .Add(db => db.SelectAsync()) .Add(db => db.SelectAsync()) .Add(db => db.SelectAsync()) .Add(db => db.SelectAsync()) .RunAsync(); var (albums, rockstars, employees, departments) = results; ``` Which just like TypeScript's destructuring returns a positionally typed tuple of the results which can be destructured back into their typed variables, e.g: ```csharp (List albums, List rockstars, List employees, List departments) = results; ``` ### Supports up to 8 Tasks It allows chaining up to **8 async Tasks in parallel** as C#'s Type System doesn't allow for preserving different positional generic types in an unbounded collection. Instead each Task returns a new Generic Type builder which preserves the positional types before it. ### Supports both Async `Task` and `Task` APIs Where `Task` and `Task` APIs can be mixed and matched interchangeably: ```csharp var builder = DbFactory.AsyncDbTasksBuilder() .Add(db => db.InsertAsync(rockstars[0],rockstars[1])) .Add(db => db.SelectAsync()) .Add(db => db.InsertAsync(albums[2],albums[3])) .Add(db => db.SelectAsync()) .Add(db => db.InsertAsync([department])) .Add(db => db.SelectAsync()) .Add(db => db.InsertAsync([employee])) .Add(db => db.SelectAsync()); ``` Where to preserve the results chain, `Task` APIs return `bool` results, e.g: ```csharp (bool r1, List r2, bool r3, List r4, bool r5, List r6, bool r7, List r8) = await builder.RunAsync(); ``` ### Error Handling Whilst tasks are executed in parallel when they're added, any Exceptions are only thrown when the task is awaited: ```csharp using var Db = await OpenDbConnectionAsync(); var builder = DbFactory.AsyncDbTasksBuilder() .Add(db => db.InsertAsync(rockstars[0])) .Add(db => db.InsertAsync(rockstars[0])); // <-- Duplicate PK Exception // Exceptions are not thrown until the task is awaited try { var task = builder.RunAsync(); } catch (Exception e) { throw; } ``` --- 👈 [OrmLite's new Configuration Model](/posts/ormlite-new-configuration) # OrmLite new Configuration Model and Defaults Source: https://servicestack.net/posts/ormlite-new-configuration ## New Configuration Model and Defaults In continuing with ServiceStack's [seamless integration with the ASP.NET Framework](https://docs.servicestack.net/releases/v8_01), providing a familiar development experience that follows .NET configuration model and Entity Framework conventions has become a priority. Implementing a new configuration model also gives us the freedom to change OrmLite's defaults which wasn't possible before given the paramount importance of maintaining backwards compatibility in a data access library that accesses existing Customer data. #### JSON used for Complex Types The biggest change that applies to all RDBMS providers is replacing the JSV serialization used for serializing Complex Types with JSON now that most RDBMS have native support for JSON. #### PostgreSQL uses default Naming Strategy The biggest change to PostgreSQL is using the same default naming strategy as other RDBMS which matches EF's convention that's used for ASP .NET's Identity Auth tables. #### SQL Server uses latest 2022 Dialect SQL Server now defaults to the latest SqlServer 2022 dialect which is also compatible with SQL Server 2016 and up. ## New Configuration Model OrmLite new modern, fluent configuration API aligns with ASP.NET Core's familiar `services.Add*()` pattern. This new approach provides a more intuitive and discoverable way to configure your database connections, with strongly-typed options for each RDBMS provider. The new configuration model starts with the `AddOrmLite()` extension method to configure its `IDbConnectionFactory` dependency by combining it with RDBMS provider-specific methods for the RDBMS you wish to use: - `UseSqlite()` in **ServiceStack.OrmLite.Sqlite.Data** - `UsePostgres()` in **ServiceStack.OrmLite.PostgreSQL** - `UseSqlServer()` in **ServiceStack.OrmLite.SqlServer.Data** - `UseMySql()` in **ServiceStack.OrmLite.MySql** - `UseMySqlConnector()` in **ServiceStack.OrmLite.MySqlConnector** - `UseOracle()` in **ServiceStack.OrmLite.Oracle** (community supported) - `UseFirebird()` in **ServiceStack.OrmLite.Firebird** (community supported) Each provider method accepts a connection string and an optional configuration callback that lets you customize the dialect's behavior with IntelliSense support. It's an alternative approach to manually instantiating `OrmLiteConnectionFactory` with specific dialect providers, offering better discoverability and a more consistent experience across different database providers. ### SQLite ```csharp services.AddOrmLite(options => options.UseSqlite(connectionString)); ``` Each RDBMS provider can be further customized to change its defaults with: ```csharp services.AddOrmLite(options => options.UseSqlite(connectionString, dialect => { // Default SQLite Configuration: dialect.UseJson = true; dialect.UseUtc = true; dialect.EnableWal = true; dialect.EnableForeignKeys = true; dialect.BusyTimeout = TimeSpan.FromSeconds(30); }) ); ``` ### PostgreSQL ```csharp services.AddOrmLite(options => options.UsePostgres(connectionString)); ``` With Dialect Configuration: ```csharp services.AddOrmLite(options => options.UsePostgres(connString, dialect => { // Default PostgreSQL Configuration: dialect.UseJson = true; dialect.NamingStrategy = new OrmLiteNamingStrategyBase(); }) ); ``` ### Removed snake_case naming strategy :::{.float-right .-mt-24! .-mr-32! .max-w-xs .pl-4} ![](/img/posts/ormlite-new-configuration/postgres-naming-strategy.webp) ::: PostgreSQL now defaults to using the same naming strategy as other RDBMS, i.e. no naming strategy, and uses the PascalCase naming of C# classes as-is. With this change OrmLite's table and columns now follow EF's convention which is used for ASP.NET's Identity Auth tables. This is more fragile in PostgreSQL as it forces needing to use quoted table and column names for all queries, e.g. ```sql SELECT "MyColumn" FROM "MyTable" ``` This is required as PostgreSQL isn't case-insensitive and lowercases all unquoted symbols, e.g: ```sql SELECT MyColumn FROM MyTable -- Translates to: SELECT mycolumn FROM mytable ``` This is already done by OrmLite, but any custom queries also need to use quoted symbols. ### SQL Server ```csharp services.AddOrmLite(options => options.UseSqlServer(connectionString)); ``` With Dialect Configuration: ```csharp services.AddOrmLite(options => options.UseSqlServer(connString, dialect => { // Default SQL Server Configuration: dialect.UseJson = true; }) ); ``` ### Uses Latest SQL Server at each .NET LTS Release To keep it modern and predictable, this will use the latest SQL Server Dialect that was released at the time of each major .NET LTS versions, currently `SqlServer2022OrmLiteDialectProvider`, which we'll keep until the next .NET LTS release. Although the **2022** dialect is also compatible with every SQL Server version from **2016+**. To use an explicit version of SQL Server you can use the generic overload that best matches your SQL Server version: ```csharp services.AddOrmLite(options => options.UseSqlServer(connString)); ``` ### MySQL ```csharp services.AddOrmLite(options => options.UseMySql(connectionString)); ``` With Dialect Configuration: ```csharp services.AddOrmLite(options => options.UseMySql(connectionString, dialect => { // Default MySql Configuration: dialect.UseJson = true; }) ); ``` For MySqlConnector use: ```csharp services.AddOrmLite(options => options.AddMySqlConnector(connectionString)); ``` ### Named Connections The new OrmLite configuration model also streamlines support for named connections, allowing you to register multiple database connections with unique identifiers in a single fluent configuration chain, e.g: ```csharp services.AddOrmLite(options => { options.UseSqlite(":memory:") .ConfigureJson(json => { json.DefaultSerializer = JsonSerializerType.ServiceStackJson; }); }) .AddSqlite("db1", "db1.sqlite") .AddSqlite("db2", "db2.sqlite") .AddPostgres("reporting", PostgreSqlDb.Connection) .AddSqlServer("analytics", SqlServerDb.Connection) .AddSqlServer( "legacy-analytics", SqlServerDb.Connection) .AddMySql("wordpress", MySqlDb.Connection) .AddMySqlConnector("drupal", MySqlDb.Connection) .AddOracle("enterprise", OracleDb.Connection) .AddFirebird("firebird", FirebirdDb.Connection); ``` ### Complex Type JSON Serialization Previously OrmLite only supported serializing Complex Types with a [single Complex Type Serializer](https://docs.servicestack.net/ormlite/complex-type-serializers) but the new configuration model now uses a more configurable `JsonComplexTypeSerializer` where you can change the default JSON Serializer OrmLite should use for serializing Complex Types as well as fine-grain control over which types should be serialized with which serializer by using the `.ConfigureJson()` extension method for each provider. ```csharp services.AddOrmLite(options => { options.UsePostgres(connectionString) .ConfigureJson(json => { // Default JSON Complex Type Serializer Configuration json.DefaultSerializer = JsonSerializerType.ServiceStackJson; json.JsonObjectTypes = [ typeof(object), typeof(List), typeof(Dictionary), ]; json.SystemJsonTypes = []; json.ServiceStackJsonTypes = []; }); }) ``` By default OrmLite uses ServiceStack.Text JSON Serializer which is less strict and more resilient than System.Text.Json for handling versioning of Types that change over time, e.g. an `int` Property that's later changed to a `string`. In addition to configuring a default you can also configure which types should be serialized with which serializer. So we could change OrmLite to use System.Text.Json for all types except for `ChatCompletion` which we want to use ServiceStack.Text JSON for: ```csharp services.AddOrmLite(options => { options.UsePostgres(connectionString) .ConfigureJson(json => { json.DefaultSerializer = JsonSerializerType.SystemJson; json.ServiceStackJsonTypes = [ typeof(ChatCompletion) ]; }); }) ``` #### Unstructured JSON with JSON Object The default Exception to this is for serialization of `object`, `List` and `Dictionary` types which is better handled by [#Script's JSON Parser](https://docs.servicestack.net/js-utils) which is able to parse any valid adhoc JSON into untyped .NET generic collections, which is both mutable and able to [utilize C# pattern matching](https://docs.servicestack.net/js-utils#getting-the-client_id-in-a-comfyui-output) for easy introspection. The new `TryGetValue` extension method on `Dictionary` makes it even more convenient for parsing adhoc JSON which can use the `out` Type parameter to reduce unnecessary type checking, e.g. here's a simple example of parsing a ComfyUI Output for the client_id used in a generation: ```csharp var comfyOutput = JSON.ParseObject(json); var prompt = (Dictionary)result.Values.First()!; if (prompt.TryGetValue("prompt", out List tuple) && tuple.Count > 3) { if (tuple[3] is Dictionary extraData && extraData.TryGetValue("client_id", out string clientId)) { Console.WriteLine(clientId); } } ``` Where as an Equivalent implementation using System.Text.Json `JsonDocument` would look like: ```csharp using System.Text.Json; var jsonDocument = JsonDocument.Parse(json); var root = jsonDocument.RootElement; // Get the first property value (equivalent to result.Values.First()) var firstProperty = root.EnumerateObject().FirstOrDefault(); if (firstProperty.Value.ValueKind == JsonValueKind.Object) { var prompt = firstProperty.Value; if (prompt.TryGetProperty("prompt", out var promptElement) && promptElement.ValueKind == JsonValueKind.Array) { var promptArray = promptElement.EnumerateArray().ToArray(); if (promptArray.Length > 3) { var extraDataElement = promptArray[3]; if (extraDataElement.ValueKind == JsonValueKind.Object && extraDataElement.TryGetProperty("client_id", out var clientIdElement) && clientIdElement.ValueKind == JsonValueKind.String) { var clientId = clientIdElement.GetString(); Console.WriteLine(clientId); } } } } ``` ### Table Aliases One potential breaking change is that table aliases are used verbatim and no longer uses a naming strategy for transforming its name which potentially affects PostgreSQL when an Alias is used that doesn't match the name of the table, e.g: ```csharp [Alias("MyTable")] //= "MyTable" public class NewMyTable { ... } [Alias("MyTable")] //= my_table public class OldMyTable { ... } ``` Aliases should either be changed to the Table name you want to use or you can use the Naming Strategy Alias dictionaries for finer-grain control over what Schema, Table, Column Names and Aliases should be used: ```csharp services.AddOrmLite(options => options.UsePostgres(connString, dialect => { dialect.NamingStrategy.TableAliases["MyTable"] = "my_table"; dialect.NamingStrategy.SchemaAliases["MySchema"] = "my_schema"; dialect.NamingStrategy.ColumnAliases["MyColumn"] = "my_columnt"; })); ``` ### Table Refs A significant internal refactor of OrmLite was done to encapsulate different ways of referring to a table in a single `TableRef` struct, which is now used in all APIs that need a table reference. The new `TableRef` struct allows for unified APIs that encapsulates different ways of referencing a table: - Type `new TableRef(typeof(MyTable))` - Model Definition `new TableRef(ModelDefinition.Definition)` - Table Name `new TableRef("MySchema")` - Schema and Table Name `new TableRef("MySchema", "MyTable"))` - Quoted Name (use verbatim) `TableRef.Literal("\"MyTable\"")` - Implicitly casts to a string as `new TableRef("MySchema")`. OrmLite handles differences between different RDBMS Providers via its `IOrmLiteDialectProvider` interface. Previously OrmLite used to maintain multiple overloads for handling some of these differences in referencing a table but they've now all been consolidated into use a single `TableRef` parameter: ```csharp public interface IOrmLiteDialectProvider { bool DoesTableExist(IDbConnection db, TableRef tableRef); string GetTableNameOnly(TableRef tableRef); string UnquotedTable(TableRef tableRef); string GetSchemaName(TableRef tableRef); string QuoteTable(TableRef tableRef); bool DoesTableExist(IDbConnection db, TableRef tableRef); bool DoesColumnExist(IDbConnection db, string columnName, TableRef tableRef); string ToAddColumnStatement(TableRef tableRef, FieldDefinition fieldDef); string ToAlterColumnStatement(TableRef tableRef, FieldDefinition fieldDef); string ToChangeColumnNameStatement(TableRef tableRef, FieldDefinition fieldDef, string oldColumn); string ToRenameColumnStatement(TableRef tableRef, string oldColumn, string newColumn); string ToDropColumnStatement(TableRef tableRef, string column); string ToDropConstraintStatement(TableRef tableRef, string constraint); string ToDropForeignKeyStatement(TableRef tableRef, string foreignKeyName); } ``` For example the `QuoteTable(TableRef)` method can be used to quote a table. Assuming our dialect was configured with the `my_table` Table Aliases, these are the results for the different ways of referencing `MyTable`: ```csharp dialect.QuoteTable("MyTable") //= "my_table" (implicit) dialect.QuoteTable(new("MyTable")) //= "my_table" dialect.QuoteTable(new("MySchema","MyTable")) //= "my_schema"."my_table" dialect.QuoteTable(TableRef.Literal("\"MyTable\"")) //= "MyTable" (verbatim) dialect.QuoteTable(new(typeof(MyTable))) //= "my_table" dialect.QuoteTable(new(ModelDefinition.Definition)) //= "my_table" ``` ### Improved Observability Significant effort was put into improving OrmLite's Observability where OrmLite's DB Connections can now be tagged to make them easier to track in hooks, logs and traces. To achieve this a new `Action` configuration callbacks were added to OrmLite Open Connection APIs which is invoked before a DB Connection is opened, e.g: ```csharp using var db = dbFactory.Open(configure: db => db.WithTag("MyTag")); using var db = dbFactory.Open(namedConnection, configure: db => db.WithTag("MyTag")); using var db = HostContext.AppHost.GetDbConnection(req, configure: db => db.WithTag("MyTag")); ``` Which ServiceStack uses internally to tag DB Connections with the feature executing it, or for `Db` connections used in Services it will tag it with the Request DTO Name. :::{.wideshot} ![](/img/posts/ormlite-new-configuration/ormlite-tags.webp) ::: If a tag is configured, it's also included in OrmLite's Debug Logging output, e.g: ```txt dbug: ServiceStack.OrmLiteLog[0] [PostgresDbJobsProvider] SQL: SELECT "Id", "ParentId", "RefId", "Worker", "Tag", "BatchId", "Callback", "DependsOn", "RunAfter", "CreatedDate", "CreatedBy", "RequestId", "RequestType", "Command", "Request", "RequestBody", "UserId", "Response", "ResponseBody", "State", "StartedDate", "CompletedDate", "NotifiedDate", "RetryLimit", "Attempts", "DurationMs", "TimeoutSecs", "Progress", "Status", "Logs", "LastActivityDate", "ReplyTo", "ErrorCode", "Error", "Args", "Meta" FROM "BackgroundJob" WHERE ("State" = :0) PARAMS: :0=Cancelled dbug: ServiceStack.OrmLiteLog[0] TIME: 1.818m ``` #### DB Command Execution Timing OrmLite's debug logging now also includes the elapsed time it took to execute the command which is also available on the `IDbCommand` `GetTag()` and `GetElapsedTime()` APIs, e.g: ```csharp OrmLiteConfig.AfterExecFilter = cmd => { Console.WriteLine($"[{cmd.GetTag()}] {cmd.GetElapsedTime()}"); }; ``` ### ExistsById APIs New `ExistsById` APIs for checking if a row exists for a given Id: ```csharp db.ExistsById(1); await db.ExistsByIdAsync(1); // Alternative to: db.Exists(x => x.Id == 1); await db.ExistsAsync(x => x.Id == 1); ``` ### ResetSequence for PostgreSQL The `ResetSequence` API is available to reset a Table's Id sequence in Postgres: ```csharp db.ResetSequence(x => x.Id); ``` #### Data Import example using BulkInsert This is useful to reset a PostgreSQL Table's auto-incrementing sequence when re-importing a dataset from a different database, e.g: ```csharp db.DeleteAll(); db.ResetSequence(x => x.Id); db.DeleteAll(); db.ResetSequence(x => x.Id); var config = new BulkInsertConfig { Mode = BulkInsertMode.Sql }; db.BulkInsert(dbSqlite.Select().OrderBy(x => x.Id), config); db.BulkInsert(dbSqlite.Select().OrderBy(x => x.Id), config); ``` ### New SqlDateFormat and SqlChar Dialect APIs The SQL Dialect functions provide an RDBMS agnostic way to call SQL functions that differs among different RDBMS's. The `DateFormat` accepts [SQLite strftime() function](https://www.w3resource.com/sqlite/sqlite-strftime.php) date and time modifiers in its format string whilst the `Char` accepts a character code, e.g: ```csharp var q = db.From(); var createdDate = q.Column(c => c.CreatedDate); var months = db.SqlList<(string month, string log)>(q .Select(x => new { Month = q.sql.DateFormat(createdDate, "%Y-%m"), Log = q.sql.Concat(new[]{ "'Prefix'", q.sql.Char(10), createdDate }) })); ``` When executed in PostgreSQL it would generate: ```sql SELECT TO_CHAR("CreatedDate", 'YYYY-MM'), 'Prefix' || CHR(10) || "CreatedDate" FROM "CompletedJob" ``` --- :::{.float-right} [OrmLite's Async Task Builder](/posts/ormlite-async-task-builder) 👉 ::: # llms.py UI Source: https://servicestack.net/posts/llms-py-ui [llms.py](https://github.com/ServiceStack/llms) is a lightweight OSS CLI, API and ChatGPT-like alternative to Open WebUI for accessing multiple LLMs that still only requires 1 (aiohttp) dependency, entirely offline, with all data kept private in browser storage. ## v2.0.24 ### Metrics and Analytics We're happy to announce the next major release of **llms.py v2.0.24** now includes API pricing for all premium LLMs, observability with detailed usage and metric insights, so you're better able to analyze and track your spend within the UI. #### Metrics everywhere - **Cost per Model** - Model selector now displays the Input and Output cost per 1M token for every premium model. - **Thread-Level Cost & Token Metrics** - Total cost and token count for every conversation thread, displayed in your history sidebar. - **Per-Message Token Breakdown** - Each individual message, both from you and the AI, now clearly shows its token count. - **Thread Summaries** - At the bottom of every new conversation, you'll find a consolidated summary detailing the total cost, tokens consumed (input and output), number of requests, and overall response time for that entire thread. :::{.wideshot} [![llms-tokens-usage.webp](/img/posts/ai-chat/llms-tokens-usage.webp)](/img/posts/ai-chat/llms-tokens-usage.webp) ::: Screenshot also shows support for new **Edit** and **Redo** features that appears after hovering over any **User** message, to modify or re-run an existing prompt. ### Dark Mode Support for Dark Mode which inherits the browser's dark mode preference or can be toggled with the **Dark Mode toggle** in the top-right of the UI. :::{.wideshot} [![dark-attach-image.webp](/img/posts/ai-chat/dark-attach-image.webp)](/img/posts/ai-chat/dark-attach-image.webp) ::: ### Monthly Cost Analytics The **Cost Analytics** page provides a comprehensive overview of the usage costs for every day within a given month. Clicking on a day will expand it to show a detailed breakdown of the costs that was spent per model and provider. :::{.wideshot} [![analytics-costs.webp](/img/posts/ai-chat/analytics-costs.webp)](/img/posts/ai-chat/analytics-costs.webp) ::: ### Monthly Tokens Analytics Similarly, the **Tokens Analytics** page provides a comprehensive overview of the token usage for every day within a given month. Clicking on a day expands it to show a detailed breakdown of the tokens that was spent per model and provider. :::{.wideshot} [![dark-analytics-tokens.webp](/img/posts/ai-chat/dark-analytics-tokens.webp)](/img/posts/ai-chat/dark-analytics-tokens.webp) ::: ### Monthly Activity Log The **Activity Log** page lets you view the individual requests that make up the daily cost and token usage, the page itself provides a comprehensive and granular overview of all your AI interactions and requests including: Model, Provider, Partial Prompt, Input & Output Tokens, Cost, Response Time, Speed, Date & Time. :::{.wideshot} [![analytics-activity.webp](/img/posts/ai-chat/analytics-activity.webp)](/img/posts/ai-chat/analytics-activity.webp) ::: Activity Logs are maintained independently of the Chat History so you can clear or cleanup your Chat History without losing the detailed Activity Logs of your AI requests. Likewise you can delete Activity Logs without losing your Chat History. ### Check Providers Another feature added in this release is the ability to check the status of configured providers to test if they're configured correctly, reachable and what their response times is for the simplest `1+1=` request: Check all models for a provider: :::sh llms --check groq ::: Check specific models for a provider: :::sh llms --check groq kimi-k2 llama4:400b gpt-oss:120b ::: :::{.wideshot} [![llms-check.webp](/img/posts/ai-chat/llms-check.webp)](/img/posts/ai-chat/llms-check.webp) ::: As they're a good indicator for the reliability and speed you can expect from different providers we've created a [test-providers.yml](https://github.com/ServiceStack/llms/actions/workflows/test-providers.yml) GitHub Action to test the response times for all configured providers and models, the results of which will be frequently published to [/checks/latest.txt](https://github.com/ServiceStack/llms/blob/main/docs/checks/latest.txt) ## ChatGPT, but Local 🎯 In keeping with the simplicity goals of [llms.py](https://github.com/ServiceStack/llms), its [/ui](https://github.com/ServiceStack/llms/tree/main/llms/ui) is small and fast by following the [Simple Modern JavaScript](https://servicestack.net/posts/javascript) approach of leveraging JS Modules support in Browsers to avoid needing any npm dependencies or build tools. ### Offline, Fast and Private OSS & Free, no sign ups, no ads, no tracking, etc. All data is stored locally in the browser's IndexedDB that can be exported and imported to transfer chat histories between different browsers. A goal for **llms.py** is to only require a single python **aiohttp** dependency for minimal risk of conflicts within multiple Python Environments, e.g. it's an easy drop-in inside a ComfyUI Custom Node as it doesn't require any additional deps. ## Install :::sh pip install llms-py ::: Start the UI and an OpenAI compatible API on port **8000**: :::sh llms --serve 8000 ::: To launch the UI at `http://localhost:8000` and an OpenAI Endpoint at `http://localhost:8000/v1/chat/completions`. ## Simple and Flexible UI This starts the Chat UI from where you can interact with any of your configured OpenAI-compatible Chat Providers - for a single unified interface for accessing both local and premium cloud LLMs from the same UI. ## Configuration You can configure which OpenAI compatible providers and models you want to use by adding them to your [llms.json](https://github.com/ServiceStack/llms/blob/main/llms/llms.json) in `~/.llms/llms.json` Whilst the [ui.json](https://github.com/ServiceStack/llms/blob/main/llms/ui.json) configuration for the UI is maintained in `~/.llms/ui.json` where you can configure your preferred system prompts and other defaults. ### Import / Export All data is stored locally in the browser's IndexedDB which as it is tied to the browser's origin, you'll be able to maintain multiple independent conversational databases by just running the server on a different port. When needed you can backup and transfer your entire chat history between different browsers using the **Export** and **Import** features on the home page. :::{.wideshot} [![llms-home.webp](/img/posts/ai-chat/llms-home.webp)](/img/posts/ai-chat/llms-home.webp) ::: **Export Request Logs** You can also export your request log history (used by Analytics) by holding down `ALT` while clicking the **Export** button, which can also be imported with **Import**. ## Rich Markdown & Syntax Highlighting To maximize readability there's full support for Markdown and Syntax highlighting for the most popular programming languages. :::{.wideshot} [![llms-syntax.webp](/img/posts/ai-chat/llms-syntax.webp)](/img/posts/ai-chat/llms-syntax.webp) ::: To quickly and easily make use of AI Responses, **Copy Code** icons are readily available on hover of all messages and code blocks. ## Rich, Multimodal Inputs The Chat UI goes beyond just text and can take advantage of the multimodal capabilities of modern LLMs with support for Image, Audio and File inputs. ### 🖼️ 1. Image Inputs & Analysis Images can be uploaded directly into your conversations with vision-capable models for comprehensive image analysis. Visual AI Responses are highly dependent on the model used. This is a typical example of the visual analysis provided by the latest Gemini Flash of our [ServiceStack Logo](/img/logo.png): :::{.wideshot} [![attach-image.webp](/img/posts/ai-chat/attach-image.webp)](/img/posts/ai-chat/attach-image.webp) ::: ### 🎤 2. Audio Input & Transcription Likewise you can upload Audio files and have them transcribed and analyzed by multi-modal models with audio capabilities. :::{.wideshot} [![attach-audio.webp](/img/posts/ai-chat/attach-audio.webp)](/img/posts/ai-chat/attach-audio.webp) ::: Example of processing audio input. Audio files can be uploaded with system and user prompts to instruct the model to transcribe and summarize its content where its multi-modal capabilities are integrated right within the chat interface. ### 📎 3. File and PDF Attachments In addition to images and audio, you can also upload documents, PDFs and other files to capable models to extract insights, summarize content or analyze data. **Document Processing Use Cases:** - **PDF Analysis**: Upload PDF documents for content extraction and analysis - **Data Extraction**: Extract specific information from structured documents - **Document Summarization**: Get concise summaries of lengthy documents - **Query Content**: Ask questions about specific content in documents - **Batch Processing**: Upload multiple files for comparative analysis Perfect for research, document review, data analysis and content extractions. :::{.wideshot} [![attach-pdf.webp](/img/posts/ai-chat/attach-pdf.webp)](/img/posts/ai-chat/attach-pdf.webp) ::: ## Custom AI Chat Requests Send Custom Chat Completion requests through the settings dialog, allowing Users to fine-tune their AI requests with advanced options including: - **Temperature** `(0-2)` for controlling response randomness - **Max Completion Tokens** to limit response length - **Seed** values for deterministic sampling - **Top P** `(0-1)` for nucleus sampling - **Frequency** & **Presence Penalty** `(-2.0 to 2.0)` for reducing repetition - **Stop** Sequences to control where the API stops generating - **Reasoning Effort** constraints for reasoning models - **Top Logprobs** `(0-20)` for token probability analysis - **Verbosity** settings :::{.wideshot} [![llms-settings.webp](/img/posts/ai-chat/llms-settings.webp)](/img/posts/ai-chat/llms-settings.webp) ::: ## Enable / Disable Providers Dynamically manage which providers you want to enable and disable at runtime. Providers are invoked in the order they're defined in `llms.json` that supports the requested model. If a provider fails, it tries the next available provider. By default Providers with Free tiers are enabled first, followed by local providers and then premium cloud providers which can all be enabled or disabled in the UI: :::{.wideshot} [![llms-providers.webp](/img/posts/ai-chat/llms-providers.webp)](/img/posts/ai-chat/llms-providers.webp) ::: ## Search History Quickly find past conversations with built-in search: :::{.wideshot} [![llms-search-python.webp](/img/posts/ai-chat/llms-search-python.webp)](/img/posts/ai-chat/llms-search-python.webp) ::: ## Smart Autocomplete for Models & System Prompts Autocomplete components are used to quickly find and select the preferred model and system prompt. Only models from enabled providers will appear in the drop down, which will be available immediately after providers are enabled. :::{.wideshot} [![llms-autocomplete.webp](/img/posts/ai-chat/llms-autocomplete.webp)](/img/posts/ai-chat/llms-autocomplete.webp) ::: ## Comprehensive System Prompt Library Access a curated collection of 200+ professional system prompts designed for various use cases, from technical assistance to creative writing. :::{.wideshot} [![llms-system-prompt.webp](/img/posts/ai-chat/llms-system-prompt.webp)](/img/posts/ai-chat/llms-system-prompt.webp) ::: System Prompts be can added, removed & sorted in `~/.llms/ui.json` ```json { "prompts": [ { "id": "it-expert", "name": "Act as an IT Expert", "value": "I want you to act as an IT expert. You will be responsible..." }, ... ] } ``` ### Reasoning Access the thinking process of advanced AI models with specialized rendering for reasoning and chain-of-thought responses: :::{.wideshot} [![llms-reasoning.webp](/img/posts/ai-chat/llms-reasoning.webp)](/img/posts/ai-chat/llms-reasoning.webp) ::: ## Get Started Today The new llms.py UI makes powerful AI accessible and private. Whether you're a developer, researcher or AI enthusiast, this UI provides helps you harness the potential of both local and cloud-based language models. ### Why llms.py UI? - **🔒 Privacy First**: All data stays local - no tracking, ads or external deps - **⚡ Lightning Fast**: Fast, async **aiohttp** client and server - **🌐 Universal Compatibility**: Works with any OpenAI-compatible API - **💰 Cost Effective**: Mix free local models with premium APIs as needed - **🎯 Feature Rich**: Multimodal support, search, autocomplete, and more - **🛠️ Developer Friendly**: Simple config, easily modifiable implementation Try llms.py UI: ```bash pip install llms-py llms --serve 8000 ``` Open your browser to `http://localhost:8000` to start chatting with your AI models. # llms.py - Lightweight OpenAI compatible CLI and server gateway for multiple LLMs Source: https://servicestack.net/posts/llms-py # Introducing llms.py 🚀 We're excited to announce the release of **[llms.py](https://github.com/ServiceStack/llms)** - a super lightweight CLI tool and OpenAI-compatible server that acts as a **configurable gateway** over multiple configurable Large Language Model (LLM) providers. As part of our work in developing a new OSS AI Generation platform we were in need of a lightweight LLM gateway for usage within [ComfyUI](https://www.comfy.org). Unfortunately the popular option for Python **litellm** is anything but - requiring [60 deps!](https://github.com/BerriAI/litellm/blob/main/requirements.txt) where its VC funded development sees its scope creep into Enterprise features. This is a deal breaker within an open Python plugin ecosystem like ComfyUI where every dependency has a chance to break a Python environment. It's also unnecessary for a simple CLI tool and server gateway where the hard work is already done by the LLM providers in their OpenAI-compatible APIs - a simplicity capitalized on by `llms.py`. ## 🎯 OpenRouter but Local **llms.py** is designed as a **unified gateway** that seamlessly connects you to multiple LLM providers through a single, consistent interface. Whether using cloud APIs or local models, `llms` provides intelligent routing and automatic failover to ensure your AI workflows connect to your chosen providers in your preferred priority - whether optimizing for cost, performance or availability. ### ⚡ Ultra-Lightweight Architecture - **Single File**: Just one [llms.py](https://github.com/ServiceStack/llms/blob/main/llms/main.py) file (easily customizable) - **Single Dependency**: Single `aiohttp` dependency - **Zero Dependencies for ComfyUI**: Ideal for use in ComfyUI Custom Nodes - **No Setup**: Just download and use, configure preferred LLMs in [llms.json](https://github.com/ServiceStack/llms/blob/main/llms/llms.json) ## 📦 Install :::sh pip install llms-py ::: ## 🔧 Quick Start ```bash # Initialize configuration (saved in ~/.llms/llms.json) llms --init # Enable providers llms --enable openrouter_free google_free groq openai anthropic grok # List available providers and models llms ls llms ls openrouter_free groq # Start chatting llms "Explain quantum computing in simple terms" # With preferred Model llms -m grok-4-fast "jq command to sort openai models by created" # With system prompt llms -s "You are a quantum computing expert" "Explain quantum computing" # Use with images llms --image photo.jpg "What's in this image?" # Use with audio llms --audio talk.mp3 "Transcribe this audio file" # Use with attached file llms --file report.pdf "Summarize this PDF" # With custom request template llms --chat request.json "Explain quantum computing in simple terms" llms --chat image-request.json --image photo.jpg "What's in this image?" llms --chat audio-request.json --audio talk.mp3 "Transcribe this audio file" llms --chat file-request.json --file report.pdf "Summarize this PDF" # Set default model llms --default grok-4-fast # Run an Open AI Chat compatible server llms --serve 8000 ``` Example client usage: ```bash curl -X POST http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "kimi-k2", "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ### 🌐 Configurable Multi-Provider Gateway Acts as an intelligent gateway that can route requests for 160+ models across: #### Cloud Providers with Free tiers - OpenRouter - Groq - Codestral - Google #### Premium Cloud Providers - OpenAI - Anthropic - Google - Grok - Qwen - Mistral #### Local Providers - Ollama - Restrict access to custom models - Or auto-discovery of installed models #### Custom Providers Use JSON config to add any OpenAI-compatible API endpoints and models ### 🔄 Intelligent Request Routing - **Automatic Failover**: If one provider fails, automatically retry with the next available provider - **Cost Optimization**: Define free/cheap/local providers first to minimize costs - **Model Mapping**: Use unified model names that map to different provider-specific names. ## 🚀 Key Features ### Multi-Modal Support - **Text Generation**: Chat completions with any supported model - **Vision Models**: Process images through vision-capable models (GPT-4V, Gemini Vision, etc.) - **Audio Processing**: Handle audio inputs through audio-capable models ### Flexible Deployment Options - **CLI Tool**: Interactive command-line interface for quick queries - **HTTP Server**: OpenAI-compatible server at `http://localhost:{PORT}/v1/chat/completions` - **Python Module**: Import and use programmatically in your applications - **ComfyUI Node**: Embed directly in ComfyUI workflows ### Simple and Customizable - **Environment Variables**: Secure API key management - **Provider Management**: Easy enable/disable of providers - **Custom Models**: Define your own model aliases and mappings - **Unified Configuration**: Single [llms.json](https://github.com/ServiceStack/llms/blob/main/llms/llms.json) to configure all providers and models ## 🎯 Use Cases ### For Developers - **API Gateway**: Centralize all LLM provider access through one endpoint - **Cost Management**: Automatically route to cheapest available providers - **Reliability**: Built-in failover ensures high availability - **Testing**: Easily switch between models and providers for comparison ### For ComfyUI Users - **Hybrid Workflows**: Combine local Ollama models with cloud APIs - **Zero Setup**: No dependency management headaches - **Provider Flexibility**: Switch providers without changing your workflow - **Cost Control**: Use free tiers and local models when possible ### For Enterprises - **Vendor Independence**: Avoid lock-in to any single LLM provider - **Scalability**: Distribute load across multiple providers - **Compliance**: Keep sensitive data local while using cloud for general tasks - **Budget Control**: Intelligent routing to optimize costs ## 🌟 Why llms.py? 1. **Simplicity**: One file, one dependency, infinite possibilities 2. **Flexibility**: Works with any OpenAI-compatible client or framework 3. **Reliability**: Automatic failover ensures your workflows never break 4. **Economy**: Intelligent routing minimizes API costs 5. **Privacy**: Mix local and cloud models based on your data sensitivity 6. **Future-Proof**: Easily add new providers as they emerge **llms.py** transforms the complexity of managing multiple LLM providers into a simple, unified experience. Whether you're researching capabilities of new models, building the next breakthrough AI application or just want reliable access to the best models available, llms.py has you covered. Get started today and avoid expensive cloud lock-ins with the freedom of provider-agnostic AI development! 🎉 --- **Links:** - 📚 [Documentation & Examples](https://github.com/ServiceStack/llms) - 📦 [PyPI Package](https://pypi.org/project/llms-py/) - 🔧 [Source Code](https://github.com/ServiceStack/llms) # Text to Blazor CRUD upgraded to use best LLMs Source: https://servicestack.net/posts/text-to-blazor-upgraded Text to Blazor has upgraded to use the best coding LLMs from Anthropic, Google, Moonshot AI, Alibaba and xAI to power its app generation capabilities to create new Blazor Admin CRUD Apps from just a text description.
Just enter in the type of App you want to create or the Data Models it needs and it will generate a new Blazor CRUD App for you.
[![](/img/posts/text-to-blazor/text-to-blazor-prompt.webp)](/text-to-blazor) This will query 5 different high quality AI models to generate 5 different Data Models, APIs, DB Migrations and Admin UIs which you can browse to find the one that best matches your requirements. [![](/img/posts/text-to-blazor-upgraded/text-to-blazor-gen.webp)](/text-to-blazor) ### Using AI to only generate Data Models Whilst the result is a working CRUD App, the approach taken is very different from most AI tools which uses AI to generate the entire App that ends up with a whole new code-base developers didn't write which they'd now need to maintain. Instead AI is only used to generate the initial Data Models within a **TypeScript Declaration file** which we've found is the best format supported by AI models that's also the best typed DSL for defining data models with minimal syntax that's easy for humans to read and write. ### Download preferred Blazor Vue CRUD App Once you've decided on the Data Models that best matches your requirements, you can download your preferred generated Blazor Vue CRUD App: [![](/img/posts/text-to-blazor/text-to-blazor-download.webp)](/text-to-blazor) ### Blazor Admin App **Admin Only** - ideal for internal Admin Apps where the Admin UI is the Primary UI ![](/img/posts/text-to-blazor/okai-blazor-admin.webp) ### Blazor Vue App **UI + Admin** - Creates a new [blazor-vue](https://blazor-vue.web-templates.io) template ideal for Internet or public facing Apps, sporting a full-featured public facing UI for a Web App's users whilst enabling a back-office CRUD UI for Admin Users to manage their data. ![](/img/posts/text-to-blazor/okai-blazor-vue.webp) Clicking on the **Admin UI** button will take you to the Admin UI at `/admin`: ![](/img/posts/text-to-blazor/okai-blazor-vue-admin.webp) ## Run Migrations In order to create the necessary tables for the new functionality, you'll need to run the DB Migrations. If migrations have never been run before, you can run the `migrate` npm script to create the initial database: :::sh npm run migrate ::: If you've already run the migrations before, you can run the `rerun:last` npm script to drop and re-run the last migration: :::sh npm run rerun:last ::: Alternatively you can nuke the App's database (e.g. `App_Data/app.db`) and recreate it from scratch with `npm run migrate`. ## Instant CRUD UI After running the DB migrations, you can hit the ground running and start using the Admin UI to manage the new Data Model RDBMS Tables: :::youtube 8buo_ce3SNM Using AutoQuery CRUD UI in a Text to Blazor App ::: ### Create new Records from Search Dialog We're continually improving the UX of the [AutoQueryGrid Component](/vue/autoquerygrid) used in generating CRUD UIs to enable a more productive and seamless workflow. A change added to that end that you can see in the above video is the ability to add new Records from a Search dialog: ![](/img/posts/text-to-blazor/autoquerygrid-new2.webp) This now lets you start immediately creating new records without needing to create any lookup entries beforehand. ## Audited Data Models The TypeScript Data Models enable a rapid development experience for defining an App's Data Models which are used to generate the necessary AutoQuery CRUD APIs to support an Admin UI. An example of the productivity of this approach is the effortless support for maintaining a detailed audit history for changes to select tables by inheriting from the `AuditBase` base class, e.g: ```ts export class Job extends AuditBase { ... } ``` Which can then be regenerated using the name of the TypeScript Model definitions: :::sh npx okai Jobs.d.ts ::: This will include additional `CreatedBy`, `CreatedDate`, `ModifiedBy`, `ModifiedDate`, `DeletedBy` and `DeletedDate` properties to the specified Table and also generates the necessary [Audit Behaviors](https://docs.servicestack.net/autoquery/crud#apply-generic-crud-behaviors) on the AutoQuery APIs to maintain the audit history for each CRUD operation. ### AutoQuery CRUD Audit Log As the **blazor-admin** and **blazor-vue** templates are configured to use the [AutoQuery CRUD Executable Audit Log](https://docs.servicestack.net/autoquery/audit-log) in its [Configure.AutoQuery.cs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp/Configure.AutoQuery.cs) the Audit Behaviors will also maintain an Audit Trail of all CRUD operations which can be viewed in the Admin UI: ![](/img/posts/text-to-blazor/okai-audit-form.webp) ## FREE Access to worlds most popular AI Models In addition to upgrading the models in text-to-blazor we've also made available them available to our generic **chat** prompt that can be used as a convenient way to conduct personal research against many of the worlds most popular Large Language Models - for Free! ![](/img/posts/okai-chat/okai-chat.webp) No API Keys, no Signups, no installs, no cost, you can just start immediately using the `npx okai chat` script to ask LLMs for assistance: :::sh npx okai chat "command to copy a folder with rsync?" ::: This will use the default model (currently gemini-flash 2.5) to answer your question. ### Select Preferred Model You can also use your preferred model with the `-m ` flag with either the model **name** or its **alias**, e.g you can use [Microsoft's PHI-4 14B](https://techcommunity.microsoft.com/blog/aiplatformblog/introducing-phi-4-microsoft%E2%80%99s-newest-small-language-model-specializing-in-comple/4357090) model with: :::sh npx okai -m phi chat "command to copy folder with rsync?" ::: ### List Available Models We're actively adding more great performing and leading experimental models as they're released. You can view the list of available models with `ls models`: :::sh npx okai ls models ::: Which at this time will return the following list of available models along with instructions for how to use them: ```txt USAGE (5 models max): a) OKAI_MODELS=codestral,llama3.3,flash b) okai -models codestral,llama3.3,flash c) okai -m flash chat FREE MODELS: claude-3-haiku (alias haiku) codestral:22b (alias codestral) deepseek-chat-v3.1:671b deepseek-r1:671b (alias deepseek-r1) deepseek-v3.1:671b (alias deepseek) gemini-2.5-flash (alias flash) gemini-2.5-flash-lite (alias flash-lite) gemini-flash-thinking-2.5 gemma2:9b gemma3:27b (alias gemma) glm-4.5 glm-4.5-air glm-4.5v gpt-4.1-mini gpt-4.1-nano gpt-4o-mini gpt-5-mini gpt-5-nano gpt-oss-120b gpt-oss-20b grok-code-fast-1 (alias grok-code) kimi-k2 llama-4-maverick llama-4-scout llama3.1:70b llama3.1:8b llama3.3:70b (alias llama3) llama4:109b (alias llama4) llama4:400b mistral-nemo:12b (alias mistral-nemo) mistral-small:24b (alias mistral-small) mistral:7b (alias mistral) mixtral:8x22b (alias mixtral) nova-lite nova-micro phi-4:14b (alias phi,phi-4) qwen3-coder qwen3-coder:30b qwen3:235b qwen3:30b qwen3:32b qwen3:8b PREMIUM MODELS: * claude-3-5-haiku claude-3-5-sonnet claude-3-7-sonnet claude-3-sonnet claude-sonnet-4-0 (alias sonnet) gemini-2.5-pro (alias gemini-pro) gpt-4.1 gpt-4o gpt-5 gpt-5-chat grok-4 (alias grok) mistral-large:123b nova-pro o4-mini o4-mini-high * requires valid license: a) SERVICESTACK_LICENSE= b) SERVICESTACK_CERTIFICATE= c) okai -models -license ``` Where you'll be able to use any of the great performing inexpensive models listed under `FREE MODELS` for Free. Whilst ServiceStack customers with an active commercial license can also use any of the more expensive and better performing models listed under `PREMIUM MODELS` by either: a) Setting the `SERVICESTACK_LICENSE` Environment Variable with your **License Key** b) Setting the `SERVICESTACK_CERTIFICATE` Variable with your **License Certificate** c) Inline using the `-license` flag with either the **License Key** or **Certificate** ### FREE for Personal Usage To be able to maintain this as a free service we're limiting usage as a tool that developers can use for personal assistance and research by limiting usage to **60 requests /hour** which should be more than enough for most personal usage and research whilst deterring usage in automated tools. :::tip info Rate limiting is implemented with a sliding [Token Bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) that replenishes 1 additional request every 60s ::: # Using llama.cpp to self-host Large Language Models in Production Source: https://servicestack.net/posts/hosting-llama-server [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai) and [Jan](https://jan.ai) have become popular choices for AI enthusiasts looking to run large language models (LLMs) locally which provide a UX-friendly intuitive interfaces for downloading, installing and running a variety of open-source models on personal workstations, acceleratable with GPUs. However, when scaling beyond personal use, these tools reveal their limitations which weren't designed with production deployment in mind, often lacking flexible and robust resource management, fine-grained authorized usage and optimizations necessary for sustained, high-demand environments. ## Enter llama-server: The Production workhorse The technology underpinning these applications is [llama.cpp](https://github.com/ggml-org/llama.cpp), a groundbreaking C/C++ implementation that enables running sophisticated language models on consumer hardware. This remarkable project, created by Georgi Gerganov, revolutionized the LLM landscape by making previously cloud-only models accessible to everyday users through clever quantization techniques and memory-efficient operations. While Ollama and LM Studio provide user-friendly wrappers around this technology, llama.cpp's [llama-server](https://github.com/ggml-org/llama.cpp/tree/master/examples/server) leverages the same core but strips away the overhead to focus exclusively on performance and stability. By directly utilizing the llama.cpp library and its server component, organizations can bypass the abstractions introduced by desktop applications and tap into the raw power of the underlying engine whose [highly configurable runtime](https://github.com/ggml-org/llama.cpp/tree/master/examples/server#usage) allows for optimized self-hosting of authorized models. This direct approach eliminates unnecessary layers that might introduce latency or unexpected behaviors, providing a more consistent and predictable experience necessary for production environments. For DevOps teams and system administrators, this translates to fewer surprises during deployment and operation — a crucial factor when incorporating self-hosting AI solutions into critical business applications. ## Hosting llama-server with Docker Organizations that have incorporated container based deployment solutions will most likely prefer a docker solution of which is available in a number of different hardware optimized configurations including CPU, CUDA for NVIDIA GPUs, ROCm for AMD GPUs and MUSA for Moore Threads GPUs. Docker containers requiring NVIDIA GPU acceleration will require installing the [NVidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) which allows you to run the `llama.cpp:server-cuda` CUDA optimized image with the `--gpus` flag to utilize your hardwares NVIDIA GPUs, e.g: ```sh docker run -p 8080:8080 -v /path/to/models:/models --gpus all \ ghcr.io/ggml-org/llama.cpp:server-cuda -m models/phi-4.Q4_K_M.gguf \ --host 0.0.0.0 --port 8080 --n-gpu-layers 999 ``` llama.cpp can run models in the [GGUF File format](https://github.com/ggml-org/ggml/blob/master/docs/gguf.md) that are commonly [hosted on hugging face](https://huggingface.co/models?library=gguf&sort=trending). As of this writing Microsoft, Google and Mistral AI have released some of the best quantized LLMs you can run on consumer GPUs: - [Phi-4 14B](https://huggingface.co/bartowski/phi-4-GGUF/tree/main) by Microsoft - [Gemma3 27B](https://huggingface.co/google/gemma-3-27b-it-qat-q4_0-gguf/tree/main) by Google - [Mistral Small 3.1 24B](https://huggingface.co/openfree/Mistral-Small-3.1-24B-Instruct-2503-Q8_0-GGUF/tree/main) by Mistral AI ### Docker compose Docker compose is a great solution for hosting llama-server in production environments which simplifies managing multiple services within declarative configurations, making deployments more repeatable and scalable. ```yml services: phi: image: ghcr.io/ggml-org/llama.cpp:server-cuda environment: - LLAMA_ARG_N_GPU_LAYERS=999 - LLAMA_ARG_MODEL=/models/phi-4.Q4_K_M.gguf ports: - "8000:8080" volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] gemma: image: ghcr.io/ggml-org/llama.cpp:server-cuda environment: - LLAMA_ARG_N_GPU_LAYERS=999 - LLAMA_ARG_MODEL=/models/gemma-3-27b-it-qat-q4_0-gguf ports: - "8001:8080" volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] mistral: image: ghcr.io/ggml-org/llama.cpp:server-cuda environment: - LLAMA_ARG_N_GPU_LAYERS=999 - LLAMA_ARG_MODEL=/models/mistral-small-3.1-24b-instruct-2503-q8_0.gguf ports: - "8002:8080" volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] ``` After saving this to `docker-compose.yml` along with the models: ```files /models phi-4.Q4_K_M.gguf gemma-3-27b-it-qat-q4_0-gguf mistral-small-3.1-24b-instruct-2503-q8_0.gguf docker-compose.yml ``` You'll be able to run and test them with: :::sh docker compose up ::: This will launch 3 container instances of llama-server configured to run different models accessible via an [OpenAI compatible API](https://platform.openai.com/docs/api-reference/chat) on ports `8000`, `8001` and `8002` which you can test using llama-server's Chat Web UI. ### Dedicated GPUs Docker containers can also be configured to run llama-server on different dedicated GPUs, identified by their GPU index: ```yml - driver: nvidia device_ids: ['0'] # Assign to GPU 0 capabilities: [gpu] ``` ## Systemd Services You can trade hosting flexibility to squeeze an extra ounce of performance and run without the overhead of a container by running a compiled llama-server natively by cloning llama.cpp repo: :::sh git clone https://github.com/ggml-org/llama.cpp ::: For NVIDIA GPUs you'll need to install [NVIDIA CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit) before running a CUDA optimized llama.cpp build with: ```sh cmake -B build -DGGML_CUDA=ON cmake --build build --config Release ``` If all goes well after a long while you'll get a freshly minted llama-server executable at `/build/bin/llama-server` which you can create a managed Systemd service with: :::sh sudo vi /etc/systemd/system/llama-server-gemma3.service ::: With the configuration of your llama-server service using your preferred model and configuration options, e.g: ```ini [Unit] Description=Llama Server: Gemma3 27B After=network.target [Service] Type=simple User=root WorkingDirectory=/home/deploy/llama.cpp Environment="CUDA_VISIBLE_DEVICES=0" ExecStart=/home/deploy/llama.cpp/build/bin/llama-server \ --model /home/deploy/llama.cpp/models/gemma-3-27b-it-qat-q4_0-gguf \ -ngl 999 --host 0.0.0.0 --port 8080 Restart=on-failure RestartSec=5s StandardOutput=file:/home/deploy/llama.cpp/logs/llama-server-gemma3.stdout.log StandardError=file:/home/deploy/llama.cpp/logs/llama-server-gemma3.stderr.log [Install] WantedBy=multi-user.target ``` To enable your new systemd service, reload systemd's configuration with: :::sh systemctl daemon-reload ::: Where you'll now be able to use systemd to start/stop/restart your llama-service with: :::sh systemctl start llama-server-gemma3 ::: ### Typed Open AI Chat APIs in 11 Languages Since [AI Server](https://openai.servicestack.net) is written in ServiceStack we're able to use its OpenAI Compatible Chat API DTOs to enable typed integrations in its [11 supported languages](https://docs.servicestack.net/ai-server/openai-chat-all-languages). ### Access llama-server from C# 1. Create an empty console application: :::sh dotnet new console ::: 2. Add the **ServiceStack.Client** NuGet package: :::sh dotnet add package ServiceStack.Client ::: 3. Download AI Server's Typed C# DTOs: :::sh npx get-dtos csharp https://openai.servicestack.net ::: 4. Call llama-server's OpenAI Chat API from C#: ```csharp // Program.cs using ServiceStack; using ServiceStack.Text; using AiServer.ServiceModel; var client = new JsonApiClient("https://localhost:8000"); var result = await client.PostAsync("/v1/chat/completions", new OpenAiChatCompletion { Messages = [ new () { Role = "user", Content = "What's the capital of France?" } ], MaxTokens = 50 }); result.PrintDump(); ``` Run the program: :::sh dotnet run ::: ### Access llama-server from [Node](https://nodejs.org) or [Bun](https://bun.sh) with TypeScript 1. Add the `@servicestack/client` client library: :::sh npm install @servicestack/client ::: 2. Download AI Server's TypeScript DTOs: :::sh npx get-dtos typescript https://openai.servicestack.net ::: Call llama-server with TypeScript DTOs and the generic `JsonServiceClient` ```ts import { JsonServiceClient, Inspect } from "@servicestack/client" import { OpenAiChatCompletion } from "./dtos" const client = new JsonServiceClient("https://localhost:8000") const result = await client.postToUrl("/v1/chat/completions", new OpenAiChatCompletion({ messages: [ { role: "user", content: "What's the capital of France?" } ], max_tokens: 50 }) ) Inspect.printDump(result) ``` ## Managed AI Server Gateway If your organization needs to maintain a number of AI integrations you may want to consider running them behind a Managed AI Gateway so that your App's only need to be configured to use a single endpoint, abstracting away all the complexity of managing multiple AI Providers, API Key managment and monitoring behind a single location. ### Open Source AI Server To support this use-case we're developing [AI Server](https://openai.servicestack.net) - an OSS self-hosted managed gateway that our production AI Applications utilize for all their AI requirements. AI Server allows you to orchestrate your systems AI requests through a single self-hosted application to control what AI Providers App's should use without impacting their client integrations. It serves as a private gateway to process LLM, AI, and Media Transformations, dynamically delegating tasks across multiple providers. [![](https://docs.servicestack.net/img/pages/ai-server/overview.svg)](https://openai.servicestack.net) :::youtube Ojo80oFQte8 AI Server ::: Benefits include: - **Unified AI Gateway** - Centralized management, load balance and monitor AI usage - **Multi Providers** - Manage multiple self-hosted llama-server/Ollama instances or API Hosted LLMs (e.g. OpenAI, Anthropic, Mistral AI, Google, OpenRouter, Groq) - **Load Balancing** - Delegate requests across multiple providers hosting same model - **Developer UX** - Simple Typed AI access to developer friendly APIs in 11 different languages supporting **Synchronous**, **Queued** and **Web Callback** integrations - **Secure access** - Only allow access from Auhtorized Apps using simple API keys - **Analytics** - Observe and monitor your Organizations AI Usage - **Background Jobs** - Monitor executing AI requests in real-time - **Audit History** - Access previous AI Request/Responses in monthly archivable DBs ### Install AI Server can be installed on Linux, macOS or WSL/Windows with Docker 1. Clone the Repository :::sh git clone https://github.com/ServiceStack/ai-server ::: 2. Run the Installer :::sh cd ai-server && cat install.sh | bash ::: This will launch a self-hosted instance of AI Server at: `https://localhost:5006` where you'll be able to Sign In with your chosen Admin password at installation and access AI Server's Admin UI at:

https://localhost:5006/admin

![](/img/posts/hosting-llama-server/admin-dashboard.webp) ## Registering llama-server endpoints To let AI Server know about your new llama-server instances create a new **AI Provider** with the **Custom** AI Provider type to register an OpenAI Chat compatible endpoint, e.g: [![](/img/posts/ai-server-2025-04/custom-openai-provider.webp)](https://docs.servicestack.net/ai-server/) As llama-server is only configured to serve a single model it can configured with any model name as it's ignored by llama-server but used by AI Server to route AI requests for that model to the custom AI Provider instance which you can try in the [Chat UI](https://docs.servicestack.net/ai-server/chat): [![](/img/posts/ai-server-2025-04/custom-openai-provider-chat.webp)](https://docs.servicestack.net/ai-server/chat) ## Create API Keys for your Apps After testing the llama-server instance is working with the Chat UI it's time to create API Keys for all your Apps so they can access AI Servers APIs with the [API Keys UI](https://docs.servicestack.net/auth/admin-apikeys#api-keys-admin-ui): ![](/img/posts/hosting-llama-server/admin-apikeys.webp) Here you can create new API keys, view existing keys, and revoke keys as needed. Keys can be created with expiration dates, and restrictions to specific API endpoints, along with notes to help identify the key's purpose. It's recommended to use a different API Key per App so they can be monitored and analyzed separately. With a valid API Key in hand your App's can use AI Server's DTOs with ServiceStack generic service clients to enable typed integrations in [11 different languages](https://docs.servicestack.net/ai-server/openai-chat-all-languages). ### Synchronous Usage Example Here's an example of Synchronous Usage in C#: ```csharp var client = new JsonApiClient("https://localhost:5006") { BearerToken = Environment.GetEnvironmentVariable("AI_SERVER_API_KEY") }; var response = await client.PostAsync(new OpenAiChatCompletion { Model = "phi4.gguf", Messages = [ new() { Role = "system", Content = "You are a helpful AI assistant" }, new() { Role = "user", Content = "How do LLMs work?" } ], MaxTokens = 50 }); var answer = response.Choices[0].Message.Content; ``` ### Queued Open AI Chat Completion Alternatively, you can call the same endpoint asynchronously which will queue the request for processing then check the status of the request and download the response when it's ready. ```csharp var response = await client.PostAsync(new QueueOpenAiChatCompletion { Request = new() { Model = "phi4.gguf", Messages = [ new() { Role = "system", Content = "You are a helpful AI assistant" }, new() { Role = "user", Content = "How do LLMs work?" } ], MaxTokens = 50 }, }); // Poll for Job Completion Status GetOpenAiChatStatusResponse status = new(); while (status.JobState is BackgroundJobState.Started or BackgroundJobState.Queued) { status = await client.GetAsync(new GetOpenAiChatStatus { RefId = response.RefId }); await Task.Delay(1000); } var answer = status.Result.Choices[0].Message.Content; ``` ### ReplyTo Callback Chat Completion A more reliable Application integration pattern is to provide a `ReplyTo` callback URL to get notified of the response when it's completed, e.g: ```csharp var response = await client.PostAsync(new QueueOpenAiChatCompletion { Request = new() { Model = "phi4.gguf", Messages = [ new() { Role = "system", Content = "You are a helpful AI assistant" }, new() { Role = "user", Content = "How do LLMs work?" } ], MaxTokens = 50 }, ReplyTo = "https://localhost:5001/api/QueueOpenAiChatResponse" }); ``` This enables a push notification integration where your response is not coupled to the client making the request and polling for the response. It's a more robust solution as the notification is handled by a managed background job with retries so that App's are still able to get notified of responses after deployments. ## AI Server Feedback We're actively developing AI Server as centralized hub to manage both self-hosted and API Hosted LLM Integrations. Feel free to reach us at [ai-server/discussions](https://github.com/ServiceStack/ai-server/discussions) with any feature requests or feedback. # Tailwind CSS v4.0 is here! Source: https://servicestack.net/posts/tailwindcss-v4 We're happy to announce that all ServiceStack's ASP .NET Core Identity Auth Tailwind Templates have been upgraded to [Tailwind CSS v4.0](https://tailwindcss.com/blog/tailwindcss-v4) - an all-new version optimized for performance and flexibility, with a reimagined configuration and customization experience, and taking full advantage of the latest advancements the web platform has to offer. Tailwindcss v4 is now the default version used in all new ServiceStack Tailwind Blazor, Razor Pages, MVC, Vue, React and Angular Templates, including: - [Vue Component Library](https://docs.servicestack.net/vue/) - [Blazor Components](https://servicestack.net/blazor) - All of ServiceStack's [Built-in Auto UIs](/auto-ui) - Build and deployment GitHub Actions ### Standalone CLI: Use Tailwind CSS without Node.js Whilst TailwindCSS v4 brings many quality of life improvements, it also posed a challenge for usage in non-Node.js templates which used `npx tailwindcss@v3` to maintain simplicity and avoid needing to install any node dependencies. Unfortunately using npx without `node_modules` is no longer possible in Tailwind v4, instead we've switched to using the [Standalone CLI](https://tailwindcss.com/blog/standalone-cli) which allows using Tailwind CSS without Node.js. `tailwindcss` is a single executable that can be added anywhere in your System `PATH`, to simplify the process we've added support for detecting, downloading and installing the `tailwindcss` executable if missing in each templates `postinstall.js` which can be run with: :::sh npm install ::: In non-Node.js templates this doesn't install any node dependencies, instead it's used to run one-off tasks like running [DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) to create the App Database, update any local 3rd Party dependencies and now downloading and installing the `tailwindcss` executable if missing on Windows, macOS and Linux operating systems, which can also be rerun at anytime with: :::sh node postinstall.js ::: ### Blazor Templates The latest TailwindCSS v4 Standalone CLI is now used in all ServiceStack's Blazor Templates:
Blazor
Blazor WASM
Blazor Vue
- **Blazor** - Identity Auth [Blazor Server Template](/posts/net8-blazor-template) - **Blazor Vue** - No compromises, Identity Auth [statically-rendered Blazor](/posts/net8-best-blazor) ### Razor Pages & MVC The TailwindCSS v4 Standalone CLI is also in Razor Pages and MVC Templates:
Razor Pages
MVC
- **razor** - Identity Auth & Entity Framework Razor Pages Template - **mvc** - Identity Auth & Entity Framework MVC Template ### React, Vue & Angular SPA Templates All React, Vue and Angular Single Page App templates have been upgraded to Vite 6 and use Tailwind v4's [First-party Vite plugin](https://tailwindcss.com/blog/tailwindcss-v4#first-party-vite-plugin) for seamless integration and even greater incremental live reload performance.
Vue SPA
React SPA
Angular SPA
- **vue-spa** - Identity Auth, Vite 6 Vue 3.5 SPA Template - **react-spa** - Identity Auth, Vite 6 React 19 SPA Template - **angular-spa** - Identity Auth, Angular 19 SPA Template ### Razor & Markdown Statically Generated Templates The [Razor SSG](https://razor-ssg.web-templates.io) template is recommended for creating any statically generated websites with Razor like Blogs, Portfolios & Marketing sites like [servicestack.net](https://servicestack.net) which can be hosted for **FREE** on [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/what-is-github-pages) CDN or [Cloudflare Pages](https://pages.cloudflare.com). Whilst [Razor Press](https://razor-press.web-templates.io) is optimized for developing and maintaining documentation-centric websites like [docs.servicestack.net](https://docs.servicestack.net):
Razor SSG
Razor Press
### React and Vue Statically Generated Templates For those preferring a pure Vite and Node.js stack we've also upgraded our [press-vue.servicestack.net](https://press-vue.servicestack.net) and [press-react.servicestack.net](https://press-react.servicestack.net) website templates to Vite 6, latest Vue and React and TailwindCSS v4:
Vite Vue
Vite React
- **press-vue** - Statically generated Vite 6 Vue 3.5 SPA Template - **press-react** - Statically generated Vite 6 React 19 SPA Template ### Universal Markdown Features These templates implement the [Vite Press Plugin](/posts/vite-press-plugin) which enables access to a suite of universal markdown-powered features that can be reused across Vue, React and .NET Razor and Blazor projects, allowing you to incorporate same set of markdown feature folders to power markdown content features across a range of websites built with different technologies. # AI Server - May 2025 Update Source: https://servicestack.net/posts/ai-server-2025-05 AI Server is our Free OSS Docker self-hosted private gateway to manage API access to multiple LLM APIs, Ollama endpoints, Media APIs, Comfy UI and FFmpeg Agents that's designed as a one-stop solution to manage an organization's AI integrations for all their System Apps with its developer friendly HTTP JSON APIs that supports any programming language or framework. [![](/img/svgs/ai-server-overview.svg)](https://openai.servicestack.net) :::youtube Ojo80oFQte8 Introducing AI Server ::: ## Built in Analytics & Logging We've brought [comprehensive Analytics](https://docs.servicestack.net/admin-ui-analytics) added in [ServiceStack v5.7](https://docs.servicestack.net/releases/v8_07) into AI Server's Admin UI to provide deep and invaluable insight into your System API Usage, device distribution, API Keys and identify IPs where most traffic generates. [![](/img/posts/ai-server-2025-04/ai-server-analytics.webp)](https://docs.servicestack.net/admin-ui-analytics) For even finer grained detail of your AI Server's usage we've also surfaced the [SQLite Request Logs](https://docs.servicestack.net/sqlite-request-logs) functionality inside AI Server's Admin UI which lets you monitor your API System usage in real-time: [![](/img/posts/ai-server-2025-04/ai-server-logging.webp)](https://docs.servicestack.net/admin-ui-profiling) ## Redesigned UI As AI Server gains more features it became necessary to group UI features under group headings and associated sub menus: ### Command Palette A new Command Palette has been added to quickly navigate between AI Server's different user-facing features which can be opened with the `/` key: ![](/img/posts/ai-server-2025-04/ai-server-command-palette.webp) ### Convert Image AI Server has now added a UI to convert images between different image formats: `.png`, `.jpg`, `.gif`, `.bmp`, `.tiff` and `.webp` ![](/img/posts/ai-server-2025-04/convert-image.webp) ### Convert Video AI Server configured with a Media Provider can also convert between different video formats: `.mp4`, `.mov`, `.webm`, `.mkv`, `.avi`, `.wmv` and `.ogg`: ![](/img/posts/ai-server-2025-04/convert-video.webp) ## Support for new Models This release continues to see a number of improvements to AI Server starting with adding support for popular LLM models added during this release, including: - **Google:** gemini-flash-2.5, gemini-pro-2.5, gemini-flash-lite-2.0, gemini-flash-thinking-2.0, gemma3 - **OpenAI:** o3, o4-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano - **Alibaba:** qwen3, qwen-turbo, qwen-plus, qwen-max - **Meta:** llama-4-scout, llama-4-maverick - **Microsoft:** phi4 - **Mistral:** mistral-small, mistral-saba ## Custom OpenAI Endpoints Prior to this release AI Requests were only able to reference LLM Models defined in AI Server's pre-defined [ai-models.json](https://github.com/ServiceStack/ai-server/blob/main/AiServer/wwwroot/lib/data/ai-models.json) or included in your [overrides folder](https://docs.servicestack.net/ai-server/configuration#custom-definitions). Model definitions facilitate load balancing requests for a specific model across multiple AI Providers utilizing different model names back to a single model definition. This approach makes it harder to run custom or fine-tuned models from OpenAI Compatible Chat Endpoints which don't map to an existing model definition. To better support these use-cases AI Server now supports registering a custom OpenAI Chat compatible endpoints with custom models with the new **Custom** AI Provider Type: [![](/img/posts/ai-server-2025-04/custom-openai-provider.webp)](https://docs.servicestack.net/ai-server/) This now lets AI Server connect with other OpenAI Compatible API's like llama.cpp's [llama-server](https://github.com/ggml-org/llama.cpp?tab=readme-ov-file#llama-server), e.g: :::sh CUDA_VISIBLE_DEVICES=0 llama-server --model ./models/phi-4-q4.gguf -ngl 999 --port 8080 ::: Which launches an OpenAI compatible API configured to serve a single model that's run entirely on an NVidia GPU at port **8080**. As it's only configured to serve a single model you can choose to configure it with any model name you wish which llama-server ignores but AI Server uses to route any AI requests for that model to the custom AI Provider instance which you can try in [AI Server's Chat UI](https://docs.servicestack.net/ai-server/chat): [![](/img/posts/ai-server-2025-04/custom-openai-provider-chat.webp)](https://docs.servicestack.net/ai-server/chat) ## Support for Ollama Vision Models By default [ImageToText](/ai-server/image-to-text) uses a purpose-specific **Florence 2 Vision model** with ComfyUI for its functionality which is capable of generating a very short description about an image, e.g: > A woman sitting on the edge of a lake with a wolf But with LLMs gaining multi modal capabilities and Ollama's recent support of Vision Models we can instead use popular Open Source models like Google's **gemma3:27b** or Mistral's **mistral-small:24b** to extract information from images. Both models are very capable vision models that's can provide rich detail about an image: ### Describe Image ### Caption Image Although our initial testing sees gemma being better at responding to a wide variety of different prompts, e.g: ## New OllamaGenerate Endpoints To support Ollama's vision models AI Server added a new feature pipeline around [Ollama's generate completion API](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-completion): - [ImageToText](https://openai.servicestack.net/ui/ImageToText) - **Model** - Whether to use a Vision Model for the request - **Prompt** - Prompt for the vision model - [OllamaGeneration](https://openai.servicestack.net/ui/OllamaGeneration): Synchronous invocation of Ollama's Generate API - [QueueOllamaGeneration](https://openai.servicestack.net/ui/QueueOllamaGeneration): Asynchronous or Web Callback invocation of Ollama's Generate API - [GetOllamaGenerationStatus](https://openai.servicestack.net/ui/GetOllamaGenerationStatus): Get the generation status of an Ollama Generate API ## Online Status Indicator Internally AI Server tracks which enabled providers are online and which have successive failures to trigger taking them offline. Periodically a scheduled task checks or offline providers to test whether they've come back online and their status is updated accordingly. This status now has a **green** and **red** visible dot indicator in the AI Provider list and in the Edit form where you can manually check the online provider status: ### AI Provider Status ![](/img/posts/ai-server-2025-04/online-status.webp) ### Media Provider Status The same feature is also available on Media Providers list and Edit page: ![](/img/posts/ai-server-2025-04/online-status-media.webp) ## Support for Thinking Responses With the rise and popularity of **Thinking** Models we've added custom rendering of *thinking* responses in a collapsible and scrollable container: [![](/img/posts/ai-server-2025-04/ai-server-thinking.png)](https://docs.servicestack.net/ai-server/chat) # Text to Blazor upgraded to use best coding LLMs Source: https://servicestack.net/posts/text-to-blazor-premium ## Text to Blazor now utilizing leading coding models After the positive reception we've received for Text to Blazor we've now upgraded it to use the best (non-thinking) coding LLMs from the leading AI and OSS companies, inc:
Google Gemini 2.5 Pro
Anthropic Claude 3.7 Sonnet
Meta Llama 4 Maverick (400B)
DeepSeek DeepSeek V3 (685B)
Mistral Codestral (22B)
These LLMs routinely top [Aider's LLM Leaderboards](https://aider.chat/docs/leaderboards/) and are also amongst the most popular LLMs used for **programming** on [Open Router](https://openrouter.ai): [![](/img/posts/text-to-blazor-premium/popular-coding-llms.webp)]([/text-to-blazor](https://openrouter.ai/rankings/programming?view=day)) ## 100+ Blazor CRUD Apps Created! Since [Text to Blazor's announcement](/posts/text-to-blazor) in ServiceStack's last release we're happy to report that more than 100+ Blazor CRUD Apps have been created and downloaded! Text to Blazor harnesses the power of AI to instantly generate new Blazor Admin CRUD Apps from just a text description.
[![](/img/posts/text-to-blazor/text-to-blazor-prompt.webp)](/text-to-blazor) This will query 5 different leading AI coding models to generate 5x different Data Models, APIs, DB Migrations and Admin UIs which you choose amongst to pick the best one that matches your requirements for your new CRUD App: [![](/img/posts/text-to-blazor-premium/text-to-blazor-premium.webp)](/text-to-blazor) ### Download preferred Blazor Vue CRUD App Once you've decided on the Data Models that best matches your requirements, you can download your preferred generated Blazor Vue CRUD App: [![](/img/posts/text-to-blazor/text-to-blazor-download.webp)](/text-to-blazor) ### Blazor Admin App **Admin Only** - is ideal for internal Admin Apps where the Admin UI is the Primary UI ![](/img/posts/text-to-blazor/okai-blazor-admin.webp) ### Blazor Vue App **UI + Admin** - Creates a new [blazor-vue](https://blazor-vue.web-templates.io) template that's ideal for Internet or public facing Apps, sporting a full-featured public facing UI for a Web App's users whilst enabling a back-office CRUD UI for Admin Users to manage their App's data. ![](/img/posts/text-to-blazor/okai-blazor-vue.webp) Clicking on the **Admin UI** button will take you to the Admin UI at `/admin`: ![](/img/posts/text-to-blazor/okai-blazor-vue-admin.webp) ## Run Migrations In order to create the necessary tables for the new functionality, you'll need to run the DB Migrations. If migrations have never been run before, you can run the `migrate` npm script to create the initial database: :::sh npm run migrate ::: If you've already run the migrations before, you can run the `rerun:last` npm script to drop and re-run the last migration: :::sh npm run rerun:last ::: Alternatively you can nuke the App's database (e.g. `App_Data/app.db`) and recreate it from scratch with `npm run migrate`. ## Instant CRUD UI After running the DB migrations, you can hit the ground running and start using the Admin UI to manage the new Data Model RDBMS Tables: :::youtube 8buo_ce3SNM Using AutoQuery CRUD UI in a Text to Blazor App ::: ### Create new Records from Search Dialog We're continually improving the UX of the [AutoQueryGrid Component](/vue/autoquerygrid) used in generating CRUD UIs to enable a more productive and seamless workflow. A change added to that end that you can see in the above video is the ability to add new Records from a Search dialog: ![](/img/posts/text-to-blazor/autoquerygrid-new2.webp) This now lets you start immediately creating new records without needing to create any lookup entries beforehand. ## Audited Data Models The TypeScript Data Models enable a rapid development experience for defining an App's Data Models which are used to generate the necessary AutoQuery CRUD APIs to support an Admin UI. An example of the productivity of this approach is the effortless support for maintaining a detailed audit history for changes to select tables by inheriting from the `AuditBase` base class, e.g: ```ts export class Job extends AuditBase { ... } ``` Which can then be regenerated using the name of the TypeScript Model definitions: :::sh npx okai Jobs.d.ts ::: This will include additional `CreatedBy`, `CreatedDate`, `ModifiedBy`, `ModifiedDate`, `DeletedBy` and `DeletedDate` properties to the specified Table and also generates the necessary [Audit Behaviors](https://docs.servicestack.net/autoquery/crud#apply-generic-crud-behaviors) on the AutoQuery APIs to maintain the audit history for each CRUD operation. ### AutoQuery CRUD Audit Log As the **blazor-admin** and **blazor-vue** templates are configured to use the [AutoQuery CRUD Executable Audit Log](https://docs.servicestack.net/autoquery/audit-log) in its [Configure.AutoQuery.cs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp/Configure.AutoQuery.cs) the Audit Behaviors will also maintain an Audit Trail of all CRUD operations which can be viewed in the Admin UI: ![](/img/posts/text-to-blazor/okai-audit-form.webp) ### Using AI to only generate Data Models Whilst the result is a working CRUD App, the approach taken is very different from most AI tools which uses AI to generate the entire App that ends up with a whole new code-base developers didn't write which they'd now need to maintain. Instead AI is only used to generate the initial Data Models within a **TypeScript Declaration file** which we've found is the best format supported by AI models that's also the best typed DSL for defining data models with minimal syntax that's easy for humans to read and write. # Complete Admin UI Management for Identity Claims and Roles Source: https://servicestack.net/posts/identityauth-claims-roles The latest ServiceStack v8.7 Release completes our transition to [ASP .NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth) with management APIs and Admin UIs for managing Identity Auth Roles and Claims for both Users and Roles. All ServiceStack Apps now includes a complete IdentityAuth Management Admin UI which can Add/Remove Roles from the new Identity **Roles** Admin UI: ![](/img/posts/identityauth-claims-roles/identityauth-roles.webp) ### Custom Application Roles If your App uses an extended `IdentityRole` data model, it can be configured with: ```csharp services.AddPlugin( new AuthFeature(IdentityAuth.For(...))); ``` If it's also configured to use a different `PrimaryKey` type, it can be configured with: ```csharp services.AddPlugin( new AuthFeature(IdentityAuth.For(...))); ``` ### IdentityAuth Role Claims The Edit Role Admin UI can also be configured to Add/Remove Claims to a Role, e.g: ![](/img/posts/identityauth-claims-roles/identityauth-role-claims.webp) Any Added or Removed Claims are only applied after clicking **Update Role**, likewise you can exit the UI without applying any changes by clicking **Cancel**. Claims added to Roles have similar behavior to having Claims individually applied to all Users with that Role such that when a User is Authenticated they're populated with all claims assigned to their Roles in addition to their individual User Claims. ### IdentityAuth User Claims Whilst the new User Claim Management UI allows you to assign Claims to individual Users: ![](/img/posts/identityauth-claims-roles/identityauth-user-claims.webp) ## Validating Claims Claims are attestations or attributes about a User which we can use to restrict access to APIs to only Users who have been assigned that claim. We could use this to implement a permission system that restricts usage with a `todos:write` permission with something like: ```csharp [ValidateHasClaim("perm", "todos:write")] class CreateTodo {} ``` Normally this would result in the generic missing claims error message: :::{.not-prose} ::: But as the `perm` claim has a customized error message: ```csharp HasClaimValidator.ClaimErrorMessages["perm"]= "`${Value} Permission Required`"; ``` It will generate that Error Response instead: :::{.not-prose} ::: Which is a good example showing how `HasClaimValidator.ClaimErrorMessages` can be used to add custom error messages for your own custom claim validation. ### Inspecting Claims inside Services You can also inspect and validate a Users Claim by inspecting the Authenticated ClaimsPrincipal, e.g: ```csharp public class TodoServices : Service { public object Any(CreateTodo request) { var user = Request.GetClaimsPrincipal(); if (!user.HasClaim("perm", "todos:write")) throw HttpError.Forbidden("todos:write Permission Required"); var allUserClaims = user.Claims.ToList(); //... } } ``` # In Depth and Interactive API Analytics available to all ServiceStack Apps Source: https://servicestack.net/posts/analytics The big ticket feature of ServiceStack v8.7 is the comprehensive API Analytics available to all ServiceStack Apps configured with [SQLite Request Logging](https://docs.servicestack.net/sqlite-request-logs). :::youtube kjLcm1llC5Y In Depth and Interactive API Analytics available to all ASP .NET Core ServiceStack Apps! ::: ### Benefits of API Analytics They provide deep and invaluable insight into your System API Usage, device distribution, its Users, API Keys and the IPs where most traffic generates: - **Visibility:** Provides a clear, visual summary of complex log data, making it easier to understand API usage and performance at a glance. - **Performance Monitoring:** Helps track key metrics like request volume and response times to ensure APIs are meeting performance expectations. - **User Understanding:** Offers insights into how users (and bots) are interacting with the APIs (devices, browsers). - **Troubleshooting:** Aids in quickly identifying trends, anomalies, or specific endpoints related to issues. - **Resource Planning:** Understanding usage patterns helps in scaling infrastructure appropriately. - **Security Insight:** Identifying bot traffic and unusual request patterns can be an early indicator of security concerns. ### Interactive Analytics Analytics are also interactive where you're able to drill down to monitor the activity of individual APIs, Users, API Keys and IPs which have further links back to the request logs which the summary analytics are derived from. As they offer significant and valuable insights they're now built into all ASP.NET Core IdentityAuth templates, existing .NET 8 IdentityAuth templates can enable it with: :::sh x mix sqlitelogs ::: .NET 8 Templates that are not configured to use [Endpoint Routing](https://docs.servicestack.net/endpoint-routing) and [ASP.NET Core IOC](https://docs.servicestack.net/net-ioc) will need to explicitly register `SqliteRequestLogger` as a singleton dependency in addition to configuring it on the `RequestLogsFeature` plugin: ```csharp public class ConfigureRequestLogs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { var logger = new SqliteRequestLogger(); services.AddSingleton(logger); services.AddPlugin(new RequestLogsFeature { RequestLogger = logger, EnableRequestBodyTracking = true, EnableErrorTracking = true }); services.AddHostedService(); if (context.HostingEnvironment.IsDevelopment()) { services.AddPlugin(new ProfilingFeature()); } }); } ``` This will enable a more feature rich Request Logging Admin UI which utilizes the full queryability of an AutoQueryGrid to filter, sort and export Request Logs. [![](/img/posts/analytics/sqlitelogs.webp)](/img/posts/analytics/sqlitelogs.webp) ## Analytics Overview Utilizing an `SqliteRequestLogger` will also enable the new **Analytics** Admin UI in the sidebar which initially displays the API Analytics Dashboard: [![](/img/posts/analytics/analytics-apis1.webp)](/img/posts/analytics/analytics-apis1.webp) ### Distribution Pie Charts Lets you quickly understand the composition of your user base and traffic sources and the distribution of users across different web browsers, device types, and to identify the proportion of traffic coming from automated bots. ### Requests per day Line Chart Lets you monitor API usage trends and performance over time. It tracks the total number of API requests and the average response time day-by-day. You can easily spot trends like peak usage hours/days, identify sudden spikes or drops in traffic, and correlate request volume with API performance which is crucial for capacity planning and performance troubleshooting. ### API tag groups Pie Chart Lets you understand the usage patterns across different functional categories of your APIs. By grouping API requests based on assigned tags (like Security, Authentication, User Management, Tech, etc.), you get a high-level view of which *types* of functionalities are most frequently used or are generating the most load. ### API Requests Bar Chart Lets you identify the most and least frequently used specific API endpoints which ranks individual API endpoints by the number of requests they receive. This helps pinpoint: - **Critical Endpoints:** The most heavily used APIs that require robust performance and monitoring. - **Optimization Targets:** High-traffic endpoints that could benefit from performance optimization. - **Underutilized Endpoints:** APIs that might be candidates for deprecation or require promotion. - **Troubleshooting:** If performance issues arise (seen in the line chart), this helps narrow down which specific endpoint might be responsible. [![](/img/posts/analytics/analytics-apis2.webp)](/img/posts/analytics/analytics-apis2.webp) ### Total Duration Bar Chart Identifies which API endpoints consume the most *cumulative processing time* over the selected period. Even if an API endpoint is relatively fast per call, if it's called extremely frequently, it can contribute significantly to overall server load. Optimizing these can lead to significant savings in server resources (CPU, memory). ### Average Duration Bar Chart Pinpoints which API endpoints are the slowest on a *per-request* basis. APIs at the top of this list are prime candidates for performance investigation and optimization, as they represent potential user-facing slowness or system bottlenecks. ### Requests by Duration Ranges Histogram Provides an overview of the performance distribution for *all* API requests. This chart shows how many requests fall into different speed buckets and helps you understand the overall responsiveness of your API system at a glance. ## Individual API Analytics Clicking on an API's bar chart displays a dedicated, detailed view of a single API endpoint's behavior, isolating its performance and usage patterns from the overall system metrics offering immediate insight into the endpoint's traffic volume and reliability. [![](/img/posts/analytics/analytics-api.webp)](/img/posts/analytics/analytics-api.webp) ### Total Requests Displays the total requests for an API during the selected month. It includes HTTP Status Breakdown which provide **direct access to the filtered request logs**. This is a major benefit for **rapid troubleshooting**, allowing you to instantly view the specific log entries corresponding to successful requests or particular error codes for this API. ### Last Request Information Provides immediate context on the most recent activity for this endpoint with *when* the last request occurred, the source **IP address** and device information to help understand recent usage and check if the endpoint is still active, or quickly investigate the very last interaction if needed. ### Duration Summary Table (Total, Min, Max) Quantifies the performance characteristics specifically for this endpoint with the cumulative (Total) processing load, the best-case performance (Min), and the worst-case performance (Max) which is useful for identifying performance outliers. ### Duration Requests Histogram Visualizes the performance distribution for this API. ### Top Users Bar Chart Identifies which authenticated users are most frequently calling this API and relies on this endpoint the most. This can be useful for identifying power users, potential API abuse by a specific user account, or understanding the impact of changes to this API on key users. ### Top IP Addresses Bar Chart Shows which source IP addresses are generating the most traffic for this API. Useful for identifying high-volume clients, specific servers interacting with this endpoint, or potentially malicious IPs. ## Users The **Users** tab will display the top 100 Users who make the most API Requests and lets you click on a Users bar chart to view their individual User analytics. [![](/img/posts/analytics/analytics-users.webp)](/img/posts/analytics/analytics-users.webp) ### Individual User Analytics Provides a comprehensive view of a single user's complete interaction history and behavior across all APIs they've accessed, shifting the focus from API performance to user experience and activity. [![](/img/posts/analytics/analytics-user.webp)](/img/posts/analytics/analytics-user.webp) ### User Info & Total Requests Identifies the user and quantifies their overall activity level. Clicking on their ID or Name will navigate to the Users Admin UI. It also shows their success/error rate via the clickable status code links. This helps gauge user engagement and baseline activity. ### Last Request Information Offers a snapshot of the user's most recent interaction for immediate context. Knowing **when**, **what** API they called, from which **IP address**, using which **client** & **device** is valuable for support, identifying their last action or checking recent activity. ### HTTP Status Pie Chart Visualizes the overall success and error rate specifically for this user's API requests. ### Performance & Request Body Summary Table Quantifies the performance experienced by this user and the data they typically send. ### Duration Requests Histogram Shows the distribution of response times for requests made by this user to help understand the typical performance this user experiences. ### Top APIs Bar Chart Reveals which API endpoints this user interacts with most frequently and help understanding user behavior and which features they use most. ### Top IP Addresses Bar Chart Identifies the primary network locations or devices the user connects from. ### User Admin UI Analytics To assist in discoverability a snapshot of a Users Analytics is also visible in the Users Admin UI: [![](/img/posts/analytics/analytics-user-adminui.webp)](/img/posts/analytics/analytics-user-adminui.webp) Clicking on **View User Analytics** takes you to the Users Analytics page to access to the full Analytics features and navigation. ## API Keys The **API Keys** tab will display the top 100 API Keys who make the most API Requests and lets you click on an API Key bar chart to view its individual API Key analytics. [![](/img/posts/analytics/analytics-apikeys.webp)](/img/posts/analytics/analytics-apikeys.webp) ### Individual API Key Analytics Provides comprehensive API Key analytics Similar to User Analytics but limited to the API Usage of a single API Key: [![](/img/posts/analytics/analytics-apikey.webp)](/img/posts/analytics/analytics-apikey.webp) ## IPs The **IP Addresses** tab will display the top 100 IPs that make the most API Requests. Click on an IP's bar chart to view its individual analytics made from that IP Address. [![](/img/posts/analytics/analytics-ips.webp)](/img/posts/analytics/analytics-ips.webp) ### Individual IP Analytics Provides comprehensive IP Address analytics Similar to User Analytics but limited to the API Usage from a single IP Address: [![](/img/posts/analytics/analytics-ip.webp)](/img/posts/analytics/analytics-ip.webp) # llms.txt and llms-full.txt support for Razor SSG and Razor Press Source: https://servicestack.net/posts/llms-txt ## Benefits for Large Language Models Large language models gain a lot of their knowledge from information on websites, but their small context windows aren't capable of handling most websites whose complex HTML Pages contain a mix of content, scripts, styles, navigation, ads and structural elements that can introduce noise and ambiguity. Parsing this content requires additional preprocessing to extract meaningful content while filtering out irrelevant elements, which can be inefficient and error-prone. Instead LLMs thrive on clean, structured text. To address this, [llmstxt.org](https://llmstxt.org) have proposed adding `/llms.txt` and `llms-full.txt` markdown files to websites to provide a more concise, expert-level information that LLMs can consume in a single, accessible location that's ideal for quickly adding context to LLMs, improves text comprehension, enhances model accuracy, streamlines content ingestion for RAG workflows and other AI-powered applications. Markdown was chosen for content as it's lightweight, clean, and structured specifically for readability. Markdown files contain pure text with minimal formatting, making them an ideal source for LLM training and retrieval. Because markdown is designed for capturing content, it ensures that LLMs receive high-quality, contextually relevant information without the distractions of complex page layouts or extraneous code. ## Support for llms.txt and llms-full.txt Fortunately [razor-ssg](https://razor-ssg.web-templates.io) and [razor-press](https://razor-press.web-templates.io) have fully embraced Markdown for maintaining their content, making it trivial to support generating `/llms.txt` and `llms-full.txt` files from your original static markdown content from the new [Llms.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Llms.cshtml) and [LlmsFull.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/LlmsFull.cshtml) pages whose content is sourced from the Websites Markdown [/_pages](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_pages) folder rendered from the links in its [sidebar.json](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/sidebar.json) and Blog posts in the [/_posts](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_posts) folder in descending order. Here are some examples of `llms.txt` and `llms-full.txt` files generated from the Razor SSG and Razor Press website Templates: | Website | llms.txt | llms-full.txt | |--------------------------------------|-----------------------------------------------------------|---------------------------------------------------------------------| | https://servicestack.net | [llms.txt](https://servicestack.net/llms.txt) | [llms-full.txt](https://servicestack.net/llms-full.txt) | | https://docs.servicestack.net | [llms.txt](https://docs.servicestack.net/llms.txt) | [llms-full.txt](https://docs.servicestack.net/llms-full.txt) | | https://razor-ssg.web-templates.io | [llms.txt](https://razor-ssg.web-templates.io/llms.txt) | [llms-full.txt](https://razor-ssg.web-templates.io/llms-full.txt) | | https://razor-press.web-templates.io | [llms.txt](https://razor-press.web-templates.io/llms.txt) | [llms-full.txt](https://razor-press.web-templates.io/llms-full.txt) | :::{.text-center} ## Create a new Razor SSG or Razor Pages Project ::: [Razor SSG](https://razor-ssg.web-templates.io) is our FREE Project Template for creating fast, statically generated Websites and Blogs with Markdown & C# Razor Pages, whilst [Razor Press](https://razor-press.web-templates.io) is more focused on maintaining documentation websites.
Razor SSG
Razor Press
# Generate CRUD APIs and Admin UIs from existing DBs Source: https://servicestack.net/posts/crud-app-from-existing-db A core piece of functionality in the [Text to Blazor CRUD App](/posts/text-to-blazor) feature is distilling an AI Prompt into TypeScript classes that can be [further customized](/posts/text-to-blazor#customize-data-models) to generate AutoQuery CRUD APIs and Admin UIs for managing the underlying RDBMS tables. ## TypeScript Data Models Using TypeScript to define models is a flexible and effortless way to define your data models as it offers a DSL-like format with minimal boilerplate that's human-friendly to read and write that can also leverage TypeScript's powerful TypeScript Type System to provide a rich authoring experience with strong typing and intellisense that's validated against the referenced [api.d.ts](https://okai.servicestack.com/api.d.ts) schema containing all the C# Types, interfaces, and attributes used in defining APIs, DTOs and Data Models. ### Blueprint for Code Generation The TypeScript Data Models then serve as the blueprint for generating everything needed to support the feature in your App, including the AutoQuery CRUD APIs, Admin UIs and DB Migrations should you prefer to re-create it from scratch. ### RDBMS Metadata AppTask The first step in generating TypeScript Data Models is to capture the metadata from the existing RDBMS tables which we can do with the `App.json` [AppTask](https://docs.servicestack.net/app-tasks) below which uses your App's configured RDBMS connection to generate the Table Definitions for all tables in the specified RDBMS connection and schema to the file of your choice (e.g `App_Data/App.json`): ```csharp AppTasks.Register("App.json", args => appHost.VirtualFiles.WriteFile("App_Data/App.json",ClientConfig.ToSystemJson( migrator.DbFactory.GetTables(namedConnection:null, schema:null)))); ``` This task can then be run from the command line with: :::sh dotnet run --AppTasks=App.json ::: Which will generate the `App_Data/App.json` file containing the table definition metadata for all tables in the specified RDBMS. ### Different Connection or DB Schema If you prefer to generate the metadata for a different connection or schema, you can create a new AppTask with your preferred `namedConnection` and/or `schema`, e.g: ```csharp AppTasks.Register("Sales.json", args => appHost.VirtualFiles.WriteFile("Sales.json", ClientConfig.ToSystemJson( migrator.DbFactory.GetTables(namedConnection:"reports",schema:"sales")))); ``` ### Generate TypeScript Data Models The next step is to generate TypeScript Data Models from the captured metadata which can be done with the `okai` tool by running the `convert` command with the path to the `App.json` JSON table definitions which will generate the TypeScript Data Models to stdout which can be redirected to a file in your **ServiceModel** project, e.g: :::sh npx okai convert App_Data/App.json > ../MyApp.ServiceModel/App.d.ts ::: ## Generate CRUD APIs and Admin UIs The data models defined in the `App.d.ts` TypeScript Declaration file is what drives the generation of the Data Models, APIs, DB Migrations and Admin UIs. ### Customize Data Models This can be further customized by editing the TypeScript Declaration file and re-running the `okai` tool with just the filename, e.g: :::sh npx okai App.d.ts ::: Which will re-generate the Data Models, APIs, DB Migrations and Admin UIs based on the updated Data Models. ![](/img/posts/okai-models/npx-okai-App.png) :::tip You only need to specify the `App.d.ts` TypeScript filename (i.e. not the filepath) from anywhere within your .NET solution ::: ### Live Code Generation If you'd prefer to see the generated code in real-time you can add the `--watch` flag to watch the TypeScript Declaration file for changes and automatically re-generate the generated files on Save: :::sh npx okai App.d.ts --watch ::: # FREE CLI Access to the worlds most popular AI Models Source: https://servicestack.net/posts/okai-chat As part of the development of [okai](/posts/okai-models) for generating [Blazor CRUD Apps from a text prompt](/posts/text-to-blazor) using your preferred AI Models, we've also made available a generic **chat** prompt that can be used as a convenient way to conduct personal research against many of the worlds most popular Large Language Models - for Free! ![](/img/posts/okai-chat/okai-chat.webp) No API Keys, no Signups, no installs, no cost, you can just start immediately using the `npx okai chat` script to ask LLMs for assistance: :::sh npx okai chat "command to copy a folder with rsync?" ::: This will use the default model (currently codestral:22b) to answer your question. ### Select Preferred Model You can also use your preferred model with the `-m ` flag with either the model **name** or its **alias**, e.g you can use [Microsoft's PHI-4 14B](https://techcommunity.microsoft.com/blog/aiplatformblog/introducing-phi-4-microsoft%E2%80%99s-newest-small-language-model-specializing-in-comple/4357090) model with: :::sh npx okai -m phi chat "command to copy folder with rsync?" ::: ### List Available Models We're actively adding more great performing and leading experimental models as they're released. You can view the list of available models with `ls models`: :::sh npx okai ls models ::: Which at this time will return the following list of available models along with instructions for how to use them: ```txt USAGE (5 models max): a) OKAI_MODELS=codestral,llama3.3,flash b) okai -models codestral,llama3.3,flash c) okai -m flash chat FREE MODELS: claude-3-haiku (alias haiku) codestral:22b (alias codestral) deepseek-r1:32b deepseek-r1:671b (alias deepseek-r1) deepseek-r1:70b deepseek-r2:32b deepseek-v3:671b (alias deepseek) gemini-flash-1.5 gemini-flash-1.5-8b (alias flash-8b) gemini-flash-2.0 (alias flash) gemini-flash-lite-2.0 (alias flash-lite) gemini-flash-thinking-2.0 (alias flash-thinking) gemini-pro-2.0 (alias gemini-pro) gemma2:9b (alias gemma) gpt-3.5-turbo (alias gpt-3.5) gpt-4o-mini llama3.1:70b (alias llama3.1) llama3.3:70b (alias llama3.3) llama3:8b (alias llama3) mistral-nemo:12b (alias mistral-nemo) mistral-small:24b (alias mistral-small) mistral:7b (alias mistral) mixtral:8x22b mixtral:8x7b (alias mixtral) nova-lite nova-micro phi-4:14b (alias phi,phi-4) qwen-plus qwen-turbo qwen2.5-coder:32b (alias qwen2.5-coder) qwen2.5:32b qwen2.5:72b (alias qwen2.5) qwq:32b (alias qwq) qwq:72b PREMIUM MODELS: * claude-3-5-haiku claude-3-5-sonnet claude-3-7-sonnet (alias sonnet) claude-3-sonnet gemini-pro-1.5 gpt-4 gpt-4-turbo gpt-4o mistral-large:123b nova-pro o1-mini o1-preview o3-mini qwen-max * requires valid license: a) SERVICESTACK_LICENSE= b) SERVICESTACK_CERTIFICATE= c) okai -models -license ``` Where you'll be able to use any of the great performing inexpensive models listed under `FREE MODELS` for Free. Whilst ServiceStack customers with an active commercial license can also use any of the more expensive and better performing models listed under `PREMIUM MODELS` by either: a) Setting the `SERVICESTACK_LICENSE` Environment Variable with your **License Key** b) Setting the `SERVICESTACK_CERTIFICATE` Variable with your **License Certificate** c) Inline using the `-license` flag with either the **License Key** or **Certificate** ### FREE for Personal Usage To be able to maintain this as a free service we're limiting usage as a tool that developers can use for personal assistance and research by limiting usage to **60 requests /hour** which should be more than enough for most personal usage and research whilst deterring usage in automated tools. :::tip info Rate limiting is implemented with a sliding [Token Bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) that replenishes 1 additional request every 60s ::: # New okai tool for Rapid App Development Source: https://servicestack.net/posts/okai-models ## AI powered Rapid App Development Workflow The new `okai` npm tool works similar to the online [Text to Blazor App](/posts/text-to-blazor) generator except it's a local tool that can add additional functionality to an existing project: The syntax for adding a new feature to your Web App is `npx okai `, e.g: :::sh npx okai "The kind of Feature you would like to add" ::: Where it will generate the Data Models, AutoQuery CRUD APIs, DB Migrations and Admin UI for the selected feature which you'll see after selecting the LLM Data Models you want to use, e.g: ```sh Selected 'deepseek-r1:70b' data models Saved: /home/mythz/src/MyApp/MyApp.ServiceModel/Jobs.d.ts Saved: /home/mythz/src/MyApp/MyApp.ServiceModel/Jobs.cs Saved: /home/mythz/src/MyApp/wwwroot/admin/sections/Jobs.mjs Saved: /home/mythz/src/MyApp/wwwroot/admin/sections/index.mjs Saved: /home/mythz/src/MyApp/Migrations/Migration1001.cs Run 'dotnet run --AppTasks=migrate' to apply new migration and create tables To regenerate classes, update 'Jobs.d.ts' then run: $ okai Jobs.d.ts ``` Where okai will generate everything needed to support the feature in your App, including: - `MyApp.ServiceModel/Jobs.d.ts` - TypeScript Data Models - `MyApp.ServiceModel/Jobs.cs` - AutoQuery CRUD APIs and Data Models - `wwwroot/admin/sections/Jobs.mjs` - Admin UI Section - requires `blazor-admin` or `blazor-vue` template - `MyApp/Migrations/Migration1001.cs` - DB Migrations - requires project with [OrmLite DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) Then to apply the migration and create the tables you can run: :::sh npm run migrate ::: ## Declarative AI powered Features The approach okai uses is very different from most AI tools which instead of using AI to generate an entire App or source code for a feature it's only used to generate the initial Data Models within a TypeScript Declaration file which we've found is best format supported by AI models that's also the best typed DSL for defining data models with minimal syntax that's easy for humans to read and write. This is possible for ServiceStack Apps since a significant portion of an App's functionality can be [declaratively applied](https://docs.servicestack.net/locode/declarative) including all [AutoQuery CRUD APIs](https://docs.servicestack.net/autoquery/crud) which can be implemented just using typed Request DTOs to define the shape of the API AutoQuery should implement. From the Data Models, the rest of the feature is generated using declarative code-first APIs depending on the template used. ### Generated Admin UI To have okai generate an Admin UI you'll need to use it within a new Blazor Admin project or Blazor Vue ([blazor-vue](https://blazor-vue.web-templates.io)) project: :::sh x new blazor-admin Acme ::: Which both support a "Modular no-touch" Admin UI which will appear under a new group in the Admin Sidebar: ![](/img/posts/text-to-blazor/okai-blazor-admin.webp) ## Customize Data Models The data models defined in the TypeScript Declaration file e.g. `Jobs.d.ts` is what drives the generation of the Data Models, APIs, DB Migrations and Admin UIs. This can be further customized by editing the TypeScript Declaration file and re-running the `okai` tool with the name of the TypeScript Declaration file, e.g. `Jobs.d.ts`: :::sh npx okai Jobs.d.ts ::: Which will re-generate the Data Models, APIs, DB Migrations and Admin UIs based on the updated Data Models. ![](/img/posts/text-to-blazor/okai-Employees.webp) :::tip You only need to specify the `Jobs.d.ts` TypeScript filename (i.e. not the filepath) from anywhere within your .NET solution ::: ### Live Code Generation If you'd prefer to see the generated code in real-time you can add the `--watch` flag to watch the TypeScript Declaration file for changes and automatically re-generate the generated files on Save: :::sh npx okai Jobs.d.ts --watch ::: # Text to Blazor Vue CRUD Apps Source: https://servicestack.net/posts/text-to-blazor Text to Blazor is our first initiative for harnessing AI to help to rapidly generate new Blazor Admin CRUD Apps from just a text description.
[![](/img/posts/text-to-blazor/text-to-blazor-prompt.webp)](/text-to-blazor) This will query 5 different high quality AI models to generate 5 different Data Models, APIs, DB Migrations and Admin UIs which you can browse to find the one that best matches your requirements. [![](/img/posts/text-to-blazor/text-to-blazor-gen.webp)](/text-to-blazor) ### Using AI to only generate Data Models Whilst the result is a working CRUD App, the approach taken is very different from most AI tools which uses AI to generate the entire App that ends up with a whole new code-base developers didn't write which they'd now need to maintain. Instead AI is only used to generate the initial Data Models within a **TypeScript Declaration file** which we've found is the best format supported by AI models that's also the best typed DSL for defining data models with minimal syntax that's easy for humans to read and write. ### Download preferred Blazor Vue CRUD App Once you've decided on the Data Models that best matches your requirements, you can download your preferred generated Blazor Vue CRUD App: [![](/img/posts/text-to-blazor/text-to-blazor-download.webp)](/text-to-blazor) ### Blazor Admin App **Admin Only** - is ideal for internal Admin Apps where the Admin UI is the Primary UI ![](/img/posts/text-to-blazor/okai-blazor-admin.webp) ### Blazor Vue App **UI + Admin** - Creates a new [blazor-vue](https://blazor-vue.web-templates.io) template that's ideal for Internet or public facing Apps, sporting a full-featured public facing UI for a Web App's users whilst enabling a back-office CRUD UI for Admin Users to manage their App's data. ![](/img/posts/text-to-blazor/okai-blazor-vue.webp) Clicking on the **Admin UI** button will take you to the Admin UI at `/admin`: ![](/img/posts/text-to-blazor/okai-blazor-vue-admin.webp) ## Modular Code Generation Instead of unleashing AI on your code-base unabated, we're only using AI to generate isolated functionality into grouped "no touch" source files that can be easily maintained and extended. Creating a new Project with a similar prompt above would create a new project with the new source files (marked with `*`) added to the existing project: ### APIs ```files /MyApp.ServiceModel Bookings.cs api.d.ts* Employees.cs* Employees.d.ts* ``` ### Migration ```files /MyApp/Migrations Migration1000.cs Migration1001.cs* ``` ### UI ```files /MyApp/wwwroot/admin /sections Bookings.mjs Employees.mjs* index.mjs* index.html ``` Which after downloading a new project just needs to run the [DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) to create the tables required for any new functionality: :::sh npm run migrate ::: ## Run Migrations In order to create the necessary tables for the new functionality, you'll need to run the DB Migrations. If migrations have never been run before, you can run the `migrate` npm script to create the initial database: :::sh npm run migrate ::: If you've already run the migrations before, you can run the `rerun:last` npm script to drop and re-run the last migration: :::sh npm run rerun:last ::: Alternatively you can nuke the App's database (e.g. `App_Data/app.db`) and recreate it from scratch with `npm run migrate`. ## Instant CRUD UI After running the DB migrations, you can hit the ground running and start using the Admin UI to manage the new Data Model RDBMS Tables: :::youtube 8buo_ce3SNM Using AutoQuery CRUD UI in a Text to Blazor App ::: ### Create new Records from Search Dialog We're continually improving the UX of the [AutoQueryGrid Component](/vue/autoquerygrid) used in generating CRUD UIs to enable a more productive and seamless workflow. A change added to that end that you can see in the above video is the ability to add new Records from a Search dialog: ![](/img/posts/text-to-blazor/autoquerygrid-new2.webp) This now lets you start immediately creating new records without needing to create any lookup entries beforehand. ## Audited Data Models The TypeScript Data Models enable a rapid development experience for defining an App's Data Models which are used to generate the necessary AutoQuery CRUD APIs to support an Admin UI. An example of the productivity of this approach is the effortless support for maintaining a detailed audit history for changes to select tables by inheriting from the `AuditBase` base class, e.g: ```ts export class Job extends AuditBase { ... } ``` Which can then be regenerated using the name of the TypeScript Model definitions: :::sh npx okai Jobs.d.ts ::: This will include additional `CreatedBy`, `CreatedDate`, `ModifiedBy`, `ModifiedDate`, `DeletedBy` and `DeletedDate` properties to the specified Table and also generates the necessary [Audit Behaviors](https://docs.servicestack.net/autoquery/crud#apply-generic-crud-behaviors) on the AutoQuery APIs to maintain the audit history for each CRUD operation. ### AutoQuery CRUD Audit Log As the **blazor-admin** and **blazor-vue** templates are configured to use the [AutoQuery CRUD Executable Audit Log](https://docs.servicestack.net/autoquery/audit-log) in its [Configure.AutoQuery.cs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp/Configure.AutoQuery.cs) the Audit Behaviors will also maintain an Audit Trail of all CRUD operations which can be viewed in the Admin UI: ![](/img/posts/text-to-blazor/okai-audit-form.webp) ## TypeScript Schema In addition to being a great DSL for defining Data Models, using TypeScript also lets us define a schema containing all the C# Types, interfaces, and attributes used in defining APIs, DTOs and Data Models in the accompanying [api.d.ts](https://okai.servicestack.com/api.d.ts) file. This now lets us use TypeScript to define the [Bookings.cs](https://github.com/NetCoreTemplates/blazor-vue/blob/main/MyApp.ServiceModel/Bookings.cs) AutoQuery APIs and Data Models which blazor-admin uses instead in its [Bookings.d.ts](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp.ServiceModel/Bookings.d.ts): ```ts /// export type Config = { prompt: "New Booking" api: "~/MyApp.ServiceModel/Bookings.cs" migration: "~/MyApp/Migrations/Migration1001.cs" uiMjs: "~/MyApp/wwwroot/admin/sections/Bookings.mjs" } export enum RoomType { Single, Double, Queen, Twin, Suite, } @Read.route("/bookings","GET") @Read.route("/bookings/{Id}","GET") @Read.description("Find Bookings") @Create.route("/bookings","POST") @Create.description("Create a new Booking") @Update.notes("Find out how to quickly create a C# Bookings App from Scratch") @Update.route("/booking/{Id}","PATCH") @Update.description("Update an existing Booking") @Delete.route("/booking/{Id}","DELETE") @Delete.description("Delete a Booking") @tag("Bookings") @icon({svg:"..."}) @notes("Captures a Persons Name & Room Booking information") @description("Booking Details") @validateHasRole("Employee") export class Booking extends AuditBase { @autoIncrement() id: number @Create.description("Name this Booking is for") @validateNotEmpty() name: string roomType: RoomType @validateGreaterThan(0) roomNumber: number @intlDateTime(DateStyle.Long) bookingStartDate: Date @intlRelativeTime() bookingEndDate?: Date @intlNumber({currency:"USD"}) @validateGreaterThan(0) cost: decimal @ref({model:"nameof(Coupon)",refId:"nameof(Coupon.Id)",refLabel:"nameof(Coupon.Description)"}) @references("typeof(Coupon)") couponId?: string @reference() discount?: Coupon @input({type:"textarea"}) notes?: string cancelled?: boolean @reference({selfId:"nameof(CreatedBy)",refId:"nameof(User.UserName)",refLabel:"nameof(User.DisplayName)"}) employee: User } @tag("Bookings") @icon({svg:"..."}) export class Coupon extends AuditBase { id: string description: string discount: number expiryDate: Date } ``` The benefit of this approach being that you can make a change to the Data Models and rerun the okai tool to regenerate the AutoQuery APIs, DB Migrations and Admin UIs. :::sh npx okai Bookings.d.ts ::: Which will regenerate its: - APIs: [MyApp.ServiceModel/Bookings.cs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp.ServiceModel/Bookings.cs) - DB Migration: [MyApp/Migrations/Migration1000.cs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp/Migrations/Migration1000.cs) - Admin UI: [/wwwroot/admin/sections/Bookings.mjs](https://github.com/NetCoreTemplates/blazor-admin/blob/main/MyApp/wwwroot/admin/sections/Bookings.mjs) What files will be generated is controlled in the `Config` section: ```ts export type Config = { prompt: "New Booking" api: "~/MyApp.ServiceModel/Bookings.cs" migration: "~/MyApp/Migrations/Migration1001.cs" uiMjs: "~/MyApp/wwwroot/admin/sections/Bookings.mjs" } ``` So if you no longer want the code regeneration to update the DB Migration for it, you can just remove it from the Config. ## Customize Data Models The data models defined in the TypeScript Declaration file e.g. `Bookings.d.ts` is what drives the generation of the Data Models, APIs, DB Migrations and Admin UIs. This can be further customized by editing the TypeScript Declaration file and re-running the `okai` tool with the name of the TypeScript Declaration file, e.g. `Bookings.d.ts`: :::sh npx okai Bookings.d.ts ::: Which will re-generate the Data Models, APIs, DB Migrations and Admin UIs based on the updated Data Models. ![](/img/posts/text-to-blazor/okai-Employees.webp) Or add `--watch` to watch the TypeScript Declaration file for changes and automatically re-generate the generated files on Save: :::sh npx okai Bookings.d.ts --watch ::: :::tip You only need to specify the `Bookings.d.ts` TypeScript filename (i.e. not the filepath) from anywhere within your .NET solution ::: One challenge with this approach is that we only have a single class to use to define our attributes for both Request and Response DTOs for all AutoQuery CRUD APIs and Data Models. ### API and Data Model attributes The okai tool resolves some of these issues with smart generation of attributes where "Data Model Attributes" like `[Icon]` class attribute and `[AutoIncrement]` property attributes are only generated on the Data Model: ```ts @icon({svg:"..."}) export class Booking { @autoIncrement() id: number @intlNumber({currency:"USD"}) cost: decimal } ``` Whilst "API Attributes" like `[Tag]` and `[ValidateHasRole]` class attribute and `[ValidateGreaterThan]` property attributes and are only generated on the APIs Request DTOs: ```ts @tag("Bookings") @validateHasRole("Employee") export class Booking { @validateGreaterThan(0) cost: decimal } ``` ### C# Types As JavaScript only has a limited set of types, the TypeScript **api.d.ts** schema also includes the built-in C# Types used when defining APIs, DTOs and Data Models which you'll be able to use when your APIs need to use a specific .NET type, e.g: ```ts export class Booking extends AuditBase { id: number name: string roomNumber: number bookingStartDate: Date bookingEndDate?: DateOnly cost: decimal cancelled?: boolean } ``` Which uses the `DateOnly` and `decimal` .NET Types to generate: ```csharp public class Booking : AuditBase { [AutoIncrement] public int Id { get; set; } public string Name { get; set; } public int RoomNumber { get; set; } public DateTime BookingStartDate { get; set; } public DateOnly? BookingEndDate { get; set; } public decimal Cost { get; set; } public bool? Cancelled { get; set; } } ``` ### API Targeted Attributes When you need to add attributes to a specific API Request DTO you can use a CRUD prefix to have it only applied to that specific AutoQuery API, e.g: ```ts @Read.route("/bookings","GET") @Read.route("/bookings/{Id}","GET") @Create.route("/bookings","POST") ``` Where it would only the generated on the AutoQuery API that it targets, e.g: ```csharp [Route("/bookings", "GET")] [Route("/bookings/{Id}", "GET")] public class QueryBookings : QueryDb { ... } [Route("/bookings", "POST")] public class CreateBooking : ICreateDb, IReturn { ... } ``` In addition to `Create.`, `Read.`, `Update.`, `Delete.` attributes to target specific AutoQuery CRUD APIs, you can also use `Write.` to target all `Create.`, `Update.`, `Delete.` Write APIs. ### Ambiguous Attributes Attributes that can be annotated on both the Data Model and API Request DTOs like `[Notes]` and `[Description]` are only generated on the Data Model and require using targeted attributes to apply to them to API Request DTOs, e.g: ```ts @Read.description("Find Bookings") @Create.description("Create a new Booking") @Update.notes("Find out how to quickly create a C# Bookings App from Scratch") @Update.description("Update an existing Booking") @Delete.description("Delete a Booking") @notes("Captures a Persons Name & Room Booking information") @description("Booking Details") export class Booking extends AuditBase { ... } ``` Where the naked `@notes` and `@description` attributes are only generated on the Data Model whilst the targeted attributes are generated on their respective DTOs, e.g: ```csharp [Description("Find Bookings")] public class QueryBookings : QueryDb { ... } [Description("Create a new Booking")] public class CreateBooking : ICreateDb, IReturn { ... } [Notes("Find out how to quickly create a C# Bookings App from Scratch")] [Description("Update an existing Booking")] public class UpdateBooking : IPatchDb, IReturn { ... } [Description("Delete a Booking")] public class DeleteBooking : IDeleteDb, IReturnVoid { ... } [Description("Booking Details")] [Notes("Captures a Persons Name & Room Booking information")] public class Booking : AuditBase { ... } ``` ### Special Attribute Values There's special behavior for `"nameof(...)"` and `"typeof(...)"` string attribute values where: ```ts export class Booking extends AuditBase { @ref({model: "nameof(Coupon)", refId: "nameof(Coupon.Id)", refLabel: "nameof(Coupon.Description)"}) @references("typeof(Coupon)") couponId?: string } ``` Will be generated with native C# syntax, i.e. instead of as strings: ```csharp public class Booking : AuditBase { [Ref(Model=nameof(Coupon),RefId=nameof(Coupon.Id),RefLabel=nameof(Coupon.Description))] [References(typeof(Coupon))] public string? CouponId { get; set; } } ``` ### Changing Default Attributes To improve the default out-of-the-box experience some attributes are included by default, including: - `[Icon]` attribute on Data Models based on the Data Model name - prevent by adding empty `@icon()` attribute - `[AutoIncrement]` on `id` number properties if no other `[PrimaryKey]` attribute is defined - prevent by adding `@primaryKey()` or `@autoId()` - `[Validate*]` attributes added to Create/Update APIs on non-nullable properties - prevent by adding empty `@validate()` attribute Here's an example which changes the default behavior for the default attributes above: ```ts @icon() export class Todo { @primaryKey() id: number @validate() name: string } ``` Which will generate the C# APIs without the `[Icon]` and `[Validate]` attributes and replace `[AutoIncrement]` with `[PrimaryKey]`, e.g: ```csharp public class CreateTodo : ICreateDb, IReturn { [ValidateGreaterThan(0)] public int Id { get; set; } public string Name { get; set; } } public class Todo { [PrimaryKey] public int Id { get; set; } public string Name { get; set; } } ``` ### Modifying ApplicationUser In many cases the AI Models will want to generate a `User` class for their AI models. But as Blazor Apps are already configured to use an ApplicationUser Identity Auth User class, the C# code generation only generates the User class in a comment so you can merge it with your existing `User` class, e.g: ```csharp /* merge with User DTO /// /// Interface defining the structure for a JobApplication. /// Represents a user's application to a specific job. /// public class User { [AutoIncrement] public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } /// /// Optional URL to the user's resume /// public string? ResumeUrl { get; set; } } */ ``` If you wish to add additional properties, you'll first need to add it your `ApplicationUser` class, e.g: ```csharp public class ApplicationUser : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public string? DisplayName { get; set; } public string? ProfileUrl { get; set; } /// /// Optional URL to the user's resume /// public string? ResumeUrl { get; set; } } ``` You'll then need to regenerate the EF Migration to update the `AspNetUsers` table with the new columns by running the `init-ef` npm script: :::sh npm run init-ef ::: Which will delete the existing Migrations and create a new Migration to update the Identity Auth tables: ```json { "scripts": { "init-ef": "node -e 'fs.readdirSync(`Migrations`).filter(x => !x.startsWith(`Migration`)).forEach(x => fs.rmSync(`Migrations/${x}`))' && dotnet ef migrations add CreateIdentitySchema", } } ``` You can then delete your Primary Database (e.g. App_Data/app.db) and re-run the `migrate` npm script to recreate it: :::sh npm run migrate ::: If you want the additional property to be included in API Responses you'll also need to add it to your `User` DTO, e.g: ```csharp /// /// Public User DTO /// [Alias("AspNetUsers")] public class User { public string Id { get; set; } public string UserName { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public string? DisplayName { get; set; } public string? ProfileUrl { get; set; } public string? ResumeUrl { get; set; } } ``` Which OrmLite and AutoQuery will use to query Identity Auth's `AspNetUsers` table. ### Custom APIs When you need more fine-grained control over the generated APIs, you can "takeover" the generation of an AutoQuery API by explicitly defining it yourself. So if you prefer to use explicit API Request DTOs instead of targeting attributes or need to control the exact properties that are generated in each API, you can define the API Request DTOs yourself where when exists will skip generation for that API. To showcase the differences between the single class approach, you can rewrite the above single class approach with an explicit class for each API: ```ts export enum RoomType { Single, Double, Queen, Twin, Suite, } @tag("Bookings") @notes("Captures a Persons Name & Room Booking information") @route("/bookings","GET") @route("/bookings/{Id}","GET") @autoApply(Behavior.AuditQuery) @description("Find Bookings") export class QueryBookings extends QueryDb { id?: number } @tag("Bookings") @route("/bookings","POST") @autoApply(Behavior.AuditCreate) @description("Create a new Booking") @validateHasRole("Employee") export class CreateBooking implements ICreateDb, IReturn { name?: string roomType?: RoomType @validateGreaterThan(0) roomNumber?: number bookingStartDate?: Date bookingEndDate?: Date @validateGreaterThan(0) cost?: decimal couponId?: string discount?: Coupon @input({type:"textarea"}) notes?: string cancelled?: boolean } @tag("Bookings") @route("/bookings","PATCH") @autoApply(Behavior.AuditModify) @description("Create a new Booking") @validateHasRole("Employee") export class UpdateBooking implements IPatchDb, IReturn { name?: string roomType?: RoomType @validateGreaterThan(0) roomNumber?: number bookingStartDate?: Date bookingEndDate?: Date @validateGreaterThan(0) cost?: decimal couponId?: string discount?: Coupon @input({type:"textarea"}) notes?: string cancelled?: boolean } @tag("Bookings") @route("/bookings/{Id}","DELETE") @autoApply(Behavior.AuditSoftDelete) @description("Delete a Booking") @validateHasRole("Manager") export class DeleteBookings implements IDeleteDb, IReturnVoid { id?: number } @tag("Bookings") @notes("Captures a Persons Name & Room Booking information") @route("/bookings","GET") @route("/bookings/{Id}","GET") @description("Find Bookings") export class Booking extends AuditBase { @autoIncrement() id: number @Create.description("Name this Booking is for") @Create.validateNotEmpty() name: string roomType: RoomType roomNumber: number @intlDateTime(DateStyle.Long) bookingStartDate: Date @intlRelativeTime() bookingEndDate?: Date @intlNumber({currency:"USD"}) cost: decimal @ref({model:"nameof(Coupon)",refId:"nameof(Coupon.Id)",refLabel:"nameof(Coupon.Description)"}) @references("typeof(Coupon)") couponId?: string @reference() discount?: Coupon @Write.input({type:"textarea"}) notes?: string cancelled?: boolean @reference({selfId:"nameof(CreatedBy)",refId:"nameof(User.UserName)",refLabel:"nameof(User.DisplayName)"}) employee: User } @description("Discount Coupons") export class Coupon extends AuditBase { id: string description: string discount: number expiryDate: Date } @tag("Bookings") @route("/coupons","GET") @autoApply(Behavior.AuditQuery) @description("Find Coupons") export class QueryCoupons extends QueryDb { id?: string } @tag("Bookings") @route("/coupons","POST") @autoApply(Behavior.AuditCreate) @description("Create a new Create") @validateHasRole("Employee") export class CreateCoupon implements ICreateDb, IReturn { id: string description: string discount: number expiryDate: Date } @tag("Bookings") @route("/coupons","PATCH") @autoApply(Behavior.AuditModify) @description("Create a new Coupon") @validateHasRole("Employee") export class UpdateCoupon implements IPatchDb, IReturnVoid { id: string description?: string discount?: number expiryDate?: Date } @tag("Bookings") @route("/coupons/{Id}","DELETE") @autoApply(Behavior.AuditSoftDelete) @description("Delete a Coupon") @validateHasRole("Manager") export class DeleteCoupon implements IDeleteDb, IReturnVoid { id?: string } ``` # Self Hosted AI Server for LLMs, Ollama, Comfy UI & FFmpeg Source: https://servicestack.net/posts/ai-server ## AI Server now ready to serve! We're excited to announce the first release of AI Server - a Free OSS self-hosted Docker private gateway to manage API access to multiple LLM APIs, Ollama endpoints, Media APIs, Comfy UI and FFmpeg Agents. :::youtube Ojo80oFQte8 Introducing AI Server ::: ### Centralized Management Designed as a one-stop solution to manage an organization's AI integrations for all their System Apps, by utilizing developer friendly HTTP JSON APIs that supports any programming language or framework. [![](/img/svgs/ai-server-overview.svg)](https://openai.servicestack.net) ### Distribute load across multiple Ollama, Open AI Gateway and Comfy UI Agents It works as a private gateway to process LLM, AI and image transformations requests that any of our Apps need where it dynamically load balances requests across our local GPU Servers, Cloud GPU instances and API Gateways running multiple instances of Ollama, Open AI Chat, LLM Gateway, Comfy UI, Whisper and ffmpeg providers. In addition to maintaining a history of AI Requests, it also provides file storage for its CDN-hostable AI generated assets and on-the-fly, cacheable image transformations. ### Native Typed Integrations Uses [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) to enable simple, native typed integrations for most popular Web, Mobile and Desktop languages including: C#, TypeScript, JavaScript, Python, Java, Kotlin, Dart, PHP, Swift, F# and VB.NET. Each AI Feature supports multiple call styles for optimal integration of different usages: - **Synchronous API** · Simplest API ideal for small workloads where the Response is returned in the same Request - **Queued API** · Returns a reference to the queued job executing the AI Request which can be used to poll for the API Response - **Reply to Web Callback** · Ideal for reliable App integrations where responses are posted back to a custom URL Endpoint ### Live Monitoring and Analytics Monitor performance and statistics of all your App's AI Usage, real-time logging of executing APIs with auto archival of completed AI Requests into monthly rolling SQLite databases. ### Protected Access with API Keys AI Server utilizes [Simple Auth with API Keys](https://docs.servicestack.net/auth/admin-apikeys) letting Admins create and distribute API Keys to only allow authorized clients to access their AI Server's APIs, which can be optionally further restricted to only [allow access to specific APIs](https://docs.servicestack.net/auth/apikeys#creating-user-api-keys). ## Install AI Server can be installed on macOS and Linux with Docker by running [install.sh](https://github.com/ServiceStack/ai-server/blob/main/install.sh): 1. Clone the AI Server repository from GitHub: :::sh `git clone https://github.com/ServiceStack/ai-server` ::: 2. Run the Installer :::sh `cd ai-server && cat install.sh | bash` ::: The installer will detect common environment variables for the supported AI Providers like OpenAI, Google, Anthropic, and others, and prompt ask you if you want to add them to your AI Server configuration. ### Optional - Install ComfyUI Agent If your server also has a GPU you can ask the installer to also install the [ComfyUI Agent](/ai-server/comfy-extension): The ComfyUI Agent is a separate Docker agent for running [ComfyUI](https://www.comfy.org), [Whisper](https://github.com/openai/whisper) and [FFmpeg](https://www.ffmpeg.org) on servers with GPUs to handle AI Server's [Image](https://docs.servicestack.net/ai-server/transform/image) and [Video transformations](https://docs.servicestack.net/ai-server/transform/video) and Media Requests, including: - [Text to Image](https://docs.servicestack.net/ai-server/text-to-image) - [Image to Text](https://docs.servicestack.net/ai-server/image-to-text) - [Image to Image](https://docs.servicestack.net/ai-server/image-to-image) - [Image with Mask](https://docs.servicestack.net/ai-server/image-with-mask) - [Image Upscale](https://docs.servicestack.net/ai-server/image-upscale) - [Speech to Text](https://docs.servicestack.net/ai-server/speech-to-text) - [Text to Speech](https://docs.servicestack.net/ai-server/text-to-speech) #### Comfy UI Agent Installer To install the ComfyUI Agent on a separate server (with a GPU), you can clone and run the ComfyUI Agent installer on that server instead: 1. Clone the Comfy Agent :::sh `git clone https://github.com/ServiceStack/agent-comfy.git` ::: 2. Run the Installer :::sh `cd agent-comfy && cat install.sh | bash` ::: ## Running in Production We've been developing and running AI Server for several months now, processing millions of LLM and Comfy UI Requests to generate Open AI Chat Answers and Generated Images used to populate the [pvq.app](https://pvq.app) and [blazordiffusion.com](https://blazordiffusion.com) websites. Our production instance with more info about AI Server is available at: :::{.m-0 .text-center .text-2xl .font-semibold .text-indigo-600} https://openai.servicestack.net ::: [![](/img/posts/ai-server/ai-server-languages.png)](https://openai.servicestack.net) ## API Explorer Whilst our production instance is protected by API Keys, you can still use it to explore available APIs in its API Explorer: :::{.m-0 .text-center .text-2xl .font-semibold .text-indigo-600} [https://openai.servicestack.net/ui/](https://openai.servicestack.net/ui/OpenAiChatCompletion) ::: ## Documentation The documentation for AI Server is being maintained at: :::{.m-0 .text-center .text-2xl .font-semibold .text-indigo-600} https://docs.servicestack.net/ai-server/ ::: ## Built-in UIs Built-in UIs allow users with API Keys access to custom UIs for different AI features [![](/img/posts/ai-server/ai-server-builtin-uis.png)](https://openai.servicestack.net) ## Admin UIs Use Admin UI to manage API Keys that can access AI Server APIs and Features [![](/img/posts/ai-server/ai-server-admin-uis.png)](https://openai.servicestack.net) ## Features The current release of AI Server supports a number of different modalities, including: ### Large Language Models - [Open AI Chat](https://docs.servicestack.net/ai-server/chat) - Support for Ollama endpoints - Support for Open Router, Anthropic, Open AI, Mistral AI, Google and Groq API Gateways ### Comfy UI Agent / Replicate / DALL-E 3 - [Text to Image](https://docs.servicestack.net/ai-server/text-to-image) ### Comfy UI Agent - [Image to Image](https://docs.servicestack.net/ai-server/image-to-image) - [Image Upscaling](https://docs.servicestack.net/ai-server/image-upscale) - [Image with Mask](https://docs.servicestack.net/ai-server/image-with-mask) - [Image to Text](https://docs.servicestack.net/ai-server/image-to-text) - [Text to Speech](https://docs.servicestack.net/ai-server/text-to-speech) - [Speech to Text](https://docs.servicestack.net/ai-server/speech-to-text) ### FFmpeg - [Image Transformations](https://docs.servicestack.net/ai-server/transform/image) - **Crop Image** - Crop an image to a specific size - **Convert Image** - Convert an image to a different format - **Scale Image** - Scale an image to a different resolution - **Watermark Image** - Add a watermark to an image - [Video Transformations](https://docs.servicestack.net/ai-server/transform/video) - **Crop Video** - Crop a video to a specific size - **Convert Video** - Convert a video to a different format - **Scale Video** - Scale a video to a different resolution - **Watermark Video** - Add a watermark to a video - **Trim Video** - Trim a video to a specific length ### Managed File Storage - Blob Storage - isolated and restricted by API Key ## AI Server API Examples To simplify integrations with AI Server each API Request can be called with 3 different call styles to better support different use-cases and integration patterns. ### Synchronous Open AI Chat Example The **Synchronous API** is the simplest API ideal for small workloads where the Response is returned in the same Request: ```csharp var client = new JsonApiClient(baseUrl); client.BearerToken = apiKey; var response = client.Post(new OpenAiChatCompletion { Model = "mixtral:8x22b", Messages = [ new() { Role = "user", Content = "What's the capital of France?" } ], MaxTokens = 50 }); var answer = response.Choices[0].Message.Content; ``` ### Synchronous Media Generation Request Example Other AI Requests can be called synchronously in the same way where its API is named after the modality it implements, e.g. you'd instead call `TextToImage` to generate an Image from a Text description: ```csharp var response = client.Post(new TextToImage PositivePrompt = "A serene landscape with mountains and a lake", Model = "flux-schnell", Width = 1024, Height = 1024, BatchSize = 1 }); File.WriteAllBytes(saveToPath, response.Results[0].Url.GetBytesFromUrl()); ``` ### Queued Open AI Chat Example The **Queued API** immediately Returns a reference to the queued job executing the AI Request: ```csharp var response = client.Post(new QueueOpenAiChatCompletion { Request = new() { Model = "gpt-4-turbo", Messages = [ new() { Role = "system", Content = "You are a helpful AI assistant." }, new() { Role = "user", Content = "How do LLMs work?" } ], MaxTokens = 50 } }); ``` Which can be used to poll for the API Response of any Job by calling `GetOpenAiChatStatusResponse` and checking when its state has finished running to get the completed `OpenAiChatResponse`: ```csharp GetOpenAiChatStatusResponse status = new(); while (status.JobState is BackgroundJobState.Started or BackgroundJobState.Queued) { status = await client.GetAsync(new GetOpenAiChatStatus { RefId = response.RefId }); await Task.Delay(1000); } var answer = status.Result.Choices[0].Message.Content; ``` ### Queued Media Artifact Generation Request Example Most other AI Server Requests are Artifact generation requests which would instead call `GetArtifactGenerationStatus` to get the artifacts response of a queued job, e.g: ```csharp var response = client.Post(new QueueTextToImage { PositivePrompt = "A serene landscape with mountains and a lake", Model = "flux-schnell", Width = 1024, Height = 1024, BatchSize = 1 }); // Poll for Job Completion Status GetArtifactGenerationStatusResponse status = new(); while (status.JobState is BackgroundJobState.Queued or BackgroundJobState.Started) { status = client.Get(new GetArtifactGenerationStatus { JobId = response.JobId }); Thread.Sleep(1000); } File.WriteAllBytes(saveToPath, status.Results[0].Url.GetBytesFromUrl()); ``` ### Queued Media Text Generation Request Example Whilst the Media API Requests that generates text like `SpeechToText` or `ImageToText` would instead call `GetTextGenerationStatus` to get the text response of a queued job, e.g: ```csharp using var fsAudio = File.OpenRead("files/test_audio.wav"); var response = client.PostFileWithRequest(new QueueSpeechToText(), new UploadFile("test_audio.wav", fsAudio, "audio")); // Poll for Job Completion Status GetTextGenerationStatusResponse status = new(); while (status.JobState is BackgroundJobState.Started or BackgroundJobState.Queued) { status = client.Get(new GetTextGenerationStatus { RefId = response.RefId }); Thread.Sleep(1000); } var answer = status.Results[0].Text; ``` ### Open AI Chat with Callback Example The Queued API also accepts a **Reply to Web Callback** for a more reliable push-based App integration where responses are posted back to a custom URL Endpoint: ```csharp var correlationId = Guid.NewGuid().ToString("N"); var response = client.Post(new QueueOpenAiChatCompletion { //... ReplyTo = $"https://example.org/api/OpenAiChatResponseCallback?CorrelationId=${correlationId}" }); ``` Your callback can add any additional metadata on the callback to assist your App in correlating the response with the initiating request which just needs to contain the properties of the `OpenAiChatResponse` you're interested in along with any metadata added to the callback URL, e.g: ```csharp public class OpenAiChatResponseCallback : IPost, OpenAiChatResponse, IReturnVoid { public Guid CorrelationId { get; set; } } public object Post(OpenAiChatResponseCallback request) { // Handle OpenAiChatResponse callabck } ``` Unless your callback API is restricted to only accept requests from your AI Server, you should include a unique Id like a `Guid` in the callback URL that can be validated against an initiating request to ensure the callback can't be spoofed. ## Feedback Feel free to reach us at [ai-server/discussions](https://github.com/ServiceStack/ai-server/discussions) with any AI Server questions. # .NET 8 Templates migrated to use Kamal for deployments Source: https://servicestack.net/posts/kamal-deployments Since introducing [GitHub Actions support to our templates](https://docs.servicestack.net/ssh-docker-compose-deploment), we've promoted the simplified deployments, focusing on tooling like SSH and Docker Compose to give the most portability to projects by default. This was partly inspired by the fact that cloud providers value offerings have been decreasing, especially over the last 5 years. We've previously showed [the significant savings](https://servicestack.net/posts/hetzner-cloud) available by utilizing of hosting providers like Hetzner (who we've been using for several years), and moved all our templates and live demos to Hetzner resulting in a roughly **$0.50 per month** cost per .NET App. Along with this decreasing in value from the major cloud vendors, and the general hardware improvements, we've also been leaning into [using SQLite for server .NET Apps](/posts/scalable-sqlite), using it as the primary database for some of our larger example applications like [pvq.app](https://pvq.app), [blazordiffusion.com](https://blazordiffusion.com), and most recently, [AI Server](https://openai.servicestack.net). We're delighted to see that the folks at BaseCamp are estimating to [save millions from their cloud exit](https://world.hey.com/dhh/our-cloud-exit-savings-will-now-top-ten-million-over-five-years-c7d9b5bd) and have doubled down on their general purpose Docker deployment solutions with their initial MRSK project, that's now known as Kamal. ### Use Kamal to deploy .NET Apps to any Linux server :::youtube -mDJfRG8mLQ Use Kamal with GitHub Actions to deploy .NET Apps to any Linux server ::: ## What is [Kamal](https://kamal-deploy.org/)? Kamal is a tool that offers the same flexibility by wrapping up the use of fundamental tooling like SSH and Docker into a great CLI tool that tries to make the management of containerized applications, enabling them to be deployed anywhere there is a Linux host that is accessible via SSH. It handles reverse proxy of web traffic automatically, as well as even the initial setup of the reverse proxy and related tooling to any target Linux host. This means you get the same great ergonomics of just pointing your DNS and configuration file to a server, and *Kamal takes care of the rest*, including TLS certificates via LetsEncrypt. It even has commands that allow you to check on your running applications, view logs etc and all you need to do is run the commands from your local repository directory. While our own templates have used the same approach for GitHub Actions, the usage was always awkward and lacked any dedicated CLI tooling you could run locally to check on your running applications. ## What's in the templates? We still believe that having a CI process is important, and while Kamal deployments are repeatable from your local machine and uses locking to avoid multiple developers deploying changes, the single consistent process of a CI is hard to beat. So while we have moved the templates to use Kamal, we've incorporated GitHub Actions by default so you can still get the benefits of running commands like `kamal app logs` locally from your development machine when looking at production issues, but have that consistent workflow for deployment on your repositories GitHub Actions. ## How it works One of the big benefits of Kamal is the focus on ergonomics and the really well done documentation that the BaseCamp team has put together. So if you need to know more about Kamal, [checkout their docs](https://kamal-deploy.org/docs/). For the ServiceStack templates, you will need to add a valid `PRIVATE_SSH_KEY` as a GitHub Actions secret to get it working along with the customization of your `config/deploy.yml` file which is a part of any Kamal setup. In short, you will need: - Get a Linux host running with SSH access - Update your DNS configuration with an A record pointing to that hosts IP address - Create a new project using one of our updated templates using a command like: :::sh x new blazor-vue MyApp ::: Update the `config/deploy.yml` with the following details: ### GitHub Container Registry Image Update with your preferred container image name: ```yml # Name of the container image image: my-user/myapp ``` ### Server Web Configure with your Linux Host IP Address: ```yml servers: # IP address of server, optionally use env variable web: - 123.123.123.123 ``` Alternatively, you can use an environment variable for the server IP address, e.g: ```yml web: - <%= ENV['KAMAL_DEPLOY_IP'] %> ``` ### Proxy Host Configure with your domain pointing to the same IP as your host: ```yml proxy: ssl: true host: myapp.example.com ``` ### Health Checks The template includes the use of ASP.NET Core Health Checks, that use the default Kamal path of `/up` to check if the application is running before deploying. ```csharp public class HealthCheck : IHealthCheck { public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken token = default) { // Perform health check logic here return HealthCheckResult.Healthy(); } } ``` Kamal checks this path before deploying your application, so you can add any custom health checks to this path to ensure your application is ready to receive traffic. ## GitHub Repository With your application created and configured for deployment, you can create a new GitHub Repository and add the GitHub Actions Secret of `PRIVATE_SSH_KEY` which should be a separate SSH key for deployments that has access to your Linux host. You can use the GitHub CLI to do of these steps. ```bash gh repo create ``` When prompted, create an empty repository. Then add the `PRIVATE_SSH_KEY` secret. ``` gh secret set PRIVATE_SSH_KEY < deploy-key ``` Where `deploy-key` is your deployment specific SSH key file. Once created, you can follow the steps in your empty repository to init your templated `MyApp` project and push your initial commit. If you're deploy.yml config and DNS was setup correctly, the GitHub Action will do the following: - Build and test your application running the MyApp.Tests project by default - Publish your application as a Docker container to GitHub's `ghcr.io` repository - Use Kamal to initialize your Linux host to be able to run Kamal applications and use their default `kamal-proxy` - Fix volume permissions your for application due to ASP.NET containerization not running as root user in the container. - Run your `AppTasks=migrate` command before running your application initializing the SQLite database - Run your AppHost using `kamal deploy -P --version latest` command. ## Summary We're excited to be moving our templates to Kamal for deployments as it has distilled the simple approach we have baked in our templates for a number of years while massively improving the ergonomics. We're excited to see what the BaseCamp team does with the project, and we're looking forward to seeing the community grow around it. If you have any questions about the templates or Kamal, feel free to reach out to us on our Discord, GitHub Discussions or Customer Forums. # DTOs in all languages downloadable without .NET Source: https://servicestack.net/posts/npx-get-dtos To make it easier to consume ServiceStack APIs in any language, we've added the ability to download Typed DTOs from any ServiceStack API in all languages without needing .NET installed with the new `npx get-dtos` npm script. It has the same syntax and functionality as the `x` dotnet tool for adding and updating ServiceStack References where in most cases you can replace `x ` with `npx get-dtos ` to achieve the same result. Running `npx get-dtos` without any arguments will display the available options: get-dtos Update all ServiceStack References in directory (recursive) get-dtos Update existing ServiceStack Reference (e.g. dtos.cs) get-dtos Add ServiceStack Reference and save to file name get-dtos csharp Add C# ServiceStack Reference (Alias 'cs') get-dtos typescript Add TypeScript ServiceStack Reference (Alias 'ts') get-dtos javascript Add JavaScript ServiceStack Reference (Alias 'js') get-dtos python Add Python ServiceStack Reference (Alias 'py') get-dtos dart Add Dart ServiceStack Reference (Alias 'da') get-dtos php Add PHP ServiceStack Reference (Alias 'ph') get-dtos java Add Java ServiceStack Reference (Alias 'ja') get-dtos kotlin Add Kotlin ServiceStack Reference (Alias 'kt') get-dtos swift Add Swift ServiceStack Reference (Alias 'sw') get-dtos fsharp Add F# ServiceStack Reference (Alias 'fs') get-dtos vbnet Add VB.NET ServiceStack Reference (Alias 'vb') get-dtos tsd Add TypeScript Definition ServiceStack Reference Options: -h, --help, ? Print this message -v, --version Print tool version version --include Include all APIs in specified tag group --qs Add query string to Add ServiceStack Reference URL --verbose Display verbose logging --ignore-ssl-errors Ignore SSL Errors ## Reusable DTOs and Reusable Clients in any language A benefit of [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) is that only an API DTOs need to be generated which can then be used to call any remote instance running that API. E.g. DTOs generated for our deployed AI Server instance at [openai.servicestack.net](https://openai.servicestack.net) can be used to call any self-hosted AI Server instance, likewise the same generic client can also be used to call any other ServiceStack API. ### Typed Open AI Chat & Ollama APIs in 11 Languages A good example of its versatility is in the [Typed OpenAI Chat & Ollama APIs](/posts/typed-openai-chat-ollama-apis) in which AI Server's Typed DTOs can be used to call **any Open AI Chat compatible API** in its 11 supported languages. ### TypeScript Example For example you can get the TypeScript DTOs for the just released [AI Server](/posts/ai-server) by: 1. Installing the `@servicestack/client` npm package: :::copy npm install @servicestack/client ::: 2. Download AI Server's TypeScript DTOs: :::copy `npx get-dtos typescript https://openai.servicestack.net` ::: Which just like the `x` tool will add the TypeScript DTOs to the `dtos.ts` file ### Calling Ollama from TypeScript Call Ollama by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```ts import { JsonServiceClient } from "@servicestack/client" import { OpenAiChatCompletion } from "./dtos" const client = new JsonServiceClient(baseUrl) const response = await client.postToUrl("/v1/chat/completions", new OpenAiChatCompletion({ model: "mixtral:8x22b", messages: [ { role: "user", content: "What's the capital of France?" } ], max_tokens: 50 }) ) const answer = response.choices[0].message.content ``` ### Update TypeScript DTOs And later update all TypeScript ServiceStack References in the current directory with: :::sh `npx get-dtos typescript` ::: ### Install and Run in a single command This can be used as a more flexible alternative to the `x` tool where it's often easier to install node in CI environments than a full .NET SDK and easier to use npx scripts than global dotnet tools. For example you can use the `--yes` flag to implicitly install (if needed) and run the `get-dtos` script in a single command, e.g: :::sh `npx --yes get-dtos typescript` ::: ### C# Example As such you may want want to replace the `x` dotnet tool with `npx get-dtos` in your C#/.NET projects as well which can either use the language name or its more wrist-friendly shorter alias, e.g: :::sh `npx get-dtos cs https://openai.servicestack.net` ::: Then later update all C# DTOs in the current directory (including sub directories) with: :::sh `npx get-dtos cs` ::: # ServiceStack.Swift client library rewritten for Swift 6 Source: https://servicestack.net/posts/swift6-upgrade ![](https://docs.servicestack.net/img/pages/servicestack-reference/swift-logo-banner.jpg) As part of the release of [AI Server](/posts/ai-server) we've upgraded all generic service client libraries to support multiple file uploads with API requests to take advantage of AI Server APIs that accept file uploads like [Image to Image](https://docs.servicestack.net/ai-server/image-to-image), [Speech to Text](https://docs.servicestack.net/ai-server/speech-to-text) or its [FFmpeg Image](https://docs.servicestack.net/ai-server/transform/image) and [Video Transforms](https://docs.servicestack.net/ai-server/transform/video). ## ServiceStack.Swift rewritten for Swift 6 [ServiceStack.Swift](https://github.com/ServiceStack/ServiceStack.Swift) received the biggest upgrade, which was also rewritten to take advantage of Swift 6 features, including Swift promises which replaced the previous [PromiseKit](https://github.com/mxcl/PromiseKit) dependency - making it now dependency-free! For example you can request a [Speech to Text](https://docs.servicestack.net/ai-server/speech-to-text) transcription by sending an audio file to the `SpeechToText` API using the new `postFilesWithRequest` method: ### Calling AI Server to transcribe an Audio Recording ```swift let client = JsonServiceClient(baseUrl: "https://openai.servicestack.net") client.bearerToken = apiKey let request = SpeechToText() request.refId = "uniqueUserIdForRequest" let response = try client.postFilesWithRequest(request:request, file:UploadFile(fileName:"audio.mp3", data:mp3Data, fieldName:"audio")) Inspect.printDump(response) ``` ### Async Upload Files with API Example Alternatively use the new `postFileWithRequestAsync` method to call the API asynchronously using [Swift 6 Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) new **async/await** feature: ```swift let response = try await client.postFileWithRequestAsync(request:request, file:UploadFile(fileName:"audio.mp3", data:mp3Data, fieldName:"audio")) Inspect.printDump(response) ``` ### Multiple file upload with API Request examples Whilst the `postFilesWithRequest` methods can be used to upload multiple files with an API Request. e.g: ```swift let request = WatermarkVideo() request.position = .BottomRight let response = try client.postFilesWithRequest(request: request, files: [ UploadFile(fileName: "video.mp4", data:videoData, fieldName:"video"), UploadFile(fileName: "mark.jpg", data:imgData, fieldName:"watermark") ]) ``` Async Example: ```swift let response = try await client.postFilesWithRequestAsync(request: request, files: [ UploadFile(fileName: "video.mp4", data:videoData, fieldName:"video"), UploadFile(fileName: "mark.jpg", data:imgData, fieldName:"watermark") ]) ``` ### Sending typed Open AI Chat Ollama Requests with Swift Even if you're not running AI Server you can still use its typed DTOs to call any compatible Open AI Chat Compatible API like a self-hosted [Ollama](https://ollama.com) API. To call an Ollama endpoint from Swift: 1. Include `ServiceStack` package in your projects `Package.swift` ```swift dependencies: [ .package(url: "https://github.com/ServiceStack/ServiceStack.Swift.git", Version(6,0,0)..` ::: Download AI Server's C# DTOs with [x dotnet tool](https://docs.servicestack.net/dotnet-tool): :::copy `x csharp https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with `JsonApiClient`: ```csharp using ServiceStack; var client = new JsonApiClient(baseUrl); var result = await client.PostAsync("/v1/chat/completions", new OpenAiChatCompletion { Model = "mixtral:8x22b", Messages = [ new () { Role = "user", Content = "What's the capital of France?" } ], MaxTokens = 50 }); ``` ## TypeScript Install the `@servicestack/client` npm package: :::copy npm install @servicestack/client ::: Download AI Server's TypeScript DTOs: :::copy `npx get-dtos typescript https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```ts import { JsonServiceClient } from "@servicestack/client" import { OpenAiChatCompletion } from "./dtos" const client = new JsonServiceClient(baseUrl) const result = await client.postToUrl("/v1/chat/completions", new OpenAiChatCompletion({ model: "mixtral:8x22b", messages: [ { role: "user", content: "What's the capital of France?" } ], max_tokens: 50 }) ) ``` ## JavaScript Save [servicestack-client.mjs](https://unpkg.com/@servicestack/client@2/dist/servicestack-client.mjs) to your project Define an Import Map referencing its saved location ```html ``` Download AI Server's ESM JavaScript DTOs: :::copy `npx get-dtos mjs https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```js import { JsonServiceClient } from "@servicestack/client" import { OpenAiChatCompletion } from "./dtos.mjs" const client = new JsonServiceClient(baseUrl) const result = await client.postToUrl("/v1/chat/completions", new OpenAiChatCompletion({ model: "mixtral:8x22b", messages: [ { role: "user", content: "What's the capital of France?" } ], max_tokens: 50 }) ) ``` ## Python Install the `servicestack` PyPI package: :::copy pip install servicestack ::: Download AI Server's Python DTOs: :::copy `npx get-dtos python https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```py from servicestack import JsonServiceClient from my_app.dtos import * client = JsonServiceClient(baseUrl) result = client.post_url("/v1/chat/completions",OpenAiChatCompletion( model="mixtral:8x22b", messages=[ OpenAiMessage(role="user",content="What's the capital of France?") ], max_tokens=50 )) ``` ## Dart Include `servicestack` package in your projects `pubspec.yaml`: :::copy servicestack: ^3.0.1 ::: Download AI Server's Dart DTOs: :::copy `npx get-dtos dart https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```dart import 'dart:io'; import 'dart:typed_data'; import 'package:servicestack/client.dart'; var client = JsonServiceClient(baseUrl); var result = await client.postToUrl('/v1/chat/completions', OpenAiChatCompletion() ..model = 'mixtral:8x22b' ..max_tokens = 50 ..messages = [ OpenAiMessage() ..role = 'user' ..content = "What's the capital of France?" ]); ``` ### PHP Include `servicestack/client` package in your projects `composer.json`: :::copy "servicestack/client": "^1.0" ::: Download AI Server's PHP DTOs: :::copy `npx get-dtos php https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```php use ServiceStack\JsonServiceClient; use dtos\OpenAiChatCompletion; use dtos\OpenAiMessage; $client = new JsonServiceClient(baseUrl); $client->bearerToken = apiKey; /** @var {OpenAiChatCompletionResponse} $response */ $result = $client->postUrl('/v1/chat/completions', body: new OpenAiChatCompletion( model: "mixtral:8x22b", messages: [ new OpenAiMessage( role: "user", content: "What's the capital of France?" ) ], max_tokens: 50 )); ``` ## Java Include `net.servicestack:client` package in your projects `build.gradle`: :::copy implementation 'net.servicestack:client:1.1.3' ::: Download AI Server's Java DTOs: :::copy `npx get-dtos java https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```java import net.servicestack.client.*; import java.util.Collections; var client = new JsonServiceClient(baseUrl); OpenAiChatResponse result = client.post("/v1/chat/completions", new OpenAiChatCompletion() .setModel("mixtral:8x22b") .setMaxTokens(50) .setMessages(Utils.createList(new OpenAiMessage() .setRole("user") .setContent("What's the capital of France?") )), OpenAiChatResponse.class); ``` ## Kotlin Include `net.servicestack:client` package in your projects `build.gradle`: :::copy implementation 'net.servicestack:client:1.1.3' ::: Download AI Server's Kotlin DTOs: :::copy `npx get-dtos kotlin https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with JsonServiceClient: ```kotlin package myapp import net.servicestack.client.* val client = JsonServiceClient(baseUrl) val result: OpenAiChatResponse = client.post("/v1/chat/completions", OpenAiChatCompletion().apply { model = "mixtral:8x22b" messages = arrayListOf(OpenAiMessage().apply { role = "user" content = "What's the capital of France?" }) maxTokens = 50 }, OpenAiChatResponse::class.java) ``` ## Swift Include `ServiceStack` package in your projects `Package.swift` ```swift dependencies: [ .package(url: "https://github.com/ServiceStack/ServiceStack.Swift.git", Version(6,0,0)..` ::: Download AI Server's F# DTOs with [x dotnet tool](https://docs.servicestack.net/dotnet-tool): :::copy `x fsharp https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with `JsonApiClient`: ```fsharp open ServiceStack open ServiceStack.Text let client = new JsonApiClient(baseUrl) let result = client.Post("/v1/chat/completions", OpenAiChatCompletion( Model = "mixtral:8x22b", Messages = ResizeArray [ OpenAiMessage( Role = "user", Content = "What's the capital of France?" ) ], MaxTokens = 50)) ``` ## VB.NET Install the `ServiceStack.Client` NuGet package: :::copy `` ::: Download AI Server's VB.NET DTOs with [x dotnet tool](https://docs.servicestack.net/dotnet-tool): :::copy `x vbnet https://openai.servicestack.net` ::: Call API by sending `OpenAiChatCompletion` Request DTO with `JsonApiClient`: ```vb Imports ServiceStack Imports ServiceStack.Text Dim client = New JsonApiClient(baseUrl) Dim result = Await client.PostAsync(Of OpenAiChatResponse)( "/v1/chat/completions", New OpenAiChatCompletion() With { .Model = "mixtral:8x22b", .Messages = New List(Of OpenAiMessage) From { New OpenAiMessage With { .Role = "user", .Content = "What's the capital of France?" } }, .MaxTokens = 50 }) ``` # Simple API Keys Credentials Provider for .NET 8 C# Microservices Source: https://servicestack.net/posts/apikey-credentials-auth The usability of the [Simple Auth with API Keys](https://docs.servicestack.net/auth/admin-apikeys) story has been significantly improved with the new `ApiKeyCredentialsProvider` which enables .NET Microservices to provide persistent UserSession-like behavior using simple API Keys which can be configured together with the `AuthSecretAuthProvider` and `ApiKeysFeature` to enable a Credentials Auth implementation which users can use with their API Keys or Admin AuthSecret. A typical configuration for .NET Microservices looking to enable Simple Auth access whose APIs are protected by API Keys and their Admin functionality protected by an Admin Auth Secret can be configured with: ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature([ new ApiKeyCredentialsProvider(), new AuthSecretAuthProvider("MyAuthSecret"), ])); services.AddPlugin(new SessionFeature()); services.AddPlugin(new ApiKeysFeature()); }) .ConfigureAppHost(appHost => { using var db = HostContext.AppHost.GetDbConnection(); appHost.GetPlugin().InitSchema(db); }); } ``` When registered a Credentials Auth dialog will appear for [ServiceStack Built-in UIs](https://servicestack.net/auto-ui) allowing users to Sign In with their **API Keys** or Admin **Auth Secret**. ![](/img/posts/apikey-credentials-auth/ai-server-auth-apiexplorer.png) ### Session Auth with API Keys Behind the scenes this creates a Server [Auth Session](https://docs.servicestack.net/auth/sessions) but instead of maintaining an Authenticated User Session it saves the API Key in the session then attaches the API Key to each request. This makes it possible to make API Key validated requests with just a session cookie instead of requiring resubmission of API Keys for each request. ### AI Server This is an ideal Auth Configuration for .NET Docker Appliances and Microservices like [AI Server](/posts/ai-server) that don't need the complexity of ASP .NET Core's Identity Auth machinery and just want to restrict access to their APIs with API Keys and restrict Admin functionality to Administrator's with an Auth Secret. The benefit of `ApiKeyCredentialsProvider` is that it maintains a persistent Session so that end users only need to enter their API Key a single time and they'll be able to navigate to all of AI Server's protected pages using their API Key maintained in their Server User Session without needing to re-enter it for each UI and every request. ### User Access with API Keys AI Server uses **API Keys** to restrict Access to their AI Features to **authorized Users** with Valid API Keys who are able to use its Built-in UIs for its AI Features with the Users preferred Name and issued API Key: ![](/img/posts/apikey-credentials-auth/ai-server-auth-user.png) After signing in a single time they'll be able to navigate to any protected page and start using AI Server's AI features: ![](/img/posts/apikey-credentials-auth/ai-server-auth-user-chat.png) ### User Access to API Explorer This also lets users use their existing Auth Session across completely different UIs like [API Explorer](https://docs.servicestack.net/api-explorer) where they'll have the same access to APIs as they would when calling APIs programatically with their API Keys, e.g: ![](/img/posts/apikey-credentials-auth/ai-server-auth-apiexplorer-api.png) ### Coarse or fine-grained API Key access By default **any** Valid API Key can access restricted services by `[ValidateApiKey]` ```csharp [ValidateApiKey] public class Hello : IGet, IReturn { public required string Name { get; set; } } ``` ### API Key Scopes API Keys can be given elevated privileges where only Keys with user defined scopes: ![](https://docs.servicestack.net/img/pages/auth/simple/admin-ui-apikeys-edit.png) Are allowed to access APIs restricted with that scope: ```csharp [ValidateApiKey("todo:read")] public class QueryTodos : QueryDb { public long? Id { get; set; } public List? Ids { get; set; } public string? TextContains { get; set; } } ``` ### Restricted API Keys to specific APIs API Keys can also be locked down to only be allowed to call specific APIs: ![](https://docs.servicestack.net/img/pages/auth/simple/admin-ui-apikeys-restrict-to.png) ## Admin Access AI Server also maintains an Admin UI and Admin APIs that are only accessible to **Admin** users who Authenticate with the App's configured Admin Auth Secret who are able to access AI Server's Admin UIs to monitor Live AI Requests, create new User API Keys, Manage registered AI Providers, etc. ![](/img/posts/apikey-credentials-auth/ai-server-auth-admin-jobs.png) ### Admin Restricted APIs You can restrict APIs to Admin Users by using `[ValidateAuthSecret]`: ```csharp [Tag(Tags.Admin)] [ValidateAuthSecret] [Api("Add an AI Provider to process AI Requests")] public class CreateAiProvider : ICreateDb, IReturn { //... } ``` Which are identified in API Explorer with a **padlock** icon whilst APIs restricted by API Key are identified with a **key** icon: ![](/img/posts/apikey-credentials-auth/ai-server-auth-apiexplorer-admin.png) # Podcasts now in Razor SSG Source: https://servicestack.net/posts/razor-ssg-podcasts ## Razor SSG now supports Podcasts! [Razor SSG](https://razor-ssg.web-templates.io) is our FREE Project Template for creating fast, statically generated Websites and Blogs with Markdown & C# Razor Pages. A benefit of using Razor SSG to maintain our [github.com/ServiceStack/servicestack.net](https://github.com/ServiceStack/servicestack.net) website is that any improvements added to **servicestack.net** end up being rolled into the Razor SSG Project Template for everyone else to enjoy. The latest feature recently added is [ServiceStack Podcasts](https://servicestack.net/podcasts), providing an easy alternative to learning about new features in our [TL;DR Release Notes](https://docs.servicestack.net/releases/v8_04) during a commute as well as a fun and more informative experience whilst reading [blog posts](https://servicestack.net/blog). The same podcast feature has now been rolled into the Razor SSG template allowing anyone to add the same feature to their Razor SSG Websites which can be developed and hosted for FREE on GitHub Pages CDN: ### Create a new Razor SSG Project
Razor SSG
### Markdown Powered The Podcast feature is very similar to the Markdown Blog Posts where each podcast is a simple `.md` Markdown page seperated by a publish date and its unique slug, e.g: **[/_podcasts](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_podcasts)** ```files /_pages /_podcasts config.json 2024-10-02_razor-ssg-podcasts.md 2024-09-19_scalable-sqlite.md 2024-09-17_sqlite-request-logs.md ... /_posts /_videos /_whatsnew ``` All editable content within different Podcast pages like the Podcast Sidebar is customizable within [_podcasts/config.json](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/config.json). [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-layout.webp)](https://razor-ssg.web-templates.io/podcasts) ### Podcast Page Whilst all content about a podcast is contained within its `.md` file and frontmatter which just like Blog Posts can contain interactive Vue Components and custom [Markdown Containers](https://razor-press.web-templates.io/containers). The [Backgrounds Jobs Podcast Page](https://razor-ssg.web-templates.io/podcasts/background-jobs) is a good example of this where its [2024-09-12_background-jobs.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/2024-09-12_background-jobs.md?plain=1) contains both a `` Vue Component as well as `sh` and `youtube` custom markdown containers to render its page: [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-page.webp)](https://razor-ssg.web-templates.io/podcasts/background-jobs) ### Audio Player Podcasts are played using the [AudioPlayer.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs) Vue Component that's enabled on each podcast page which will appear at the bottom of the page when played: [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-audioplayer.webp)](https://razor-ssg.web-templates.io/podcasts) The `AudioPlayer` component is also independently usable as a standard Vue Component in markdown content like [this .md page](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_posts/2024-10-02_razor-ssg-podcasts.md?plain=1#L72): ```html ``` :::{.py-8 .mx-auto .w-2/3 .not-prose} ::: It can also be embeddable inside Razor `.cshtml` pages using [Declarative Vue Components](https://servicestack.net/posts/net8-best-blazor#declarative-vue-components), e.g: ```html @{ var episode = Podcasts.GetEpisodes().FirstOrDefault(x => x.Slug == doc.Slug);
} ``` ### Dark Mode As Razor SSG is built with Tailwind CSS, Dark Mode is also easily supported: [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-dark.webp)](https://razor-ssg.web-templates.io/podcasts/background-jobs) ### Browse by Tags Just like [blog post archives](https://razor-ssg.web-templates.io/posts/), the frontmatter collection of `tags` is used to generate related podcast pages, aiding discoverability by grouping related podcasts by **tag** at the following route: /podcasts/tagged/{tag} https://razor-ssg.web-templates.io/podcasts/tagged/release [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-tag.webp)](https://razor-ssg.web-templates.io/podcasts/tagged/release) ### Browse by Year Likewise podcast archives are also browsable by the year their published at the route: /podcasts/year/{year} https://razor-ssg.web-templates.io/podcasts/year/2024 [![](/img/posts/razor-ssg-podcasts/razor-ssg-podcast-year.webp)](https://razor-ssg.web-templates.io/podcasts/year/2024) ### iTunes-compatible Podcast RSS Feed The information in [config.json](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/config.json) is also used in the generated podcast RSS feed at: [/podcasts/feed.xml](https://razor-ssg.web-templates.io/podcasts/feed.xml) Which is a popular format podcast Applications can use to get notified when new Podcast episodes are available. The RSS Feed is also compatible with [podcasters.apple.com](https://podcasters.apple.com) and can be used to publish your podcast to [Apple Podcasts](https://podcasts.apple.com). ```xml Their Side https://razor-ssg.web-templates.io/podcasts https://razor-ssg.web-templates.io/img/posts/cover.png Their Side /podcasts razor-ssg Razor SSG Wed, 02 Oct 2024 03:54:03 GMT email@example.org (Razor SSG) email@example.org (Razor SSG) Razor SSG Razor SSG email@example.org ... ``` # Scalable Server SQLite Apps Source: https://servicestack.net/posts/scalable-sqlite Ever since adding [support for Litestream](https://docs.servicestack.net/ormlite/litestream) in our project's templates [GitHub Action Deployments](https://servicestack.net/posts/kubernetes_not_required) we've been using SQLite as the backend for our new .NET C# Apps as it's the [most cost-effective option](https://docs.servicestack.net/ormlite/litestream#savings-at-scale) that frees us from needing to use a cloud managed database which lets us make use of Hetzner's much cheaper [US Cloud VMs](https://www.hetzner.com/cloud/). We're also seeing increased usage of SQLite Server Apps with [Bluesky Social](https://github.com/bluesky-social/atproto/pull/1705) having moved to SQLite and all of 37 Signals new [Once](https://once.com) Web Apps [using SQLite](https://world.hey.com/dhh/multi-tenancy-is-what-s-hard-about-scaling-web-services-dd1e0e81), Tailscale having migrated their [primary database to SQLite](https://tailscale.com/blog/database-for-2022) whos ex-Google founders have [been using since 2018](https://www.youtube.com/watch?v=RqubKSF3wig) and Cloud Providers building distributed databases on top of SQLite like [Cloudflare D1](https://blog.cloudflare.com/introducing-d1/) and Fly.io's multi-region distributed [LiteFS](https://fly.io/docs/litefs/) solution. SQLite is a highly-performant DB that can handle a large number of concurrent read operations and [35% Faster](https://www.sqlite.org/fasterthanfs.html) filesystem performance for write operations with next to no latency that's often faster than other RDBMS's courtesy of its proximity to the running application which gives it unique advantages over traditional client/server RDBMS's where it's not susceptible to the [N+1 Queries problem](https://www.sqlite.org/np1queryprob.html) and is also able to execute your custom C# Logic inside SQL Queries using [Application SQL Functions](https://www.sqlite.org/appfunc.html). With [litestream.io](https://litestream.io) taking care of real-time replication to managed storage we just need to workaround SQLite's single concurrent writer to unlock the value, performance and unique features of SQLite in our Apps which we cover in this release with integrated support for Database Locks and Sync Commands. ## Single Concurrent Writer The primary limitation of SQLite is that it only supports a single concurrent writer, which means if you have multiple requests writing to the same database at the same time, they will need to coordinate access. As long as we can overcome this limitation SQLite can be an excellent choice to power many Web Apps. In the previous ServiceStack v8.3 release we [worked around this limitation](https://docs.servicestack.net/commands#use-case-sqlite-writes) by using [MQ Command DTOs](https://docs.servicestack.net/commands#mq-command-dtos) to route all DB Writes to be executed by a single Background MQ Thread. This works great for [messaging-based architectures](https://docs.servicestack.net/commands#messaging-workflow) where you can queue commands to be processed serially, but the overhead of using commands for all DB writes can be cumbersome when needing to perform sporadic writes within complex logic. ## Multiple SQLite Databases Firstly a great way to reduce contention is to use separate SQLite databases for different subsystems of your Application that way load is distributed across multiple DBs and writes across each SQLite database can be executed concurrently. This is especially important for write heavy operations like [SQLite Request Logging](/posts/sqlite-request-logs) or if your App stores every interaction of your App for A/B testing, storing them in separate `analytics.db` databases will remove any contention from your primary database. The other techniques below demonstrates concurrent safe techniques for accessing an SQLite DB: ### Always use Synchronous APIs for SQLite Generally it's recommended to use non-blocking Async APIs for any I/O Operations however as SQLite doesn't make Network I/O requests and its native implementation is blocking, its Async DB APIs are just pseudo-async wrappers around SQLite's blocking APIs which just adds unnecessary overhead. For this reason we recommend **always** using synchronous APIs for SQLite, especially as it's not possible to await inside a lock: ```csharp lock (Locks.AppDb) { //Can't await inside a lock //await Db.UpdateAsync(row); Db.Update(row); } ``` It's also safe to assume SQLite will always block since all [Asynchronous I/O efforts](https://www.sqlite.org/asyncvfs.html) were abandoned in favor of [WAL mode](https://www.sqlite.org/wal.html) which mitigates the cost of blocking **fsync()**. ## Database Locks The new `Locks` class maintains an object lock for each registered database connection that can be used to synchronize **write access** for different SQLite databases, e.g: ```js var row = db.SingleById
(request.Id); row.PopulateWithNonDefaultValues(request); lock (Locks.AppDb) { Db.Update(row); } ``` `Locks.AppDb` can be used synchronize db writes for the App's primary database, e.g. `App_Data/app.db`. Whilst `Locks.GetDbLock(namedConnection)` can be used to get the DB Write Lock for any other [registered SQLite Database](https://docs.servicestack.net/ormlite/multi-database-app) by using the same named connection the SQLite Database Connection was registered against, e.g: ```csharp var dbFactory = new OrmLiteConnectionFactory(connStr, SqliteDialect.Provider); dbFactory.RegisterConnection(Databases.Search, $"DataSource=App_Data/search.db;Cache=Shared", SqliteDialect.Provider); dbFactory.RegisterConnection(Databases.Analytics, $"DataSource=App_Data/analytics.db;Cache=Shared", SqliteDialect.Provider); //... using var dbSearch = dbFactory.Open(Database.Search); lock (Locks.GetDbLock(Database.Search)) { dbSearch.Insert(row); } using var dbAnalytics = dbFactory.Open(Database.Analytics); lock (Locks.GetDbLock(Database.Analytics)) { dbAnalytics.Insert(row); } ``` ## Queuing DB Writes with SyncCommand `Locks` are a great option for synchronizing DB Writes that need to be executed within complex logic blocks, however locks can cause contention in highly concurrent Apps. One way to remove contention is to serially execute DB Writes instead which we can do by executing DB Writes within `SyncCommand*` commands and using a named `[Worker(Workers.AppDb)]` attribute for Writes to the primary database, e.g: ```csharp [Worker(Workers.AppDb)] public class DeleteCreativeCommand(IDbConnection db) : SyncCommand { protected override void Run(DeleteCreative request) { var artifactIds = request.ArtifactIds; db.Delete(x => artifactIds.Contains(x.ArtifactId)); db.Delete(x => artifactIds.Contains(x.ArtifactId)); db.Delete(x => artifactIds.Contains(x.ArtifactId)); db.Delete(x => x.CreativeId == request.Id); db.Delete(x => x.CreativeId == request.Id); db.Delete(x => x.CreativeId == request.Id); db.Delete(x => x.Id == request.Id); } } ``` Other databases should use its named connection for its named worker, e.g: ```csharp [Worker(Databases.Search)] public class DeleteSearchCommand(IDbConnectionFactory dbFactory) : SyncCommand { protected override void Run(DeleteSearch request) { using var db = dbFactory.Open(Databases.Search); db.DeleteById(request.Id); //... } } ``` Where it will be executed within its Database Lock. ## Executing Commands Now everytime the commands are executed they will be added to a ConcurrentQueue where they'll be serially executed by the worker's Background Task: ```csharp public class MyServices(IBackgroundJobs jobs) : Service { public void Any(DeleteCreative request) { // Queues a durable job to execute the command with the named worker var jobRef = jobs.EnqueueCommand(request); // Returns immediately with a reference to the Background Job } public async Task Any(DeleteSearch request) { // Executes a transient (i.e. non-durable) job with the named worker var result = await jobs.RunCommandAsync(request); // Returns after the command is executed with its result (if any) } } ``` When using any `SyncCommand*` base class, its execution still uses database locks but any contention is alleviated as they're executed serially by a single worker thread. ### AutoQuery Crud Database Write Locks To avoid SQLite concurrency write exceptions all DB Writes should be executed within its database lock or a named worker. Including the auto-generated [AutoQuery Crud](https://docs.servicestack.net/autoquery/crud) APIs which will implicitly use Database Locks if the **primary database is SQLite**. AutoQuery CRUD can also be explicitly configured to always be executed within Database Locks with: ```csharp services.AddPlugin(new AutoQueryFeature { UseDatabaseWriteLocks = true }); ``` ## SQLite Web Apps That's about it, by using any of the above techniques to guard against concurrent writes you can take advantage of the [simplicity, value and performance benefits](https://docs.servicestack.net/ormlite/litestream#the-right-time-for-server-side-sqlite) of SQLite in your Apps and utilize a solution like [litestream.io](https://litestream.io) for real-time replication of your SQLite databases to highly reliable managed storage. SQLite's [Checklist For Choosing The Right Database Engine](https://www.sqlite.org/whentouse.html#checklist_for_choosing_the_right_database_engine) covers the few situations when a traditional Client/Server RDBMS will be more appropriate. The primary use-case would be when your App needs to be distributed across multiple App Servers as using SQLite essentially forces you into scaling up, which gets more appealing every year with hardware getting cheaper and faster and cheap hosting providers like [hetzner.com](https://www.hetzner.com) where you can get bare metal 48 Core/96 vCore EPYC Servers with fast NVMe SSDs for **€236** per month - a fraction of the cost of comparable performance with any cloud managed solution [![](/img/posts/scalable-sqlite/hetzner-epyc-48.webp)](https://www.hetzner.com/dedicated-rootserver/) Which is a fraction of what it would cost for comparable performance using cloud managed databases: [![](/img/posts/scalable-sqlite/azure-sql-database.webp)](https://azure.microsoft.com/en-us/pricing/details/azure-sql-database/single/) In the rare cases where you need to scale beyond a single server you'll initially be able to scale out your different App databases onto different servers. Beyond that, if your App permits you may be able to adopt a multi-tenant architecture like [Bluesky Social](https://bsky.social/about) with each tenant having their own SQLite database to effectively enable infinite scaling. For further info on using high performance SQLite in production web apps check out [@aarondfrancis](https://x.com/aarondfrancis) comprehensive website and course at [highperformancesqlite.com](https://highperformancesqlite.com) - which contains a lot of great content accessible for free. ## Example SQLite Apps Our confidence in SQLite being the best choice for many web applications has led us to adopt it to power our latest web applications which are all [deployed to a shared Hetzner VM](/posts/kubernetes_not_required) whose [inexpensive hosting costs](/posts/jamstacks_hosting) allows us to host and make them **available for free!** All projects are open-source and employ the different techniques detailed above that should serve as a great resource of how they're used in real-world Web Applications: ### Blazor Diffusion Generate images for free using custom [Civit AI](https://civitai.com) and [FLUX-schnell](https://huggingface.co/black-forest-labs/FLUX.1-schnell) models: [![](/img/posts/scalable-sqlite/blazordiffusion.webp)](https://blazordiffusion.com) - Website: [blazordiffusion.com](https://blazordiffusion.com) - GitHub: [github.com/NetCoreApps/BlazorDiffusionVue](https://github.com/NetCoreApps/BlazorDiffusionVue/) ### pvq.app An OSS alternative to StackOverflow which uses the best proprietary and OSS Large Language Models to answer your technical questions. [pvq.app](https://pvq.app) is populated with over **1M+ answers** for the highest rated StackOverflow questions - checkout [pvq.app/leaderboard](https://pvq.app/leaderboard) to find the best performing LLM models (results are surprising!) [![](/img/posts/scalable-sqlite/pvq.webp)](https://pvq.app) - Website: [pvq.app](https://pvq.app) - GitHub: [github.com/ServiceStack/pvq.app](https://github.com/ServiceStack/pvq.app) ### AI Server The independent Microservice used to provide all AI Features used by the above applications. It's already been used to execute millions of LLM and Comfy UI Requests to generate Open AI Chat Answers and Generated Images used to populate the [blazordiffusion.com](https://blazordiffusion.com) and [pvq.app](https://pvq.app) websites. It was the project used to develop and test [Background Jobs](/posts/background-jobs) in action where it serves as a private gateway to process all LLM, AI and image transformations requests that any of our Apps need where it dynamically delegates requests across multiple Ollama, Open AI Chat, LLM Gateway, Comfy UI, Whisper and ffmpeg providers. [![](/img/posts/scalable-sqlite/ai-server.webp)](https://openai.servicestack.net) [![](/img/posts/scalable-sqlite/ai-server-chat.webp)](https://openai.servicestack.net) - Website: [openai.servicestack.net](https://openai.servicestack.net) - GitHub: [github.com/ServiceStack/ai-server](https://github.com/ServiceStack/ai-server) In addition to maintaining a history of AI Requests, it also provides file storage for its CDN-hostable AI generated assets and on-the-fly, cacheable image transformations. ### Private AI Gateway We're developing AI Server as a **Free OSS Product** that runs as a single Docker Container Microservice that Admins can use its built-in UIs to add multiple Ollama instances, Open AI Gateways to execute LLM requests and Client Docker agents installed with Comfy UI, ffmpeg and Whisper to handle all other non-LLM Requests. #### Multiple Ollama, Open AI Gateway and Comfy UI Agents The AI Server Docker container itself wont require any infrastructure dependencies or specific hardware requirements, however any Ollama endpoints or Docker Comfy UI Agents added will need to run on GPU-equipped servers. #### Native end-to-end Typed Integrations to most popular languages ServiceStack's [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) feature is used to provide native typed integrations to C#, TypeScript, JavaScript, Python, PHP, Swift, Java, Kotlin, Dart, F# and VB.NET projects which organizations can drop into their heterogeneous environments to manage their private AI Services used across their different Apps. #### Protected Access with API Keys AI Server utilizes [Simple Auth with API Keys](https://docs.servicestack.net/auth/admin-apikeys) letting Admins create and distribute API Keys to only allow authorized clients to access their AI Server's APIs, which can be optionally further restricted to only [allow access to specific APIs](https://docs.servicestack.net/auth/apikeys#creating-user-api-keys). ### AI Server V1 [AI Server V1](/posts/ai-server) is now released! The initial V1 release comes packed with features, including: #### Large Language Models - Open AI Chat - Support for Ollama endpoints - Support for Open Router, Open AI, Mistral AI, Google and Groq API Gateways #### Comfy UI Agent / Replicate / DALL-E 3 - Text to Image #### Comfy UI Agent - Image to Image - Image Upscaling - Image with Mask - Image to Text - Text to Audio - Text to Speech - Speech to Text #### ffmpeg - image/video/audio format conversions - image/video scaling - image/video cropping - image/video watermarking - video trimming #### Managed File Storage - Blob Storage - isolated and restricted by API Key ### AI Server Feedback Feel free to reach us at [ai-server/discussions](https://github.com/ServiceStack/ai-server/discussions) with any AI Server questions. # SQLite C# Request Logs Source: https://servicestack.net/posts/sqlite-request-logs Up until this release all of ServiceStack's database features like [AutoQuery](https://servicestack.net/autoquery) have been database agnostic courtesy of OrmLite's [support for popular RDBMS's](https://docs.servicestack.net/ormlite/installation) so that they integrate into an App's existing configured database. [Background Jobs](/posts/background-jobs) is our first foray into a SQLite-only backend, as it's the only RDBMS that enables us to provide encapsulated black-box functionality without requiring any infrastructure dependencies. It's low latency, high-performance and ability to create lightweight databases on the fly make it ideal for self-managing isolated appliance backends like Background Jobs and Request Logging which don't benefit from integrating with your existing RDBMS. The new [ServiceStack.Jobs](https://nuget.org/packages/ServiceStack.Jobs) NuGet package allows us to deliver plug and play SQLite backed features into .NET 8 C# Apps that are configured with any RDBMS or without one. The next feature added is a SQLite backed provider for [Request Logs](https://docs.servicestack.net/request-logger) with the new `SqliteRequestLogger` which can be added to existing .NET 8 Apps with the [mix tool](https://docs.servicestack.net/mix-tool): :::sh x mix sqlitelogs ::: Which adds a reference to **ServiceStack.Jobs** and the [Modular Startup](https://docs.servicestack.net/modular-startup) config below: ```csharp using ServiceStack.Jobs; using ServiceStack.Web; [assembly: HostingStartup(typeof(MyApp.ConfigureRequestLogs))] namespace MyApp; public class ConfigureRequestLogs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddPlugin(new RequestLogsFeature { RequestLogger = new SqliteRequestLogger(), EnableResponseTracking = true, EnableRequestBodyTracking = true, EnableErrorTracking = true }); services.AddHostedService(); }); } public class RequestLogsHostedService(ILogger log, IRequestLogger requestLogger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var dbRequestLogger = (SqliteRequestLogger)requestLogger; using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) { dbRequestLogger.Tick(log); } } } ``` This will use a Hosted Background Service to flush Request Logs to the requests SQLite database every **3** seconds (configurable in the PeriodicTimer). If your App is already using `RequestLogsFeature` configured (e.g. with Profiling) you'll want to remove it: ```csharp public class ConfigureProfiling : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { // services.AddPlugin(new RequestLogsFeature()); services.AddPlugin(new ProfilingFeature { IncludeStackTrace = true, }); }); } ``` ## Rolling Monthly SQLite Databases The benefit of using SQLite is that databases can be created on-the-fly where Requests will be persisted into isolated **requests** Monthly databases which can be easily archived into managed file storage instead of a singular growing database, visible in the [Database Admin UI](https://docs.servicestack.net/admin-ui-database): ![](/img/posts/sqlite-request-logs/sqlite-databases.webp) SQLite logs will also make it easier to generate monthly aggregate reports to provide key insights into the usage of your App. ## AutoQuery Grid Admin Logging UI As SQLite Requests Logs also makes it efficiently possible to sort and filter through logs, the Logging UI will switch to using a fully queryable `AutoQueryGrid` when using `SqliteRequestLogger`: ![](/img/posts/sqlite-request-logs/sqlite-request-logs.webp) # Simple C# Background Jobs & Recurring Tasks for .NET 8 Source: https://servicestack.net/posts/background-jobs We're excited to announce **Background Jobs** our effortless solution for queueing and managing background jobs and scheduled tasks in any .NET 8 C# App, implemented in true ServiceStack fashion where it seamlessly integrates into existing Apps and call existing APIs and sports a [built-in](/auto-ui) Management UI to provide real-time monitoring, inspection and management of background jobs. :::youtube 2Cza_a_rrjA Durable C# Background Jobs and Scheduled Tasks for .NET ::: ### Durable and Infrastructure-Free Prior to Background Jobs we've been using [Background MQ](https://docs.servicestack.net/background-mq) for executing our background tasks which lets you queue any Request DTO to execute its API in a background worker. It's been our preferred choice as it didn't require any infrastructure dependencies since its concurrent queues are maintained in memory, this also meant they were non-durable that didn't survive across App restarts. Whilst [ServiceStack MQ](https://docs.servicestack.net/messaging) enables an additional endpoint for your APIs our main use-case for using it was for executing background tasks which would be better suited by purpose-specific software designed for the task. #### SQLite Persistence It uses SQLite as the backing store for its durability since it's low latency, [fast disk persistence](https://www.sqlite.org/fasterthanfs.html) and embeddable file-based database makes it ideally suited for the task which allows creation of naturally partition-able and archivable monthly databases on-the-fly without any maintenance overhead or infrastructure dependencies making it easy to add to any .NET App without impacting or adding increased load to their existing configured databases. ### Queue APIs or Commands For even greater reuse you're able to queue your existing ServiceStack APIs as a Background Job in addition to [Commands](https://docs.servicestack.net/commands) added in the [last v8.3 release](https://docs.servicestack.net/releases/v8_03) for encapsulating units of logic into internal invokable, inspectable and auto-retryable building blocks. ### Real Time Admin UI The Background Jobs Admin UI provides a real time view into the status of all background jobs including their progress, completion times, Executed, Failed and Cancelled Jobs, etc. which is useful for monitoring and debugging purposes. ![](/img/posts/background-jobs/jobs-dashboard.webp) View Real-time progress of queued Jobs ![](/img/posts/background-jobs/jobs-queue.webp) View real-time progress logs of executing Jobs ![](/img/posts/background-jobs/jobs-logs.webp) View Job Summary and Monthly Databases of Completed and Failed Jobs ![](/img/posts/background-jobs/jobs-completed.webp) View full state and execution history of each Job ![](/img/posts/background-jobs/jobs-failed.webp) Cancel Running jobs and Requeue failed jobs ### Feature Overview Even in its v1 release it packs all the features we wanted in a Background Jobs solution: - No infrastructure dependencies - Monthly archivable rolling Databases with full Job Execution History - Execute existing APIs or versatile Commands - Commands auto registered in IOC - Scheduled Reoccurring Tasks - Track Last Job Run - Serially execute jobs with the same named Worker - Queue Jobs dependent on successful completion of parent Job - Queue Jobs to be executed after a specified Date - Execute Jobs within the context of an Authenticated User - Auto retry failed jobs on a default or per-job limit - Timeout Jobs on a default or per-job limit - Cancellable Jobs - Requeue Failed Jobs - Execute custom callbacks on successful execution of Job - Maintain Status, Logs and Progress of Executing Jobs - Execute transitive (i.e. non-durable) jobs using named workers - Attach optional `Tag`, `BatchId`, `CreatedBy`, `ReplyTo` and `Args` with Jobs Please [let us know](https://servicestack.net/ideas) of any other missing features you'd love to see implemented. ## Install As it's more versatile and better suited, we've replaced the usage of Background MQ with **ServiceStack.Jobs** in all **.NET 8 Identity Auth Templates** for sending Identity Auth Confirmation Emails when SMTP is enabled. So the easiest way to get started with ServiceStack.Jobs is to [create a new Identity Auth Project](https://servicestack.net/start), e.g: :::sh x new blazor-vue MyApp ::: ### Exiting .NET 8 Templates Existing .NET 8 Projects can configure their app to use **ServiceStack.Jobs** by mixing in: :::sh x mix jobs ::: Which adds the `Configure.BackgroundJobs.cs` [Modular Startup](https://docs.servicestack.net/modular-startup) configuration and a **ServiceStack.Jobs** NuGet package reference to your project. ## Usage Any API, Controller or Minimal API can execute jobs with the `IBackgroundJobs` dependency, e.g. here's how you can run a background job to send a new email when an API is called in any new Identity Auth template: ```csharp class MyService(IBackgroundJobs jobs) : Service { public object Any(MyOrder request) { var jobRef = jobs.EnqueueCommand(new SendEmail { To = "my@email.com", Subject = $"Received New Order {request.Id}", BodyText = $""" Order Details: {request.OrderDetails.DumpTable()} """, }); //... } } ``` Which records and immediately executes a worker to execute the `SendEmailCommand` with the specified `SendEmail` Request argument. It also returns a reference to a Job which can be used later to query and track the execution of a job. Alternatively a `SendEmail` API could be executed with just the Request DTO: ```csharp var jobRef = jobs.EnqueueApi(new SendEmail { To = "my@email.com", Subject = $"Received New Order {request.Id}", BodyText = $""" Order Details: {request.OrderDetails.DumpTable()} """, }); ``` Although Sending Emails is typically not an API you want to make externally available and would want to either [Restrict access](https://docs.servicestack.net/auth/restricting-services) or [limit usage to specified users](https://docs.servicestack.net/auth/identity-auth#declarative-validation-attributes). In both cases the `SendEmail` Request is persisted into the Jobs SQLite database for durability that gets updated as it progresses through the queue. For execution the API or command is resolved from the IOC before being invoked with the Request. APIs are executed via the [MQ Request Pipeline](https://docs.servicestack.net/order-of-operations) and commands executed using the [Commands Feature](https://docs.servicestack.net/commands) where they'll also be visible in the [Commands Admin UI](https://docs.servicestack.net/commands#command-admin-ui). ### Background Job Options The behavior for each `Enqueue*` method for executing background jobs can be customized with the following options: - `Worker` - Serially process job using a named worker thread - `Callback` - Invoke another command with the result of a successful job - `DependsOn` - Execute jobs after successful completion of a dependent job - If parent job fails all dependent jobs are cancelled - `UserId` - Execute within an Authenticated User Context - `RunAfter` - Queue jobs that are only run after a specified date - `RetryLimit` - Override default retry limit for how many attempts should be made to execute a job - `TimeoutSecs` - Override default timeout for how long a job should run before being cancelled - `RefId` - Allow clients to specify a unique Id (e.g Guid) to track job - `Tag` - Group related jobs under a user specified tag - `CreatedBy` - Optional field for capturing the owner of a job - `BatchId` - Group multiple jobs with the same Id - `ReplyTo` - Optional field for capturing where to send notification for completion of a Job - `Args` - Optional String Dictionary of Arguments that can be attached to a Job ## Schedule Recurring Tasks In addition to queueing jobs to run in the background, it also supports scheduling recurring tasks to execute APIs or Commands at fixed intervals. :::youtube DtB8KaXXMCM Schedule your Reoccurring Tasks with Background Jobs! ::: APIs and Commands can be scheduled to run at either a `TimeSpan` or [CRON Expression](https://github.com/HangfireIO/Cronos?tab=readme-ov-file#cron-format) interval, e.g: ### CRON Expression Examples ```csharp // Every Minute Expression jobs.RecurringCommand(Schedule.Cron("* * * * *")); // Every Minute Constant jobs.RecurringCommand(Schedule.EveryMinute, new CheckUrls { Urls = urls }); ``` ### CRON Format You can use any **unix-cron format** expression supported by the [HangfireIO/Cronos](https://github.com/HangfireIO/Cronos) library: ```txt |------------------------------- Minute (0-59) | |------------------------- Hour (0-23) | | |------------------- Day of the month (1-31) | | | |------------- Month (1-12; or JAN to DEC) | | | | |------- Day of the week (0-6; or SUN to SAT) | | | | | | | | | | * * * * * ``` The allowed formats for each field include: | Field | Format of valid values | |------------------|--------------------------------------------| | Minute | 0-59 | | Hour | 0-23 | | Day of the month | 1-31 | | Month | 1-12 (or JAN to DEC) | | Day of the week | 0-6 (or SUN to SAT; or 7 for Sunday) | #### Matching all values To match all values for a field, use the asterisk: `*`, e.g here are two examples in which the minute field is left unrestricted: - `* 0 1 1 1` - the job runs every minute of the midnight hour on January 1st and Mondays. - `* * * * *` - the job runs every minute (of every hour, of every day of the month, of every month, every day of the week, because each of these fields is unrestricted too). #### Matching a range To match a range of values, specify your start and stop values, separated by a hyphen (-). Do not include spaces in the range. Ranges are inclusive. The first value must be less than the second. The following equivalent examples run at midnight on Mondays, Tuesdays, Wednesdays, Thursdays, and Fridays (for all months): - `0 0 * * 1-5` - `0 0 * * MON-FRI` #### Matching a list Lists can contain any valid value for the field, including ranges. Specify your values, separated by a comma (,). Do not include spaces in the list, e.g: - `0 0,12 * * *` - the job runs at midnight and noon. - `0-5,30-35 * * * *` - the job runs in each of the first five minutes of every half hour (at the top of the hour and at half past the hour). ### TimeSpan Interval Examples ```csharp jobs.RecurringCommand( Schedule.Interval(TimeSpan.FromMinutes(1))); // With Example jobs.RecurringApi(Schedule.Interval(TimeSpan.FromMinutes(1)), new CheckUrls { Urls = urls }); ``` That can be registered with an optional **Task Name** and **Background Options**, e.g: ```csharp jobs.RecurringCommand("Check URLs", Schedule.EveryMinute, new() { RunCommand = true // don't persist job }); ``` :::info If no name is provided, the Command's Name or APIs Request DTO will be used ::: ### Idempotent Registration Scheduled Tasks are idempotent where the same registration with the same name will either create or update the scheduled task registration without losing track of the last time the Recurring Task, as such it's recommended to always define your App's Scheduled Tasks on Startup: ```csharp public class ConfigureBackgroundJobs : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context,services) => { services.AddPlugin(new CommandsFeature()); services.AddPlugin(new BackgroundsJobFeature()); services.AddHostedService(); }).ConfigureAppHost(afterAppHostInit: appHost => { var services = appHost.GetApplicationServices(); var jobs = services.GetRequiredService(); // App's Scheduled Tasks Registrations: jobs.RecurringCommand(Schedule.Hourly); }); } ``` ### Background Jobs Admin UI The last job the Recurring Task ran is also viewable in the Jobs Admin UI: ![](/img/posts/background-jobs/jobs-scheduled-tasks-last-job.webp) ### Executing non-durable jobs `IBackgroundJobs` also supports `RunCommand*` methods for executing background jobs transiently (i.e. non-durable), which is useful for commands that want to be serially executed by a named worker but don't need to be persisted. #### Execute in Background and return immediately You could use this to queue system emails to be sent by the same **smtp** worker and are happy to not have its state and execution history tracked in the Jobs database. ```csharp var job = jobs.RunCommand(new SendEmail { ... }, new() { Worker = "smtp" }); ``` In this case `RunCommand` returns the actual `BackgroundJob` instance that will be updated by the worker. #### Execute in Background and wait for completion You can also use `RunCommandAsync` if you prefer to wait until the job has been executed. Instead of a Job it returns the **Result** of the command if it returned one. ```csharp var result = await jobs.RunCommandAsync(new SendEmail {...}, new() { Worker = "smtp" }); ``` ### Serially Execute Jobs with named Workers By default jobs are executed immediately in a new Task, we can also change the behavior to instead execute jobs one-by-one in a serial queue by specifying them to use the same named worker as seen in the example above. Alternatively you can annotate the command with the `[Worker]` attribute if you **always** want all jobs executing the command to use the same worker: ```csharp [Worker("smtp")] public class SendEmailCommand(IBackgroundJobs jobs) : SyncCommand { //... } ``` ### Use Callbacks to process the results of Commands Callbacks can be used to extend the lifetime of a job to include processing a callback to process its results. This is useful where you would like to reuse the the same command but handle the results differently, e.g. the same command can email results or invoke a webhook by using a callback: ```csharp jobs.EnqueueCommand(new CheckUrls { Urls = allUrls }, new() { Callback = nameof(EmailUrlResultsCommand), }); jobs.EnqueueCommand(new CheckUrls { Urls = criticalUrls }, new() { Callback = nameof(WebhookUrlResultsCommand), ReplyTo = callbackUrl }); ``` Callbacks that fail are auto-retried the same number of times as their jobs, which if they all fail then the entire job is also marked as failed. ### Run Job dependent on successful completion of parent Jobs can be queued to only run after the successful completion of another job, this is useful for when you need to kick off multiple jobs after a long running task has finished like generating monthly reports after monthly data has been aggregated, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }); jobs.EnqueueCommand(new () { DependsOn = jobRef.Id, }); jobs.EnqueueCommand(new () { DependsOn = jobRef.Id, }); ``` Inside your command you can get a reference to your current job with `Request.GetBackgroundJob()` which will have its `ParentId` populated with the parent job Id and `job.ParentJob` containing a reference to the completed Parent Job where you can access its Request, Results and other job information: ```csharp public class GenerateSalesReportCommand(ILogger log) : SyncCommand { protected override void Run() { var job = Request.GetBackgroundJob(); var parentJob = job.ParentJob; } } ``` ### Atomic Batching Behavior We can also use `DependsOn` to implement atomic batching behavior where from inside our executing command we can queue new jobs that are dependent on the successful execution of the current job, e.g: ```csharp public class CheckUrlsCommand(IHttpClientFactory factory, IBackgroundJobs jobs) : AsyncCommand { protected override async Task RunAsync(CheckUrls req, CancellationToken ct) { var job = Request.GetBackgroundJob(); var batchId = Guid.NewGuid().ToString("N"); using var client = factory.CreateClient(); foreach (var url in req.Urls) { var msg = new HttpRequestMessage(HttpMethod.Get, url); var response = await client.SendAsync(msg, ct); response.EnsureSuccessStatusCode(); jobs.EnqueueCommand(new SendEmail { To = "my@email.com", Subject = $"{new Uri(url).Host} status", BodyText = $"{url} is up", }, new() { DependsOn = job.Id, BatchId = batchId, }); } } } ``` Where any dependent jobs are only executed if the job was successfully completed. If instead an exception was thrown during execution, the job will be failed and all its dependent jobs cancelled and removed from the queue. ### Executing jobs with an Authenticated User Context If you have existing logic dependent on a Authenticated `ClaimsPrincipal` or ServiceStack `IAuthSession` you can have your APIs and Commands also be executed with that user context by specifying the `UserId` the job should be executed as: ```csharp var openAiRequest = new CreateOpenAiChat { Request = new() { Model = "gpt-4", Messages = [ new() { Content = request.Question } ] }, }; // Example executing API Job with User Context jobs.EnqueueApi(openAiRequest, new() { UserId = Request.GetClaimsPrincipal().GetUserId(), CreatedBy = Request.GetClaimsPrincipal().GetUserName(), }); // Example executing Command Job with User Context jobs.EnqueueCommand(openAiRequest, new() { UserId = Request.GetClaimsPrincipal().GetUserId(), CreatedBy = Request.GetClaimsPrincipal().GetUserName(), }); ``` Inside your API or Command you access the populated User `ClaimsPrincipal` or ServiceStack `IAuthSession` using the same APIs that you'd use inside your ServiceStack APIs, e.g: ```csharp public class CreateOpenAiChatCommand(IBackgroundJobs jobs) : AsyncCommand { protected override async Task RunAsync( CreateOpenAiChat request, CancellationToken token) { var user = Request.GetClaimsPrincipal(); var session = Request.GetSession(); //... } } ``` ### Queue Job to run after a specified date Using `RunAfter` lets you queue jobs that are only executed after a specified `DateTime`, useful for executing resource intensive tasks at low traffic times, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }, new() { RunAfter = DateTime.UtcNow.Date.AddDays(1) }); ``` ### Attach Metadata to Jobs All above Background Job Options have an effect on when and how Jobs are executed. There are also a number of properties that can be attached to a Job that can be useful in background job processing despite not having any effect on how jobs are executed. These properties can be accessed by commands or APIs executing the Job and are visible and can be filtered in the Jobs Admin UI to help find and analyze executed jobs. ```csharp var jobRef = jobs.EnqueueCommand(openAiRequest, new() { // Group related jobs under a common tag Tag = "ai", // A User-specified or system generated unique Id to track the job RefId = request.RefId, // Capture who created the job CreatedBy = Request.GetClaimsPrincipal().GetUserName(), // Link jobs together that are sent together in a batch BatchId = batchId, // Capture where to notify the completion of the job to ReplyTo = "https:example.org/callback", // Additional properties about the job that aren't in the Request Args = new() { ["Additional"] = "Metadata" } }); ``` ### Querying a Job A job can be queried by either it's auto-incrementing `Id` Primary Key or by a unique `RefId` that can be user-specified. ```csharp var jobResult = jobs.GetJob(jobRef.Id); var jobResult = jobs.GetJobByRefId(jobRef.RefId); ``` At a minimum a `JobResult` will contain the Summary Information about a Job as well as the full information about a job depending on where it's located: ```csharp class JobResult { // Summary Metadata about a Job in the JobSummary Table JobSummary Summary // Job that's still in the BackgroundJob Queue BackgroundJob? Queued // Full Job information in Monthly DB CompletedJob Table CompletedJob? Completed // Full Job information in Monthly DB FailedJob Table FailedJob? Failed // Helper to access full Job Information BackgroundJobBase? Job => Queued ?? Completed ?? Failed } ``` ### Job Execution Limits Default Retry and Timeout Limits can be configured on the Backgrounds Job plugin: ```csharp services.AddPlugin(new BackgroundsJobFeature { DefaultRetryLimit = 2, DefaultTimeout = TimeSpan.FromMinutes(10), }); ``` These limits are also overridable on a per-job basis, e.g: ```csharp var jobRef = jobs.EnqueueCommand(new Aggregate { Month = DateTime.UtcNow }, new() { RetryLimit = 3, Timeout = TimeSpan.FromMinutes(30), }); ``` ### Logging, Cancellation an Status Updates We'll use the command for checking multiple URLs to demonstrate some recommended patterns and how to enlist different job processing features. ```csharp public class CheckUrlsCommand( ILogger logger, IBackgroundJobs jobs, IHttpClientFactory clientFactory) : AsyncCommand { protected override async Task RunAsync(CheckUrls req, CancellationToken ct) { // 1. Create Logger that Logs and maintains logging in Jobs DB var log = Request.CreateJobLogger(jobs,logger); // 2. Get Current Executing Job var job = Request.GetBackgroundJob(); var result = new CheckUrlsResult { Statuses = new() }; using var client = clientFactory.CreateClient(); for (var i = 0; i < req.Urls.Count; i++) { // 3. Stop processing Job if it's been cancelled ct.ThrowIfCancellationRequested(); var url = req.Urls[i]; try { var msg = new HttpRequestMessage(HttpMethod.Get,url); var response = await client.SendAsync(msg, ct); result.Statuses[url] = response.IsSuccessStatusCode; log.LogInformation("{Url} is {Status}", url, response.IsSuccessStatusCode ? "up" : "down"); // 4. Optional: Maintain explicit progress and status updates log.UpdateStatus(i/(double)req.Urls.Count,$"Checked {i} URLs"); } catch (Exception e) { log.LogError(e, "Error checking {Url}", url); result.Statuses[url] = false; } } // 5. Send Results to WebHook Callback if specified if (job.ReplyTo != null) { jobs.EnqueueCommand(result, new() { ParentId = job.Id, ReplyTo = job.ReplyTo, }); } } } ``` We'll cover some of the notable parts useful when executing Jobs: #### 1. Job Logger We can use a Job logger to enable database logging that can be monitored in real-time in the Admin Jobs UI. Creating it with both `BackgroundJobs` and `ILogger` will return a combined logger that both Logs to standard output and to the Jobs database: ```csharp var log = Request.CreateJobLogger(jobs,logger); ``` Or just use `Request.CreateJobLogger(jobs)` to only save logs to the database. #### 2. Resolve Executing Job If needed the currently executing job can be accessed with: ```csharp var job = Request.GetBackgroundJob(); ``` Where you'll be able to access all the metadata the jobs were created with including `ReplyTo` and `Args`. #### 3. Check if Job has been cancelled To be able to cancel a long running job you'll need to periodically check if a Cancellation has been requested and throw a `TaskCanceledException` if it has to short-circuit the command which can be done with: ```csharp ct.ThrowIfCancellationRequested(); ``` You'll typically want to call this at the start of any loops to prevent it from doing any more work. #### 4. Optionally record progress and status updates By default Background Jobs looks at the last API or Command run and worker used to estimate the duration and progress for how long a running job will take. If preferred your command can explicitly set a more precise progress and optional status update that should be used instead, e.g: ```csharp log.UpdateStatus(progress:i/(double)req.Urls.Count, $"Checked {i} URLs"); ``` Although generally the estimated duration and live logs provide a good indication for the progress of a job. #### 5. Notify completion of Job Calling a Web Hook is a good way to notify externally initiated job requests of the completion of a job. You could invoke the callback within the command itself but there are a few benefits to initiating another job to handle the callback: - Frees up the named worker immediately to process the next task - Callbacks are durable, auto-retried and their success recorded like any job - If a callback fails the entire command doesn't need to be re-run again We can queue a callback with the result by passing through the `ReplyTo` and link it to the existing job with: ```csharp if (job.ReplyTo != null) { jobs.EnqueueCommand(result, new() { ParentId = job.Id, ReplyTo = job.ReplyTo, }); } ``` Which we can implement by calling the `SendJsonCallbackAsync` extension method with the Callback URL and the Result DTO it should be called with: ```csharp public class NotifyCheckUrlsCommand(IHttpClientFactory clientFactory) : AsyncCommand { protected override async Task RunAsync( CheckUrlsResult request, CancellationToken token) { await clientFactory.SendJsonCallbackAsync( Request.GetBackgroundJob().ReplyTo, request, token); } } ``` #### Callback URLs `ReplyTo` can be any URL which by default will have the result POST'ed back to the URL with a JSON Content-Type. Typically URLs will contain a reference Id so external clients can correlate a callback with the internal process that initiated the job. If the callback API is publicly available you'll want to use an internal Id that can't be guessed so the callback can't be spoofed, like a Guid, e.g: `$"https://api.example.com?refId={RefId}"` If needed the callback URL can be customized on how the HTTP Request callback is sent. If the URL contains a space, the text before the space is treated as the HTTP method: `"PUT https://api.example.com"` If the auth part contains a colon `:` it's treated as Basic Auth: `"username:password@https://api.example.com"` If name starts with `http.` sends a HTTP Header `"http.X-API-Key:myApiKey@https://api.example.com"` Otherwise it's sent as a Bearer Token: `"myToken123@https://api.example.com"` Bearer Token or HTTP Headers starting with `$` is substituted with Environment Variable if exists: `"$API_TOKEN@https://api.example.com"` When needed headers, passwords and tokens can be URL encoded if they contain any delimiter characters. ## Implementing Commands At a minimum a command need only implement the simple [IAsyncCommand interface](https://docs.servicestack.net/commands#commands-feature): ```csharp public interface IAsyncCommand { Task ExecuteAsync(T request); } ``` Which is the singular interface that can execute any command. However commands executed via Background Jobs have additional context your commands may need to access during execution, including the `BackgroundJob` itself, the `CancellationToken` and an Authenticated User Context. To reduce the effort in creating commands with a `IRequest` context we've added a number ergonomic base classes to better capture the different call-styles a unit of logic can have including **Sync** or **Async** execution, whether they require **Input Arguments** or have **Result Outputs**. Choosing the appropriate Abstract base class benefits from IDE tooling in generating the method signature that needs to be implemented whilst Async commands with Cancellation Tokens in its method signature highlights any missing async methods that are called without the token. ### Sync Commands - `SyncCommand` - Requires No Arguments - `SyncCommand` - Requires TRequest Argument - `SyncCommandWithResult` - Requires No Args and returns Result - `SyncCommandWithResult` - Requires Arg and returns Result ```csharp public record MyArgs(int Id); public record MyResult(string Message); public class MyCommandNoArgs(ILogger log) : SyncCommand { protected override void Run() { log.LogInformation("Called with No Args"); } } public class MyCommandArgs(ILogger log) : SyncCommand { protected override void Run(MyArgs request) { log.LogInformation("Called with {Id}", request.Id); } } public class MyCommandWithResult(ILogger log) : SyncCommandWithResult { protected override MyResult Run() { log.LogInformation("Called with No Args and returns Result"); return new MyResult("Hello World"); } } public class MyCommandWithArgsAndResult(ILogger log) : SyncCommandWithResult { protected override MyResult Run(MyArgs request) { log.LogInformation("Called with {Id} and returns Result", request.Id); return new MyResult("Hello World"); } } ``` ### Async Commands - `AsyncCommand` - Requires No Arguments - `AsyncCommand` - Requires TRequest Argument - `AsyncCommandWithResult` - Requires No Args and returns Result - `AsyncCommandWithResult` - Requires Arg and returns Result ```csharp public class MyAsyncCommandNoArgs(ILogger log) : AsyncCommand { protected override async Task RunAsync(CancellationToken token) { log.LogInformation("Async called with No Args"); } } public class MyAsyncCommandArgs(ILogger log) : AsyncCommand { protected override async Task RunAsync(MyArgs request, CancellationToken t) { log.LogInformation("Async called with {Id}", request.Id); } } public class MyAsyncCommandWithResult(ILogger log) : AsyncCommandWithResult { protected override async Task RunAsync(CancellationToken token) { log.LogInformation("Async called with No Args and returns Result"); return new MyResult("Hello World"); } } public class MyAsyncCommandWithArgsAndResult(ILogger log) : AsyncCommandWithResult { protected override async Task RunAsync( MyArgs request, CancellationToken token) { log.LogInformation("Called with {Id} and returns Result", request.Id); return new MyResult("Hello World"); } } ``` # Utilize C# Commands to build more robust and observable systems Source: https://servicestack.net/posts/commands-feature Much of ServiceStack has been focused on providing a productive [API First Development](https://docs.servicestack.net/api-first-development) experience and adding value-added features around your System's external APIs, including: - [Native end-to-end typed API integrations](https://docs.servicestack.net/add-servicestack-reference) to **11 popular languages** - Built-in [API Explorer](https://docs.servicestack.net/api-explorer) to discover, browse and invoke APIs - Instant CRUD UIs with [Auto Query](https://docs.servicestack.net/autoquery/) and [Locode](https://docs.servicestack.net/locode/) - Custom CRUD UIs with [Blazor components](https://blazor-gallery.jamstacks.net) and [Vue Components](https://docs.servicestack.net/vue/) As well as [Declarative Validation](https://docs.servicestack.net/declarative-validation), multiple [Auth Integrations](https://docs.servicestack.net/auth/) and other extensive [Declarative Features](https://docs.servicestack.net/locode/declarative) to enhance your external facing APIs. ### Internal API Implementation Little attention has been given to internal implementations of APIs since it can use anything that fulfils its service contract by returning the APIs populated Response DTO. How code-bases are structured is largely a matter of developer preference, however we believe we've also been able to add value in this area by introducing an appealing option with our new managed **Commands** Feature. :::youtube SXPdBHbncPc Use C# Commands to build robust and observable systems with Admin UI ::: ## Code Architecture Ultimately nothing beats the simplicity of "No Architecture" by maintaining all logic within a Service Implementation which just needs to call a few App dependencies to implement its functionality and return a populated Response DTO: ```csharp public object Any(MyRequest request) => new MyResponse { ... }; ``` This is still the best option for small implementations where the Service is the only consumer of the logic that should be run on the HTTP Worker Request Thread. #### When to restructure The times when you may want to consider moving logic out of your Service into separate classes include: - **Code Reuse**: Make it easier to reuse your Service logic in other Services - **Complexity**: Break down complex logic into smaller more manageable pieces - **Testability**: Make it easier to test your Logic in isolation - **Observability**: Make it easier to log and monitor - **Robustness**: Make it easier to handle, retry and recover from errors - **Flexibility**: Make it easier to run in parallel or in a different managed thread We'll look at how the new **Commands Feature** can help with these concerns. ### Code Reuse Following principles of YAGNI in doing the simplest thing that could possibly work, whenever we want to reuse logic across Services we'd first start by moving it to an extension method on the dependency that it uses, e.g. ```csharp public static async Task> GetActiveSubscribersAsync( this IDbConnection db, MailingList mailingList) { return await db.SelectAsync(db.From(db.TableAlias("c")) .Where(x => x.DeletedDate == null && x.UnsubscribedDate == null && x.VerifiedDate != null && (mailingList & x.MailingLists) == mailingList) .WhereNotExists(db.From() .Where((c,e) => e.EmailLower == Sql.TableAlias(c.EmailLower, "c")) .Select(x => x.Id)) ); } ``` Which does a great job at encapsulating logic and making it reusable and readable: ```csharp foreach (var sub in await Db.GetActiveSubscribersAsync(MailingList.Newsletter)) { //... } ``` Where it can be reused without referencing any external classes whilst also being easily discoverable via intellisense. This works great for 1 or 2 dependencies, but becomes more cumbersome as the number of dependencies grows, e.g: ```csharp public static async Task> GetActiveSubscribersAsync( this IDbConnection db, ILogger log, ICacheClient cache, MailingList mailingList) ``` In which the complexity of the extension method dependencies leaks and impacts all calling classes that need to include them and also starts to impact its readability, e.g: ```csharp public class MyService(ILogger log, ICacheClient cache, IDbConnection db) : Service { public object Any(MyRequest request) { var subs = await Db.GetActiveSubscribersAsync(log, cache, request.MailList); } } ``` ### Refactoring Logic into separate classes The solution to this is to refactor the logic into a separate class and leverage the IOC to inject the dependencies it needs, fortunately with Primary Constructors this now requires minimal boilerplate code, e.g: ```csharp class MyLogic(ILogger log, ICacheClient cache, IDbConnection db) { //... } ``` But it still requires manual registration adding additional complexity to your Host project `Program.cs` or [Modular Configurations](https://docs.servicestack.net/modular-startup) which needs to manage registration for all these new logic classes, e.g: ```csharp builder.Services.AddTransient(); ``` ## Commands Feature Which touches on the first benefit of the **Commands Feature** which like ServiceStack Services auto registers all classes implementing the intentionally simple and impl-free `IAsyncCommand` interface, e.g: ```csharp public interface IAsyncCommand { Task ExecuteAsync(T request); } ``` Allowing for maximum flexibility in how to implement your logic classes, which are essentially encapsulated units of logic with a single method to execute it, e.g: ```csharp public class AddTodoCommand(ILogger log, IDbConnection db) : IAsyncCommand { public async Task ExecuteAsync(CreateTodo request) { var newTodo = request.ConvertTo(); newTodo.Id = await db.InsertAsync(newTodo, selectIdentity:true); log.LogDebug("Created Todo {Id}: {Text}", newTodo.Id, newTodo.Text); } } ``` Where we immediately get the benefits of code reuse, encapsulation, and readability without needing to manually register and pollute your App's configuration with them. By default Commands are registered as transient dependencies, but you can also register them with a different lifetime scope using the `[Lifetime]` attribute, e.g: ```csharp [Lifetime(Lifetime.Scoped)] public class AddTodoCommand(ILogger log, IDbConnection db) : IAsyncCommand {} ``` Or by manually registering them, if you need a custom registration: ```csharp services.AddTransient(c => CreateAddTodoCommand(c)); ``` ### Commands with Results For maximum flexibility, we want to encourage temporal decoupling by separating initiating a command from its execution, so instead of adding a different method to execute commands with results, we're instead recommending the convention of storing the result of a command in a `Result` property, e.g: ```csharp public interface IAsyncCommand : IAsyncCommand, IHasResult { } public interface IHasResult { T Result { get; } } ``` So we could implement a command with a result like: ```csharp public class AddTodoCommand(ILogger log, IDbConnection db) : IAsyncCommand { public Todo Result { get; private set; } public async Task ExecuteAsync(CreateTodo request) { Result = request.ConvertTo(); Result.Id = await db.InsertAsync(newTodo, selectIdentity:true); log.LogDebug("Created Todo {Id}: {Text}", Result.Id, Result.Text); } } ``` ### Messaging Although for better resilience and scalability we recommend utilizing a messaging pattern to notify the outputs of a command by publishing messages to invoke dependent logic instead of returning a result, e.g: ```csharp public class AddTodoCommand(IDbConnection db, IMessageProducer mq) : IAsyncCommand { public async Task ExecuteAsync(CreateTodo request) { var newTodo = request.ConvertTo(); newTodo.Id = await db.InsertAsync(newTodo, selectIdentity:true); mq.Publish(new SendNotification { TodoCreated = newTodo }); } } ``` Which decouples the sender and receiver of the message, allowing it to finish without needing to wait and concern itself on how subsequent logic is processed, e.g. how to handle errors, whether to execute it in a different managed thread, in parallel, etc. Messaging encourages adopting a more reliable asynchronous one-way workflow instead of implementing logic serially where the sender is timely coupled to the successful execution of all subsequent logic before being able to complete, e.g: ```csharp await cmd.ExecuteAsync(createTodo); var newTodo = cmd.Result; await SendNewTodoNotificationAsync(newTodo); ``` It allows for more reliable and observable workflows that removes the temporal coupling between components where each execution step can be executed on different threads, independently monitored and retried if needed. ```txt [A] -> [B] -> [C] ``` ### Commands as Application Building Blocks As they're not dependent on any framework and can support multiple execution patterns, we believe Commands make great building blocks for insulating units of logic as they're simple and testable and allow for managed execution which can easily add logging, monitoring, and resilience around your logic. ### Background MQ It should be noted adopting a messaging pattern doesn't require additional infrastructure complexity of an external MQ Server as you can use the [Background MQ](https://docs.servicestack.net/background-mq) to execute messages in configurable managed background threads. ### Executing Commands Commands are effectively a pattern to structure your logic that doesn't depend on any implementation assembly or framework, so they can just be executed directly, e.g: ```csharp using var db = dbFactory.Open(); var cmd = new AddTodoCommand(new NullLogger(), db); await cmd.ExecuteAsync(new CreateTodo { Text = "New Todo" }); ``` ### Command Executor They also allow for a managed execution which the **CommandsFeature** provides with its `ICommandExecutor` which can be executed like: ```csharp public class MyService(ICommandExecutor executor) : Service { public object Any(MyRequest request) { var cmd = executor.Command(); await cmd.ExecuteAsync(new AddTodoCommand { Text = "New Todo" }); } } ``` This still results in the same behavior where exceptions are bubbled but also adds observability and resilience and other niceties like executing any Fluent or Declarative Validation on Command Requests. ### Retry Failed Commands We can make commands more resilient by adding the `[Retry]` attribute to opt into auto retrying failed commands: ```csharp [Retry] public class AddTodoCommand() : IAsyncCommand {} ``` Which will automatically retry the command as per the default Retry Policy: ```csharp services.AddPlugin(new CommandsFeature { DefaultRetryPolicy = new( Times: 3, Behavior: RetryBehavior.FullJitterBackoff, DelayMs: 100, MaxDelayMs: 60_000, DelayFirst: false ) }); ``` That can be overridden on a per-command basis with the `[Retry]` attribute, e.g: ```csharp [Retry(Times=4, MaxDelayMs=300_000, Behavior=RetryBehavior.LinearBackoff)] public class AddTodoCommand() : IAsyncCommand {} ``` The different Retry Behaviors available include: ```csharp public enum RetryBehavior { // Use the default retry behavior Default, // Always retry the operation after the same delay Standard, // Should be retried with a linear backoff delay strategy LinearBackoff, // Should be retried with an exponential backoff strategy ExponentialBackoff, // Should be retried with a full jittered exponential backoff strategy FullJitterBackoff, } ``` ## Command Admin UI Which can be inspected in the new **Command Admin UI** where you can view summary stats of all executed Commands and **APIs** in the **Summary** tab, e.g: [![](/img/posts/commands-feature/AddTodoCommand-summary.png)](/img/posts/commands-feature/AddTodoCommand-summary.png) ### Latest Command Executions It also maintains a rolling log of the latest executed commands in the **Latest** tab: [![](/img/posts/commands-feature/AddTodoCommand-latest.png)](/img/posts/commands-feature/AddTodoCommand-latest.png) ### Failed Command Executions Whilst the **Errors** tab shows a list of all failed **Command** and **API** executions: [![](/img/posts/commands-feature/AddTodoCommand-errors.png)](/img/posts/commands-feature/AddTodoCommand-errors.png) ### Execute Internal Commands A benefit of using Commands as the building block for your internal logic is that they enjoy many of the same benefits of ServiceStack's message-based Services where they can be invoked using just the Command **Name** and a **Request** Body which allows them to be discovered and executed from the **Explore** Tab: [![](/img/posts/commands-feature/AddTodoCommand-execute.png)](/img/posts/commands-feature/AddTodoCommand-execute.png) In this way they can be treated like **Internal APIs** for being able to invoke internal functionality that's only accessible by **Admin** Users. ### Group Commands by Tag Just like ServiceStack Services they can be grouped by **Tag** which can be used to group related commands: ```csharp [Tag("Todos")] public class AddTodoCommand() : IAsyncCommand {} ``` ## MQ Integration Although `CommandsFeature` is a standalone feature we're registering it in the new Identity Auth Templates `Configure.Mq.cs` which already uses the Background MQ to execute messages in managed background threads where it's used to send Identity Auth emails: ```csharp public class ConfigureMq : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(c => new BackgroundMqService()); services.AddPlugin(new CommandsFeature()); }) .ConfigureAppHost(afterAppHostInit: appHost => { var mqService = appHost.Resolve(); //Register ServiceStack APIs you want to be able to invoke via MQ mqService.RegisterHandler(appHost.ExecuteMessage); mqService.Start(); }); } ``` Despite being 2 independent features, they work well together as the Background MQ can be used to execute Commands in managed background threads of which a single thread is used to execute each Request Type by default (configurable per request). You'd typically want to use queues to improve scalability by reducing locking and concurrency contention of heavy resources by having requests queued and executed in a managed background thread where it's able to execute requests as fast as it can without contention. Queues are also a great solution for working around single thread limitations of resources like writes to SQLite databases. ## Use Case - SQLite Writes As we've started to use server-side SQLite databases for our new Apps given its [many benefits](https://docs.servicestack.net/ormlite/litestream) we needed a solution to workaround its limitation of not being able to handle multiple writes concurrently. One of the benefits of using SQLite is creating and managing multiple databases is relatively cheap, so we can mitigate this limitation somewhat by maintaining different subsystems in separate databases, e.g: [![](/img/posts/commands-feature/pvq-databases.png)](/img/posts/commands-feature/pvq-databases.png) But each database can only be written to by a single thread at a time, which we can now easily facilitate with **Background MQ** and **MQ Command DTOs**. ### MQ Command DTOs We can use the new `[Command]` attribute to be able to execute multiple commands on a single Request DTO Properties, e.g: ```csharp [Tag(Tag.Tasks)] [Restrict(RequestAttributes.MessageQueue), ExcludeMetadata] public class DbWrites : IGet, IReturn { [Command] public Vote? CreatePostVote { get; set; } [Command] public Vote? CreateCommentVote { get; set; } [Command] public Post? CreatePost { get; set; } [Command] public Post? UpdatePost { get; set; } [Command] public DeletePosts? DeletePosts { get; set; } [Command] public DeleteAnswers? DeleteAnswers { get; set; } [Command] public Post? CreateAnswer { get; set; } [Command] public PostSubscriptions? PostSubscriptions { get; set; } [Command] public TagSubscriptions? TagSubscriptions { get; set; } //... } ``` Then to execute the commands we can use the `Request.ExecuteCommandsAsync` extension method for its Background MQ API implementation: ```csharp public class BackgroundMqServices : Service { public Task Any(DbWrites request) => Request.ExecuteCommandsAsync(request); } ``` Which goes through all Request DTO properties to execute all populated properties with their associated command, using it as the request for the command. So after registering the `DbWrites` Command DTO with the MQ Service: ```csharp mqService.RegisterHandler(appHost.ExecuteMessage); ``` We can now publish a single `DbWrites` message to execute multiple commands in a single managed background thread, e.g: ```csharp public class NotificationServices(MessageProducer mq) : Service { public object Any(Watch request) { var userName = Request.GetClaimsPrincipal().GetUserName(); mq.Publish(new DbWrites { PostSubscriptions = request.PostId == null ? null : new() { UserName = userName, Subscriptions = [request.PostId.Value], }, TagSubscriptions = request.Tag == null ? null : new() { UserName = userName, Subscriptions = [request.Tag], }, }); mq.Publish(new AnalyticsTasks { WatchRequest = request, }); } } ``` We also benefit from its natural parallelism where write requests to different Databases are executed in parallel. # Simple Auth Story for .NET 8 C# Microservices Source: https://servicestack.net/posts/simple-auth-microservices With ServiceStack now [fully integrated with ASP.NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth) our latest [.NET 8 Tailwind Templates](/start) now include a full-featured Auth Configuration complete with User Registration, Login, Password Recovery, Two Factory Auth, and more. Whilst this is great for C# Web Applications which need it, it neglects the class of Apps which don't need User Auth and the additional complexity it brings with Identity and Password Management, EF Migrations, Token Expirations, etc. For these stand-alone Apps, Microservices and Docker Appliances that would still like to restrict Access to their APIs but don't need the complexity of ASP .NET Core's Authentication machinery, a simpler Auth Story is ideal. With the [introduction of API Keys](/posts/apikeys) we're able to provide a simpler Auth Story for stand-alone .NET 8 Microservices that's easy for **Admin** Users to manage and control which trusted clients and B2B Integrations can access their functionality. :::youtube 0ceU91ZBhTQ Simple Auth Story with API Keys ideal for .NET 8 Microservices ::: The easiest way to get started is by creating a new Empty project with API Keys enabled with your preferred database to store the API Keys in. SQLite is a good choice for stand-alone Apps as it doesn't require any infrastructure dependencies.

Create a new Empty project with API Keys

### Existing Projects Existing projects not configured with Authentication can enable this simple Auth configuration by running: :::sh x mix apikeys-auth ::: Which will add the [ServiceStack.Server](https://nuget.org/packages/ServiceStack.Server) dependency and the [Modular Startup](https://docs.servicestack.net/modular-startup) configuration below: ```csharp public class ConfigureApiKeys : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature(new AuthSecretAuthProvider("p@55wOrd"))); services.AddPlugin(new ApiKeysFeature { // Optional: Available Scopes Admin Users can assign to any API Key // Features = [ // "Paid", // "Tracking", // ], // Optional: Available Features Admin Users can assign to any API Key // Scopes = [ // "todo:read", // "todo:write", // ], }); }) .ConfigureAppHost(appHost => { using var db = appHost.Resolve().Open(); var feature = appHost.GetPlugin(); feature.InitSchema(db); }); } ``` Which configures the **AuthSecretAuthProvider** with the **Admin** password and the **ApiKeysFeature** to enable support for [API Keys](https://docs.servicestack.net/auth/apikeys). ### Admin UI The **Admin** password will give you access to the [Admin UI](https://docs.servicestack.net/admin-ui) at: :::{.text-4xl .text-center .text-indigo-800} /admin-ui ::: ![](/img/posts/simple-auth-microservices/admin-ui-signin.png) ![](/img/posts/simple-auth-microservices/admin-ui-dashboard.png) ### API Keys Admin UI Clicking on **API Keys** menu item will take you to the API Keys Admin UI where you'll be able to create new API Keys that you can distribute to different API consumers you want to be able to access your APIs: ![](/img/posts/simple-auth-microservices/admin-ui-apikeys.png) The **ApiKeysFeature** plugin will let you control different parts of the UI, including what **Features** you want to assign to API Keys and what **Scopes** you want individual API Keys to be able to have access to. ```csharp services.AddPlugin(new ApiKeysFeature { Features = [ "Paid", "Tracking", ], Scopes = [ "todo:read", "todo:write", ], // ExpiresIn =[ // new("", "Never"), // new("30", "30 days"), // new("365", "365 days"), // ], // Hide = ["RestrictTo","Notes"], }); ``` Any configuration on the plugin will be reflected in the UI: ![](/img/posts/simple-auth-microservices/admin-ui-apikeys-new.png) The API Keys Admin UI also lets you view and manage all API Keys in your App, including the ability to revoke API Keys, extend their Expiration date as well as manage any Scopes and Features assigned to API Keys. ![](/img/posts/simple-auth-microservices/admin-ui-apikeys-edit.png) ### Protect APIs with API Keys You'll now be able to protect APIs by annotating Request DTOs with the `[ValidateApiKey]` attribute: ```csharp [ValidateApiKey] public class Hello : IGet, IReturn { public required string Name { get; set; } } ``` Which only allows requests with a **valid API Key** to access the Service. ### Scopes We can further restrict API access by assigning them a scope which will only allow access to Valid API Keys configured with that scope, e.g: ```csharp [ValidateApiKey("todo:read")] public class QueryTodos : QueryDb { public long? Id { get; set; } public List? Ids { get; set; } public string? TextContains { get; set; } } [ValidateApiKey("todo:write")] public class CreateTodo : ICreateDb, IReturn { [ValidateNotEmpty] public required string Text { get; set; } public bool IsFinished { get; set; } } [ValidateApiKey("todo:write")] public class UpdateTodo : IUpdateDb, IReturn { public long Id { get; set; } [ValidateNotEmpty] public required string Text { get; set; } public bool IsFinished { get; set; } } [ValidateApiKey("todo:write")] public class DeleteTodos : IDeleteDb, IReturnVoid { public long? Id { get; set; } public List? Ids { get; set; } } ``` ### Restrict To APIs Scopes allow for coarse-grained access control allowing a single scope to access a logical group of APIs. For more fine-grained control you can use **Restrict To APIs** to specify just the APIs an API Key can access: ![](/img/posts/simple-auth-microservices/admin-ui-apikeys-restrict-to.png) Unlike scopes which can access APIs with the **same scope** or **without a scope**, Valid API Keys configured with **Restrict To APIs** can only access those specific APIs. ### Features Features are user-defined strings accessible within your Service implementation to provide different behavior based on Features assigned to the API Key, e.g: ```csharp public object Any(QueryTodos request) { if (Request.GetApiKey().HasFeature("Paid")) { //... } } ``` ### API Explorer Support for API Keys is also integrated into the [API Explorer](https://docs.servicestack.net/api-explorer) allowing users to use their API Keys to access API Key protected Services which are highlighted with a **Key** Icon: ![](/img/posts/simple-auth-microservices/apiexplorer-requires-apikey.png) Users can enter their API Key by clicking on the **Key** Icon in the top right, or the link in the Warning alert when trying to access an API Key protected Service: ![](/img/posts/simple-auth-microservices/apiexplorer-apikey-dialog.png) ### Client Usage All HTTP and existing [Service Clients](https://docs.servicestack.net/clients-overview) can be configured to use API Keys for machine-to-machine communication, which like most API Key implementations can be passed in a [HTTP Authorization Bearer Token](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1) that can be configured in Service Clients with: #### C# ```csharp var client = new JsonApiClient(BaseUrl) { BearerToken = apiKey }; ``` #### TypeScript ```ts const client = new JsonServiceClient(BaseUrl) client.bearerToken = apiKey ``` ### API Key HTTP Header Alternatively, API Keys can also be passed in the `X-Api-Key` HTTP Header which allows clients to be configured with an alternative Bearer Token allowing the same client to call both **Authenticated** and **API Key** protected APIs, e.g: #### C# ```csharp var client = new JsonApiClient(BaseUrl) { BearerToken = jwt, Headers = { [HttpHeaders.XApiKey] = apiKey } }; ``` #### TypeScript ```ts const client = new JsonServiceClient(BaseUrl) client.bearerToken = apiKey client.headers.set('X-Api-Key', apiKey) ``` ## Conclusion We hope this shows how stand-alone .NET 8 Microservices and self-contained Docker Apps can use the simple **Admin** and **API Keys** configuration to easily secure their APIs with API Keys, complete with **Management UI** and **typed Service Client** integrations. # Using API Keys to secure .NET 8 C# APIs Source: https://servicestack.net/posts/apikeys As we continue to embrace and natively integrate with ASP.NET Core's .NET 8 platform, we've reimplemented the last major feature missing from ServiceStack Auth - support for API Keys that's now available from **ServiceStack v8.3**. ### What are API Keys? API Keys are a simple and effective way to authenticate and authorize access to your C# APIs, which are typically used for machine-to-machine communication, where a client application needs to access an API without user intervention. API Keys are often used to control access to specific resources or features in your API, providing a simple way to manage access control. ### Redesigning API Keys Building on our experience with API Keys in previous versions of ServiceStack, we've taken the opportunity to redesign how API Keys work to provide a more flexible and powerful way to manage access control for your APIs. The existing [API Key Auth Provider](https://docs.servicestack.net/auth/api-key-authprovider) was implemented as another Auth Provider that provided another way to authenticate a single user. The consequences of this was: - Initial API Request was slow as it required going through the Authentication workflow to authenticate the user and setup authentication for that request - No support for fine-grained access control as API Keys had same access as the authenticated user - API Keys had to be associated with a User which was unnecessary for machine-to-machine communication Given the primary use-case for API Keys is for machine-to-machine communication where the client is not a User, nor do they want systems they give out their API Keys to, to have access to their User Account, we've changed how API Keys work in .NET 8. ## .NET 8 API Keys Feature :::youtube U4vqOIHOs_Q New .NET 8 API Keys Feature with Built-In UIs! ::: The first design decision to overcome the above issues was to separate API Keys from Users and Authentication itself, where the new `ApiKeysFeature` is now just a plugin instead of an Auth Provider, which can be added to existing Identity Auth Apps with: :::sh x mix apikeys ::: Which will add the API Keys [Modular Startup](https://docs.servicestack.net/modular-startup) to your Host project, a minimal example of which looks like: ```csharp public class ConfigureApiKeys : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new ApiKeysFeature()); }) .ConfigureAppHost(appHost => { using var db = appHost.Resolve().Open(); var feature = appHost.GetPlugin(); feature.InitSchema(db); }); } ``` Where it registers the `ApiKeysFeature` plugin and creates the `ApiKey` table in the App's configured database if it doesn't already exist. ### Creating Seed API Keys The plugin can also be used to programmatically generate API Keys for specified Users: ```csharp if (feature.ApiKeyCount(db) == 0) { var createApiKeysFor = new [] { "admin@email.com", "manager@email.com" }; var users = IdentityUsers.GetByUserNames(db, createApiKeysFor); foreach (var user in users) { // Create a super API Key for the admin user List scopes = user.UserName == "admin@email.com" ? [RoleNames.Admin] : []; var apiKey = feature.Insert(db, new() { Name="Seed Key", UserId=user.Id, UserName=user.UserName, Scopes=scopes }); var generatedApiKey = apiKey.Key; } } ``` ### Basic Usage With the plugin registered, you can now use the `ValidateApiKey` attribute to limit APIs to only be accessible with a valid API Key, e.g: ```csharp [ValidateApiKey] public class MyRequest {} ``` ### Use API Keys with our without Users and Authentication API Keys can optionally be associated with a User, but they don't have to be, nor do they run in the context of a User or are able to invoke any Authenticated APIs on their own. Users who create them can also limit their scope to only call APIs they have access to, which can be done with user-defined scopes: ### Scopes Scopes are user-defined strings that can be used to limit APIs from only being accessible with API Keys that have the required scope. For example, we could create generate API Keys that have **read only**, **write only** or **read/write** access to APIs by assigning them different scopes, e.g: ```csharp public static class Scopes { public const string TodoRead = "todo:read"; public const string TodoWrite = "todo:write"; } [ValidateApiKey(Scopes.TodoRead)] public class QueryTodos : QueryDb {} [ValidateApiKey(Scopes.TodoWrite)] public class CreateTodo : ICreateDb, IReturn {} [ValidateApiKey(Scopes.TodoWrite)] public class UpdateTodo : IUpdateDb, IReturn {} [ValidateApiKey(Scopes.TodoWrite)] public class DeleteTodos : IDeleteDb, IReturnVoid {} ``` Where only API Keys with the `todo:read` scope can access the `QueryTodos` API, and only API Keys with the `todo:write` scope can access the `CreateTodo`, `UpdateTodo` and `DeleteTodos` APIs. APIs that aren't assigned a scope can be accessed by any valid API Key. The only built-in Scope is `Admin` which like the `Admin` role enables full access to all `[ValidateApiKeys]` APIs. ### Fine-grained Access Control Alternatively API Keys can be restricted to only be able to access specific APIs. ### Features In addition to scopes, API Keys can also be tagged with user-defined **Features** which APIs can inspect to enable different behavior, e.g. a **Paid** feature could be used to increase rate limits or return premium content whilst a **Tracking** feature could be used to keep a record of API requests, etc. These can be accessed in your Services with: ```csharp public object Any(QueryTodos request) { if (Request.GetApiKey().HasFeature(Features.Paid)) { // return premium content } } ``` ## Integrated UIs Like many of ServiceStack's other premium features, API Keys are fully integrated into [ServiceStack's built-in UIs](https://servicestack.net/auto-ui) including [API Explorer](https://docs.servicestack.net/api-explorer) and the [Admin UI](https://docs.servicestack.net/admin-ui). ### API Explorer Your Users and API Consumers can use API Explorer to invoke protected APIs with their API Key. API Key protected APIs will display a **key** icon next to the API instead of the **padlock** which is used to distinguish APIs that require Authentication. Users can configure API Explorer with their API Key by either clicking the **key** icon on the top right or by clicking the **API Key** link on the alert message that appears when trying to access an API requiring an API Key: ![](/img/posts/apikeys/apiexplorer-apikeys.png) Both of these will open the **API Key** dialog where they can paste their API Key: ![](/img/posts/apikeys/apiexplorer-apikeys-dialog.png) :::info NOTE API Keys are not stored in localStorage and only available in the current session ::: ### Admin UI Whilst **Admin** users can view and manage API Keys in the API Key [Admin UI](https://docs.servicestack.net/admin-ui) at: :::{.text-4xl .text-center .text-indigo-800} /admin-ui/apikeys ::: ![](/img/posts/apikeys/admin-ui-apikeys.png) This will let you view and manage all API Keys in your App, including the ability to revoke API Keys, extend their Expiration date as well as manage any Scopes and Features assigned to API Keys. ### Customizing API Key UIs The `ApiKeysFeature` plugin can be configured to specify which **Scopes** and **Features** can be assigned to API Keys as well as the different Expiration Options you want available in the API Key management UIs, e.g: ```csharp services.AddPlugin(new ApiKeysFeature { // Optional: Available Scopes Admin Users can assign to any API Key Features = [ Features.Paid, Features.Tracking, ], // Optional: Available Features Admin Users can assign to any API Key Scopes = [ Scopes.TodoRead, Scopes.TodoWrite, ], // Optional: Limit available Expiry options that can be assigned to API Keys // ExpiresIn = [ // new("", "Never"), // new("7", "7 days"), // new("30", "30 days"), // new("365", "365 days"), // ], }); ``` ### Admin User API Keys When the `ApiKeysFeature` plugin is registered, the [User Admin UI](https://docs.servicestack.net/admin-ui-identity-users) will be enhanced to include the ability to create and manage API Keys for the user at the bottom of the **Edit User** form: ![](/img/posts/apikeys/admin-ui-user-apikeys.png) #### Creating User API Keys When creating API Keys, you can assign them a **Name**, its **Expiration** date and any **Scopes**, **Features** and **Notes**. ![](/img/posts/apikeys/admin-ui-user-apikeys-create.png) ### Restrict to APIs `Scopes` provide a simple way to logically group a collection of related APIs behind UX-friendly names without Users needing to know the behavior of each individual API. In addition, Users who want fine-grained control can also restrict API Keys to only be able to access specific APIs that their systems make use of by selecting them from the **Restrict to APIs** option: ![](/img/posts/apikeys/apikeys-restrict-to.png) #### One Time only access of generated API Key All UIs limit access to the generated API Key token so that it's only accessible at the time of creation: ![](/img/posts/apikeys/admin-ui-user-apikeys-create-dialog.png) #### Editing User API Keys Everything about the API Key can be edited after it's created except for the generated API Key token itself, in addition to be able to cancel and revoke the API Key: ![](/img/posts/apikeys/admin-ui-user-apikeys-edit.png) Invalid API Keys that have expired or have been disabled will appear disabled in the UI: ![](/img/posts/apikeys/admin-ui-user-apikeys-disabled.png) ## User Management API Keys In addition to the built-in Admin UIs to manage API Keys, all Identity Auth Tailwind templates have also been updated to include support for managing API Keys in their User Account pages: The templates aren't configured to use API Keys by default, but new projects can be configured to use API Keys by selecting the **API Keys** feature on the [Start Page](/start): [![](/img/posts/apikeys/start-apikeys.png)](/start) Or by mixing the `apikeys` project in your host project: :::sh x mix apikeys ::: Which add the `Configure.ApiKeys.cs` modular startup to your Host project, which registers the `ApiKeysFeature` plugin where you'd use the `UserScopes` and `UserFeatures` collections instead to control which scopes and features Users can assign to their own API Keys, e.g: ```csharp services.AddPlugin(new ApiKeysFeature { // Optional: Available Scopes Admin Users can assign to any API Key Features = [ Features.Paid, Features.Tracking, ], // Optional: Available Features Admin Users can assign to any API Key Scopes = [ Scopes.TodoRead, Scopes.TodoWrite, ], // Optional: Limit available Scopes Users can assign to their own API Keys UserScopes = [ Scopes.TodoRead, ], // Optional: Limit available Features Users can assign to their own API Keys UserFeatures = [ Features.Tracking, ], }); ``` ### Identity Auth API Keys When enabled users will be able to create and manage their own API Keys from their Identity UI pages which will use any configured `UserScopes` and `UserFeatures`: ![](/img/posts/apikeys/identity-auth-apikeys.png) ### Client Usage Like most API Key implementations, API Keys can be passed in a [HTTP Authorization Bearer Token](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1) that can be configured in ServiceStack Service Clients with: #### C# ```csharp var client = new JsonApiClient(BaseUrl) { BearerToken = apiKey }; ``` #### TypeScript ```ts const client = new JsonServiceClient(BaseUrl) client.bearerToken = apiKey ``` ### API Key HTTP Header Alternatively, API Keys can also be passed in the `X-Api-Key` HTTP Header which allows clients to be configured with an alternative Bearer Token allowing the same client to call both **Authenticated** and **API Key** protected APIs, e.g: #### C# ```csharp var client = new JsonApiClient(BaseUrl) { BearerToken = jwt, Headers = { [HttpHeaders.XApiKey] = apiKey } }; ``` #### TypeScript ```ts const client = new JsonServiceClient(BaseUrl) client.bearerToken = apiKey client.headers.set('X-Api-Key', apiKey) ``` Or use a different HTTP Header by configuring `ApiKeysFeature.HttpHeader`, e.g: ```csharp services.AddPlugin(new ApiKeysFeature { HttpHeader = "X-Alt-Key" }); ``` # Support for RHEL 9's hardened cryptography policy Source: https://servicestack.net/posts/rhel9-cryptography A consequence of RedHat Enterprise Linux 9's hardened [system-wide cryptographic policies](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening) is that it's incompatible with ServiceStack's current licensing mechanism which uses RSA encryption and SHA1 hashing algorithm to protect and validate license keys. Unfortunately this makes it no longer possible to use License Keys to run unrestricted ServiceStack Apps on default installs of RHEL 9. The difficulty being we can't both support RHEL 9's hardened cryptography policy and maintain compatibility with being able to use newer License Keys on all previous versions of ServiceStack - vital for enabling frictionless rotation of License Keys. As a system-wide policy we're unable to work around this restriction in the library to allow usage of RSA+SHA1 to just validate License Keys which is the only place it's used. As it only affected a small number of users initially we recommend that they just switch to use [RHEL's Legacy Cryptography Policy](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening) which allows for maximum compatibility with existing software. ### Road to Solution ![](/img/posts/rhel9-cryptography/bg-redhat.webp) As more customers upgraded to RHEL 9 and started experiencing the same issue, we've decided to invest time to try and address this issue starting with adding support for a configurable Hashing algorithm when creating and validating License Keys. We still have the issue of not being able to generate a new License Key that would be compatible with both default RHEL 9 and all previous versions of ServiceStack. The solutions under consideration were: - Generate a new License Key that's compatible with RHEL 9's hardened cryptography policy, but inform customers that they'll be unable to use the new License Key on their existing versions of ServiceStack and to continue to use their existing License Key for existing versions - Generate 2 License Keys, and explain to Customers which key to use for previous versions of ServiceStack and which key to use for RHEL 9 - Provide a way for customers to regenerate their License Key to support RHEL 9's hardened cryptography policy Since this issue only affected a minority of our Customers we decided to go with the last option to avoid inflicting any additional complexity on the majority of our Customers who are unaffected by this issue. ### Generate License Key for RHEL 9+ Starting from ServiceStack v8.3+ Customers can regenerate a new License Key with a stronger **SHA512** Hash Algorithm that's compatible with RHEL 9's default hardened cryptography policy by visiting: :::{.text-3xl .text-indigo-600} https://account.servicestack.net/regenerate-license ::: ### Future We'll need to wait at least 1-2 years before we can make the stronger Hash Algorithm the default in order to reduce the impact of not being able to use new License Keys on versions of ServiceStack prior to **v8.2**. After the switch is made regenerating license keys will no longer be necessary. # Using ASP.NET Core Output Caching Source: https://servicestack.net/posts/redis-outputcache With the release of ServiceStack 8.1, we've embraced tighter integration with ASP.NET Core, including support for registering ServiceStack services with ASP.NET Core's Endpoint Routing system. This opens up exciting opportunities to leverage more of ASP.NET Core's rich feature set in your ServiceStack applications. One such feature is ASP.NET Core's built-in support for Output Caching (also known as Response Caching). Output Caching allows you to dramatically improve the performance of your APIs by caching the output and serving it directly from the cache for subsequent requests. This can significantly reduce the load on your server and database for frequently accessed, cacheable responses. ## Enabling Output Caching To utilize Output Caching with your ServiceStack Endpoints, you first need to add the Output Caching middleware to your ASP.NET Core request pipeline in the `Configure` method of your `Program.cs`: ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); var services = builder.Services; app.UseOutputCache(); // ... app.UseServiceStack(new AppHost(), options => options.MapEndpoints()); ``` Then in `ConfigureServices` you need to add the Output Caching services: ```csharp services.AddOutputCache(); ``` The order of adding OutputCache to your request pipeline can be very sensitive to change, so this will depend largely on your application and dependencies you are already using. For example, below is an example of using it in a Blazor application. ```csharp var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAntiforgery(); // Add OutputCache after Antiforgery and before Auth related middleware app.UseOutputCache(); // Required for OutputCache app.UseAuthentication(); app.UseAuthorization(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); // Add additional endpoints required by the Identity /Account Razor components. app.MapAdditionalIdentityEndpoints(); app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); ``` ## Configuring Caching Behavior With the middleware in place, you can now configure caching behaviors for your ServiceStack Endpoints by registering against the Route Handlers within the ServiceStack `options`. ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); options.RouteHandlerBuilders.Add((routeHandlerBuilder, operation, verb, route) => { routeHandlerBuilder.CacheOutput(c => { // Use Cache Profiles c.UseProfile("Default30"); // Or configure caching per-request c.Expire(TimeSpan.FromSeconds(30)); c.VaryByAll(); }); }); }); ``` You can also vary the cache by specific properties, e.g: ```csharp builder.CacheOutput(c => c.VaryBy("userRole","region")); ``` Or use Cache Profiles for reusable caching strategies: ```csharp builder.Services.AddOutputCache(options => { options.AddPolicy("Default30", p => p.Expire(TimeSpan.FromSeconds(30))); }); ``` Then apply the named profile to your endpoints: ```csharp builder.CacheOutput(c => c.UseProfile("Default30")); ``` ## Finer-grained Control For more granular control, you can apply the `[OutputCache]` attribute directly on your Service class, and use the ServiceStack AppHost metadata in your `RouteHandlerBuilder`s `Add` method to detect and cache only the routes that are attributed with `OutputCache`. ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); options.RouteHandlerBuilders.Add((routeHandlerBuilder, operation, verb, route) => { // Initialized appHost and allServiceTypes var appHost = HostContext.AppHost; var allServiceTypes = appHost.Metadata.ServiceTypes; // Find the service matching the RequestType of the operation var operationType = operation.RequestType; // Match with operation, verb and route appHost.Metadata.OperationsMap.TryGetValue(operationType, out var operationMap); var serviceType = operationMap?.ServiceType; if (serviceType == null) return; if (serviceType.HasAttributeOf()) { // Handle duration from OutputCacheAttribute var outputCacheAttribute = serviceType.FirstAttribute(); routeHandlerBuilder.CacheOutput(policyBuilder => { policyBuilder.Cache().Expire(TimeSpan.FromSeconds(outputCacheAttribute.Duration)); }); } }); }); ``` ```csharp [OutputCache(Duration = 60)] public class MyServices : Service { public object Any(Hello request) { return new HelloResponse { Result = $"Hello, {request.Name}!" }; } } ``` This enables for fine grained control of the built in `OutputCache` functionality compatible with using the same attribute with your MVC Controllers, and you can extend your use by updating the code above within the ServiceStack options. ## ServiceStack Redis Distributed Cache The above examples so far have been using a cache store that comes with the OutputCache package. This is just an in memory store, so isn't suitable for a distributed application. Thankfully, you can override the IOutputCacheStore interface in your IoC to change out the implementation that uses a centralized system like a Redis server. ```csharp public class RedisOutputCacheStore(IRedisClientsManager redisManager) : IOutputCacheStore { public async ValueTask GetAsync(string key, CancellationToken cancellationToken) { await using var redis = await redisManager.GetClientAsync(token: cancellationToken); var value = await redis.GetAsync(key, cancellationToken); return value; } public async ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken) { await using var redis = await redisManager.GetClientAsync(token: cancellationToken); // First persist in normal cache hashset await redis.SetAsync(key, value, validFor, cancellationToken); if (tags == null) return; foreach (var tag in tags) { await redis.AddItemToSetAsync($"tag:{tag}", key, cancellationToken); } } public async ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { await using var redis = await redisManager.GetClientAsync(token: cancellationToken); var keys = await redis.GetAllItemsFromListAsync($"tag:{tag}", cancellationToken); foreach (var key in keys) { await redis.RemoveEntryAsync(key); await redis.RemoveItemFromSetAsync($"tag:{tag}", key, cancellationToken); } } } ``` The above is a simple implementation of the IOutputCacheStore using the ServiceStack.Redis client to handle a centralized distributed cache. Using the class above, we can create a `Configure.OutputCache.cs` file that registers our IoC dependencies. ```csharp [assembly: HostingStartup(typeof(BlazorOutputCaching.ConfigureOutputCache))] namespace BlazorOutputCaching; public class ConfigureOutputCache : IHostingStartup { public void Configure(IWebHostBuilder builder) { builder.ConfigureServices(services => { services.AddSingleton(c => new BasicRedisClientManager("localhost:6379")); services.AddSingleton(); }); } } ``` We register out Redis client manager for our RedisOutputCacheStore, and then the store itself. ## Summary ASP.NET Core Output Caching is a powerful tool for improving the performance of your ServiceStack endpoints. With ServiceStack 8.1's tight integration with ASP.NET Core Endpoint Routing, utilizing this feature is now straightforward. As always, caching is a balancing act. Apply it judiciously to frequently accessed, cacheable data. And be sure to implement appropriate invalidation strategies to keep your application's data fresh. By leveraging Output Caching effectively, you can dramatically improve the scalability and responsiveness of your ServiceStack powered applications. Try it out in your ServiceStack 8.1+ projects and let us know how it goes! # Using ASP.NET Core Rate Limiter Middleware Source: https://servicestack.net/posts/asp-rate-limiter-middleware Introduction Rate limiting is an important technique for protecting web APIs and applications from excessive traffic and abuse. By throttling the number of requests a client can make in a given time period, rate limiting helps ensure fair usage, maintains performance and availability, and defends against denial-of-service attacks. ASP.NET Core provides built-in middleware for rate limiting based on client IP address or client ID. And now with the latest release, ServiceStack has added support for ASP.NET Core endpoints, making it possible to leverage the same rate limiting middleware across all ASP.NET Core endpoints, including ServiceStack APIs. In this post, we'll look at how to enable rate limiting in an ASP.NET Core app using the standard middleware. Then we'll explore some more advanced options to fine-tune the rate limiting behavior. Finally, we'll see how to implement per-user rate limiting for multi-tenant SaaS applications using ASP.NET Core Identity and ServiceStack Mapped Endpoints. Setting Up Rate Limiting To get started, let's enable the basic rate limiting middleware in an ASP.NET Core application: 1. Install the `Microsoft.AspNetCore.RateLimiting` NuGet package 2. In the `Program.cs`, add `AddRateLimiter` to register the rate limiting services: ```csharp builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); options.OnRejected = (context, cancellationToken) => { if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString(); } context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; context.HttpContext.Response.WriteAsync("Too many requests. Please try again later."); return new ValueTask(); }; }); ``` This sets up a fixed window rate limiter with a limit of 100 requests per minute, partitioned by either authenticated username or client host name. 1. Add the `UseRateLimiter` middleware to the pipeline: ```csharp app.UseRateLimiter(); ``` With this basic setup, the API is now protected from excessive requests from individual clients. If a client exceeds the limit of 100 requests/minute, subsequent requests will receive a `HTTP 429 Too Many Requests` response. Advanced Options The rate limiting middleware provides several options to customize the behavior: - `PermitLimit` - The maximum number of requests allowed in the time window - `QueueLimit` - The maximum number of requests that can be queued when the limit is exceeded. Set to 0 to disable queueing. - `Window` - The time window for the limit, e.g. 1 minute, 1 hour, etc. - `AutoReplenishment` - Whether the rate limit should reset automatically at the end of each window For example, to allow short bursts but constrain average rate, we could implement a sliding window algorithm: ```csharp options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => RateLimitPartition.GetSlidingWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new SlidingWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, QueueLimit = 25, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 4 })); options.OnRejected = (context, cancellationToken) => { if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString(); } context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; context.HttpContext.Response.WriteAsync("Too many requests. Please try again later."); return new ValueTask(); }; ``` This allows up to 100 requests per minute on average, with the ability to burst up to 25 additional requests which are queued. ## Per-User Rate Limiting for SaaS Applications In a typical SaaS application, each user or tenant may have a different subscription plan that entitles them to a certain level of API usage. We can implement this per-user rate limiting by leveraging ASP.NET Core Identity to authenticate users and retrieve their plan details, and then configuring the rate limiter accordingly. First, ensure you have ASP.NET Core Identity set up in your application to handle user authentication. Then, add a property to your user class to store the rate limit for each user based on their plan: ```csharp public class ApplicationUser : IdentityUser { public int RateLimit { get; set; } } ``` Next, update the rate limiter configuration to partition by user and read the rate limit from the user's plan: ```csharp builder.Services.AddRateLimiter(options => { options.AddPolicy("per-user", context => { var user = context.User.Identity?.Name; if (string.IsNullOrEmpty(user)) { // Fallback to host name for unauthenticated requests return RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }); } // User exists // Get the user's rate limit from their plan var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var userManager = context.RequestServices.GetService>(); var appUser = userManager.FindByIdAsync(userId).Result; var rateLimit = appUser?.RateLimit ?? 0; // Create a user-specific rate limiter return RateLimitPartition.GetFixedWindowLimiter( partitionKey: user, factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = rateLimit, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }); }); }); ``` This configuration first checks if the request is authenticated. If not, it falls back to the default host-based rate limiting. For authenticated requests, it retrieves the user ID from the authentication claims and looks up the user in the ASP.NET Core Identity `UserManager`. It then reads the `RateLimit` property from the user object, which should be set based on the user's subscription plan. Finally, it creates a user-specific rate limiter using the `PartitionedRateLimiter` with the user's ID as the partition key and their personal rate limit as the `PermitLimit`. With this setup, each user will be rate limited independently based on their plan allowance. If a user exceeds their personal limit, they will receive a `429 Too Many Requests` response, while other users can continue making requests up to their own limits. Not only that, our rate handling is consistent across ASP.NET Core Endpoints regardless of how they are implemented, be it ServiceStack APIs, MVC Controllers, Minimal APIs etc. If you do need to target ServiceStack APIs with a specific policy name, you can create one with a policy name and use it when calling `UseServiceStack`. ```csharp services.AddRateLimiter(options => { // Policy name "per-user" is used by ServiceStack Mapped Endpoints options.AddPolicy("per-user", context => { var user = context.User.Identity?.Name; if (string.IsNullOrEmpty(user)) { // Fallback to host name for unauthenticated requests return RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }); } // User exists // Get the user's rate limit from their plan var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var userManager = context.RequestServices.GetService>(); var appUser = userManager.FindByIdAsync(userId).Result; var rateLimit = appUser?.RateLimit ?? 0; // Create a user-specific rate limiter return RateLimitPartition.GetFixedWindowLimiter( partitionKey: user, factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = rateLimit, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) }); }); // ... }); //... // Make sure to call UseRateLimiter app.UseRateLimiter(); // Specify which policy is used by ServiceStack Mapped Endpoints app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); options.RouteHandlerBuilders.Add((routeBuilder, operation, method, route) => { routeBuilder.RequireRateLimiting(policyName: "per-user"); }); }); ``` By combining ASP.NET Core rate limiting with ASP.NET Core Identity in this way, you can implement flexible, per-user rate limiting suitable for multi-tenant SaaS applications. The same approach can be extended to handle different rate limits for different API endpoints or user roles as needed. By upgrading ServiceStack to use ASP.NET Core Endpoints, you can now leverage the same rate limiting middleware across all ASP.NET Core Endpoints, including ServiceStack APIs. # Kotlin Compose Multiplatform with end-to-end typed Kotlin & C# APIs Source: https://servicestack.net/posts/kotlin-compose-multiplatform The last few years of neglect of Xamarin has slid it into irrelevance, removing itself from consideration in the already shortlist of viable development options for creating native multi-platform iOS, Android and Desktop Apps, which leaves us just Flutter and React Native as the only viable options. Thanks to the vast language ecosystem covered by [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference), which ever technology you end up choosing to develop native Mobile and Desktop Apps with, you'll always be able to develop with the productivity and type safety benefits of end-to-end typed APIs in your preferred language, whether it's [TypeScript](https://docs.servicestack.net/typescript-add-servicestack-reference) or [JavaScript](https://docs.servicestack.net/javascript-add-servicestack-reference) for React Native, [Dart](https://docs.servicestack.net/dart-add-servicestack-reference) for Flutter, [Java](https://docs.servicestack.net/java-add-servicestack-reference) or [Kotlin](https://docs.servicestack.net/kotlin-add-servicestack-reference) for Android, or [Swift](https://docs.servicestack.net/swift-add-servicestack-reference) for iOS. Fortunately JetBrains has stepped in to fill the void with Compose Multiplatform offering a modern alternative for creating native Mobile, Desktop & Web Apps which can also leverage [Kotlin ServiceStack Reference](https://docs.servicestack.net/kotlin-add-servicestack-reference) for end-to-end typed APIs. [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) builds on [Jetpack Compose](https://developer.android.com/jetpack/compose) - Google's modern toolkit for building native Android UIs bringing it to more platforms, including Windows, macOS and Linux Desktops, Web UIs with [Kotlin Wasm](https://kotlinlang.org/docs/wasm-overview.html) and on iOS with [Kotlin/Native](https://kotlinlang.org/docs/native-overview.html). We'll look at the latest [Compose Multiplatform v1.6 Release](https://blog.jetbrains.com/kotlin/2024/02/compose-multiplatform-1-6-0-release/) and use it to build a cross-platform Desktop App integrated with a .NET API backend utilizing [Kotlin ServiceStack Reference](https://docs.servicestack.net/kotlin-add-servicestack-reference) to generate Kotlin DTOs that can be used with the generic ServiceStack Java `JsonServiceClient` to enable its end-to-end typed API integration. ### JVM Platform Required Whilst Compose Multiplatform supports both JVM and non-JVM platforms, targeting a non JVM platform is very limited as you won't be able to reference and use any Java packages like ServiceStack's Java Client library in `net.servicestack:client` which is required for this example utilizing [Kotlin ServiceStack Reference](https://docs.servicestack.net/kotlin-add-servicestack-reference) typed Kotlin DTOs. ## Compose Multiplatform iOS & Android Apps :::youtube r6T3B7o1GYE JetBrains Compose Multiplatform iOS & Android Apps ::: The quickest way to a working Compose Multiplatform App integrated with a .NET API backend is to create a new project from the Compose Desktop template:

Create a new Compose Desktop App

Create a new Kotlin Multiplatform App with your preferred project name:

Or install from the command-line with the [x dotnet tool](https://docs.servicestack.net/dotnet-tool): :::sh x new kmp-desktop MyApp ::: ### Install JetBrains IDE As a JetBrains technology, you're spoilt for choice for which IDE to use. #### Android Studio If you're primarily developing for Android, Android Studio is likely the the best option, which you can setup by following their [Getting Started with Android Studio](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-setup.html) guide. #### JetBrains Fleet Otherwise if you're primarily developing a Desktop App it's recommended to use [Fleet](https://www.jetbrains.com/fleet/) - JetBrains alternative to VS Code as a lightweight IDE for Kotlin Multiplatform Development. It's the preferred IDE when developing against a .NET API as you can develop both Kotlin front-end UI and backend .NET APIs from a single IDE. To get setup with Fleet, follow the [Getting Started with JetBrains Fleet](https://www.jetbrains.com/help/kotlin-multiplatform-dev/fleet.html). ### Open Project with Fleet Once you've installed Fleet, you can open your Desktop App project by opening the Folder in the Fleet IDE, or like VS Code you can launch it to open your Project's folder from the command-line with: :::sh fleet MyApp ::: ### Setup Fleet When first opening fleet you'll start with an empty canvas. I'd recommend adding the **Files** tool window on the left panel to manage the Kotlin UI and the **Solution** tool window on the bottom left panel to manage the .NET API backend. ### Run .NET API and Kotlin Desktop App Once setup, you can run both the Desktop App and the .NET API backend with from the Run Dialog with the `Ctrl+R` keyboard shortcut, or by clicking on the play button icon in the top menu bar: ![](/img/posts/kotlin-compose-multiplatform/fleet-run.webp) You'll want to run the .NET API backend first by selecting your Project Name which should launch your browser at `https://localhost:5001`, then launch the Desktop App by selecting the **composeApp [Desktop]** configuration which should launch a working Desktop App that calls your .NET API on each keystroke to search for matching files in your project: ![](/img/posts/kotlin-compose-multiplatform/search-files-app.webp) The majority of the UI is maintained in [/commonMain/kotlin/App.kt](https://github.com/NetCoreTemplates/kmp-desktop/blob/main/kmp/composeApp/src/commonMain/kotlin/App.kt) created using Jetpack Compose's declarative Kotlin UI. ### Update Kotlin DTOs The typed Kotlin DTOs for your .NET APIs is generated in [dtos.kt](https://github.com/NetCoreTemplates/kmp-desktop/blob/main/kmp/composeApp/src/commonMain/kotlin/dtos.kt). Which can be regenerated by running **Update DTOs** in the Run Dialog. Alternatively they can also be regenerated by running the `dtos` npm script from the command-line in your .NET Host project: :::sh npm run dtos ::: #### Android Studio If you're using Android Studio, you can also install the [ServiceStack Plugin](https://plugins.jetbrains.com/plugin/7749-servicestack) from the JetBrains Marketplace: ![](/img/posts/kotlin-compose-multiplatform/android-studio-plugins.webp) Which provides a **Add ServiceStack Reference** UI on the Context Menu, by right-clicking the folder where you want the DTOs generated: ![](/img/posts/kotlin-compose-multiplatform/add-servicestack-reference-dialog.webp) Then to update just right-click the `dtos.kt` and click **Update ServiceStack Reference** on the context menu: ![](/img/posts/kotlin-compose-multiplatform/update-servicestack-reference-dialog.webp) ### Command Line For any other Text Editors or IDEs a Kotlin ServiceStack Reference can also be added from the command-line using the [x dotnet tool](https://docs.servicestack.net/dotnet-tool) by specifying the BaseUrl where the ServiceStack APIs are hosted, e.g: :::sh x kotlin https://localhost:5001 ::: To update and regenerate all Kotlin DTOs within a folder, run: :::sh x kotlin ::: ## Create a new Kotlin Multiplatform App from Scratch For a customized Compose Multiplatform App, you can create a new App with [Kotlin Multiplatform Wizard](https://kmp.jetbrains.com) with just the options you need: [![](/img/posts/kotlin-compose-multiplatform/kmp-wizard.webp)](https://kmp.jetbrains.com) Which you can download in an empty Web Project: :::sh x new web MyApp ::: Then open the folder with both the Kotlin Multiplatform and .NET Web App in fleet: :::sh fleet MyApp ::: # New Vue SPA Template Source: https://servicestack.net/posts/net8-vue-spa-template With ServiceStack [now fully integrated with .NET 8](/posts/servicestack-endpoint-routing), our focus has shifted from providing platform-agnostic solutions that supports all ServiceStack's .NET Framework and .NET hosts to building on the new capabilities of .NET 8 by enhancing ASP .NET Core's built-in features and templates with ServiceStack's high-productivity features. ## ServiceStack Vue SPA Template The latest [Vue SPA](https://vue-spa.web-templates.io) template is a good example of this, building on and enhancing the built-in ASP.NET Core Vue SPA template with many high-productivity features.

Vite Vue SPA Template

Explore the high productivity features in the new ServiceStack Vue SPA template

:::{.text-center} ## Live Demo ::: :::{.shadow .pb-1} [![](https://raw.githubusercontent.com/ServiceStack/Assets/master/csharp-templates/vue-spa.png)](https://vue-spa.web-templates.io) ::: ## ASP.NET Core Vue SPA Template The [Vue and ASP.NET Core](https://learn.microsoft.com/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue) template provides a seamless starting solution which runs both the .NET API backend and Vite Vue frontend during development. It's a modern template capturing the best Vue has to offer, configured with Vite's fast HMR (Hot Module Reload) and TypeScript support - it allows App's to be developed with Vue's typed [Single File Components](https://vuejs.org/guide/scaling-up/sfc.html) enabling both a productive development experience and an optimal high-performance production build at runtime. ### Minimal API integration Whilst a great starting point, it's still only a basic template configured with a bare-bones Vue Vite App that's modified to show an example of calling a Minimal API. ### Built-in API Integration Although the approach used isn't very scalable, with a proxy rule needed for every user-defined API route: ```ts export default defineConfig({ //... server: { proxy: { '^/weatherforecast': { target, secure: false } }, } }) ``` And the need for hand maintained Types to describe the shape of the API responses with [Stringly Typed](https://wiki.c2.com/?StringlyTyped) fetch API calls referencing **string** routes: ```ts import { defineComponent } from 'vue'; type Forecasts = { date: string, temperatureC: string, temperatureF: string, summary: string }[]; interface Data { loading: boolean, post: null | Forecasts } export default defineComponent({ data(): Data { return { loading: false, post: null }; }, created() { // fetch the data when the view is created and the data is // already being observed this.fetchData(); }, watch: { // call again the method if the route changes '$route': 'fetchData' }, methods: { fetchData(): void { this.post = null; this.loading = true; fetch('weatherforecast') .then(r => r.json()) .then(json => { this.post = json as Forecasts; this.loading = false; return; }); } }, }); ``` Which is used to render the API response in a hand rolled table: ```html
Date Temp. (C) Temp. (F) Summary
{{ forecast.date }} {{ forecast.temperatureC }} {{ forecast.temperatureF }} {{ forecast.summary }}
``` ### ServiceStack API Integration Fortunately ServiceStack can significantly improve this development experience with the [/api pre-defined route](https://docs.servicestack.net/endpoint-routing#api-pre-defined-route) where only a single proxy rule is needed to proxy all APIs: ```ts export default defineConfig({ //... server: { proxy: { '^/api': { target, secure: false } }, } }) ``` ### End-to-end Typed APIs Instead of hand-rolled types and Stringly Typed API calls, it utilizes server [generated TypeScript DTOs](https://docs.servicestack.net/typescript-add-servicestack-reference) with a generic JsonServiceClient to enable end-to-end Typed APIs: ```ts import { ref, onMounted } from 'vue' import { ApiResult } from "@servicestack/client" import { useClient } from "@servicestack/vue" import { GetWeatherForecast } from "@/dtos" const client = useClient() const api = ref(new ApiResult()) onMounted(async () => { api.value = await client.api(new GetWeatherForecast()) }) ``` This benefits in less code to maintain, immediate static typing analysis to ensure correct usage of APIs and valuable feedback when APIs are changed, that's easily updated with a single command: :::sh npm run dtos ::: ### High Productivity Vue Components With access to the [ServiceStack Vue Components](https://docs.servicestack.net/vue/) library there's also less code to maintain in the UI, where you can render a beautiful tailwind styled DataGrid with just: ```html ``` ## ServiceStack Vue SPA Features Other high-productivity features available in the ServiceStack Vue SPA template include: ### Integrated Identity Auth Pre-configured with ASP.NET Core Identity Auth, including Sign In and Custom Registration APIs and UI Pages which can be customized as needed, examples of Role-based security as well as a turn key solution for Integrating Identity Auth Registration workflow with your [SMTP Provider](https://docs.servicestack.net/auth/identity-auth#smtp-iemailsender) with all emails sent from a managed non-blocking [Background MQ](https://docs.servicestack.net/background-mq) for optimal responsiveness and execution. ### tailwindcss [Tailwind](https://tailwindcss.com) has quickly become the best modern CSS framework for creating scalable, [mobile-first](https://tailwindcss.com/#mobile-first) responsive websites built upon a beautiful expert-crafted constraint-based [Design System](https://tailwindcss.com/#constraint-based) that enables effortless reuse of a growing suite of [Free Community](https://tailwindcomponents.com) and professionally-designed [Tailwind UI Component](https://tailwindui.com) Libraries, invaluable for quickly creating beautiful websites. [![](/img/pages/blazor/tailwindui.png)](https://tailwindcss.com) ### Dark Mode In addition to revolutionizing how we style mobile-first responsive Apps, Tailwind's [Dark Mode](https://tailwindcss.com/#dark-mode) does the same for enabling Dark Mode a feature supported throughout all of ServiceStack's [Vue Component Library](https://docs.servicestack.net/vue/). ![](/img/whatsnew/v6.5/dark-and-light-mode.png) ### Built for Productivity So that you're immediately productive out-of-the-box, the template includes a rich set of high-productivity features, including: | | | |----------------------------------------------------------------------------|--------------------------------------------------------------| | [tailwind/typography](https://tailwindcss-typography.vercel.app) | Beautiful css typography for markdown articles & blog posts | | [tailwind/forms](https://github.com/tailwindlabs/tailwindcss-forms) | Beautiful css form & input styles that's easily overridable | | [Markdown](https://github.com/markdown-it/markdown-it) | Native Markdown integration | | [plugin/press](https://github.com/ServiceStack/vite-plugin-press) | Static markdown for creating blogs, videos and other content | | [plugin/vue-router](https://github.com/posva/unplugin-vue-router) | Conventional file system based routing for Vue 3 on Vite | | [plugin/layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) | Support for multiple page layouts | | [plugin/components](https://github.com/antfu/unplugin-vue-components) | Auto importing & registering of components on-demand | | [plugin/svg](https://github.com/jpkleemans/vite-svg-loader) | Load SVG files as Vue components | | [Iconify](https://iconify.design) | Unified registry to access 100k+ high quality SVG icons | ### Bookings CRUD Pages Bookings CRUD example shows how you can rapidly develop beautiful responsive, customized CRUD UIs with minimal effort using [AutoQuery APIs](https://docs.servicestack.net/autoquery/), [AutoForms](https://docs.servicestack.net/autoform) & [AutoQueryGrid](https://blazor-gallery.servicestack.net/gallery/autoquerygrid) Vue Components. ### Admin Pages Whilst Bookings CRUD is a good example of creating custom UI for end users, you may also want to quickly develop a set of back-office CRUD Admin UIs to manage your App's Database tables, which is easily achievable AutoQueryGrid's default behavior:
The development UX of Admin Pages is further improved in Vue Vite which is able to use SFC Pages and conventional file system routing to quickly add Admin Pages to manage an App's back-end tables, e.g: #### [/admin/coupons.vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/admin/coupons.vue) ```html ``` #### [/admin/bookings.vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/admin/bookings.vue) ```html ``` ## Vite Press Plugin [![](https://images.unsplash.com/photo-1524668951403-d44b28200ce0?crop=entropy&fit=crop&h=384&w=768)](https://vue-spa.web-templates.io/posts/vite-press-plugin) Most Apps typically have a mix of dynamic functionality and static content which in our experience is best maintained in Markdown, which is why excited about the new [Vite Press Plugin](https://vue-spa.web-templates.io/posts/vite-press-plugin) which brings the same Markdown features in our [razor-ssg](https://razor-ssg.web-templates.io), [razor-press](https://razor-press.web-templates.io) and our [blazor-vue](https://blazor-vue.web-templates.io) templates, and re-implements them in Vite where they can be used to add the same rich content features to Vite Vue and Vite React Apps. A goal for vite-press-plugin is to implement a suite of universal markdown-powered features that can be reused across all our Vue, React and .NET Razor and Blazor project templates, allowing you to freely copy and incorporate same set of markdown feature folders to power markdown content features across a range of websites built with different technologies. All of Razor SSG's features are available in Vite Press Plugin, including: - [Blog](https://vue-spa.web-templates.io/blog) - Full Featured, beautiful Tailwind Blog with multiple discoverable views - [What's New](https://vue-spa.web-templates.io/whatsnew) - Build Product and Feature Release pages - [Videos](https://vue-spa.web-templates.io/videos) - Maintain Video Libraries and Playlists - [Metadata APIs](https://vue-spa.web-templates.io/posts/vite-press-plugin#metadata-apis-feature) - Generate queryable static .json metadata APIs for all content - [Includes](https://vue-spa.web-templates.io/posts/vite-press-plugin#includes-feature) - Create and reuse Markdown fragments It also supports an enhanced version of markdown for embedding richer UI markup in markdown content where most of [VitePress Containers](https://vitepress.dev/guide/markdown#custom-containers) are supported, including: - [Custom Markdown Containers](https://vue-spa.web-templates.io/posts/vite-press-plugin#markdown-containers) - **Alerts** - `info` - `tip` - `warning` - `danger` - `copy` - `sh` - `youtube` - [Markdown Fenced Code Blocks](https://vue-spa.web-templates.io/posts/vite-press-plugin#markdown-fenced-code-blocks) - Convert fenced code blocks into Richer UIs ### Vue Components In Markdown At the cost of reduced portability, you’re also able to embed richer Interactive Vue components directly in markdown: - [Vue Components in Markdown](https://vue-spa.web-templates.io/posts/markdown-components-in-vue) # Vite Press Plugin Source: https://servicestack.net/posts/vite-press-plugin The Vite Press Plugin is an alternative to [VitePress](https://vitepress.dev) for adding Markdown features to existing Vite Vue or React projects. It's a non-intrusive plugin for Vue and React Vite apps that want to add markdown powered content features without needing to adopt an opinionated framework for their entire App. ## Universal Markdown Features A goal for **vite-press-plugin** is to implement a suite of universal markdown-powered features that can be reused across Vue, React and .NET Razor and Blazor projects, allowing you to incorporate same set of markdown feature folders to power markdown content features across a range of websites built with different technologies. ### Vite Apps with vite-press-plugin The **vite-press-plugin** currently powers the markdown features in the static Vite Vue and React templates which are ideal for creating static websites, blogs, documentation and marketing websites that can be hosted FREE on [GitHub Pages CDN](https://pages.github.com): #### Static Vite Templates with vite-press-plugin - [press-vue](https://press-vue.servicestack.net) - Vite Vue App - [press-react](https://press-react.servicestack.net) - Vite React App The **vite-press-plugin** makes the Markdown features available to the Vite App, whilst the markdown rendering itself is optimally implemented in: - Vue Templates - with [markdown-it](https://github.com/markdown-it/markdown-it) in [Vue SFC](https://vuejs.org/guide/scaling-up/sfc.html) Components - React Templates - with [remark](https://github.com/remarkjs/remark) and [MDX](https://mdxjs.com) in [React](https://react.dev) Components #### .NET 8 API backend with Vite Vue & React SPA frontend When more capabilities are required and you want a .NET API backend to your Vite Vue or React SPA frontend, you can use one of our integrated .NET 8 SPA templates: - [vue-spa](https://vue-spa.web-templates.io) - .NET 8 API with Vite Vue SPA frontend - [react-spa](https://react-spa.web-templates.io) - .NET 8 API with Vite React SPA frontend ### .NET Templates with C# and Markdig Whilst the same Markdown feature folders are [implemented in C#](https://razor-ssg.web-templates.io/posts/razor-ssg) and rendered with [Markdig](https://github.com/xoofx/markdig) and either Razor Pages or Blazor Components: #### .NET 8 Razor SSG and Blazor SSR Templates - [razor-ssg](https://razor-ssg.web-templates.io) - .NET Razor SSG Blog and Marketing Website with **Markdig** - [razor-press](https://razor-press.web-templates.io) - .NET Razor SSG Documentation Website with **Markdig** - [blazor-vue](https://blazor-vue.web-templates.io) - .NET 8 Blazor Server Rendered Website with **Markdig** ### Markdown Feature Folders The content for each Markdown feature is maintained within its own feature folder with a `_` prefix: ```files /_includes /_posts /_videos /_whatsnew ``` #### Markdown Document Structure Additional metadata for each markdown page is maintained in the frontmatter of each markdown page, e.g. the front matter for this blog post contains: ```md --- title: Vite Press Plugin summary: Introducing the Vite Press Plugin author: Lucy Bates tags: [docs,markdown] image: https://picsum.photos/2000/1000 --- ``` The frontmatter is used in combination with file attributes to populate the document metadata. The schema used to support the current markdown features include: ```ts type Doc = { title: string // title of Markdown page (frontmatter) slug: string // slug to page (populated) path: string // path to page (populated) fileName: string // filename of markdown file (populated) content: string // markdown content (populated) date: string // date of page (frontmatter) tags: string[] // related tags (frontmatter) order?: number // explicit page ordering (frontmatter) group?: string // which group page belongs to (populated) draft?: boolean // make visible in production (frontmatter) wordCount: number // (populated) lineCount: number // (populated) minutesToRead: number // (populated) } type Post = Doc & { summary: string // short summary of blog post (frontmatter) author: string // author of blog post (frontmatter) image: string // hero image of blog post (frontmatter) } type Video = Doc & { url: string // URL of YouTube Video } type WhatsNew = Doc & { url: string // URL of YouTube Video image: string // Image to display for feature } ``` Markdown files can contain additional frontmatter which is also merged with the document metadata. ### Accessing Markdown Metadata In Vue App's the Metadata is available as an injected dependency that's navigable with the typed `VirtualPress` schema, e.g: ```ts import type { VirtualPress } from "vite-plugin-press" const press:VirtualPress = inject('press')! ``` In React App's it's available via an injected context: ```ts import { PressContext } from "@/contexts" const press = useContext(PressContext) ``` Which is defined as: ```ts import { createContext } from 'react' import type { VirtualPress } from 'vite-plugin-press' export const PressContext = createContext({} as VirtualPress) ``` This `VirtualPress` metadata is used to power all markdown features. ### Blog The blog maintains its markdown posts in a flat [/_posts](https://github.com/NetCoreTemplates/vue-spa/tree/main/MyApp.Client/src/_posts) folder which each Markdown post containing its publish date and URL slug it should be published under, e.g: ```files /_posts 2023-01-21_start.md 2024-02-11_jwt-identity-auth.md 2024-03-01_vite-press-plugin.md ``` Supporting all Blog features requires several different pages to render each of its view: | Description | Example | Vue | React | | - | - | - | - | | Main Blog layout | [/blog](/blog) | [blog.vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/blog.vue) | [blog.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/blog.tsx) | | Navigable Archive of Posts | [/posts](/posts) | [index.vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/posts/index.vue) | [index.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/posts/index.tsx) | | Individual Blog Post (like this!) | [/posts/vite-press-plugin](/posts/vite-press-plugin) | [\[slug\].vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/posts/%5Bslug%5D.vue) | [\[slug\].tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/posts/%5Bslug%5D.tsx) | | Display Posts by Author | [/posts/author/lucy-bates](/posts/author/lucy-bates) | [\[name\].vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/posts/author/%5Bname%5D.vue) | [\[name\].tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/posts/author/%5Bname%5D.tsx) | | Display Posts by Tag | [/posts/tagged/markdown](/posts/tagged/markdown) | [\[tag\].vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/posts/tagged/%5Btag%5D.vue) | [\[tag\].tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/posts/tagged/%5Btag%5D.tsx) | | Display Posts by Year | [/posts/year/2024](/posts/year/2024) | [\[year\].vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/posts/year/%5Byear%5D.vue) | [\[year\].tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/posts/year/%5Byear%5D.tsx) | #### Configuration Additional information about the Website Blog is maintained in `_posts/config.json` ```json { "localBaseUrl": "http://localhost:5173", "publicBaseUrl": "https://press-vue.servicestack.net", "siteTwitter": "@Vue", "blogTitle": "From the blog", "blogDescription": "Writing on software design and aerospace industry.", "blogEmail": "email@example.org (Vue)", "blogImageUrl": "https://servicestack.net/img/logo.png" } ``` #### Authors Whilst information about Post Authors are maintained in `_posts/authors.json` ```json [ { "name": "Lucy Bates", "email": "lucy@email.org", "bio": "Writing on software design and aerospace industry.", "profileUrl": "/img/profiles/user1.svg", "twitterUrl": "https://twitter.com/lucy", "threadsUrl": "https://threads.net/@lucy", "gitHubUrl": "https://github.com/lucy" }, ] ``` To associate an Author the **name** property is used to match a posts frontmatter **author**. ### General Features Most unique markdown features are captured in their Markdown's frontmatter metadata, but in general these features are broadly available for all features: - **Live Reload** - Latest Markdown content is displayed during **Development** - **Drafts** - Prevent posts being worked on from being published with `draft: true` - **Future Dates** - Posts with a future date wont be published until that date ### What's New Feature The [/whatsnew](/whatsnew) page is an example of creating a custom Markdown feature to implement a portfolio or a product releases page where a new folder is created per release, containing both release date and release or project name, with all features in that release maintained markdown content sorted in alphabetical order: ```files /_whatsnew /2023-03-08_Animaginary feature1.md /2023-03-18_OpenShuttle feature1.md /2023-03-28_Planetaria feature1.md ``` What's New follows the same structure as Pages feature which is rendered in: - [whatsnew.vue](https://github.com/NetCoreTemplates/vue-spa/blob/main/MyApp.Client/src/pages/whatsnew.vue) - [whatsnew.tsx](https://github.com/NetCoreTemplates/react-spa/blob/main/MyApp.Client/src/pages/whatsnew.tsx) ### Videos Feature The videos feature maintained in the `_videos` folder allows grouping of related videos into different folder groups, e.g: ```files /_videos /vue admin.md autoquerygrid.md components.md /react locode.md bookings.md nextjs.md ``` These can then be rendered as UI fragments using the `` component, e.g: ```tsx ``` ### Includes Feature The includes feature allows maintaining reusable markdown fragments in the `_includes` folder, e.g: ```files /_includes /features videos.md whatsnew.md privacy.md ``` Which can be included in other Markdown files with: ```md :::include privacy.md::: :::include features/include.md::: ``` Alternatively they can be included in other Vue, React or Markdown pages with the `` component, e.g: ```tsx ``` ### Metadata APIs Feature To support external clients from querying static markdown metadata you can export it to pre-rendered static `*.json` data structures by configuring `metadataPath` to the location you the `*.json` files published to, e.g: ```ts export default defineConfig({ plugins: [ Press({ metadataPath: 'public/api', }), ] }) ``` This will publish all the content of each content type in the year they were published in, along with an `all.json` containing all content published in that year as well aso for all time, e.g: ```files /meta /2022 all.json posts.json videos.json /2023 all.json posts.json /2024 all.json posts.json videos.json whatsnew.json all.json index.json ``` With this you can fetch the metadata of all the new **Blog Posts** added in **2023** from: [/api/2024/blog.json](/api/2024/blog.json) Or all the website content added in **2024** from: [/api/2024/all.json](/api/2024/all.json) Or **ALL** the website metadata content from: [/api/all.json](/api/all.json) This feature makes it possible to support use-cases like CreatorKit's [Generating Newsletters](https://servicestack.net/creatorkit/portal-mailruns#generating-newsletters) feature which generates a Monthly Newsletter Email with all new content added within a specified period. ## Markdown Containers Most of [VitePress Containers](https://vitepress.dev/guide/markdown#custom-containers) are also implemented, enabling rich markup to enhance markdown content and documentation universally across all Markdown App implementations: #### Input :::info This is an info box. ::: :::tip This is a tip. ::: :::warning This is a warning. ::: :::danger This is a dangerous warning. ::: #### Output :::info This is an info box. ::: :::tip This is a tip. ::: :::warning This is a warning. ::: :::danger This is a dangerous warning. ::: ### Custom Title You can specify a custom title by appending the text right after the container type: #### Input :::danger STOP Danger zone, do not proceed ::: #### Output :::danger STOP Danger zone, do not proceed ::: ### copy The **copy** container is ideal for displaying text snippets in a component that allows for easy copying: #### Input :::copy Copy Me! ::: #### Output :::copy Copy Me! ::: HTML or XML fragments can also be copied by escaping them first: #### Input ```md :::copy `` ::: ``` #### Output :::copy `` ::: ### sh Similarly the **sh** container is ideal for displaying and copying shell commands: #### Input :::sh npm run dev ::: #### Output :::sh npm run dev ::: ### YouTube For embedding YouTube Videos, optimally rendered using the `` component, e.g: #### Input :::youtube YIa0w6whe2U Vue Components Library ::: #### Output :::youtube YIa0w6whe2U Vue Components Library ::: ## Markdown Fenced Code Blocks For more flexibility you can utilize custom fenced components like the `files` fenced code block which can be used to capture ascii representation of a structured documentation like a folder & file structure, e.g: ```files /_videos /vue admin.md autoquerygrid.md components.md /react locode.md bookings.md nextjs.md ``` That we can render into a more UX-friendly representation by calling the `Files` component with the body of the code-block to convert the structured ascii layout into a more familiar GUI layout: ```files /_videos /vue admin.md autoquerygrid.md components.md /react locode.md bookings.md nextjs.md ``` The benefit of this approach of marking up documentation is that the markdown content still remains in an optimal human-readable form even when the markdown renderer lacks the custom fenced components to render the richer UI. ## Components In Markdown Up till now all above features will let you render the same markdown content in all available Vue, React, Razor or Blazor templates. At the cost of reduced portability, you're also able to embed rich Interactive Vue or React components directly in markdown. :::include component-links.md::: # New Blazor Interactive Auto Template with Custom Admin UIs Source: https://servicestack.net/posts/blazor-8-admin Since the release of .NET 8, we have been upgrading our [templates](https://github.com/NetCoreTemplates) and example applications to take advantage of some of the new features, especially for Blazor. Our templates now make use of static Server Side Rendering (SSR) for Blazor, which allows for faster initial page loads and better SEO, and our `blazor-wasm` template uses `InteractiveAuto` by default to provide a more responsive UI.
## What is InteractiveAuto? Blazor for .NET 8 has [four different rendering modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#render-modes) you can take advantage of: - Static Server (static SSR) - Interactive Server - Interactive WebAssembly (WASM) - Interactive Auto For non-interactive pages, the static SSR mode is the fastest, as it renders the page on the server and sends the HTML to the client. However, when your page needs to be interactive, you need to use one of the interactive modes. Prior to .NET 8, there was a trade-off between the two available render modes (static server rendering wasn't yet available). The `Interactive Server` mode was faster to load, but the `Interactive WASM` mode was more responsive. The initial load times for `Interactive WASM` could be quite slow, as the entire application and all its dependencies needed to be downloaded before the page could render most of the content. > The initial load time for the `Interactive WASM` mode can be quite slow even for a minimal app Our templates previously worked around this limitation with a custom Pre-Rendering solution, as the wait times were too long for a good user experience. With .NET 8, the new `Interactive Auto` mode provides the best of both worlds as pre-rendering is now enabled by default. When the page is first loaded, it uses the `Interactive Server` mode, which is faster than `Interactive WASM` as it doesn't need to download WASM resources. So the user can start interacting with the page straight away, but with a slight delay for each of their interactions due to having to perform round-trips to the server for each interaction. In the background, the WASM resources are downloaded which can then be used to render the site on the client for subsequent visits. ## Using InteractiveAuto in your Blazor application In Blazor for .NET 8, render modes can be set on both a per-page and per-component basis. ```html @page "/counter" @rendermode InteractiveAuto ``` ```html ``` ## ServiceStack.Blazor Components The [ServiceStack.Blazor Components](https://blazor-gallery.jamstacks.net) have been updated for .NET 8 and work with the new `InteractiveAuto` render mode. This means you can focus more on your application logic and less on the UI, as the components provide a high-productivity UI for common tasks such as CRUD operations.
### AutoQueryGrid The [AutoQueryGrid](https://blazor-gallery.servicestack.net/gallery/autoquerygrid) component provides a full-featured data grid that can be used to display and edit data from an AutoQuery service. This is ideal for creating custom admin pages for your application. By integrating your admin screens into your application, you can optimize the user experience for specific workflows and get a huge amount of reuse of your existing AutoQuery services. ```html ``` ![](/img/posts/blazor-8-admin/autoquerygrid.png) For BlazorDiffusion, our StableDiffusion example application, we used the AutoQueryGrid to create a custom admin page for managing the modifiers in the application. ![](/img/posts/blazor-8-admin/blazordiffusion-modifiers.png) This is the simplest and fastest use of the AutoQueryGrid component, but it can also be heavily customized for lots of different use cases. In BlazorDiffusion we customize the grid to enable easy navigation contextually between separate customized admin screens for each Creative, linking to related table data. ![](/img/posts/blazor-8-admin/blazordiffusion-creatives.png) ```html ``` In the above example, we use the `ConfigureQuery` parameter to customize the query used by the AutoQueryGrid when displaying values. This is ideal if you want to filter the data for specific workflows, for example, only showing the data that is relevant to the current user. We combine this with a `Tabs` component to provide a navigation bar for the user to switch between the different filters on the same AutoQueryGrid. ```html ``` ![](/img/posts/blazor-8-admin/blazordiffusion-tab.png) ![](/img/posts/blazor-8-admin/blazordiffusion-tab1.png) We also use the `EditForm` parameter to customize the edit form for the AutoQueryGrid, so the workflow for editing a Creative is optimized using your own completely custom UI. ```html ``` ## Upgrading to .NET 8 BlazorDiffusion was an example application we originally developed for .NET 6. We upgraded the production release of this application to use our `blazor-vue` template, which can be perfect for public-facing web applications and teams that don't mind including a JavaScript framework in their application. However, to show the flexibility of Blazor for .NET 8, we also upgraded the whole application from our updated `blazor-wasm` template to take advantage of the new `InteractiveAuto` mode. ### Component Compatibility Since the ServiceStack.Blazor library has been updated for .NET 8, we just needed to bring over the shared components from the original application and update the references to the new library. When upgrading your application pages and components, you will need to avoid any JavaScript interop that runs during the `InitializeAsync` lifecycle method, as this is not supported in the `InteractiveAuto` mode. ### Running on both Server vs Client When using the `InteractiveAuto` mode, first visits will be running on the server, so your pages and components need to be available to both projects, as well as have any required dependencies registered in both projects `Program.cs` files. By placing your shared pages and components in a shared project like the `.Client` project in the `blazor-wasm` template, you can easily share them between the two projects. Look for any of your pages or components that use the `@injects` directive, as these will need to be registered in both projects. :::info Avoid sharing sensitive information via dependency injection, as this will be available to the client at runtime which will be able to be decompiled and inspected. ::: ## Source code and live demo The source code for the upgraded `BlazorDiffusionAuto` application is [available on GitHub](https://github.com/NetCoreApps/BlazorDiffusionAuto) and you can view a live demo of the application at [auto.blazordiffusion.com](https://auto.blazordiffusion.com). ## Conclusion The new `InteractiveAuto` mode in Blazor for .NET 8 provides the best of both worlds for Blazor applications. A built in pre-rendering solution means that you can have a fast initial load time, but still have a responsive UI for subsequent visits. And since the ServiceStack.Blazor components have been updated for .NET 8, you can take advantage of the high-productivity UI components to quickly create customizable and professional-looking admin pages in a Blazor application. ## Feedback If you have any questions or feedback, please feel free to reach out to us on [our forums](https://forums.servicestack.net) or [GitHub Discussions](https://servicestack.net/ask). # ASP.NET Core JWT Identity Auth Source: https://servicestack.net/posts/jwt-identity-auth JWTs enable stateless authentication of clients without servers needing to maintain any Auth state in server infrastructure or perform any I/O to validate a token. As such, [JWTs are a popular choice for Microservices](https://docs.servicestack.net/auth/jwt-authprovider#stateless-auth-microservices) as they only need to configured with confidential keys to validate access. ### ASP.NET Core JWT Authentication ServiceStack's JWT Identity Auth reimplements many of the existing [ServiceStack JWT AuthProvider](https://docs.servicestack.net/auth/jwt-authprovider) features but instead of its own implementation, integrates with and utilizes ASP.NET Core's built-in JWT Authentication that's configurable in .NET Apps with the `.AddJwtBearer()` extension method, e.g: #### Program.cs ```csharp services.AddAuthentication() .AddJwtBearer(options => { options.TokenValidationParameters = new() { ValidIssuer = config["JwtBearer:ValidIssuer"], ValidAudience = config["JwtBearer:ValidAudience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(config["JwtBearer:IssuerSigningKey"]!)), ValidateIssuerSigningKey = true, }; }) .AddIdentityCookies(options => options.DisableRedirectsForApis()); ``` Then use the `JwtAuth()` method to enable and configure ServiceStack's support for ASP.NET Core JWT Identity Auth: #### Configure.Auth.cs ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature(IdentityAuth.For( options => { options.SessionFactory = () => new CustomUserSession(); options.CredentialsAuth(); options.JwtAuth(x => { // Enable JWT Auth Features... }); }))); }); } ``` ### Enable in Swagger UI Once configured we can enable JWT Auth in Swagger UI by installing **Swashbuckle.AspNetCore**: :::copy `` ::: Then enable Open API, Swagger UI, ServiceStack's support for Swagger UI and the JWT Bearer Auth option: ```csharp public class ConfigureOpenApi : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { if (context.HostingEnvironment.IsDevelopment()) { services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); services.AddServiceStackSwagger(); services.AddJwtAuth(); //services.AddBasicAuth(); services.AddTransient(); } }); public class StartupFilter : IStartupFilter { public Action Configure(Action next) => app => { // Provided by Swashbuckle library app.UseSwagger(); app.UseSwaggerUI(); next(app); }; } } ``` This will enable the **Authorize** button in Swagger UI where you can authenticate with a JWT Token: ![](/img/posts/jwt-identity-auth/jwt-swagger-ui.png) ### JWT Auth in Built-in UIs This also enables the **JWT** Auth Option in ServiceStack's built-in [API Explorer](https://docs.servicestack.net/api-explorer), [Locode](https://docs.servicestack.net/locode/) and [Admin UIs](https://docs.servicestack.net/admin-ui): ### Authenticating with JWT JWT Identity Auth is a drop-in replacement for ServiceStack's JWT AuthProvider where Authenticating via Credentials will convert the Authenticated User into a JWT Bearer Token returned in the **HttpOnly**, **Secure** `ss-tok` Cookie that will be used to Authenticate the client: ```csharp var client = new JsonApiClient(BaseUrl); await client.SendAsync(new Authenticate { provider = "credentials", UserName = Username, Password = Password, }); var bearerToken = client.GetTokenCookie(); // ss-tok Cookie ``` ## JWT Refresh Tokens Refresh Tokens can be used to allow users to request a new JWT Access Token when the current one expires. To enable support for JWT Refresh Tokens your `IdentityUser` model should implement the `IRequireRefreshToken` interface which will be used to store the 64 byte Base64 URL-safe `RefreshToken` and its `RefreshTokenExpiry` in its persisted properties: ```csharp public class ApplicationUser : IdentityUser, IRequireRefreshToken { public string? RefreshToken { get; set; } public DateTime? RefreshTokenExpiry { get; set; } } ``` Now after successful authentication, the `RefreshToken` will also be returned in the `ss-reftok` Cookie: ```csharp var refreshToken = client.GetRefreshTokenCookie(); // ss-reftok Cookie ``` ### Transparent Server Auto Refresh of JWT Tokens To be able to terminate a users access, Users need to revalidate their eligibility to verify they're still allowed access (e.g. deny Locked out users). This JWT revalidation pattern is implemented using Refresh Tokens which are used to request revalidation of their access and reissuing a new JWT Access Token which can be used to make authenticated requests until it expires. As Cookies are used to return Bearer and Refresh Tokens ServiceStack is able to implement the revalidation logic on the server where it transparently validates Refresh Tokens, and if a User is eligible will reissue a new JWT Token Cookie that replaces the expired Access Token Cookie. Thanks to this behavior HTTP Clients will be able to Authenticate with just the Refresh Token, which will transparently reissue a new JWT Access Token Cookie and then continue to perform the Authenticated Request: ```csharp var client = new JsonApiClient(BaseUrl); client.SetRefreshTokenCookie(RefreshToken); var response = await client.SendAsync(new Secured { ... }); ``` There's also opt-in sliding support for extending a User's RefreshToken after usage which allows Users to treat their Refresh Token like an API Key where it will continue extending whilst they're continuously using it to make API requests, otherwise expires if they stop. How long to extend the expiry of Refresh Tokens after usage can be configured with: ```csharp options.JwtAuth(x => { // How long to extend the expiry of Refresh Tokens after usage (default None) x.ExtendRefreshTokenExpiryAfterUsage = TimeSpan.FromDays(90); }); ``` ## Convert Session to Token Service Another useful Service that's available is being able to Convert your current Authenticated Session into a Token with the `ConvertSessionToToken` Service which can be enabled with: ```csharp options.JwtAuth(x => { x.IncludeConvertSessionToTokenService = true; }); ``` This can be useful for when you want to Authenticate via an external OAuth Provider that you then want to convert into a stateless JWT Token by calling the `ConvertSessionToToken` on the client, e.g: #### .NET Clients ```csharp await client.SendAsync(new ConvertSessionToToken()); ``` #### TypeScript/JavaScript ```ts fetch('/session-to-token', { method:'POST', credentials:'include' }) ``` The default behavior of `ConvertSessionToToken` is to remove the Current Session from the Auth Server which will prevent access to protected Services using our previously Authenticated Session. If you still want to preserve your existing Session you can indicate this with: ```csharp await client.SendAsync(new ConvertSessionToToken { PreserveSession = true }); ``` ### JWT Options Other configuration options available for Identity JWT Auth include: ```csharp options.JwtAuth(x => { // How long should JWT Tokens be valid for. (default 14 days) x.ExpireTokensIn = TimeSpan.FromDays(14); // How long should JWT Refresh Tokens be valid for. (default 90 days) x.ExpireRefreshTokensIn = TimeSpan.FromDays(90); x.OnTokenCreated = (req, user, claims) => { // Customize which claims are included in the JWT Token }; // Whether to invalidate Refresh Tokens on Logout (default true) x.InvalidateRefreshTokenOnLogout = true; // How long to extend the expiry of Refresh Tokens after usage (default None) x.ExtendRefreshTokenExpiryAfterUsage = null; }); ``` # Built-In Identity Auth Admin UI Source: https://servicestack.net/posts/identity-auth-admin-ui With ServiceStack now [deeply integrated into ASP.NET Core Apps](/posts/servicestack-endpoint-routing) we're back to refocusing on adding value-added features that can benefit all .NET Core Apps. ## Registration The new Identity Auth Admin UI is an example of this, which can be enabled when registering the `AuthFeature` Plugin: ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature(IdentityAuth.For( options => { options.SessionFactory = () => new CustomUserSession(); options.CredentialsAuth(); options.AdminUsersFeature(); }))); }); } ``` Which just like the ServiceStack Auth [Admin Users UI](https://docs.servicestack.net/admin-ui-users) enables a Admin UI that's only accessible to **Admin** Users for managing **Identity Auth** users at `/admin-ui/users`. ## User Search Results Which displays a limited view due to the minimal properties on the default `IdentityAuth` model:
### Custom Search Result Properties These User's search results are customizable by specifying the `ApplicationUser` properties to display instead, e.g: ```csharp options.AdminUsersFeature(feature => { feature.QueryIdentityUserProperties = [ nameof(ApplicationUser.Id), nameof(ApplicationUser.DisplayName), nameof(ApplicationUser.Email), nameof(ApplicationUser.UserName), nameof(ApplicationUser.LockoutEnd), ]; }); ```
### Custom Search Result Behavior The default display Order of Users is also customizable: ```csharp feature.DefaultOrderBy = nameof(ApplicationUser.DisplayName); ``` As well as the Search behavior which can be replaced to search any custom fields, e.g: ```csharp feature.SearchUsersFilter = (q, query) => { var queryUpper = query.ToUpper(); return q.Where(x => x.DisplayName!.Contains(query) || x.Id.Contains(queryUpper) || x.NormalizedUserName!.Contains(queryUpper) || x.NormalizedEmail!.Contains(queryUpper)); }; ``` ## Default Create and Edit Users Forms The default Create and Edit Admin Users UI are also limited to editing the minimal `IdentityAuth` properties:
Whilst the Edit page includes standard features to lockout users, change user passwords and manage their roles:
### Custom Create and Edit Forms By default Users are locked out indefinitely, but this can also be changed to lock users out to a specific date, e.g: ```csharp feature.ResolveLockoutDate = user => DateTimeOffset.Now.AddDays(7); ``` The forms editable fields can also be customized to include additional properties, e.g: ```csharp feature.FormLayout = [ Input.For(x => x.UserName, c => c.FieldsPerRow(2)), Input.For(x => x.Email, c => { c.Type = Input.Types.Email; c.FieldsPerRow(2); }), Input.For(x => x.FirstName, c => c.FieldsPerRow(2)), Input.For(x => x.LastName, c => c.FieldsPerRow(2)), Input.For(x => x.DisplayName, c => c.FieldsPerRow(2)), Input.For(x => x.PhoneNumber, c => { c.Type = Input.Types.Tel; c.FieldsPerRow(2); }), ]; ``` That can override the new `ApplicationUser` Model that's created and any Validation: ### Custom User Creation ```csharp feature.CreateUser = () => new ApplicationUser { EmailConfirmed = true }; feature.CreateUserValidation = async (req, createUser) => { await IdentityAdminUsers.ValidateCreateUserAsync(req, createUser); var displayName = createUser.GetUserProperty(nameof(ApplicationUser.DisplayName)); if (string.IsNullOrEmpty(displayName)) throw new ArgumentNullException(nameof(AdminUserBase.DisplayName)); return null; }; ```
### Admin User Events Should you need to, Admin User Events can use used to execute custom logic before and after creating, updating and deleting users, e.g: ```csharp feature.OnBeforeCreateUser = (request, user) => { ... }; feature.OnAfterCreateUser = (request, user) => { ... }; feature.OnBeforeUpdateUser = (request, user) => { ... }; feature.OnAfterUpdateUser = (request, user) => { ... }; feature.OnBeforeDeleteUser = (request, userId) => { ... }; feature.OnAfterDeleteUser = (request, userId) => { ... }; ``` # System.Text.Json ServiceStack APIs Source: https://servicestack.net/posts/system-text-json-apis In continuing our focus to enable ServiceStack to become a deeply integrated part of .NET 8 Application's, ServiceStack latest .NET 8 templates now default to using standardized ASP.NET Core features wherever possible, including: - [ASP.NET Core Identity Auth](/posts/net8-identity-auth) - [ASP.NET Core IOC](/posts/servicestack-endpoint-routing#asp.net-core-ioc) - [Endpoint Routing](/posts/servicestack-endpoint-routing#endpoint-routing) - [Swashbuckle for Open API v3 and Swagger UI](/posts/openapi-v3-support) - [System.Text.Json APIs](/posts/system-text-json-apis) This reduces friction for integrating ServiceStack into existing .NET 8 Apps, encourages greater knowledge and reuse and simplifies .NET development as developers have a reduced number of concepts to learn, fewer technology implementations to configure and maintain that are now applied across their entire .NET App. The last integration piece supported was utilizing **System.Text.Json** - the default high-performance async JSON serializer used in .NET Applications, can now be used by ServiceStack APIs to serialize and deserialize its JSON API Responses that's enabled by default when using **Endpoint Routing**. This integrates ServiceStack APIs more than ever where just like Minimal APIs and Web API, uses **ASP.NET Core's IOC** to resolve dependencies, uses **Endpoint Routing** to Execute APIs that's secured with **ASP.NET Core Identity Auth** then uses **System.Text.Json** to deserialize and serialize its JSON payloads. ### Enabled by Default when using Endpoint Routing ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); ``` ### Enhanced Configuration ServiceStack uses a custom `JsonSerializerOptions` to improve compatibility with existing ServiceStack DTOs and ServiceStack's rich ecosystem of generic [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) Service Clients, which is configured to: - Not serialize `null` properties - Supports Case Insensitive Properties - Uses `CamelCaseNamingPolicy` for property names - Serializes `TimeSpan` and `TimeOnly` Data Types with [XML Schema Time format](https://www.w3.org/TR/xmlschema-2/#isoformats) - Supports `[DataContract]` annotations - Supports Custom Enum Serialization ### Benefits all Add ServiceStack Reference Languages This compatibility immediately benefits all of ServiceStack's [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) native typed integrations for **11 programming languages** which all utilize ServiceStack's JSON API endpoints - now serialized with System.Text.Json ### Support for DataContract Annotations Support for .NET's `DataContract` serialization attributes was added using a custom `TypeInfoResolver`, specifically it supports: - `[DataContract]` - When annotated, only `[DataMember]` properties are serialized - `[DataMember]` - Specify a custom **Name** or **Order** of properties - `[IgnoreDataMember]` - Ignore properties from serialization - `[EnumMember]` - Specify a custom value for Enum values ### Custom Enum Serialization Below is a good demonstration of the custom Enum serialization support which matches ServiceStack.Text's behavior: ```csharp public enum EnumType { Value1, Value2, Value3 } [Flags] public enum EnumTypeFlags { Value1, Value2, Value3 } public enum EnumStyleMembers { [EnumMember(Value = "lower")] Lower, [EnumMember(Value = "UPPER")] Upper, } return new EnumExamples { EnumProp = EnumType.Value2, // String value by default EnumFlags = EnumTypeFlags.Value2 | EnumTypeFlags.Value3, // [Flags] as int EnumStyleMembers = EnumStyleMembers.Upper, // Serializes [EnumMember] value NullableEnumProp = null, // Ignores nullable enums }; ``` Which serializes to: ```json { "enumProp": "Value2", "enumFlags": 3, "enumStyleMembers": "UPPER" } ``` ### Custom Configuration You can further customize the `JsonSerializerOptions` used by ServiceStack by using `ConfigureJsonOptions()` to add any customizations that you can optionally apply to ASP.NET Core's JSON APIs and MVC with: ```csharp builder.Services.ConfigureJsonOptions(options => { options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }) .ApplyToApiJsonOptions() // Apply to ASP.NET Core's JSON APIs .ApplyToMvcJsonOptions(); // Apply to MVC ``` ### Control over when and where System.Text.Json is used Whilst `System.Text.Json` is highly efficient, it's also very strict in the inputs it accepts where you may want to revert back to using ServiceStack's JSON Serializer for specific APIs, especially when you need to support external clients that can't be updated. This can done by annotating Request DTOs with `[SystemJson]` attribute, e.g: you can limit to only use `System.Text.Json` for an **APIs Response** with: ```csharp [SystemJson(UseSystemJson.Response)] public class CreateUser : IReturn { //... } ``` Or limit to only use `System.Text.Json` for an **APIs Request** with: ```csharp [SystemJson(UseSystemJson.Request)] public class CreateUser : IReturn { //... } ``` Or not use `System.Text.Json` at all for an API with: ```csharp [SystemJson(UseSystemJson.Never)] public class CreateUser : IReturn { //... } ``` ### JsonApiClient Support When Endpoints Routing is configured, the `JsonApiClient` will also be configured to utilize the same `System.Text.Json` options to send and receive its JSON API Requests which also respects the `[SystemJson]` specified behavior. Clients external to the .NET App can be configured to use `System.Text.Json` with: ```csharp ClientConfig.UseSystemJson = UseSystemJson.Always; ``` Whilst any custom configuration can be applied to its `JsonSerializerOptions` with: ```csharp TextConfig.ConfigureSystemJsonOptions(options => { options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }); ``` ### Scoped JSON Configuration We've also added partial support for [Customized JSON Responses](https://docs.servicestack.net/customize-json-responses) for the following customization options: :::{.table,w-full} | Name | Alias | |------------------------------|-------| | EmitCamelCaseNames | eccn | | EmitLowercaseUnderscoreNames | elun | | EmitPascalCaseNames | epcn | | ExcludeDefaultValues | edv | | IncludeNullValues | inv | | Indent | pp | ::: These can be applied to the JSON Response by returning a decorated `HttpResult` with a custom `ResultScope`, e.g: ```csharp return new HttpResult(responseDto) { ResultScope = () => JsConfig.With(new() { IncludeNullValues = true, ExcludeDefaultValues = true }) }; ``` They can also be requested by API consumers by adding a `?jsconfig` query string with the desired option or its alias, e.g: ```csharp /api/MyRequest?jsconfig=EmitLowercaseUnderscoreNames,ExcludeDefaultValues /api/MyRequest?jsconfig=eccn,edv ``` ### SystemJsonCompatible Another configuration automatically applied when `System.Text.Json` is enabled is: ```csharp JsConfig.SystemJsonCompatible = true; ``` Which is being used to make ServiceStack's JSON Serializer more compatible with `System.Text.Json` output so it's easier to switch between the two with minimal effort and incompatibility. Currently this is only used to override `DateTime` and `DateTimeOffset` behavior which uses `System.Text.Json` for its Serialization/Deserialization. # OpenAPI v3 and Swagger UI Source: https://servicestack.net/posts/openapi-v3 In the ServiceStack v8.1 release, we have introduced a way to better incorporate your ServiceStack APIs into the larger ASP.NET Core ecosystem by mapping your ServiceStack APIs to standard [ASP.NET Core Endpoints](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0#endpoints). This enables your ServiceStack APIs integrate with your larger ASP.NET Core application in the same way other middleware does, opening up more opportunities for reuse of your ServiceStack APIs. This opens up the ability to use common third party tooling. A good example of this is adding OpenAPI v3 specification generation for your endpoints offered by the `Swashbuckle.AspNetCore` package. Included in the v8.1 Release is the `ServiceStack.AspNetCore.OpenApi` package to make this integration as easy as possible, and incorporate additional information from your ServiceStack APIs into Swagger metadata. ![](/img/posts/openapi-v3/openapi-v3-swagger-ui.png) Previously, without the ability to map Endpoints, we've maintained a ServiceStack specific OpenAPI specification generation via the `OpenApiFeature` plugin. While this provided a lot of functionality by accurately describing your ServiceStack APIs, it could be tricky to customize those API descriptions to the way some users wanted to. In this post we will look at how you can take advantage of the new OpenAPI v3 Swagger support using mapped Endpoints, customizing the generated specification, as well as touch on other related changes to ServiceStack v8.1. ## AppHost Initialization To use ServiceStack APIs as mapped Endpoints, the way ServiceStack is initialized in . To convert your App to use [Endpoint Routing and ASP.NET Core IOC](/posts/servicestack-endpoint-routing) your ASPNET Core application needs to be updated to replace any usage of `Funq` IoC container to use ASP.NET Core's IOC. Previously, the following was used to initialize your ServiceStack `AppHost`: #### Program.cs ```csharp app.UseServiceStack(new AppHost()); ``` The `app` in this example is a `WebApplication` resulting from an `IHostApplicationBuilder` calling `builder.Build()`. Whilst we still need to call `app.UseServiceStack()`, we also need to move the discovery of your ServiceStack APIs to earlier in the setup before the `WebApplication` is built, e.g: ```csharp // Register ServiceStack APIs, Dependencies and Plugins: services.AddServiceStack(typeof(MyServices).Assembly); var app = builder.Build(); //... // Register ServiceStack AppHost app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); app.Run(); ``` Once configured to use Endpoint Routing we can the [mix](https://docs.servicestack.net/mix-tool) tool to apply the [openapi3](https://gist.github.com/gistlyn/dac47b68e77796902cde0f0b7b9c6ac2) Startup Configuration with: :::sh x mix openapi3 ::: ### Manually Configure OpenAPI v3 and Swagger UI This will install the required ASP.NET Core Microsoft, Swashbuckle and ServiceStack Open API NuGet packages: ```xml ``` Then add the `Configure.OpenApi.cs` [Modular Startup](https://docs.servicestack.net/modular-startup) class to your project: ```csharp [assembly: HostingStartup(typeof(MyApp.ConfigureOpenApi))] namespace MyApp; public class ConfigureOpenApi : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { if (context.HostingEnvironment.IsDevelopment()) { services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); // Swashbuckle services.AddServiceStackSwagger(); services.AddBasicAuth(); // Enable HTTP Basic Auth //services.AddJwtAuth(); // Enable & Use JWT Auth services.AddTransient(); } }); public class StartupFilter : IStartupFilter { public Action Configure(Action next) => app => { // Provided by Swashbuckle library app.UseSwagger(); app.UseSwaggerUI(); next(app); }; } } ``` All this setup is done for you in ServiceStack's updated [Identity Auth .NET 8 Templates](https://servicestack.net/start), but for existing applications, you will need to do [convert to use Endpoint Routing](https://docs.servicestack.net/endpoints-migration) to support this new way of running your ServiceStack applications. ## More Control One point of friction with our previous `OpenApiFeature` plugin was the missing customization ability to the OpenAPI spec to somewhat disconnect from the defined ServiceStack service, and related C# Request and Response Data Transfer Objects (DTOs). Since the `OpenApiFeature` plugin used class and property attributes on your Request DTOs, making the *structure* of the OpenAPI schema mapping quite ridged, preventing the ability for certain customizations. For example, if we have an `UpdateTodo` Request DTO that looks like the following: ```csharp [Route("/todos/{Id}", "PUT")] public class UpdateTodo : IPut, IReturn { public long Id { get; set; } [ValidateNotEmpty] public string Text { get; set; } public bool IsFinished { get; set; } } ``` Previously, we would get a default Swagger UI that enabled all the properties as `Paramters` to populate. ![](/img/posts/openapi-v3/openapi-v2-defaults.png) While this correctly describes the Request DTO structure, sometimes as developers we get requirements for how we want to present our APIs to our users from within the Swagger UI. With the updated SwaggerUI, and the use of the `Swashbuckle` library, we get the following UI by default. ![](/img/posts/openapi-v3/openapi-v3-defaults-application-json.png) These are essentially the same, we have a CRUD Todo API that takes a `UpdateTodo` Request DTO, and returns a `Todo` Response DTO. ServiceStack needs to have uniquely named Request DTOs, so we can't have a `Todo` schema as the Request DTO despite the fact that it is the same structure as our `Todo` model. This is a good thing, as it allows us to have a clean API contract, and separation of concerns between our Request DTOs and our models. However, it might not be desired to present this to our users, since it can be convenient to think about CRUD services as taking the same resource type as the response. To achieve this, we use the Swashbuckle library to customize the OpenAPI spec generation. Depending on what you want to customize, you can use the `SchemaFilter` or `OperationFilter` options. In this case, we want to customize the matching operation to reference the `Todo` schema for the Request Body. First, we create a new class that implements the `IOperationFilter` interface. ```csharp public class OperationRenameFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { if (context.ApiDescription.HttpMethod == "PUT" && context.ApiDescription.RelativePath == "todos/{Id}") { operation.RequestBody.Content["application/json"].Schema.Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Todo" }; } } } ``` The above matches some information about the `UpdateTodo` request we want to customize, and then sets the `Reference` property of the `RequestBody` to the `Todo` schema. We can then add this to the `AddSwaggerGen` options in the `Program.cs` file. ```csharp builder.Services.AddSwaggerGen(o => { o.OperationFilter(); }); ``` The result is the following Swagger UI. ![](/img/posts/openapi-v3/openapi-v3-customized-application-json.png) This is just one simple example of how you can customize the OpenAPI spec generation, and `Swashbuckle` has some great documentation on the different ways you can customize the generated spec. And these customizations impact any of your ASP.NET Core Endpoints, not just your ServiceStack APIs. ## Closing Now that ServiceStack APIs can be mapped to standard ASP.NET Core Endpoints, it opens up a lot of possibilities for integrating your ServiceStack APIs into the larger ASP.NET Core ecosystem. The use of the `Swashbuckle` library via the `ServiceStack.AspNetCore.OpenApi` library is just one example of how you can take advantage of this new functionality. # ServiceStack Endpoint Routing Source: https://servicestack.net/posts/servicestack-endpoint-routing In an effort to reduce friction and improve integration with ASP.NET Core Apps, we've continued the trend from last year for embracing ASP.NET Core's built-in features and conventions which saw the latest ServiceStack v8 release converting all its newest .NET 8 templates to adopt [ASP.NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth). This is a departure from building upon our own platform-agnostic abstractions which allowed the same ServiceStack code-base to run on both .NET Core and .NET Framework. Our focus going forward will be to instead adopt De facto standards and conventions of the latest .NET platform which also means ServiceStack's new value-added features are only available in the latest **.NET 8+** release. ### ServiceStack Middleware Whilst ServiceStack integrates into ASP.NET Core Apps as custom middleware into ASP.NET Core's HTTP Request Pipeline, it invokes its own black-box of functionality from there, implemented using its own suite of overlapping features. Whilst this allows ServiceStack to have full control over how to implement its features, it's not as integrated as it could be, with there being limits on what ServiceStack Functionality could be reused within external ASP .NET Core MVC Controllers, Razor Pages, etc. and inhibited the ability to apply application-wide authorization policies across an Application entire surface area, using and configuring different JSON Serialization implementations. ### Areas for tighter integration The major areas we've identified that would benefit from tighter integration with ASP.NET Core include: - [Funq IOC Container](https://docs.servicestack.net/ioc) - [ServiceStack Routing](https://docs.servicestack.net/routing) and [Request Pipeline](https://docs.servicestack.net/order-of-operations) - [ServiceStack.Text JSON Serializer](https://docs.servicestack.net/json-format) ### ServiceStack v8.1 is fully integrated! We're happy to announce the latest release of ServiceStack v8.1 now supports utilizing the optimal ASP.NET Core's standardized features to reimplement all these key areas - fostering seamless integration and greater reuse which you can learn about below: - [ASP.NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth) - [ASP.NET Core IOC](https://docs.servicestack.net/releases/v8_01#asp.net-core-ioc) - [Endpoint Routing](https://docs.servicestack.net/releases/v8_01#endpoint-routing) - [System.Text.Json APIs](https://docs.servicestack.net/releases/v8_01#system.text.json) - [Open API v3 and Swagger UI](https://docs.servicestack.net/releases/v8_01#openapi-v3) - [ASP.NET Core Identity Auth Admin UI](https://docs.servicestack.net/releases/v8_01#asp.net-core-identity-auth-admin-ui) - [JWT Identity Auth](https://docs.servicestack.net/releases/v8_01#jwt-identity-auth) Better yet, this new behavior is enabled by default in all of ServiceStack's new ASP .NET Identity Auth .NET 8 templates! ### Migrating to ASP.NET Core Endpoints To assist ServiceStack users in upgrading their existing projects we've created a migration guide walking through the steps required to adopt these new defaults: ### ASP .NET Core IOC The primary limitation of ServiceStack using its own Funq IOC is that any dependencies registered in Funq are not injected into Razor Pages, Blazor Components, MVC Controllers, etc. That's why our [Modular Startup](https://docs.servicestack.net/modular-startup) configurations recommend utilizing custom `IHostingStartup` configurations to register application dependencies in ASP .NET Core's IOC where they can be injected into both ServiceStack Services and ASP.NET Core's external components, e.g: ```csharp [assembly: HostingStartup(typeof(MyApp.ConfigureDb))] namespace MyApp; public class ConfigureDb : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(new OrmLiteConnectionFactory( context.Configuration.GetConnectionString("DefaultConnection"), SqliteDialect.Provider)); }); } ``` But there were fundamental restrictions on what could be registered in ASP .NET Core's IOC as everything needed to be registered before AspNetCore's `WebApplication` was built and before ServiceStack's AppHost could be initialized, which prohibited being able to register any dependencies created by the AppHost including Services, AutoGen Services, Validators and internal functionality like App Settings, Virtual File System and Caching providers, etc. ## Switch to use ASP .NET Core IOC To enable ServiceStack to switch to using ASP .NET Core's IOC you'll need to move registration of all dependencies and Services to before the WebApplication is built by calling the `AddServiceStack()` extension method with the Assemblies where your ServiceStack Services are located, e.g: ```csharp builder.Services.AddServiceStack(typeof(MyServices).Assembly); var app = builder.Build(); //... app.UseServiceStack(new AppHost()); ``` Which now registers all ServiceStack dependencies in ASP .NET Core's IOC, including all ServiceStack Services prior to the AppHost being initialized which no longer needs to specify the Assemblies where ServiceStack Services are created and no longer needs to use Funq as all dependencies should now be registered in ASP .NET Core's IOC. ### Registering Dependencies and Plugins Additionally ASP.NET Core's IOC requirement for all dependencies needing to be registered before the WebApplication is built means you'll no longer be able to register any dependencies or plugins in ServiceStack's `AppHost.Configure()` method. ```csharp public class AppHost() : AppHostBase("MyApp"), IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { // Register IOC Dependencies and ServiceStack Plugins }); public override void Configure() { // DO NOT REGISTER ANY PLUGINS OR DEPENDENCIES HERE } } ``` Instead anything that needs to register dependencies in ASP.NET Core IOC should now use the `IServiceCollection` extension methods: - Use `IServiceCollection.Add*` APIs to register dependencies - Use `IServiceCollection.AddPlugin` API to register ServiceStack Plugins - Use `IServiceCollection.RegisterService*` APIs to dynamically register ServiceStack Services in external Assemblies This can be done whenever you have access to `IServiceCollection`, either in `Program.cs`: ```csharp builder.Services.AddPlugin(new AdminDatabaseFeature()); ``` Or in any Modular Startup `IHostingStartup` configuration class, e.g: ```csharp public class ConfigureDb : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(new OrmLiteConnectionFactory( context.Configuration.GetConnectionString("DefaultConnection"), SqliteDialect.Provider)); // Enable Audit History services.AddSingleton(c => new OrmLiteCrudEvents(c.GetRequiredService())); // Enable AutoQuery RDBMS APIs services.AddPlugin(new AutoQueryFeature { MaxLimit = 1000, }); // Enable AutoQuery Data APIs services.AddPlugin(new AutoQueryDataFeature()); // Enable built-in Database Admin UI at /admin-ui/database services.AddPlugin(new AdminDatabaseFeature()); }) .ConfigureAppHost(appHost => { appHost.Resolve().InitSchema(); }); } ``` The `ConfigureAppHost()` extension method can continue to be used to execute any startup logic that requires access to registered dependencies. ### Authoring ServiceStack Plugins To enable ServiceStack Plugins to support both Funq and ASP .NET Core IOC, any dependencies and Services a plugin needs should be registered in the `IConfigureServices.Configure(IServiceCollection)` method as seen in the refactored [ServerEventsFeature.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack/ServerEventsFeature.cs) plugin, e.g: ```csharp public class ServerEventsFeature : IPlugin, IConfigureServices { //... public void Configure(IServiceCollection services) { if (!services.Exists()) { services.AddSingleton(new MemoryServerEvents { IdleTimeout = IdleTimeout, HouseKeepingInterval = HouseKeepingInterval, OnSubscribeAsync = OnSubscribeAsync, OnUnsubscribeAsync = OnUnsubscribeAsync, OnUpdateAsync = OnUpdateAsync, NotifyChannelOfSubscriptions = NotifyChannelOfSubscriptions, Serialize = Serialize, OnError = OnError, }); } if (UnRegisterPath != null) services.RegisterService(UnRegisterPath); if (SubscribersPath != null) services.RegisterService(SubscribersPath); } public void Register(IAppHost appHost) { //... } } ``` #### All Plugins refactored to support ASP .NET Core IOC All of ServiceStack's Plugins have been refactored to make use of `IConfigureServices` which supports registering in both Funq and ASP.NET Core's IOC when enabled. #### Funq IOC implements IServiceCollection and IServiceProvider interfaces To enable this Funq now implements both `IServiceCollection` and`IServiceProvider` interfaces to enable 100% source-code compatibility for registering and resolving dependencies with either IOC, which we now recommend using over Funq's native Registration and Resolution APIs to simplify migration efforts to ASP.NET Core's IOC in future. ## Dependency Injection The primary difference between the IOC's is that ASP.NET Core's IOC does not support property injection by default, which will require you to refactor your ServiceStack Services to use constructor injection of dependencies, although this has become a lot more pleasant with C# 12's [Primary Constructors](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) which now requires a lot less boilerplate to define, assign and access dependencies, e.g: ```csharp public class TechStackServices(IAutoQueryDb autoQuery) : Service { public async Task Any(QueryTechStacks request) { using var db = autoQuery.GetDb(request, base.Request); var q = autoQuery.CreateQuery(request, Request, db); return await autoQuery.ExecuteAsync(request, q, db); } } ``` This has become our preferred approach for injecting dependencies in ServiceStack Services which have all been refactored to use constructor injection utilizing primary constructors in order to support both IOC's. To make migrations easier we've also added support for property injection convention in **ServiceStack Services** using ASP.NET Core's IOC where you can add the `[FromServices]` attribute to any public properties you want to be injected, e.g: ```csharp public class TechStackServices : Service { [FromServices] public required IAutoQueryDb AutoQuery { get; set; } [FromServices] public MyDependency? OptionalDependency { get; set; } } ``` This feature can be useful for Services wanting to access optional dependencies that may or may not be registered. :::info NOTE `[FromServices]` is only supported in ServiceStack Services (i.e. not other dependencies) ::: ### Built-in ServiceStack Dependencies This integration now makes it effortless to inject and utilize optional ServiceStack features like [AutoQuery](https://docs.servicestack.net/autoquery/) and [Server Events](https://docs.servicestack.net/server-events) in other parts of ASP.NET Core inc. Blazor Components, Razor Pages, MVC Controllers, Minimal APIs, etc. Whilst the Built-in ServiceStack features that are registered by default and immediately available to be injected, include: - `IVirtualFiles` - Read/Write [Virtual File System](https://docs.servicestack.net/virtual-file-system), defaults to `FileSystemVirtualFiles` at `ContentRootPath` - `IVirtualPathProvider` - Multi Virtual File System configured to scan multiple read only sources, inc `WebRootPath`, In Memory and Embedded Resource files - `ICacheClient` and `ICacheClientAsync` - In Memory Cache, or distributed Redis cache if [ServiceStack.Redis](https://docs.servicestack.net/redis/) is configured - `IAppSettings` - Multiple [AppSettings](https://docs.servicestack.net/appsettings) configuration sources With ASP.NET Core's IOC now deeply integrated we moved onto the next area of integration: API Integration and Endpoint Routing. ## Endpoint Routing Whilst ASP.NET Core's middleware is a flexible way to compose and execute different middleware in a HTTP Request pipeline, each middleware is effectively their own island of functionality that's able to handle HTTP Requests in which ever way they see fit. In particular ServiceStack's middleware would execute its own [Request Pipeline](https://docs.servicestack.net/order-of-operations) which would execute ServiceStack API's registered at user-defined routes with its own [ServiceStack Routing](https://docs.servicestack.net/routing). We're happy to announce that ServiceStack **.NET 8** Apps support an entirely new and integrated way to run all of ServiceStack requests including all APIs, metadata and built-in UIs with support for [ASP.NET Core Endpoint Routing](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) - enabled by calling the `MapEndpoints()` extension method when configuring ServiceStack, e.g: ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); ``` Which configures ServiceStack APIs to be registered and executed along-side Minimal APIs, Razor Pages, SignalR, MVC and Web API Controllers, etc, utilizing the same routing, metadata and execution pipeline. #### View ServiceStack APIs along-side ASP.NET Core APIs Amongst other benefits, this integration is evident in endpoint metadata explorers like the `Swashbuckle` library which can now show ServiceStack APIs in its Swagger UI along-side other ASP.NET Core APIs in ServiceStack's new [Open API v3](/posts/openapi-v3) support. ### Routing Using Endpoint Routing also means using ASP.NET Core's Routing System which now lets you use ASP.NET Core's [Route constraints](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-constraints) for defining user-defined routes for your ServiceStack APIs, e.g: ```csharp [Route("/users/{Id:int}")] [Route("/users/{UserName:string}")] public class GetUser : IGet, IReturn { public int? Id { get; set; } public int? UserName { get; set; } } ``` For the most part ServiceStack Routing implements a subset of ASP.NET Core's Routing features so your existing user-defined routes should continue to work as expected. ### Wildcard Routes The only incompatibility we found was when using wildcard paths which in ServiceStack Routing would use an '*' suffix, e.g: `[Route("/wildcard/{Path*}")]` which will need to change to use a ASP.NET Core's Routing prefix, e.g: ```csharp [Route("/wildcard/{*Path}")] [Route("/wildcard/{**Path}")] public class GetFile : IGet, IReturn { public string Path { get; set; } } ``` #### ServiceStack Routing Compatibility To improve compatibility with ASP.NET Core's Routing, ServiceStack's Routing (when not using Endpoint Routing) now supports parsing ASP.NET Core's Route Constraints but as they're inert you would need to continue to use [Custom Route Rules](https://docs.servicestack.net/routing#custom-rules) to distinguish between different routes matching the same path at different specificity: ```csharp [Route("/users/{Id:int}", Matches = "**/{int}")] [Route("/users/{UserName:string}")] public class GetUser : IGet, IReturn { public int? Id { get; set; } public int? UserName { get; set; } } ``` It also supports defining Wildcard Routes using ASP.NET Core's syntax which we now recommend using instead for compatibility when switching to use Endpoint Routing: ```csharp [Route("/wildcard/{*Path}")] [Route("/wildcard/{**Path}")] public class GetFile : IGet, IReturn { public string Path { get; set; } } ``` ### Primary HTTP Method Another difference is that an API will only register its Endpoint Route for its [primary HTTP Method](https://docs.servicestack.net/api-design#all-apis-have-a-preferred-default-method), if you want an API to be registered for multiple HTTP Methods you can specify them in the `Route` attribute, e.g: ```csharp [Route("/users/{Id:int}", "GET,POST")] public class GetUser : IGet, IReturn { public required int Id { get; set; } } ``` As such we recommend using the IVerb `IGet`, `IPost`, `IPut`, `IPatch`, `IDelete` interface markers to specify the primary HTTP Method for an API. This isn't needed for [AutoQuery Services](https://docs.servicestack.net/autoquery/) which are implicitly configured to use their optimal HTTP Method. If no HTTP Method is specified, the Primary HTTP Method defaults to HTTP **POST**. ### Authorization Using Endpoint Routing also means ServiceStack's APIs are authorized the same way, where ServiceStack's [Declarative Validation attributes](https://docs.servicestack.net/auth/#declarative-validation-attributes) are converted into ASP.NET Core's `[Authorize]` attribute to secure the endpoint: ```csharp [ValidateIsAuthenticated] [ValidateIsAdmin] [ValidateHasRole(role)] [ValidateHasClaim(type,value)] [ValidateHasScope(scope)] public class Secured {} ``` #### Authorize Attribute on ServiceStack APIs Alternatively you can now use ASP.NET Core's `[Authorize]` attribute directly to secure ServiceStack APIs should you need more fine-grained Authorization: ```csharp [Authorize(Roles = "RequiredRole")] [Authorize(Policy = "RequiredPolicy")] [Authorize(AuthenticationSchemes = "Identity.Application,Bearer")] public class Secured {} ``` #### Configuring Authentication Schemes ServiceStack will default to using the major Authentication Schemes configured for your App to secure the APIs endpoint with, this can be overridden to specify which Authentication Schemes to use to restrict ServiceStack APIs by default, e.g: ```csharp app.UseServiceStack(new AppHost(), options => { options.AuthenticationSchemes = "Identity.Application,Bearer"; options.MapEndpoints(); }); ``` ### Hidden ServiceStack Endpoints Whilst ServiceStack Requests are registered and executed as endpoints, most of them are marked with `builder.ExcludeFromDescription()` to hide them from polluting metadata and API Explorers like Swagger UI and [API Explorer](https://docs.servicestack.net/api-explorer). To also hide your ServiceStack APIs you can use `[ExcludeMetadata]` attribute to hide them from all metadata services or use `[Exclude(Feature.ApiExplorer)]` to just hide them from API Explorer UIs: ```csharp [ExcludeMetadata] [Exclude(Feature.ApiExplorer)] public class HiddenRequest {} ``` ### Content Negotiation An example of these hidden routes is the support for invoking and returning ServiceStack APIs in different Content Types via hidden Endpoint Routes mapped with the format `/api/{Request}.{format}`, e.g: - [/api/QueryBookings](https://blazor-vue.web-templates.io/api/QueryBookings) - [/api/QueryBookings.jsonl](https://blazor-vue.web-templates.io/api/QueryBookings.jsonl) - [/api/QueryBookings.csv](https://blazor-vue.web-templates.io/api/QueryBookings.csv) - [/api/QueryBookings.xml](https://blazor-vue.web-templates.io/api/QueryBookings.xml) - [/api/QueryBookings.html](https://blazor-vue.web-templates.io/api/QueryBookings.html) #### Query String Format That continues to support specifying the Mime Type via the `?format` query string, e.g: - [/api/QueryBookings?format=jsonl](https://blazor-vue.web-templates.io/api/QueryBookings?format=jsonl) - [/api/QueryBookings?format=csv](https://blazor-vue.web-templates.io/api/QueryBookings?format=csv) ### Predefined Routes Endpoints are only created for the newer `/api/{Request}` [pre-defined routes](https://docs.servicestack.net/routing#pre-defined-routes), which should be easier to use with less conflicts now that ServiceStack APIs are executed along-side other endpoint routes APIs which can share the same `/api` base path with non-conflicting routes, e.g: `app.MapGet("/api/minimal-api")`. As a result clients configured to use the older `/json/reply/{Request}` pre-defined route will need to be configured to use the newer `/api` base path. No change is required for C#/.NET clients using the recommended `JsonApiClient` JSON Service Client which is already configured to use the newer `/api` base path. ```csharp var client = new JsonApiClient(baseUri); ``` Older .NET clients can be configured to use the newer `/api` pre-defined routes with: ```csharp var client = new JsonServiceClient(baseUri) { UseBasePath = "/api" }; var client = new JsonHttpClient(baseUri) { UseBasePath = "/api" }; ``` To further solidify that `/api` as the preferred pre-defined route we've also **updated all generic service clients** of other languages to use `/api` base path by default: #### JavaScript/TypeScript ```ts const client = new JsonServiceClient(baseUrl) ``` #### Dart ```dart var client = ClientFactory.api(baseUrl); ``` #### Java/Kotlin ```java JsonServiceClient client = new JsonServiceClient(baseUrl); ``` #### Python ```python client = JsonServiceClient(baseUrl) ``` #### PHP ```php $client = new JsonServiceClient(baseUrl); ``` ### Revert to Legacy Predefined Routes You can unset the base path to revert back to using the older `/json/reply/{Request}` pre-defined route, e.g: #### JavaScript/TypeScript ```ts client.basePath = null; ``` #### Dart ```dart var client = ClientFactory.create(baseUrl); ``` #### Java/Kotlin ```java client.setBasePath(); ``` #### Python ```python client.set_base_path() ``` #### PHP ```php $client->setBasePath(); ``` ### Customize Endpoint Mapping You can register a RouteHandlerBuilders to customize how ServiceStack APIs endpoints are registered which is also what ServiceStack uses to annotate its API endpoints to enable its new [Open API v3](/posts/openapi-v3) support: ```csharp options.RouteHandlerBuilders.Add((builder, operation, method, route) => { builder.WithOpenApi(op => { ... }); }); ``` ### Endpoint Routing Compatibility Levels The default behavior of `MapEndpoints()` is the strictest and recommended configuration that we want future ServiceStack Apps to use, however if you're migrating existing App's you may want to relax these defaults to improve compatibility with existing behavior. The configurable defaults for mapping endpoints are: ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(use:true, force:true, useSystemJson:UseSystemJson.Always); }); ``` - `use` - Whether to use registered endpoints for executing ServiceStack APIs - `force` - Whether to only allow APIs to be executed through endpoints - `useSystemJson` - Whether to use System.Text.Json for JSON API Serialization So you could for instance register endpoints and not `use` them, where they'll be visible in endpoint API explorers like [Swagger UI](https://docs.servicestack.net/releases/v8_01#openapi-v3) but continue to execute in ServiceStack's Request Pipeline. `force` disables fallback execution of ServiceStack Requests through ServiceStack's Request Pipeline for requests that don't match registered endpoints. You may need to disable this if you have clients calling ServiceStack APIs through multiple HTTP Methods, as only the primary HTTP Method is registered as an endpoint. When enabled `force` ensures the only ServiceStack Requests that are not executed through registered endpoints are `IAppHost.CatchAllHandlers` and `IAppHost.FallbackHandler` handlers. `useSystemJson` is a new feature that lets you specify when to use `System.Text.Json` for JSON API Serialization, which is our next exciting feature to standardize on using [ASP.NET Core's fast async System.Text.Json](https://docs.servicestack.net/releases/v8_01#system.text.json) Serializer. ## Endpoint Routing Everywhere Whilst the compatibility levels of Endpoint Routing can be relaxed, we recommend new projects use the strictest and most integrated defaults that's now configured on all [ASP.NET Core Identity Auth .NET 8 Projects](/start). For additional testing we've also upgraded many of our existing .NET Example Applications, which are now all running with our latest recommended Endpoint Routing configuration: - [BlazorDiffusionVue](https://github.com/NetCoreApps/BlazorDiffusionVue) - [BlazorDiffusionAuto](https://github.com/NetCoreApps/BlazorDiffusionAuto) - [TypeChatExamples](https://github.com/NetCoreApps/TypeChatExamples) - [TalentBlazor](https://github.com/NetCoreApps/TalentBlazor) - [TechStacks](https://github.com/NetCoreApps/TechStacks) - [Validation](https://github.com/NetCoreApps/Validation) - [NorthwindAuto](https://github.com/NetCoreApps/NorthwindAuto) - [FileBlazor](https://github.com/NetCoreApps/FileBlazor) - [Chinook](https://github.com/NetCoreApps/Chinook) - [Chat](https://github.com/NetCoreApps/Chat) # Migrating to ASP.NET Core Identity for Authentication Source: https://servicestack.net/posts/identity-migration ## ASP.NET Core Identity Since the release of ServiceStack v8 we have started to include the use of [ASP.NET Core Identity for authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-8.0&tabs=visual-studio) in [our templates](https://github.com/NetCoreTemplates). This gives developers the option to use the built-in ASP.NET Core Identity authentication system or ServiceStack's own authentication system when building their next system. This provides a closer alignment with the ASP.NET Core ecosystem and allows developers to use the built-in ASP.NET Core Identity authentication system if they are already familiar with it. If you are already using ServiceStack's authentication system, you can continue to do so, but if you are looking to migrate to ASP.NET Core Identity, this guide will walk you through the process with a concrete example of migrating our [BlazorDiffusion](https://github.com/NetCoreApps/BlazorDiffusion) example application. ## Overview of the migration process The migration process can be broken down into the following steps: - Add NuGet dependencies - Create ASP.NET Core Identity `AspNetUsers` class based on your existing custom `UserAuth` class - Create ASP.NET Core Identity `AspNetRoles`, ensure matching primary key type to `AspNetUsers` - Create ASP.NET Core Identity `ApplicationDbContext` class, again matching primary key type to `AspNetUsers` - Create EntityFrameworkCore migration to initialize ASP.NET Core Identity tables - Update `AuthFeature` registration to use ASP.NET Core Identity - Update `Program.cs` to use ASP.NET Core Identity - Implement the Migrate Users Task - Migrating Roles - Migrate Foreign Keys from UserAuth to AspNetUsers In this guide we will walk through each of these steps in detail and show how we migrated our BlazorDiffusion example application over to ASP.NET Core Identity to help you with your own migration. ### Add ASP.NET Core Identity EntityFrameworkCore NuGet package The first step is to add the required ASP.NET Core Identity NuGet packages to your project. This can be done using the dotnet CLI or via Visual Studio's NuGet package manager. :::shell dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Tools ::: Since BlazorDiffusion was an existing Blazor project, [we created a new `blazor-wasm` project](https://github.com/NetCoreTemplates/blazor-wasm) using `x new blazor-wasm BlazorDiffusion` and migrated the Services and Components over to the new project. We can do this because the `blazor-wasm` template and others have been [updated to use ASP.NET Core Identity by default](https://docs.servicestack.net/auth/identity-auth). So if your project previously used a ServiceStack template, first check if there is an updated version of the template available with ASP.NET Core Identity support. ### Create ASP.NET Core Identity `AspNetUsers` class Next you will need to create a class that inherits from `IdentityUser` to represent our users. [This class will be used by ASP.NET Core Identity to store user information in the database](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-8.0). You will want to mirror customizations from your own `UserAuth` class to this new class which will have the name in the database of `AspNetUsers`. To minimize changes, you can rename your existing `AppUser` class to something like `OldUserAuth` and then create a new `AppUser` class that inherits from `IdentityUser` and copy over any customizations from `OldUserAuth`. :::info In this case `AppUser` is the name of our custom `UserAuth` class. ::: You will still need to reference your `OldAppUser` class for migrating users, so you will want to point it to the `AppUser` table by using the `[Alias("AppUser")]` attribute. ```csharp [Alias("AppUser")] public class OldAppUser { [AutoIncrement] public int Id { get; set; } public string UserName { get; set; } public string DisplayName { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Handle { get; set; } public string Email { get; set; } public string PasswordHash { get; set; } public string? ProfileUrl { get; set; } public string? Avatar { get; set; } //overrides ProfileUrl public string? LastLoginIp { get; set; } public DateTime? LastLoginDate { get; set; } public string RefIdStr { get; set; } public DateTime? LockedDate { get; set; } public DateTime CreatedDate { get; set; } public DateTime ModifiedDate { get; set; } } ``` When creating your new `AppUser` class, you will want to copy over any customizations from your `OldAppUser` class. In this case we have added a `Handle` property to our `OldAppUser` class, so this will need to be included in the new `AppUser` class as well. Essentially your custom EF IdentityUser will want a copy of all the properties you want to migrate other than Id, Email, and PasswordHash that's already defined in the base IdentityUser class. ```csharp // Add profile data for application users by adding properties to the AppUser class [Alias("AspNetUsers")] public class AppUser : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public string? DisplayName { get; set; } public string? ProfileUrl { get; set; } [Input(Type = "file"), UploadTo("avatars")] public string? Avatar { get; set; } //overrides ProfileUrl public string? Handle { get; set; } public int? RefId { get; set; } public string RefIdStr { get; set; } = Guid.NewGuid().ToString(); public bool IsArchived { get; set; } public DateTime? ArchivedDate { get; set; } public string? LastLoginIp { get; set; } public DateTime? LastLoginDate { get; set; } public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime ModifiedDate { get; set; } = DateTime.UtcNow; } ``` ### Create ASP.NET Core Identity `AspNetRoles` Next you will need to create a class that inherits from `IdentityRole` to represent your user roles. This class will be used by ASP.NET Core Identity to store role information in the database. ```csharp [Alias("AspNetRoles")] public class AppRole : IdentityRole { public AppRole() {} public AppRole(string roleName) : base(roleName) {} } ``` Again, because our `AppUser` class is using a different primary key type than the default `string` type, you will need to specify a matching primary key type for your `AppRole` class. ### Create ASP.NET Core Identity `ApplicationDbContext` class Now to use our `AppUser` and `AppRole` classes, you will need to create a class that inherits from `IdentityDbContext` to represent our database context. Just like with any EntityFrameworkCore database context, this class will be used to query and save data to the database. ```csharp public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) { protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity() .HasIndex(x => x.Handle) .IsUnique(); } } ``` Above uses the `Handle` property on the `AppUser` class to create a unique index on the `Handle` column in the `AspNetUsers` table. You can add other custom restrictions to your schema here as well as needed. ### Create EntityFrameworkCore migration to initialize ASP.NET Core Identity tables Now that you have your `AppUser` and `AppRole` classes, and can access them via your newly created `ApplicationDbContext` class, you can create an EntityFrameworkCore migration to initialize the ASP.NET Core Identity tables. [You can generate your initial migration using the dotnet CLI or via Visual Studio's Package Manager Console](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). :::shell dotnet ef migrations add CreateIdentitySchema ::: You should run this command from the AppHost project directory, which in our case is `BlazorDiffusion`. This will generate your new EntityFrameworkCore migration in the `Migrations` directory of your AppHost project, along side your ServiceStack migrations. With your new migration created, you can now update your database schema to include the ASP.NET Core Identity tables. :::shell dotnet ef database update ::: Using the [dotnet EntityFramework CLI is great for local development](https://learn.microsoft.com/en-us/ef/core/cli/dotnet), but for production deployments you will need to run the migrations on your server. You can do this using ServiceStack's AppTasks feature prior to the standard ServiceStack migrations. ```csharp public class ConfigureDbMigrations : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { var migrator = new Migrator(appHost.Resolve(), typeof(Migration1000).Assembly); AppTasks.Register("migrate", _ => { var log = appHost.GetApplicationServices().GetRequiredService>(); log.LogInformation("Running EF Migrations..."); var scopeFactory = appHost.GetApplicationServices().GetRequiredService(); using (var scope = scopeFactory.CreateScope()) { using var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.EnsureCreated(); dbContext.Database.Migrate(); } }); ``` In the above example we are ensuring the database is created which creates the required schema, and then running the migrations to update the schema to the latest version. ### Update `AuthFeature` registration to use ASP.NET Core Identity With your ASP.NET Core Identity tables created, you can now update your [`AuthFeature` registration](https://docs.servicestack.net/auth/authentication-and-authorization) to use ASP.NET Core Identity. ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { appHost.Plugins.Add(new AuthFeature(IdentityAuth.For(options => { options.EnableCredentialsAuth = true; options.SessionFactory = () => new CustomUserSession(); }))); }); } ``` In the above example we are using the `IdentityAuth` class to register ASP.NET Core Identity with ServiceStack. This class is a wrapper around the standard ASP.NET Core Identity registration process and allows you to configure ASP.NET Core Identity options. [ServiceStack uses a compatible Identity v2 password hashing format](https://docs.servicestack.net/auth/migrate-to-identity-auth), which should let you migrate your users to Identity Auth without the need to reset their passwords. ### Update `Program.cs` to use ASP.NET Core Identity Now you will need to configure IdentityCore middleware in your `Program.cs` file. ```csharp services.AddAuthentication(options => { options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; }) .AddIdentityCookies(); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo("App_Data")); // $ dotnet ef migrations add CreateIdentitySchema // $ dotnet ef database update var connectionString = config.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); services.AddDbContext(options => options.UseSqlite(connectionString, b => b.MigrationsAssembly(nameof(BlazorDiffusion)))); services.AddDatabaseDeveloperPageExceptionFilter(); services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) .AddRoles() .AddEntityFrameworkStores() .AddSignInManager() .AddDefaultTokenProviders(); ``` Since BlazorDiffusion is a .NET 8 Blazor WASM application, we also needed some additional dependencies setup. ```csharp services.AddCascadingAuthenticationState(); services.AddScoped(); services.AddScoped(); services.AddScoped(); ``` If you are migrating to Identity from an existing Blazor application, our templates have [tailwind-css styled login and register pages that you can use to get started](https://github.com/NetCoreTemplates/blazor-wasm/tree/main/MyApp/Components/Account). For these, you will also need the additional IdentityEndpoints mapped. ```csharp // Add additional endpoints required by the Identity /Account Razor components. app.MapAdditionalIdentityEndpoints(); ``` ### Implement the Migrate Users Task So far we have prepared the application to use ASP.NET Core Identity, but we still need to migrate our existing users to the new ASP.NET Core Identity tables. This will require: - Migrating users from the `AppUser` table to the `AspNetUsers` table - Migrating custom roles table to the `AspNetRoles` table - Migrating foreign keys from the `UserAuth` table to the `AspNetUsers` table - Migrating foreign keys from the `UserAuthRole` table to the `AspNetUserRoles` table (if any) To do this we will create a new AppTask that will migrate our users to the new ASP.NET Core Identity tables. ```csharp AppTasks.Register("migrate.users", _ => { var log = appHost.GetApplicationServices().GetRequiredService>(); log.LogInformation("Running migrate.users..."); var scopeFactory = appHost.GetApplicationServices().GetRequiredService(); using var scope = scopeFactory.CreateScope(); using var dbContext = scope.ServiceProvider.GetRequiredService(); using var db = scope.ServiceProvider.GetRequiredService().Open(); var migrateUsers = db.Select(db.From().OrderBy(x => x.Id)); log.LogInformation("Migrating {Count} Existing ServiceStack Users to Identity Auth Users...", migrateUsers.Count); MigrateExistingUsers(dbContext, scope.ServiceProvider, migrateUsers).Wait(); }); ``` In the above example we are using the `IDbConnectionFactory` to open a connection to our database and select all of our existing users from the `AppUser` table. The `MigrationExistingUsers` method will then migrate our existing users to the new ASP.NET Core Identity tables. ```csharp private async Task MigrateExistingUsers(ApplicationDbContext dbContext, IServiceProvider services, List migrateUsers, string tempPassword="p@55wOrd") { var userManager = services.GetRequiredService>(); var now = DateTime.UtcNow; foreach (var user in migrateUsers) { var appUser = new AppUser { Id = user.Id, UserName = user.Email, Email = user.Email, DisplayName = user.DisplayName, FirstName = user.FirstName, LastName = user.LastName, Handle = user.Handle, ProfileUrl = user.ProfileUrl, Avatar = user.Avatar, RefIdStr = user.RefIdStr ?? Guid.NewGuid().ToString(), LockoutEnabled = true, LockoutEnd = user.LockedDate != null ? now.AddYears(10) : now, LastLoginDate = user.LastLoginDate, LastLoginIp = user.LastLoginIp, CreatedDate = user.CreatedDate, ModifiedDate = user.ModifiedDate, EmailConfirmed = true, }; await userManager.CreateAsync(appUser, tempPassword); if (user.PasswordHash != null) { // Update raw PasswordHash (which uses older ASP.NET Identity v2 format), after users successfully signs in // the password will be re-hashed using the latest ASP.NET Identity v3 implementation dbContext.Users .Where(x => x.Id == user.Id) .ExecuteUpdate(setters => setters.SetProperty(x => x.PasswordHash, user.PasswordHash)); } } } ``` In the above example we are using [the `UserManager`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1?view=aspnetcore-8.0) to create a new `AppUser` for each of our existing users and then updating the `PasswordHash` property from the `OldAppUser` table. ### Migrating Roles If you are using custom roles, you will also need to migrate these to the new ASP.NET Core Identity tables, and then assign them to your users based on their existing roles in your previous setup. Your ServiceStack Authentication roles will be stored in the `UserAuthRole` table separately or in the `Roles` property of your `UserAuth` class. You will need to migrate these roles to the new ASP.NET Core Identity tables and then assign them to your users. ```csharp foreach (var roleName in allRoles) { var roleExist = await roleManager.RoleExistsAsync(roleName); if (!roleExist) { //create the roles and seed them to the database assertResult(await roleManager.CreateAsync(new AppRole(roleName))); } } ``` If your use of roles is static, you can create the list of all your roles from your applications code. If your use of roles is dynamic, you will need to query your database for all the roles that exist in your `UserAuthRole` table. One difference between ServiceStack Authentication and ASP.NET Core Identity is the use of the `Admin` role. In ServiceStack Authentication, the `Admin` role is a special role that gives the user access to all protected resources. In ASP.NET Core Identity, the `Admin` role is just a regular role that can be assigned to users. This means for users with the `Admin` role in your existing application, additional roles will need to be assigned to them in ASP.NET Core Identity to give them access to the same protected resources. In BlazorDiffusion we used the Admin role as well as others, but to preserve the existing behavior, we assigned all roles to users with the `Admin` role. ```csharp foreach (var user in Users.All) { var appUser = new AppUser { Id = user.Id, Email = user.Email, DisplayName = user.DisplayName, UserName = user.Email, Handle = user.Handle, Avatar = user.Avatar, EmailConfirmed = true, }; if(appUser.Email == "admin@email.com") await EnsureUserAsync(appUser, "p@55wOrd", AppRoles.All); else await EnsureUserAsync(appUser, "p@55wOrd", user.Roles); } ``` The `EnsureUserAsync` method will assign the roles to the user. ```csharp async Task EnsureUserAsync(AppUser user, string password, string[]? roles = null) { var existingUser = await userManager.FindByEmailAsync(user.Email!); if (existingUser != null) return; await userManager!.CreateAsync(user, password); if (roles?.Length > 0) { var newUser = await userManager.FindByEmailAsync(user.Email!); assertResult(await userManager.AddToRolesAsync(user, roles)); } } ``` ### Migrate Foreign Keys from UserAuth to AspNetUsers If you are using foreign keys in your existing application, you will need to migrate these to the new ASP.NET Core Identity tables. For databases like PostgreSQL, you can use the `ALTER TABLE` command to add a foreign key constraint to the `AspNetUsers` table, and we will want to remove the foreign key constraint from the `UserAuth` or `AppUser` table. In BlazorDiffusion for example, the `Creative` table was using a foreign key to the `AppUser` table, so we needed to replace this with a foreign key to the `AspNetUsers` table. ```sql ALTER TABLE "Creative" DROP CONSTRAINT "FK_Creative_AppUser_UserId"; ALTER TABLE "Creative" ADD CONSTRAINT "FK_Creative_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ; ``` For databases like SQLite, you will need to create a new table with the foreign key constraint and then copy the data over from the old table. We use SQLite for BlazorDiffusion since it makes it easy deploy the application, and SQLite is a great option for small applications that don't need to scale. Since we have to migrate several tables, we can create a `ReplaceForeignKeyConstraint` method to handle this for us. ```csharp private void ReplaceForeignKeyConstraint() { var modelDef = typeof(TModel).GetModelMetadata(); var createTable = SqliteDialect.Provider.ToCreateTableStatement(typeof(TModel)); var sql = $@"PRAGMA foreign_keys = OFF; ALTER TABLE {modelDef.ModelName} RENAME TO {modelDef.ModelName}_old; {createTable} INSERT INTO {modelDef.ModelName} SELECT * FROM {modelDef.ModelName}_old; -- DROP TABLE {modelDef.ModelName}_old; PRAGMA foreign_keys = ON;"; Db.ExecuteSql(sql); } ``` When replacing the tables like this, you will need to be aware of the order in which you replace the tables. For example, if you have a foreign key from the `Creative` table to the `AppUser` table, and a foreign key from the `Artifact` table to the `Creative` table, you will need to replace the `Creative` table first, and then the `Artifact` table. This is because the `Artifact` table has a foreign key to the `Creative` table, and if you replace the `Artifact` table first, the foreign key will still be pointing to the old `Creative` table. #### Incorrect migration order result
Incorrect DB mapping after migration.
#### Correct migration order result
Correct DB mapping after migration.
The rule of thumb is you will want to replace the tables from most depended on to least depended on. Another limitation of the SQL above is that the order of the columns in the new table must match the order of the columns in the old table. `INSERT INTO` will insert the data into the new table based on the order of the columns in the new table, so if the order of the columns is different, the data will be inserted into the wrong columns. During the migration of BlazorDiffusion, we hit this issue with the `Artifact` table. The `Artifact` C# class uses the `AuditBase` base class which has the `CreatedDate` and `ModifiedDate` properties. In a previous migration we added some additional columns as features were added. So when creating a copy of the `Artifact` class in the ServiceStack migration to handle fixing the foreign key, the order of the columns was different. Thankfully, since internal classes used in migrations are completely separate for repeatable migrations, we can just create the `Artfact` class to be specific for this migration. So instead of inheriting from `AuditBase`, we can just copy the properties from `AuditBase` into the `Artifact` class in the order required. Putting it all together, we have a migration `Up` method that looks like: ```csharp public override void Up() { var appHost = HostContext.AppHost; var log = appHost.GetApplicationServices().GetRequiredService>(); log.LogInformation("Migrating FKs from AppUser to AspNetUsers..."); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); ReplaceForeignKeyConstraint(); } ``` In a separate migration, we can then drop the old tables after confirming the migration was successful, and the previous data has been migrated to the new tables. ```csharp public class Migration1007 : MigrationBase { public override void Up() { DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); DropOldTable(); } private void DropOldTable() { var modelDef = typeof(TModel).GetModelMetadata(); Db.ExecuteSql($@"PRAGMA foreign_keys = OFF; DROP TABLE IF EXISTS {modelDef.ModelName}_old; PRAGMA foreign_keys = ON;"); } } ``` ### Migrate Foreign Keys from UserAuthRole to AspNetUserRoles If you have any tables that use a foreign key to your custom `UserAuthRole` table, you will need to do the same as above and migrate these to the new ASP.NET Core Identity tables. ## Why Migrate to ASP.NET Core Identity? ServiceStack's built-in authentication system is a great option for many applications. It provides a simple and easy to use authentication system that works out of the box with ServiceStack's built-in features like Sessions, Caching, and OrmLite. However, as a part of making ServiceStack more compatible with the ASP.NET Core ecosystem, we have started to include the use of ASP.NET Core Identity since a lot of ASP.NET developers are already familiar with it. It also provides features like two-factor authentication, external authentication providers, and more that are not available in ServiceStack's built-in authentication system. ![](/img/posts/identity-migration/two-factor-auth-example.png) So while you don't have to migrate to ASP.NET Core Identity, it is a great option if you are already familiar with it, or if you are looking to use some of the additional features it provides. If you are looking to migrate to ASP.NET Core Identity, we hope this guide helps you with your migration. If you have any questions, feel free to reach out on our [forums](https://forums.servicestack.net). # Docker Containerization in .NET 8 Source: https://servicestack.net/posts/net8-docker-containers ### All .NET Project Templates upgraded to .NET 8 Included in the release of [ServiceStack v8](https://docs.servicestack.net/releases/v8_00) all of ServiceStack's [.NET project templates](https://github.com/NetCoreTemplates/) have been upgraded to use **ServiceStack v8** and **.NET 8** target framework, in addition the built-in CI/CD deployment GitHub Actions have been upgraded to use the [secure rootless Linux Docker containers](https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/) that's now built into .NET 8 which allow you to effortlessly deploy your containerized .NET 8 Apps with Docker and GitHub Registry via SSH to any Linux Server.

.NET 8 Docker Containers

Learn about the latest streamlined containerization support built into .NET 8

### .NET 8 Docker Containerization .NET 8 simplifies Docker integration using functionality built into the .NET SDK tooling where it's able to use `dotnet publish` to publish your .NET 8 App to a container image without a Dockerfile, adhering to the latest Least privilege and hardened security best practices of running App's as non-root by default. This **publish to container** feature also supports creating Docker images for different architectures like ARM64 which sees [significant improvements in .NET 8](https://devblogs.microsoft.com/dotnet/this-arm64-performance-in-dotnet-8/) making deploying your .NET Apps to ARM64 an [even better value proposition](https://servicestack.net/posts/cloud-value-between-architectures). If you need more control over your Docker image, you can still use a Dockerfile to customize your image, and there are [even tools to generate Dockerfiles for you from your project](https://github.com/tmds/build-image). ### GitHub Action Deployments In today's DevOps ecosystem, [GitHub Actions](https://github.com/features/actions) stand out as an invaluable asset for automating CI/CD workflows directly within your GitHub repository. The introduction of .NET 8 takes this a step further, offering a streamlined approach to generating Docker images through the `DefaultContainer` setting in your `.csproj`. This ensures consistent application packaging, making it deployment-ready by just using `dotnet publish`. ServiceStack's project templates bring additional flexibility, by utilizing foundational tools like [Docker](https://www.docker.com) for containerization and [SSH](https://en.wikipedia.org/wiki/Secure_Shell) for secure deployments, it's able to deploy your Dockerized .NET applications to any Linux server, whether self-hosted or on any cloud provider. #### Live Demos use their GitHub Actions to deploy themselves Each template's Live demo are themselves utilizing their included GitHub Actions to deploy itself to a Linux server running on a **€13.60 /month** shared 8BG RAM [Hetzner Cloud VM](https://www.hetzner.com/cloud) that's currently running 50+ Docker Containers. This guide aims to walk you through the hosting setup and the GitHub Actions release process as introduced in the ServiceStack's latest .NET 8 project templates. ## Deployment Files Deployment files that are included in project templates to facilitate GitHub Actions deployments: #### .deploy/ - [nginx-proxy-compose.yml](https://github.com/NetCoreTemplates/blazor/blob/master/.deploy/nginx-proxy-compose.yml) - Manage nginx reverse proxy and Lets Encrypt companion container (one-time setup per server) - [docker-compose.yml](https://github.com/NetCoreTemplates/blazor/blob/main/.deploy/docker-compose.yml) - Manage .NET App Docker Container #### .github/workflows/ - [build.yml](https://github.com/NetCoreTemplates/blazor/blob/master/.github/workflows/build.yml) - Build .NET Project and Run Tests - [release.yml](https://github.com/NetCoreTemplates/blazor/blob/master/.github/workflows/release.yml) - Build container, Push to GitHub Packages Registry, SSH deploy to Linux server, Run DB Migrations and start new Docker Container if successful otherwise revert Migration ## Prerequisites Before your Linux server can accept GitHub Actions deployments, we need to setup your Linux deployment server. For a step-by-step walk through of these steps and more information about this solution, checkout our video guide below:

Use GitHub Actions for Auto Deployments

### Setup Deployment Server #### 1. Install Docker and Docker-Compose Follow [Docker's installation instructions](https://docs.docker.com/engine/install/ubuntu/) to install the latest version of Docker. #### 2. Configure SSH for GitHub Actions Generate a dedicated SSH key pair to be used by GitHub Actions: :::sh ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions ::: Add the **public key** to your server's SSH **authorized_keys**: :::sh cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys ::: Add the **private key** to your repo's `DEPLOY_KEY` GitHub Action Secret which GitHub Actions will use to securely SSH into the server. #### 3. Set Up nginx-reverse-proxy You should have a `docker-compose` file similar to the `nginx-proxy-compose.yml` in your repository. Upload this file to your server: :::sh scp nginx-proxy-compose.yml user@your_server:~/ ::: To bring up the nginx reverse proxy and its companion container for handling TLS certificates, run: :::sh docker compose -f ~/nginx-proxy-compose.yml up -d ::: This will start an nginx reverse proxy along with a companion LetsEncrypt container that will automatically watch for additional Docker containers on the same network and initialize them with valid TLS certificates. ### Ready to host containerized .NET Apps Your Linux server is now ready to accept multiple .NET App deployments from GitHub Actions. The guide below walks through the process of setting up your GitHub repository to deploy new ServiceStack .NET Apps to your Linux server. ## Step-by-Step Guide ### 1. Create Your ServiceStack Application Start by creating your ServiceStack application, either from [ServiceStack's Start Page](https://servicestack.net/start) or by using the [x dotnet tool](https://docs.servicestack.net/dotnet-tool): :::sh x new blazor ProjectName ::: Replace `ProjectName` with your desired project name to generate a new ServiceStack application pre-configured with the necessary Docker compose files and GitHub Action workflows as above. ### 2. Configure DNS for Your Application You need a domain to point to your Linux server. Create an A Record in your DNS settings that points to the IP address of your Linux server: - **Subdomain**: `app.example.org` - **Record Type**: A - **Value/Address**: IP address of your Linux server This ensures that any requests to `app.example.org` are directed to your server. ### 3. Setting Up GitHub Secrets Navigate to your GitHub repository's settings, find the "Secrets and variables" section, and add the following secrets: - `DEPLOY_HOST`: IP address or hostname of your Linux server - `DEPLOY_USERNAME`: SSH Username to use for deployments - `DEPLOY_KEY`: Private key generated for GitHub Actions to SSH into your server - `LETSENCRYPT_EMAIL`: Your email address for Let's Encrypt notifications #### Using GitHub CLI for Secret Management You can use the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) for a quicker setup of these GitHub Action Secrets, e.g: ```bash gh secret set DEPLOY_HOST --body="linux-server-host" gh secret set DEPLOY_USERNAME --body="linux-server-username" gh secret set DEPLOY_KEY --bodyFile="path/to/ssh-private-key" gh secret set LETSENCRYPT_EMAIL --body="your-email@example.org" ``` These secrets will populate environment variables within your GitHub Actions workflow and other configuration files, enabling secure and automated deployment of your ServiceStack applications. ### 4. Push to Main Branch to Trigger Deployment With everything set up, pushing code to the main branch of your repository will trigger the GitHub Action workflow, initiating the deployment process: ```bash git add . git commit -m "Initial commit" git push origin main ``` ### 5. Verifying the Deployment After the GitHub Actions workflow completes, you can verify the deployment by: - Checking the workflow's logs in your GitHub repository to ensure it completed successfully - Navigating to your application's URL (e.g., `https://app.example.org`) in a web browser. You should see your ServiceStack application up and running with a secure HTTPS connection ## Features ### DB Migrations The GitHub Actions workflow includes a step to run database migrations on the remote server in the **Run remote db migrations** step which automatically runs the `migrate` AppTask in the `app-migration` companion Docker container on the Linux Host Server to validate migration was successful before completing deployment of the new App. A failed migration will cause deployment to fail and the previous App version to continue to run. ### Patch appsettings.json with production secrets One way to maintain sensitive information like API keys and connection strings for your Production App outside of its source code GitHub repository is to patch the `appsettings.json` file with a [JSON Patch](https://jsonpatch.com) that's stored in your repo's `APPSETTINGS_PATCH` GitHub Action Secret which will be applied with the deployed App's `appsettings.json` file. For example this JSON Patch below will replace values and objects in your App's **appsettings.json**: ```json [ { "op":"add", "path":"/oauth.facebook.AppSecret", "value":"xxxx" }, { "op":"add", "path":"/oauth.microsoft.AppSecret", "value":"xxxx" }, { "op":"add", "path":"/smtp", "value":{ "UserName": "xxxx", "Password": "xxxx", "Host": "smtp-server.example.org", "Port": 587, "From": "noreply@example.org", "FromName": "No Reply" } } ] ``` You can test your JSON Patch by saving it to `appsettings.json.patch` and applying it with the [patch feature](https://docs.servicestack.net/dotnet-tool#patch-json-files) of the `x` dotnet tool: :::sh x patch appsettings.json.patch ::: ## Anatomy of GitHub Actions Workflow GitHub Actions workflows are defined in YAML files, and they provide a powerful way to automate your development process. This guide will take you through the key sections of the workflow to give you a comprehensive understanding of how it functions. ## Permissions In this workflow, two permissions are specified: - `packages: write`: Allows the workflow to upload Docker images to GitHub Packages - `contents: write`: Required to access the repository content Specifying permissions ensures that the GitHub Actions runner has just enough access to perform the tasks in the workflow. ## Jobs This workflow consists of two jobs: `push_to_registry` and `deploy_via_ssh`. ### push_to_registry This job runs on an Ubuntu 22.04 runner and is responsible for pushing the Docker image to the GitHub Container Registry. It proceeds only if the previous workflow did not fail. The job includes the following steps: 1. **Checkout**: Retrieves the latest or specific tag of the repository's code 2. **Env variable assignment**: Assigns necessary environment variables for subsequent steps 3. **Login to GitHub Container Registry**: Authenticates to the GitHub Container Registry 4. **Setup .NET Core**: Prepares the environment for .NET 8 5. **Build and push Docker image**: Creates and uploads the Docker image to GitHub Container Registry (ghcr.io) ### deploy_via_ssh This job also runs on an Ubuntu 22.04 runner and depends on the successful completion of the `push_to_registry` job. Its role is to deploy the application via SSH. The steps involved are: 1. **Checkout**: Retrieves the latest or specific tag of the repository's code 2. **Repository name fix and env**: Sets up necessary environment variables 3. **Create .env file**: Generates a .env file required for deployment 4. **Copy files to target server via scp**: Securely copies files to the remote server 5. **Run remote db migrations**: Executes database migrations on the remote server 6. **Remote docker-compose up via ssh**: Deploys the Docker image with the application ## Triggers (on) The workflow is designed to be triggered by: 1. **New GitHub Release**: Activates when a new release is published 2. **Successful Build action**: Runs whenever the specified Build action completes successfully on the main or master branches 3. **Manual trigger**: Allows for rollback to a specific release or redeployment of the latest release, with an input for specifying the version tag Understanding these sections will help you navigate and modify the workflow as per your needs, ensuring a smooth and automated deployment process. ## Deployment Server Setup Expanded ### Ubuntu as the Reference Point Though our example leverages Ubuntu, it's important to emphasize that the primary requirements for this deployment architecture are a Linux operating system, Docker, and SSH. Many popular Linux distributions like CentOS, Fedora, or Debian will work just as efficiently, provided they support Docker and SSH. ### The Crucial Role of SSH in GitHub Actions **SSH** (Secure SHell) is not just a protocol to remotely access your server's terminal. In the context of GitHub Actions: - SSH offers a **secure channel** between GitHub Actions and your Linux server - Enables GitHub to **execute commands directly** on your server - Provides a mechanism to **transfer files** (like Docker-compose configurations or environment files) from the GitHub repository to the server By generating a dedicated SSH key pair specifically for GitHub Actions (as above), we ensure a secure and isolated access mechanism. Only the entities possessing the private key (in this case, only GitHub Actions) can initiate an authenticated connection. ### Docker & Docker-Compose: Powering the Architecture **Docker** encapsulates your ServiceStack application into containers, ensuring consistency across different environments. Some of its advantages include: - **Isolation**: Your application runs in a consistent environment, irrespective of where Docker runs. - **Scalability**: Easily replicate containers to handle more requests. - **Version Control for Environments**: Create, maintain, and switch between different container images. **Docker-Compose** extends Docker's benefits by orchestrating the deployment of multi-container applications: - **Ease of Configuration**: Describe your application's entire stack, including the application, database, cache, etc., in a single YAML file. - **Consistency Across Multiple Containers**: Ensures that containers are spun up in the right order and with the correct configurations. - **Simplifies Commands**: Instead of a long string of Docker CLI commands, a single `docker-compose up` brings your whole stack online. ### NGINX Reverse Proxy: The Silent Workhorse Using an **nginx reverse proxy** in this deployment design offers several powerful advantages: - **Load Balancing**: Distributes incoming requests across multiple ServiceStack applications, ensuring optimal resource utilization. - **TLS Management**: Together with its companion container, nginx reverse proxy automates the process of obtaining and renewing TLS certificates. This ensures your applications are always securely accessible over HTTPS. - **Routing**: Directs incoming traffic to the correct application based on the domain or subdomain. - **Performance**: Caches content to reduce load times and reduce the load on your ServiceStack applications. With an nginx reverse proxy, you can host multiple ServiceStack (or non-ServiceStack) applications on a single server while providing each with its domain or subdomain. ## Additional Resources ### Docker & Docker-Compose - **[Docker Documentation](https://docs.docker.com/)**: Core concepts, CLI usage, and practical applications - **[Docker-Compose Documentation](https://docs.docker.com/compose/)**: Define and manage multi-container applications ### GitHub Actions - **[GitHub Actions Documentation](https://docs.github.com/en/actions)**: Creating workflows, managing secrets, and automation tips - **[Starter Workflows](https://github.com/actions/starter-workflows)**: Templates for various languages and tools ### SSH & Security - **[SSH Key Management](https://www.ssh.com/academy/ssh/keygen)**: Guidelines on generating and managing SSH keys - **[GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)**: Securely store and use sensitive information # PHP typed client DTOs for .NET APIs Source: https://servicestack.net/posts/php-typed-apis We're happy to announce the **11th** [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) language to enjoy end-to-end typed support for calling .NET APIs - [PHP](https://www.php.net)! The **Add ServiceStack Reference** feature enables a simple way for PHP clients and Applications to generate native PHP DTO classes to access to your ServiceStack APIs.

End-to-end typed PHP

Learn about the rich JsonServiceClient & end-to-end typed API support for PHP

### PhpStorm ServiceStack Plugin PHP developers of [PhpStorm](https://www.jetbrains.com/phpstorm/) can get a simplified development experience for consuming ServiceStack Services by installing the [ServiceStack Plugin](https://plugins.jetbrains.com/plugin/7749-servicestack) from the JetBrains Marketplace: [![](/img/posts/php-typed-apis/phpstorm-servicestack-plugin.webp)](https://plugins.jetbrains.com/plugin/7749-servicestack) Where you'll be able to right-click on a directory and click on **ServiceStack Reference** on the context menu: ![](/img/posts/php-typed-apis/phpstorm-add-servicestack-reference.webp) To launch the **Add PHP ServiceStack Reference** dialog where you can enter the remote URL of the ServiceStack endpoint you wish to call to generate the Typed PHP DTOs for all APIs which by default will saved to `dtos.php`: ![](/img/posts/php-typed-apis/phpstorm-add-servicestack-reference-dialog.webp) Then just import the DTOs and `JsonServiceClient` to be able to consume any of the remote ServiceStack APIs: ```php send(new FindTechnologies( ids: [1,2,4,6], vendorName: "Google")); print_r($response); ``` If any of the the remote APIs change their DTOs can be updated by right-clicking on `dtos.php` and clicking **Update ServiceStack Reference**: ![](/img/posts/php-typed-apis/phpstorm-update-servicestack-reference.webp) ### Install PHP ServiceStack Client The only requirements for PHP apps to perform typed API Requests are the generated PHP DTOs and the generic `JsonServiceClient` which can be installed in Composer projects with: ```bash $ composer require servicestack/client ``` Or by adding the package to your `composer.json` then installing the dependencies: ```json { "require": { "servicestack/client": "^1.0" } } ``` ## First class development experience [PHP](https://www.php.net) is one of the worlds most popular programming languages thanks to its ease of use, platform independence, large standard library, flexibility and fast development experience which sees it excels as a popular language for web development and for development of popular CMS products like WordPress, Drupal and Joomla thanks to its flexibility, embeddability and ease of customization. To maximize the experience for calling ServiceStack APIs within these environments ServiceStack now supports PHP as a 1st class Add ServiceStack Reference supported language which gives PHP developers an end-to-end typed API for consuming ServiceStack APIs, complete with IDE integration in [PhpStorm](https://www.jetbrains.com/phpstorm/) as well as [built-in support in x dotnet tool](https://docs.servicestack.net/dotnet-tool#addupdate-servicestack-references) to generate Typed and annotated PHP DTOs for a remote ServiceStack instance from a single command-line. ### Ideal idiomatic Typed Message-based API To maximize the utility of PHP DTOs and enable richer tooling support and greater development experience, PHP DTOs are generated as Typed [JsonSerializable](https://www.php.net/manual/en/class.jsonserializable.php) classes with [promoted constructors](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion) and annotated with [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) - that's invaluable when scaling large PHP code-bases and greatly improves discoverability of a remote API. DTOs are also enriched with interface markers and Annotations which enables its optimal end-to-end typed API: The PHP DTOs and `JsonServiceClient` library follow [PHP naming conventions](https://infinum.com/handbook/wordpress/coding-standards/php-coding-standards/naming) so they'll naturally fit into existing PHP code bases. Here's a sample of [techstacks.io](https://techstacks.io) generated PHP DTOs containing string and int Enums, an example AutoQuery and a standard Request & Response DTO showcasing the rich typing annotations and naming conventions used: ```php enum TechnologyTier : string { case ProgrammingLanguage = 'ProgrammingLanguage'; case Client = 'Client'; case Http = 'Http'; case Server = 'Server'; case Data = 'Data'; case SoftwareInfrastructure = 'SoftwareInfrastructure'; case OperatingSystem = 'OperatingSystem'; case HardwareInfrastructure = 'HardwareInfrastructure'; case ThirdPartyServices = 'ThirdPartyServices'; } enum Frequency : int { case Daily = 1; case Weekly = 7; case Monthly = 30; case Quarterly = 90; } // @Route("/technology/search") #[Returns('QueryResponse')] /** * @template QueryDb of Technology * @template QueryDb1 of TechnologyView */ class FindTechnologies extends QueryDb implements IReturn, IGet, JsonSerializable { public function __construct( /** @var array|null */ public ?array $ids=null, /** @var string|null */ public ?string $name=null, /** @var string|null */ public ?string $vendorName=null, /** @var string|null */ public ?string $nameContains=null, /** @var string|null */ public ?string $vendorNameContains=null, /** @var string|null */ public ?string $descriptionContains=null ) { } /** @throws Exception */ public function fromMap($o): void { parent::fromMap($o); if (isset($o['ids'])) $this->ids = JsonConverters::fromArray('int', $o['ids']); if (isset($o['name'])) $this->name = $o['name']; if (isset($o['vendorName'])) $this->vendorName = $o['vendorName']; if (isset($o['nameContains'])) $this->nameContains = $o['nameContains']; if (isset($o['vendorNameContains'])) $this->vendorNameContains = $o['vendorNameContains']; if (isset($o['descriptionContains'])) $this->descriptionContains = $o['descriptionContains']; } /** @throws Exception */ public function jsonSerialize(): mixed { $o = parent::jsonSerialize(); if (isset($this->ids)) $o['ids'] = JsonConverters::toArray('int', $this->ids); if (isset($this->name)) $o['name'] = $this->name; if (isset($this->vendorName)) $o['vendorName'] = $this->vendorName; if (isset($this->nameContains)) $o['nameContains'] = $this->nameContains; if (isset($this->vendorNameContains)) $o['vendorNameContains'] = $this->vendorNameContains; if (isset($this->descriptionContains)) $o['descriptionContains'] = $this->descriptionContains; return empty($o) ? new class(){} : $o; } public function getTypeName(): string { return 'FindTechnologies'; } public function getMethod(): string { return 'GET'; } public function createResponse(): mixed { return QueryResponse::create(genericArgs:['TechnologyView']); } } // @Route("/orgs/{Id}", "DELETE") class DeleteOrganization implements IReturnVoid, IDelete, JsonSerializable { public function __construct( /** @var int */ public int $id=0 ) { } /** @throws Exception */ public function fromMap($o): void { if (isset($o['id'])) $this->id = $o['id']; } /** @throws Exception */ public function jsonSerialize(): mixed { $o = []; if (isset($this->id)) $o['id'] = $this->id; return empty($o) ? new class(){} : $o; } public function getTypeName(): string { return 'DeleteOrganization'; } public function getMethod(): string { return 'DELETE'; } public function createResponse(): void {} } ``` The smart PHP `JsonServiceClient` available in the [servicestack/client](https://packagist.org/packages/servicestack/client) packagist package enables the same productive, typed API development experience available in our other 1st-class supported client platforms. Using promoted constructors enables DTOs to be populated using a single constructor expression utilizing named parameters which together with the generic `JsonServiceClient` enables end-to-end typed API Requests in a single LOC: ```php use Servicestack\JsonServiceClient; use dtos\Hello; $client = new JsonServiceClient("https://test.servicestack.net"); /** @var HelloResponse $response */ $response = client->get(new Hello(name:"World")); ``` > The `HelloResponse` optional type hint doesn't change runtime behavior but enables static analysis tools and IDEs like PyCharm to provide rich intelli-sense and development time feedback. For more usage examples and information about ServiceStack's PHP support checkout the [PHP Add ServiceStack Reference](https://docs.servicestack.net/php-add-servicestack-reference) docs. # ASP.NET Core Identity Auth in .NET 8 Source: https://servicestack.net/posts/net8-identity-auth ### ASP.NET Core Identity Auth now the default A significant change from **ServiceStack v8** is the adoption of the same ASP.NET Core Identity Authentication that's configured in Microsoft's default Projects templates in ServiceStack's new Project Templates. ## History of ServiceStack Authentication ServiceStack has always maintained its own [Authentication and Authorization](https://docs.servicestack.net/auth/authentication-and-authorization) provider model, primarily as it was the only way to provide an integrated and unified Authentication model that worked across all our supported hosting platforms, inc. .NET Framework, ASP.NET Core on .NET Framework, HttpListener and .NET (fka .NET Core). Whilst the Authentication story in ASP.NET has undergone several cycles of changes over the years, the ServiceStack Auth Model has remained relatively consistent and stable, with no schema changes required since release whilst still providing flexible options for [extending UserAuth tables](https://docs.servicestack.net/auth/auth-repository#extending-userauth-tables) and typed [User Sessions](https://docs.servicestack.net/auth/sessions#using-typed-sessions-in-servicestack). ### .NET Framework considered legacy Although the multi-platform support of the unified Authentication model has been vital for Organizations migrating their systems to .NET (Core) where ServiceStack Customers have been able to enjoy [Exceptional Code reuse](https://docs.servicestack.net/netcore#exceptional-code-reuse), it's become clear that the .NET platform (e.g. .NET 8) is the only platform that should be considered for new projects and that .NET Framework should only be considered a stable legacy platform for running existing systems on. Given Microsoft has committed to [Authentication Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/whats-new-with-identity-in-dotnet-8/) it's become more important to easily integrate ServiceStack with new and existing .NET projects to access these new features than to continue recommending ServiceStack's unified Auth Providers as the default option for new projects. ### ServiceStack will use Identity Auth in new projects ASP.NET Core Identity Auth is the default Auth Model adopted in new ServiceStack projects which closely follows the same approach as the Microsoft Project Template it integrates ServiceStack with, e.g. the .NET 8 **Blazor** and **Blazor Vue** project templates adopts the exact same Auth configuration as Microsoft's default Blazor Project Template configured with **Individual** Identity Auth, likewise with the **Bootstrap** and **Tailwind** styled **MVC** and **Razor Pages** templates. You can find ServiceStack Integrated Identity Auth Templates for each of ASP.NET Core's major Blazor, Razor Pages and MVC Project Templates:

Create a Project with ASP.NET Identity Auth

### Identity Auth Template Live Demos For a quick preview of what these look like, checkout out their Internet Hosted Live Demos: The configuration and source code for the above projects are a good reference for how to configure ServiceStack with Identity Auth in your own projects: - [blazor](https://github.com/NetCoreTemplates/blazor) - [blazor-vue](https://github.com/NetCoreTemplates/blazor-vue) - [razor](https://github.com/NetCoreTemplates/razor) - [mvc](https://github.com/NetCoreTemplates/mvc) - [razor-bootstrap](https://github.com/NetCoreTemplates/razor-bootstrap) - [mvc-bootstrap](https://github.com/NetCoreTemplates/mvc-bootstrap) The **Bootstrap** versions use same Individual Identity Auth Pages that Microsoft's **Razor Pages** and **MVC** templates use, whilst the **Tailwind** versions have been enhanced to use **Tailwind CSS** instead of Bootstrap, includes a **visual QR Code** implementation that was missing and includes an `IEmailSender` SMTP solution that's easily enabled via Configuration to use your preferred **SMTP Server**. ## Migrating to ASP.NET Core Identity Auth Migrating from ServiceStack Auth to Identity Auth should be relatively straight-forward as ServiceStack uses a compatible Identity v2 password hashing format, which should let you migrate your users to Identity Auth without them noticing. ## ServiceStack's Identity Auth Integration ServiceStack's Identity Auth integration is focused on high compatibility so existing ServiceStack Customers require minimal effort to migrate existing code bases to use the new Identity Auth integration, despite Identity Auth being an entirely different Auth Provider model and implementation. It does this by retaining a lot of the existing user-facing Authentication and Session abstractions that ServiceStack APIs use for Authorization as well as existing endpoints and Request/Response DTOs that ServiceStack Clients use to Authenticate, but replace their internal implementation to use ASP.NET Identity Auth instead. The new Identity Auth integration is contained in the .NET 6+ **ServiceStack.Extensions** NuGet package: ```xml ``` Which at a minimum lets you configure ServiceStack to use Identity Auth by simply registering the existing `AuthFeature` plugin with the Application's custom EF `ApplicationUser` Data Model: ```csharp Plugins.Add(new AuthFeature(IdentityAuth.For())); ``` It requires minimal configuration as all Authorization is configured using ASP.NET Core's standard APIs, any configuration in this plugin is then just used to customize Identity Auth's integration with ServiceStack. There's also no new concepts to learn as all ASP .NET Core endpoints, pages and controllers continue to Authenticate against the populated `ClaimsPrincipal` whilst all ServiceStack APIs continue to Authenticate against the populated typed [User Session](https://docs.servicestack.net/auth/sessions). The `AuthFeature` works by registering the following Identity Auth Providers: ### Identity Auth Providers - [IdentityApplicationAuthProvider](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.Extensions/Auth/IdentityApplicationAuthProvider.cs) - Converts an Identity Auth `ClaimsPrincipal` into a ServiceStack Session - [IdentityCredentialsAuthProvider](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.Extensions/Auth/IdentityCredentialsAuthProvider.cs) - Implements ServiceStack's `Authenticate` API using Identity Auth - [IdentityJwtAuthProvider](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack.Extensions/Auth/IdentityJwtAuthProvider.cs) - Converts an Identity Auth JWT into an Authenticated ServiceStack Session Only the `IdentityApplicationAuthProvider` is registered by default which is required to convert Identity Auth's `ClaimPrincipal` into an Authenticated ServiceStack [Session](https://docs.servicestack.net/auth/sessions). The other Auth Providers are required if you want to enable authentication with ServiceStack's endpoints. E.g. ServiceStack's [Built-in UIs](https://servicestack.net/auto-ui) would require the Credentials Auth to be enabled to authenticate via the built-in Sign In dialogs. ### Configuring Auth Providers Which is what all the Blazor and MVC Identity Auth templates enable by default in [Configure.Auth.cs](https://github.com/NetCoreTemplates/blazor/blob/main/MyApp/Configure.Auth.cs): ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { appHost.Plugins.Add(new AuthFeature(IdentityAuth.For( // Configure ServiceStack's Integration with Identity Auth options => { options.EnableCredentialsAuth = true; options.SessionFactory = () => new CustomUserSession(); }) )); }); } ``` If you're using a `CustomUserSession` you'll also need to register it with the `SessionFactory` for it to be used. Each of the Identity Auth Providers can also be customized individually: ```csharp Plugins.Add(new AuthFeature(IdentityAuth.For(options => { // Configure IdentityApplicationAuthProvider options.AuthApplication... // Configure IdentityCredentialsAuthProvider options.EnableCredentialsAuth = true; options.AuthCredentials... // Configure IdentityJwtAuthProvider options.EnableJwtAuth = true; options.AuthJwt... }) )); ``` Typically you'll want to use the included Identity UI Pages and dependencies to register new users and assign roles, but if you have any existing client integrations that use ServiceStack APIs they can also be enabled with: ```csharp Plugins.Add(new AuthFeature(IdentityAuth.For(options => { // Include ServiceStack's Register API options.IncludeRegisterService = true; // Include ServiceStack's AssignRoles and UnAssignRoles APIs options.IncludeAssignRoleServices = true; )); ``` ### Extending Identity Auth Cookies and User Sessions By default all [well known Claim Names](https://github.com/ServiceStack/ServiceStack/blob/3ab3d23c85cf48435b8cd9386f25afab79fcb542/ServiceStack/src/ServiceStack.Extensions/Auth/IdentityApplicationAuthProvider.cs#L49) are used to populate the User Session, but you can also include additional claims in the Identity Auth Cookie and use them to populate the User Session by overriding `PopulateFromClaims()` in your [CustomUserSession.cs](https://github.com/NetCoreTemplates/blazor/blob/main/MyApp.ServiceInterface/Data/CustomUserSession.cs), e.g: ```csharp public class CustomUserSession : AuthUserSession { public override void PopulateFromClaims(IRequest httpReq, ClaimsPrincipal principal) { // Populate Session with data from Identity Auth Claims ProfileUrl = principal.FindFirstValue(JwtClaimTypes.Picture); } } // Add additional claims to the Identity Auth Cookie public class AdditionalUserClaimsPrincipalFactory(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) : UserClaimsPrincipalFactory(userManager, roleManager, optionsAccessor) { public override async Task CreateAsync(ApplicationUser user) { var principal = await base.CreateAsync(user); var identity = (ClaimsIdentity)principal.Identity!; var claims = new List(); // Add additional claims here if (user.ProfileUrl != null) { claims.Add(new Claim(JwtClaimTypes.Picture, user.ProfileUrl)); } identity.AddClaims(claims); return principal; } } ``` ### Custom Application User Primary Key The default `IdentityUser` uses a `string` as the primary key populated with a Guid, but you could also change it to use an `int` by having your EF IdentityUser Data Model inherit from `IdentityUser` instead: ```csharp public class AppUser : IdentityUser { //... } ``` You'll also need to specify the Key Type when registering the `AuthFeature` plugin: ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { appHost.Plugins.Add(new AuthFeature(IdentityAuth.For( options => { options.EnableCredentialsAuth = true; options.SessionFactory = () => new CustomUserSession(); }) )); }); } ``` Which the new .NET 8 BlazorDiffusion App does in [Configure.Auth.cs](https://github.com/NetCoreApps/BlazorDiffusionVue/blob/main/BlazorDiffusion/Configure.Auth.cs) to be compatible with its existing ServiceStack `UserAuth` tables which used an `int` primary key. ## Using Identity Auth in ServiceStack Apps One of the primary benefits of adopting Identity Auth is the wealth of documentation and resources available for it, which also applies to how you would use Identity Auth to secure your own Apps. If you're new to Identity Auth we recommend starting with the official introduction from Microsoft: - [Introduction to Identity on ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity) To learn about securing Blazor Apps, go to: - [ASP.NET Core Blazor authentication and authorization](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/) ### Declarative Validation Attributes The recommended way to protect your ServiceStack APIs is to continue to use the [Declarative Validation](https://docs.servicestack.net/declarative-validation) attributes which are decoupled from any implementation so be safely annotated on Request DTOs without adding any implementation dependencies, where they're also accessible to Clients and UIs using the Request DTOs to invoke your APIs. The available Typed Authorization Attributes include: | Attribute | Description | |-----------------------------|--------------------------------------------------------| | `[ValidateIsAuthenticated]` | Restrict access toAuthenticated Users only | | `[ValidateIsAdmin]` | Restrict access to Admin Users only | | `[ValidateHasRole]` | Restrict access to only Users assigned with this Role | | `[ValidateHasClaim]` | Restrict access to only Users assigned with this Claim | | `[ValidateHasScope]` | Restrict access to only Users assigned with this Scope | That can be annotated on **Request DTOs** to protect APIs: ```csharp [ValidateIsAuthenticated] [ValidateIsAdmin] [ValidateHasRole(role)] [ValidateHasClaim(type,value)] [ValidateHasScope(scope)] public class Secured {} ``` ## Migrating from ServiceStack Auth Migrating from ServiceStack Auth to Identity Auth should be relatively straight-forward as ServiceStack uses a compatible Identity v2 password hashing format, which should let you migrate your users to Identity Auth without them noticing. :::info TIP Please ensure your App database is backed up before running any migrations ::: #### 1. Rename old AppUser table You'll want to use a different name so it doesn't conflict with the new Identity Auth `AppUser` Data Model. This would only be needed to query the User data to migrate to Identity Auth, you'll be able to remove it after successfully migrating all your Users. You don't need to include all the properties of the `UserAuth` base table, just the ones you want to migrate to Identity Auth, which for Blazor Diffusion was only: ```csharp // Used by OrmLite to fetch User data to migrate from old ServiceStack `AppUser` table [Alias("AppUser")] public class OldAppUser { [AutoIncrement] public int Id { get; set; } public string UserName { get; set; } public string DisplayName { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Handle { get; set; } public string Email { get; set; } public string PasswordHash { get; set; } public string? ProfileUrl { get; set; } public string? Avatar { get; set; } //overrides ProfileUrl public string? LastLoginIp { get; set; } public DateTime? LastLoginDate { get; set; } public string RefIdStr { get; set; } public DateTime? LockedDate { get; set; } public DateTime CreatedDate { get; set; } public DateTime ModifiedDate { get; set; } } ``` #### 2. Create Identity Auth Data Model If you have a lot of existing references to the `AppUser` name you'll want to retain the same name so the existing references wont need to be updated. Essentially your custom EF IdentityUser will want a copy of all the properties you want to migrate other than `Id`, `Email`, and `PasswordHash` that's already defined in the base `IdentityUser` class: ```csharp [Alias("AspNetUsers")] // Tell OrmLite which table this EF Data Model maps to public class AppUser : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public string? DisplayName { get; set; } public string? ProfileUrl { get; set; } [Input(Type = "file"), UploadTo("avatars")] public string? Avatar { get; set; } //overrides ProfileUrl public string? Handle { get; set; } public int? RefId { get; set; } public string RefIdStr { get; set; } = Guid.NewGuid().ToString(); public bool IsArchived { get; set; } public DateTime? ArchivedDate { get; set; } public string? LastLoginIp { get; set; } public DateTime? LastLoginDate { get; set; } public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime ModifiedDate { get; set; } = DateTime.UtcNow; } ``` The `AppUser` Data Model and `int` primary key would also need to be registered in your [Configure.Auth.cs](https://github.com/NetCoreApps/BlazorDiffusionVue/blob/main/BlazorDiffusion/Configure.Auth.cs) configuration class: ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { appHost.Plugins.Add(new AuthFeature(IdentityAuth.For( options => { options.EnableCredentialsAuth = true; options.SessionFactory = () => new CustomUserSession(); }) )); }); } ``` #### 3. Add Authentication Configuration You'll need to configure Entity Framework and add your desired ASP.NET Identity Auth configuration to your App's `Program.cs`. We'd recommend copying from a new Microsoft or [ServiceStack .NET 8 Project](https://docs.servicestack.net/auth/identity-auth) which closely matches the Authentication options you want to enable, e.g. you can start with the recommended Authentication for a new Blazor Project from its [Program.cs](https://github.com/NetCoreTemplates/blazor/blob/main/MyApp/Program.cs): ```csharp services.AddAuthentication(IdentityConstants.ApplicationScheme) .AddIdentityCookies(); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo("App_Data")); // $ dotnet ef migrations add CreateIdentitySchema // $ dotnet ef database update var connectionString = config.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); services.AddDbContext(options => options.UseSqlite(connectionString, b => b.MigrationsAssembly(nameof(MyApp)))); services.AddDatabaseDeveloperPageExceptionFilter(); services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) .AddRoles() .AddEntityFrameworkStores() .AddSignInManager() .AddDefaultTokenProviders(); services.AddSingleton(); services.AddScoped, AdditionalUserClaimsPrincipalFactory>(); ``` Alternatively if you want to add support for external OAuth logins you can copy from the **MVC Tailwind** Authentication configuration in its [Program.cs](https://github.com/NetCoreTemplates/mvc/blob/main/MyApp/Program.cs) which will also require adding the NuGet dependencies of the OAuth providers you want to support which you can get from its [MyApp.csproj](https://github.com/NetCoreTemplates/mvc/blob/main/MyApp/MyApp.csproj) #### 4. Create and Run EF Migrations After your App is properly configured you'll want to create the EF Migrations for your the Identity Auth User tables by installing the [dotnet-ef tool](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) and running: :::sh dotnet ef migrations add CreateIdentitySchema ::: Which should create the EF Migrations in the `/Migrations` folder, you can then run the migrations to create the Identity Auth tables in your App's configured database: :::sh dotnet ef database update ::: #### 5. Implement the Migrate Users Task This could be implemented in a separate Application or Unit Test although we've found the easiest way to migrate existing users is to implement a custom [App Task](https://docs.servicestack.net/app-tasks) as it's able to make use of your App's configured Authentication, EF and OrmLite dependencies that can then be run from the command-line. The implementation should be fairly straight-forward, you'll basically just need to create a new Identity Auth User using the `UserManager` dependency for each of your existing users: ```csharp public class ConfigureDbMigrations : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { AppTasks.Register("migrate.users", _ => { var log = appHost.GetApplicationServices().GetRequiredService>(); log.LogInformation("Running migrate.users..."); var scopeFactory = appHost.GetApplicationServices().GetRequiredService(); using var scope = scopeFactory.CreateScope(); using var dbContext = scope.ServiceProvider.GetRequiredService(); using var db = scope.ServiceProvider.GetRequiredService().Open(); var migrateUsers = db.Select(db.From().OrderBy(x => x.Id)); log.LogInformation("Migrating {Count} Existing ServiceStack Users to Identity Auth Users...", migrateUsers.Count); MigrateExistingUsers(dbContext, scope.ServiceProvider, migrateUsers).Wait(); }); AppTasks.Run(); }); private async Task MigrateExistingUsers(ApplicationDbContext dbContext, IServiceProvider services, List migrateUsers, string tempPassword="p@55wOrd") { var userManager = services.GetRequiredService>(); var now = DateTime.UtcNow; foreach (var user in migrateUsers) { var appUser = new AppUser { Id = user.Id, UserName = user.Email, Email = user.Email, DisplayName = user.DisplayName, FirstName = user.FirstName, LastName = user.LastName, Handle = user.Handle, ProfileUrl = user.ProfileUrl, Avatar = user.Avatar, RefIdStr = user.RefIdStr ?? Guid.NewGuid().ToString(), LockoutEnabled = true, LockoutEnd = user.LockedDate != null ? now.AddYears(10) : now, LastLoginDate = user.LastLoginDate, LastLoginIp = user.LastLoginIp, CreatedDate = user.CreatedDate, ModifiedDate = user.ModifiedDate, // Verify you want existing Users emails to be confirmed EmailConfirmed = true, }; await userManager.CreateAsync(appUser, tempPassword); // Update raw Password Hash using EF if (user.PasswordHash != null) { dbContext.Users .Where(x => x.Id == user.Id) .ExecuteUpdate(setters => setters.SetProperty(x => x.PasswordHash, user.PasswordHash)); } } } } ``` As there's no official API for updating the raw `PasswordHash` you'll need to use EF's `ExecuteUpdate()` API to update it on the `AspNetUsers` table directly. It should be noted that ServiceStack Auth still uses ASP.NET Core's previous Identity v2 format for hashing its passwords, this will be automatically re-hashed using the latest ASP.NET Identity v3 format after users successfully sign in. #### Optimizing the PasswordHash Update Whilst migrating users should be a once-off task, if you have a lot of users you may want to optimize the `PasswordHash` update from a **N+1** query per user to a single query that updates all users in a single command. You'll need to use the **UPDATE FROM** syntax that's supported by your RDBMS's, here's an example of how to do it in SQLite: ```sql UPDATE AspNetUsers SET PasswordHash = u.PasswordHash FROM (SELECT Email, PasswordHash FROM AppUser WHERE PasswordHash is NOT NULL) AS u WHERE u.Email = AspNetUsers.Email; ``` #### Migrating Roles Migrating Roles will depend how their stored in your App, you'll first need to ensure each role is created in the `AspNetRoles` table with: ```csharp string[] allRoles = [...]; // All Roles in your App var roleManager = services.GetRequiredService>(); foreach (var roleName in allRoles) { var roleExist = await roleManager.RoleExistsAsync(roleName); if (!roleExist) { await roleManager.CreateAsync(new IdentityRole(roleName)); } } ``` You can then assign Roles to Users using the `UserManager`, e.g: ```csharp string[] roles = [...]; // Roles to assign to User var newUser = await userManager.FindByEmailAsync(user.Email!); await userManager.AddToRolesAsync(user, roles); ``` #### 6. Run the migrate.users Task With everything in place, all that's left is to run the `migrate.users` App Task from the command-line: :::sh dotnet run --AppTasks=migrate.users ::: #### 7. Verify Users can Sign In After successfully migrating all your users you should check the new `IdentityUser` table to verify all the User data you want has been migrated as well as verifying they can sign in with their existing credentials. #### Create a new ASP.NET Identity Auth Project to copy from The easiest way to include the Identity Auth UI Pages to your App is to copy your Application into a new .NET 8 Project that already includes them, you can create a new Blazor App with: :::sh x new blazor ProjectName ::: Or create a new Razor Pages Tailwind or Bootstrap App: :::sh x new razor ProjectName ::: :::sh x new razor-bootstrap ProjectName ::: Or new MVC Tailwind or Bootstrap App with: :::sh x new mvc ProjectName ::: :::sh x new mvc-bootstrap ProjectName ::: Alternatively you can manually copy the pages from the project template repositories, for Blazor most of the Identity Auth UI Pages are in the [Components/Identity](https://github.com/NetCoreTemplates/blazor/tree/main/MyApp/Components/Identity) and [Pages/Account](https://github.com/NetCoreTemplates/blazor/tree/main/MyApp/Components/Pages/Account) folders. For MVC, most of the Identity UI are in the [Account](https://github.com/NetCoreTemplates/mvc/blob/main/MyApp/Controllers/AccountController.cs) and [Manage](https://github.com/NetCoreTemplates/mvc/blob/main/MyApp/Controllers/ManageController.cs) controllers as well as their [Views/Account](https://github.com/NetCoreTemplates/mvc/tree/main/MyApp/Views/Account) and [Views/Manage](https://github.com/NetCoreTemplates/mvc/tree/main/MyApp/Views/Manage) folders. ### SMTP IEmailSender The .NET 8 Templates also include a nice solution for sending Identity Auth emails through the `IEmailSender` interface which drops the Email Request in the registered Background MQ in [Configure.Mq.cs](https://github.com/NetCoreTemplates/blazor/blob/main/MyApp/Configure.Mq.cs) which uses it to invoke the `SendEmail` API in [EmailServices](https://github.com/NetCoreTemplates/blazor/blob/main/MyApp.ServiceInterface/EmailServices.cs) in a managed background worker: ```csharp public class EmailSender(IMessageService messageService) : IEmailSender { public Task SendEmailAsync(string email, string subject, string htmlMessage) { using var mqClient = messageService.CreateMessageProducer(); mqClient.Publish(new SendEmail { To = email, Subject = subject, BodyHtml = htmlMessage, }); return Task.CompletedTask; } } ``` To enable it you'll need to register your preferred SMTP Server in your App's `appsettings.json`: ```json { "SmtpConfig": { "Username": "username", "Password": "password", "Host": "smtp.mailtrap.io", "Port": 587, "FromEmail": "mail@example.org" } } ``` Then uncomment the `EmailSender` registration in your `Program.cs` ```csharp services.AddSingleton(); ``` ### Send any App Email The nice part about this solution is that it's not limited to just sending Identity Auth emails, you can also use it to send any App Email, either by publishing a message to the registered MQ with `PublishMessage` or by using the [Service Gateway](https://docs.servicestack.net/service-gateway) to invoke the API directly, e.g: ```csharp public class MyServices : Service { public object Any(MyRequest request) { // Send Email in managed Background MQ Worker PublishMessage(new SendEmail { To = email, Subject = subject, BodyHtml = body, }); // Block until Email is sent to SMTP Server Gateway.Send(new SendEmail { To = email, Subject = subject, BodyHtml = body, }); } } ``` # .NET 8's Best Blazor is not Blazor as we know it Source: https://servicestack.net/posts/net8-best-blazor The best way to find out what's new in .NET 8 Blazor is to watch the excellent [Full stack web UI with Blazor in .NET 8](https://www.youtube.com/watch?v=QD2-DwuOfKM) presentation by Daniel Roth and Steve Sanderson, which covers how Blazor has become a Full Stack UI Web Technology for developing any kind of .NET Web App.
## Your first .NET 8 Blazor App You don't get to appreciate what this means until you create your first .NET 8 Blazor App where you'll be pleasantly surprised that Blazor Apps render fast, clean HTML without needing to load large Web Assembly assets needed for Blazor WebAssembly Apps or starting a stateful Web Socket connection required for Blazor Server Interactive Apps. This is because the **default rendering mode** for Blazor uses neither of these technologies, instead it returns to traditional Web App development where Blazor Pages now return clean, glorious HTML - courtesy of Blazor's [Static render mode](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes). [![](/img/posts/net8-best-blazor/blazor-ssr.png)](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes) ## Choose your compromise Previously we were forced to choose upfront whether we wanted to build a Blazor Web Assembly App or a Blazor Server App and the compromises that came with them, which for public Internet Web Apps wasn't even a choice as Blazor Server Apps perform poorly over high latency Internet connections. This meant choosing Blazor Web Assembly Apps which required downloading a large Web Assembly runtime with users experiencing a long delay before the App was functional. To minimize this impact our Blazor WebAssembly Tailwind template included [built-in prerendering](https://github.com/LegacyTemplates/blazor-tailwind/blob/main/MyApp.Client/wwwroot/content/prerender.md) where as part of deployment would generate **static .html pages** that were deployed with the Blazor Web Assembly front-end UI that can be hosted on CDN edge networks to further improve load times. Whilst this meant the App's UI would be rendered immediately, it still wouldn't be functional until the Web Assembly runtime was downloaded and initialized, which would flicker as the static UI was replaced with Blazor's WASM rendered UI, then later Authenticated users would experience further delay and UI jank whilst the App signs in the Authenticated User. Whilst prerendering is an improvement over Blazor WASM's default blank loading screen, it's still not ideal for public facing Web Apps. ## .NET 8 Blazor is a Game Changer The situation has greatly improved in .NET 8 where your entire App no longer needs to be bound to a single Interactivity mode. Even better, Blazor's default **static rendering** mode results in the best UX where the Website Layout and important landing pages can be rendered instantly. ### Interactive only when you need it Only pages that need Blazor's interactivity features can opt-in to whichever Blazor interactive rendering mode makes the most sense, either on a page-by-page or component basis, or by choosing `RenderMode.InteractiveAuto` which uses **InteractiveWebAssembly** if the WASM runtime is loaded or **InteractiveServer** if it isn't. ### Enhanced Navigation FTW Ultimately I expect Blazor's new **Enhanced Navigation** is likely the feature that will deliver the biggest UX improvement users will experience given it's enabled by default and gives traditional statically rendered Web Apps instant SPA-like navigation responsiveness where new pages are swapped in without needing to perform expensive full page reloads. It's beauty lies in being able to do this as a mostly transparent detail without the traditional SPA complexity of needing to manage complex state or client-side routing. It's a smart implementation that's able to perform fine-grained DOM updates to only parts of pages that have changed, providing the ultimate UX of preserving page state, like populated form fields and scroll position, to deliver a fast and responsive UX that previously wasn't attainable from the simplicity of a Server Rendered App. Its implementation does pose some challenges in implementing certain features, but we'll cover some approaches below we've used to overcome them below. ### Full Stack Web UI Blazor's static rendering with enhanced navigation and its opt-in flexibility makes .NET 8 Blazor a game changer, expanding it from a very niche set of use-cases that weren't too adversely affected by its Interactivity mode downsides, to becoming a viable solution for developing any kind of .NET Web App, especially as it can also be utilized within existing ASP.NET MVC and Razor Pages Apps. ### Benefits over MVC and Razor Pages In addition, Blazor's superior component model allows building better encapsulated, more reusable and easier-to-use UI components which has enabled Blazor's rich 3rd Party library ecosystem to flourish, that we ourselves utilize to develop the high productivity Tailwind Components in the [ServiceStack.Blazor](https://blazor-gallery.servicestack.net) component library. So far there's only upsides for .NET Web App development, the compromises only kick in when you need Blazor's interactivity features, luckily these can now be scoped to just the Pages and Components that need them. But how often do we need them? ### When do you need Blazor's Interactivity features? It ultimately depends on what App your building, but a lot of Websites can happily display dynamic content, navigate quickly with enhanced navigation, fill out and submit forms - all in Blazor's default static rendering mode. Not even advanced features like **Streaming Rendering** used in Blazor Template's [Weather.razor](https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor) page require interactivity, as its progressive rendered UI updates are achieved in a single request without interactivity. In fact the only time `@rendermode InteractiveServer` is needed in the default Blazor template is in the [Counter.razor](https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Counter.razor#L3) page whose C# Event Handling require it. Ultimately some form of Interactivity is going to be required in order to add behavior or client-side functionality that runs after pages have been rendered, but you still have some options left before being forced to opt into an Interactive Blazor solution. ### Interactive Feature Options We can see some of these options utilized in the Blazor Template [NavMenu.razor](https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor) component which uses JavaScript `onclick` event handlers to add client-side behavior to simulate mouse clicks to toggle UI elements: ```html