
ServiceStack's Add ServiceStack Reference feature allows clients to generate Native Types from a simple npx get-dtos command-line script - providing a simple way to give clients typed access to your ServiceStack Services.
Dart - One typed client for every Flutter target​
The servicestack package generates DTOs and a JsonServiceClient that works unchanged across every platform Flutter builds for - iOS, Android, web, desktop and server-side Dart:
var client = JsonServiceClient(baseUrl);
var response = await client.get(Hello(name: 'World'));
print(response.result);
That's the real leverage for a team that chose Flutter to build one App for several platforms: one API contract, one client and one set of DTOs behind all of them, with native dart:io and Browser implementations sharing the same IServiceClient interface. Structured errors, authentication, typed AutoQuery, batch and one-way requests and file uploads.
Dart Android Example using Android Studio​
Dart ServiceStack Reference​
Dart ServiceStack Reference supports all Dart 3 platforms, including Flutter and AngularDart or Dart Web Apps - in the same optimal development workflow pioneered in Add ServiceStack Reference where it doesn't requiring any additional tooling, transformers or build steps.
Due to the lack of reflection and Mirror support, consuming JSON APIs can be quite cumbersome in Flutter. But we've been able to achieve the same productive development experience available in all supported languages where you can use the generated Dart DTOs from any remote v5.1+ ServiceStack endpoint with ServiceStack's Smart generic JsonServiceClient available in the servicestack Dart package, to enable an end-to-end Typed API for calling Services by sending and receiving native DTOs.
Install​
You can use the same cross-platform get-dtos script to easily Add and Update ServiceStack References for all supported languages, which can be run with Node.js without needing to install anything:
npx get-dtos
Running it without any arguments displays the available options for adding and updating ServiceStack References.
Example Usage​
You can then execute npx get-dtos dart with the URL of the remote ServiceStack Instance you want to generated DTOs for, e.g:
npx get-dtos dart https://techstacks.io
This will generate Dart DTOs for the entire TechStacks API:
Saved to: dtos.dart
INFO
If no name is specified in the 2nd argument, it uses dtos if it doesn't exist, otherwise falls back to infer it from the URL
To make API calls we need to use the JsonServiceClient, installed by adding the servicestack package to our Dart projects pubspec.yaml:
dependencies:
servicestack: ^3.0.3
Saving pubspec.yaml in VS Code with the Dart Code Extension automatically calls pub get or flutter packages get (in Flutter projects) to add any new dependencies to your project.
We now have everything we need to be able to make typed API requests to any of TechStacks APIs with a shared JsonServiceClient instance populated with the base URL of the remote endpoint, e.g:
import 'package:servicestack/client.dart';
import 'dtos.dart';
var client = JsonServiceClient("https://techstacks.io");
main() async {
var response = await client.get(GetTechnology(slug: "flutter"));
print("${response.technology.name}: ${response.technology.vendorUrl}");
}
Like C#, Dart has Generics and Type Inference so the response returned is the typed HelloResponse DTO giving us rich intelli-sense and compiler type safety.
Platform neutral usage​
Both dart:io JsonServiceClient and Browser JsonWebClient implement the same shared IServiceClient interface which support a platform-neutral source-compatible API using the
ClientFactory APIs, e.g:
import 'package:servicestack/web_client.dart' if (dart.library.io) 'package:servicestack/client.dart';
main() async {
var client = ClientFactory.create('https://techstacks.io');
var response = await client.get(GetTechnology(slug: "flutter"));
print("${response.technology.name}: ${response.technology.vendorUrl}");
}
For advanced configuration you can use the createWith(ClientOptions) API, e.g you can use dev.servicestack.com which resolves to 10.0.2.2
which in Android you can use to access 127.0.0.1 of the host OS allowing you to access your local development server.
In this example we'll create either a typed Service Client to our local development server in Debug during development (ignoring its self-signed certificate) and our production server in Release mode:
import 'package:servicestack/web_client.dart' if (dart.library.io) 'package:servicestack/client.dart';
import 'package:flutter/foundation.dart';
main() async {
var client = kDebugMode
? ClientFactory.createWith(ClientOptions(baseUrl:'https://dev.servicestack.com:5001', ignoreCert:true))
: ClientFactory.create('https://techstacks.io');
}
Tip
If you add a 127.0.0.1 dev.servicestack.com mapping in your OS's hosts file you'll also be able to use dev.servicestack.com to access your local dev server in your Host OS
Shared Initialization Configuration​
For more advanced configuration you can use the ClientConfig.initClient client factory filter to customize all service client instances
as done in this example to configure all client instances with any previously saved JWT Bearer & Refresh Tokens to enable authenticated access:
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:servicestack/web_client.dart' if (dart.library.io) 'package:servicestack/client.dart';
SharedPreferences prefs;
IServiceClient client;
AuthenticateResponse auth;
Future<void> main() async {
runApp(MyApp());
ClientConfig.initClient = (client) {
if (auth != null) {
client.bearerToken = auth.bearerToken;
client.refreshToken = auth.refreshToken;
}
};
prefs = await SharedPreferences.getInstance();
var json = prefs.getString('auth');
auth = json != null
? AuthenticateResponse.fromJson(jsonDecode(json))
: null;
client = ClientFactory.createWith(ClientOptions(
baseUrl:'https://dev.servicestack.com:5001', ignoreCert:kDebugMode));
}
AngularDart and Dart Web Usage​
For AngularDart or Dart Web Apps import web_client.dart and use the JsonWebClient:
import 'package:servicestack/web_client.dart';
import 'dtos.dart'; // Add ServiceStack Reference DTOs
//...
var client = JsonWebClient("https://techstacks.io");
var response = await client.get(GetTechnology(slug: "flutter"));
See the HelloAngularDart project for a working example.
Platform agnostic Usage​
For creating libraries that can be consumed in any Dart platform reference servicestack.dart and use the IServiceClient interface which
is implemented by both JsonServiceClient and JsonWebClient:
import 'package:servicestack/servicestack.dart';
import 'dtos.dart'; // Add ServiceStack Reference DTOs
//...
fetchTechnologies(IServiceClient client) async {
var response = await client.get(GetTechnology(slug: "flutter"));
}
Rich Generated Models​
Thanks to the direct C# to Dart model code generation we're able to create the ideal idiomatic message-based APIs utilizing rich typed models with broad support for many of the C#/.NET features used when defining DTOs inc. Generics, Inheritance, multiple Interfaces, Enums (inc. int and Flag Enums), Tuples, metadata Attributes emitted in comments (emitting additional documentation in the generated models) whilst also taking care of mapping built-in C# Types like DateTime, TimeSpan, byte[] and Stream into their equivalent native Dart DateTime, Duration and Uint8List types, C# generic collections are also converted into their equivalent Dart generic collection Type.
Dart Type Mappings​
C# DTOs are mapped to their closest equivalent Dart Type:
| C# Type | Dart Type |
|---|---|
string |
String |
bool |
bool |
char |
String |
byte short int long |
int |
ushort uint ulong |
int |
float double decimal |
double |
Guid |
String |
DateTime DateTimeOffset |
DateTime |
TimeSpan |
Duration |
byte[] Stream |
Uint8List |
T[] List<T> |
List<T> |
Dictionary<K,V> |
Map<K,V> |
KeyValuePair<K,V> |
KeyValuePair<K,V> |
Tuple<T1,T2> |
Tuple2<T1,T2> |
enum |
enum |
[Flags] enum |
class with int values |
Nullable<T> |
T? |
Where all C# enum types are supported, including [Flags] enums (generated as classes of int constants), enums with explicit values and enums using custom Enum Styles, which are all serializable in Enum properties, List<Enum> and Map<Enum,Enum> collections.
JsonCodec compatible​
The generated DTOs follow Dart's JsonCodec pattern allowing them to be individually serializable with Dart's universal JSON encode/decode APIs, e.g:
//Serialization
var dto = MyDto(name:"foo");
String jsonString = json.encode(dto);
//Deserialization
Map<String,dynamic> jsonObj = json.decode(jsonString);
var fromJson = MyDto.fromJson(jsonObj);
Default Constructor​
All DTOs also include a default constructor containing all properties as optional arguments providing a wrist-friendly syntax for creating and populating DTOs in a single constructor expression, e.g:
var request = MyDto(name:"foo");
Only the properties of each DTO are included in its default constructor so you'll need to use property accessors to initialize any fields in base classes, but thanks to Dart's support for method cascades you can still populate an entire DTO with a single expression, e.g:
var request = FindTechnologies(name:"Flutter")
..fields = "id,slug,vendorName,productUrl"
..orderBy = "created,-viewCount"
..take = 1;
IConvertible​
All DTOs implement the IConvertible interface below where each instance can be converted to and from a Map of values, giving each model dynamism that's otherwise not possible in Flutter:
abstract class IConvertible
{
TypeContext context;
fromMap(Map<String, dynamic> map);
Map<String, dynamic> toJson();
}
The conversion logic that handles the behind-the-scenes conversion into and out of Dart Types is maintained in the extensible JsonConverters class which lets you replace built-in converters with your own implementation or register new Converters when you want to take over handling of specific types.
JsonServiceClient​
The JsonServiceClient is a smart full-featured Service Client implementation with a number of high-level features that make consuming ServiceStack APIs a seamless experience, with built-in support for:
- HTTP Basic Auth (inc. API Key support)
- Cookies, Sessions and Credential Auth
- JWT, including using RefreshTokens to auto-fetch new JWT Bearer Tokens
- Structured Error Handling
- Auto Batched Requests
- Global and per-instance Request, Response, URL and Exception Filters
onAuthenticationRequiredhook to handle re-authentication and transparent replay of failed 401 requestsapi()andapiVoid()APIs returning a typedApiResult<T>encapsulating an API Response or Error- Typed and untyped APIs for calling any HTTP Verb, custom URLs and raw
StringandUint8Listresponses multipart/form-dataFile Uploads with a typed Request DTO- Configurable Base Path for calling either the
/apior legacy/json/replypre-defined routes
Behind the scenes JsonServiceClient leverages the optimal HttpClient in dart:io to perform HTTP Requests in Flutter and Dart VM Apps.
JsonWebClient​
The JsonWebClient performs HTTP Requests using package:http's BrowserClient which uses the browsers built-in XMLHttpRequest object. Despite their implementation differences JsonWebClient also supports the same feature-set as the Dart VM's JsonServiceClient above.
AngularDart or Dart Web Apps can use JsonWebClient by importing web_client.dart, e.g:
import 'package:servicestack/web_client.dart';
var client = JsonWebClient("https://techstacks.io");
IServiceClient API​
Both JSON Service Client variants implement the same flexible IServiceClient API below, use the same DTOs and implementation and throws the same structured WebServiceException which results in Typed API Requests being source-compatible between Dart Web Apps, Dart VM Server and AOT compiled Flutter Web Apps:
abstract class IServiceClient {
String? baseUrl;
String? replyBaseUrl;
String? oneWayBaseUrl;
String? bearerToken;
String? refreshToken;
String? userName;
String? password;
AsyncCallbackFunction? onAuthenticationRequired;
String? getTokenCookie();
String? getRefreshTokenCookie();
void clearCookies();
Future<ApiResult<T>> api<T>(IReturn<T> request, {Map<String, dynamic>? args, String? method});
Future<ApiResult<EmptyResponse>> apiVoid(IReturnVoid request, {Map<String, dynamic>? args, String? method});
Future<T> get<T>(IReturn<T> request, {Map<String, dynamic>? args});
Future<Map<String, dynamic>> getUrl(String path, {Map<String, dynamic>? args});
Future<T> getAs<T>(String path, {Map<String, dynamic>? args, T? responseAs});
Future<T> post<T>(IReturn<T> request, {dynamic body, Map<String, dynamic>? args});
Future<Map<String, dynamic>> postToUrl(String path, dynamic body, {Map<String, dynamic>? args});
Future<T> postAs<T>(String path, dynamic body, {Map<String, dynamic>? args, T? responseAs});
Future<T> delete<T>(IReturn<T> request, {Map<String, dynamic>? args});
Future<Map<String, dynamic>> deleteUrl(String path, {Map<String, dynamic>? args});
Future<T> deleteAs<T>(String path, {Map<String, dynamic>? args, T? responseAs});
Future<T> put<T>(IReturn<T> request, {dynamic body, Map<String, dynamic>? args});
Future<Map<String, dynamic>> putToUrl(String path, dynamic body, {Map<String, dynamic>? args});
Future<T> putAs<T>(String path, dynamic body, {Map<String, dynamic>? args, T? responseAs});
Future<T> patch<T>(IReturn<T> request, {dynamic body, Map<String, dynamic>? args});
Future<Map<String, dynamic>> patchToUrl(String path, dynamic body, {Map<String, dynamic> args});
Future<T> patchAs<T>(String path, dynamic body, {Map<String, dynamic>? args, T? responseAs});
Future<List<T>> sendAll<T>(Iterable<IReturn<T>> requests);
Future<void> sendAllOneWay<T>(Iterable<IReturn<T>> requests);
Future<T> send<T>(IReturn<T> request, {String? method, Map<String, dynamic>? args, T? responseAs});
void close({bool force = false});
}
Concrete-specific functionality​
In addition to implementing the IServiceClient above, each Service Client includes additional concrete specific functionality allowing for finer-grained access to their underlying HTTP Clients, e.g. as the Request/Response filters have different Type signatures (dart:io's HttpClientResponse vs Browser's Response) they can't be declared in the shared IServiceClient interface, but thanks to Dart's type inference many of the extended concrete APIs are still source-compatible, e.g:
var vmClient = JsonServiceClient(baseUrl)
..responseFilter = (res) => print(res.headers["X-Args"]);
var webClient = JsonWebClient(baseUrl)
..responseFilter = (res) => print(res.headers["X-Args"]);
Making Typed API Requests​
Each IServiceClient supports sending a Request DTO with any HTTP Verb where the typed Response DTO is inferred from the IReturn<T> marker on the Request DTO:
var client = JsonServiceClient("https://test.servicestack.net");
HelloResponse response = await client.get(Hello(name: "World"));
response = await client.post(Hello(name: "World"));
response = await client.put(Hello(name: "World"));
response = await client.patch(Hello(name: "World"));
response = await client.delete(Hello(name: "World"));
Where all URLs, QueryStrings and JSON Request Bodies are handled for you, e.g. GET and DELETE requests send all populated properties on the QueryString whilst POST, PUT and PATCH requests send them in a JSON Request Body.
Unicode is supported throughout, e.g. sending Hello(name: "üöäß") returns "Hello, üöäß!".
Resolving the HTTP Method​
Instead of specifying the HTTP Method yourself you can use send() which uses the HTTP Verb interface marker on the Request DTO to resolve which HTTP Method to send it with, which you can inspect with resolveHttpMethod():
resolveHttpMethod(SendGet()); //= GET
resolveHttpMethod(SendPost()); //= POST
resolveHttpMethod(SendPut()); //= PUT
var response = await client.send(SendGet(id: 1)); // sent as GET
Request DTOs without a Verb interface marker are sent as a POST request.
The api() and apiVoid() APIs​
Whilst the get/post/put/patch/delete APIs throw a WebServiceException for error responses, the api() and apiVoid() APIs return a typed ApiResult<T> which encapsulates either a successful Response or a structured API Error - ideal for Flutter Apps where displaying validation errors next to their Form fields is part of normal control flow:
var api = await client.api(Hello(name: "World"));
if (api.succeeded) {
print(api.response!.result); // Hello, World!
} else {
print(api.errorMessage);
}
You can override which HTTP Method it's sent with using the optional method argument, e.g. client.api(request, method:"PUT") whilst apiVoid() should be used for IReturnVoid APIs which returns an ApiResult<EmptyResponse>.
The ApiResult<T> includes a number of APIs for inspecting an API Error Response:
| Property / Method | Description |
|---|---|
response |
The typed Response DTO, null if the API failed |
error |
The structured ResponseStatus error, if it failed |
succeeded |
Whether the API returned a successful Response |
failed |
Whether the API returned an error |
completed |
Whether the API returned either a Response or an Error |
errorCode |
The ErrorCode of the error, e.g. NotFound |
errorMessage |
The error message |
errors |
All field validation errors |
fieldError(name) |
The ResponseError for a field, if it has one |
fieldErrorMessage(name) |
The validation error message for a field, if it has one |
hasFieldError(name) |
Whether a field has a validation error |
errorSummary |
The error message when it's not a field validation error |
showSummary([exceptFields]) |
Whether to display a summary error message for the specified fields |
addFieldError(name,message) |
Add a client-side validation error to the result |
Which lets you handle each class of error appropriately:
var api = await client.api(ThrowValidation(email: "invalidemail"));
if (api.failed) {
// Display validation errors next to their fields
print(api.fieldErrorMessage("Email")); //= 'Email' is not a valid email address.
print(api.hasFieldError("Age")); //= true
for (var error in api.errors) {
print("${error.fieldName}: ${error.errorCode} - ${error.message}");
}
// Only show the summary message when the field errors aren't displayed
if (api.showSummary(["Email"])) {
print(api.errorSummary);
}
}
Whilst errorCode lets you handle different classes of errors, e.g. APIs that require Authentication return a 401 error code:
var api = await client.api(RequiresAdmin());
if (api.errorCode == "401") {
// Re-authenticate...
}
Error Handling​
All non api() requests throw a structured WebServiceException containing the same information available in ServiceStack's structured Error Responses:
try {
await client.post(ThrowType(type: "NotFound", message: "not here"));
} on WebServiceException catch (e) {
print(e.statusCode); //= 404
print(e.statusDescription); //= Not Found
print(e.message); //= not here
var status = e.responseStatus!;
print(status.errorCode); //= NotFound
print(status.message); //= not here
print(status.stackTrace);
}
Where APIs with failed Validation Rules return all field validation errors in responseStatus.errors:
try {
await client.post(ThrowValidation(email: "invalidemail"));
} on WebServiceException catch (e) {
var errors = e.responseStatus!.errors!;
print(errors[0].errorCode); //= InclusiveBetween
print(errors[0].fieldName); //= Age
print(errors[0].message); //= 'Age' must be between 1 and 120. You entered 0.
print(errors[2].errorCode); //= Email
print(errors[2].fieldName); //= Email
print(errors[2].message); //= 'Email' is not a valid email address.
}
Connection and transport errors are also surfaced as a WebServiceException (with a 500 status code) so failed requests can be handled the same way, e.g:
var client = JsonServiceClient("http://unknown-zzz.net")
..connectionTimeout = Duration(seconds: 1);
var api = await client.api(Hello(name: "World"));
print(api.error!.errorCode); //= 500
print(api.error!.message); //= SocketException: ...
Should you need to inspect all errors in a single place you can register a client exceptionFilter:
var client = JsonServiceClient(baseUrl)
..exceptionFilter = (res, e) {
if (e is WebServiceException) {
print("ERROR: ${json.encode(e.responseStatus)}");
}
};
Or handle every error in your App with the JsonServiceClient.globalExceptionFilter.
Sending Runtime Arguments​
Every IServiceClient API supports an optional args parameter for sending additional arguments that aren't defined on the typed Request DTO which are sent on the QueryString, useful for calling AutoQuery APIs with any of its supported Implicit Conventions:
var response = await client.get(FindTechnologies()..take = 3,
args: {"VendorName": "Amazon"});
It can also be used to send JS Config options to change how the Response is serialized, e.g:
var api = await client.api(ThrowValidation(),
args: {"jsconfig": "EmitCamelCaseNames:true"});
Calling APIs with Custom URLs​
APIs can also be called with any relative or absolute URL where their JSON Response is returned in a Map<String,dynamic> that can be converted into a typed DTO with its fromJson() constructor:
// Any of the different ways to call the same API:
var jsonObj = await client.getUrl("/hello/World"); // Path Info
var jsonObj = await client.getUrl("https://test.servicestack.net/hello/World"); // Absolute URL
var jsonObj = await client.getUrl("/hello", args: {"name": "World"}); // QueryString
var response = HelloResponse.fromJson(jsonObj);
Alternatively use the *As APIs with a responseAs instance to have the Response deserialized into a typed DTO, String or Uint8List:
HelloResponse dto = await client.getAs("/hello",
args: {"name": "World"}, responseAs: HelloResponse());
String jsonStr = await client.getAs("/hello",
args: {"name": "World"}, responseAs: TypeAs.string);
Uint8List bytes = await client.getAs("/hello",
args: {"name": "World"}, responseAs: TypeAs.bytes);
The same APIs are available for each HTTP Verb, e.g. postToUrl, postAs, putToUrl, putAs, patchToUrl, patchAs, deleteUrl and deleteAs, which can also send custom JSON or plain text Request Bodies:
// Send a Map as the JSON Request Body
var jsonObj = await client.postToUrl("/sendjson", {"foo": "bar"},
args: toMap(SendJson(id: 1, name: "name")));
// Send a raw JSON String Request Body with a typed Request DTO
var jsonStr = await client.post(SendJson(id: 1, name: "name"),
body: json.encode({"foo": "bar"}));
// Send a raw text Request Body
var str = await client.post(
SendText(id: 1, name: "name", contentType: "text/plain"), body: "foo");
Generic Response Types can be deserialized by initializing them with the context of your generated DTOs:
var response = await client.getAs("/technology/search",
args: {"Take": 3, "VendorName": "Amazon"},
responseAs: QueryResponse<Technology>()..context = FindTechnologies().context);
Raw Data Responses​
Services that return raw data are also seamlessly supported where APIs implementing IReturn<String> return the raw HTTP Body in a String whilst IReturn<byte[]> and IReturn<Stream> APIs return a Uint8List:
String str = await client.get(HelloString(name: "World"));
Uint8List bytes = await client.get(HelloImage(name: "Flutter",
fontFamily: "Roboto", background: "#0091EA", width: 500, height: 170));
Which in Flutter can be rendered directly with the Image widget, e.g. Image.memory(bytes).
Batched Requests​
The sendAll and sendAllOneWay APIs let you use ServiceStack's Auto Batched Requests feature to send multiple Request DTOs of the same Type in a single Request (sent to /api/Hello[]), that returns all their Responses in a single Response:
var requests = ["foo", "bar", "baz"].map((name) => Hello(name: name));
List<HelloResponse> responses = await client.sendAll(requests);
//= [Hello, foo!, Hello, bar!, Hello, baz!]
Use sendAllOneWay for fire-and-forget batched requests that ignore their Responses, which also supports IReturnVoid Request DTOs:
await client.sendAllOneWay([1, 2, 3].map((id) => HelloReturnVoid(id: id)));
Authentication​
The Service Clients support all of ServiceStack's Authentication options.
HTTP Basic Auth​
Use setCredentials() to send HTTP Basic Auth credentials on each request:
client.setCredentials("test", "test");
var response = await client.get(TestAuth());
Credentials Auth​
Alternatively you can authenticate using the Credentials Auth Provider by sending an Authenticate Request DTO, where any Session Cookies or Tokens returned are automatically retained on the client instance and re-sent on subsequent requests:
var authResponse = await client.post(Authenticate(
provider: "credentials", userName: "test", password: "test"));
print(authResponse.userId); //= 1
print(authResponse.sessionId);
print(authResponse.bearerToken);
print(authResponse.refreshToken);
// Client is now authenticated
var response = await client.get(TestAuth());
Sign out by sending a logout Authenticate request, e.g. await client.post(Authenticate(provider:"logout")).
JWT and API Keys​
Populate bearerToken to authenticate with a JWT Token or an API Key:
client.bearerToken = jwt; // JWT Bearer Token
client.bearerToken = "ak-3d3...4f6"; // API Key
Refresh Tokens​
Instead of a JWT you can populate the client with just a long-lived Refresh Token where the client will transparently fetch a new JWT Bearer Token and replay any requests that returned 401 Unauthorized:
var client = JsonServiceClient(baseUrl)
..refreshToken = authResponse.refreshToken;
var response = await client.get(TestAuth()); // Auto fetches JWT + retries
The same transparent behavior is also applied when the Server returns Token Cookies where an expired ss-tok Access Token Cookie is automatically refreshed using its ss-reftok Refresh Token Cookie which you can access with:
var accessToken = client.getTokenCookie(); // ss-tok
var refreshToken = client.getRefreshTokenCookie(); // ss-reftok
Should the Refresh Token itself be invalid or expired, a WebServiceException with a RefreshTokenException type is thrown so your App can prompt the User to sign in again:
try {
await client.get(TestAuth());
} on WebServiceException catch (e) {
if (e.type == WebServiceExceptionType.RefreshTokenException) {
// Refresh Token invalid or expired, sign in again...
}
}
Transparently handle 401 Unauthorized Responses​
The onAuthenticationRequired async callback lets you handle re-authenticating your client where the failed request is transparently replayed with the new authenticated client, e.g:
client.onAuthenticationRequired = () async {
client.userName = "test";
client.password = "test";
};
// Fails with 401, re-authenticates then transparently retries the request
var response = await client.get(TestAuth());
Use client.clearCookies() to clear any Session and Token Cookies retained on the client.
Request, Response and URL Filters​
Each client supports both instance and global filters for inspecting and modifying HTTP Requests and Responses, e.g. to add a custom HTTP Header on all requests or to inspect the HTTP Headers returned:
var client = JsonServiceClient(baseUrl)
..requestFilter = (req) => req.headers.add("X-Custom", "Value")
..responseFilter = (res) => print(res.headers["X-Args"]);
JsonServiceClient.globalRequestFilter = (req) => print(req.uri);
JsonServiceClient.globalResponseFilter = (res) => print(res.statusCode);
Instance filters are invoked before their global counterparts, i.e. requestFilter, globalRequestFilter, responseFilter, globalResponseFilter.
The urlFilter can be used to inspect the URL each API is sent to which is useful for debugging and verifying which URLs are being called:
client.urlFilter = (url) => print(url);
INFO
As the Request/Response filters have different Type signatures in the Dart VM (HttpClientRequest/HttpClientResponse) and Web (Request/Response) clients they're not declared on the shared IServiceClient interface, but thanks to Dart's type inference most filter source code remains source-compatible between both clients
Client Configuration​
JSON /api Route​
JsonServiceClient and JsonWebClient are configured to call the modern /api pre-defined route by default, i.e:
var client = JsonServiceClient("https://test.servicestack.net");
client.replyBaseUrl; //= https://test.servicestack.net/api/
client.oneWayBaseUrl; //= https://test.servicestack.net/api/
To call older ServiceStack Servers you can use the legacy() constructors (or set basePath = null) to use the pre-existing /json/reply and /json/oneway routes:
var client = JsonServiceClient.legacy("https://test.servicestack.net");
// or with the platform-neutral factory:
// var client = ClientFactory.legacy("https://test.servicestack.net");
client.replyBaseUrl; //= https://test.servicestack.net/json/reply/
client.oneWayBaseUrl; //= https://test.servicestack.net/json/oneway/
Or change it at anytime with the basePath setter, e.g. client.basePath = "api".
Configuring Clients​
Both concrete clients support additional configuration for customizing behavior of their underlying HTTP Clients:
var client = JsonServiceClient(baseUrl)
..headers = {HttpHeaders.acceptHeader: "application/json"} // Default Headers
..connectionTimeout = Duration(seconds: 10)
..maxRetries = 5; // Max retries for failed connections
client.close(); // Close the client and its open connections
Ignoring Self-Signed Certificates​
ClientOptions lets you create clients that ignore the self-signed certificates used in local development, either for its baseUrl with ignoreCert:true or for a specific list of hosts:
// Ignore the self-signed certificate of the client's baseUrl
var client = ClientFactory.createWith(ClientOptions(
baseUrl: 'https://dev.servicestack.com:5001', ignoreCert: true));
// Or ignore the certificates of a specific list of hosts
var options = ClientOptions(baseUrl: 'https://dev.servicestack.com:5001')
..ignoreCertificatesFor.addAll([
'https://dev.servicestack.com:5001',
'http://local.servicestack.com',
]);
var devClient = ClientFactory.createWith(options);
Uploading Files​
postFileWithRequest lets you upload a file with a populated Request DTO which is sent as a multipart/form-data request where all populated Request DTO properties are sent as form fields alongside the file:
var audioFile = File('audio.wav');
var response = await client.postFileWithRequest(SpeechToText(), UploadFile(
fieldName: 'audio',
fileName: audioFile.uri.pathSegments.last,
contentType: 'audio/wav',
contents: await audioFile.readAsBytes()));
This example calls AI Server's SpeechToText API, use postFilesWithRequest to upload multiple files in the same request:
var request = TestFileUploads()
..id = 1
..refId = "zid";
var files = [
UploadFile(fieldName: 'audio', fileName: 'test.txt',
contentType: 'text/plain', contents: utf8.encode("Hello World")),
UploadFile(fieldName: 'content', fileName: 'test.md',
contentType: 'text/markdown', contents: utf8.encode("## Heading")),
];
var response = await client.postFilesWithRequest(request, files);
print(response.files![0].fileName); //= test.txt
print(response.files![0].contentLength); //= 11
Both APIs accept optional requestFilter and responseFilter arguments for customizing the multipart Request whilst postFilesWithRequestAs lets you upload files to a custom URL:
var response = await client.postFilesWithRequestAs("/custom/path", request, files,
responseAs: TestFileUploadsResponse());
INFO
File Uploads are implemented in the dart:io JsonServiceClient
Built-in ServiceStack DTOs​
The servicestack package includes typed DTOs for ServiceStack's built-in APIs letting you call them without needing to generate them, including Authenticate, Register, AssignRoles, UnAssignRoles, GetApiKeys, RegenerateApiKeys, ConvertSessionToToken, GetAccessToken and GetNavItems as well as the built-in ResponseStatus, ResponseError, QueryResponse<T>, EmptyResponse, IdResponse, StringResponse, StringsResponse, KeyValuePair<K,V> and Tuple<T1,T2> types:
import 'package:servicestack/client.dart';
var authResponse = await client.post(Authenticate(
provider: "credentials", userName: "test", password: "test"));
var apiKeys = await client.get(GetApiKeys());
var navItems = await client.get(GetNavItems());
Background Jobs DTOs like BackgroundJob, JobSummary, ScheduledTask, CompletedJob and FailedJob are also included.
Inspecting API Responses​
The Inspect utils are useful for quickly viewing the contents of API Responses and Dart collections during development, where Inspect.dump() renders any object graph in a human-friendly format that ignores null properties:
import 'package:servicestack/inspect.dart';
var response = await client.get(FindTechnologies()
..vendorName = "Google"
..take = 3
..orderByDesc = "ViewCount"
..fields = "Id,Name,VendorName,Tier");
print(Inspect.dump(response.results!.first));
{
id: 7,
name: AngularJS,
vendorName: Google,
tier: Client
}
Whilst Inspect.dumpTable() renders any Iterable of records into a text table with auto-aligned columns:
print(Inspect.dumpTable(response.results!));
+----------------------------------------------------------+
| id | name | vendorName | tier |
|----------------------------------------------------------|
| 7 | AngularJS | Google | Client |
| 18 | Go | Google | ProgrammingLanguage |
| 77 | Protocol Buffers | Google | Server |
+----------------------------------------------------------+
Use Inspect.printDump() and Inspect.printDumpTable() to print them directly or Inspect.vars() to dump the state of multiple variables at once:
Inspect.vars({'response': response, 'results': response.results});
Logging​
Both clients log to the Log abstraction which by default only logs Warn and Error messages to the Console. Enable debug logging to troubleshoot Type resolution and serialization issues:
Log.levels = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error];
Log.logger = ConsoleLogger(); // or NullLogger() to disable logging
Custom JSON Converters​
The conversion into and out of Dart Types is maintained in the extensible JsonConverters class where you can replace built-in converters with your own implementation or register new converters to take over handling of specific types:
class MyDateConverter implements IConverter {
@override
dynamic toJson(dynamic value, TypeContext context) => ...;
@override
dynamic fromJson(dynamic value, TypeContext context) => ...;
}
JsonConverters.Converters['DateTime'] = MyDateConverter();
Built-in converters include DateTimeConverter (ISO 8601 and WCF /Date(…)/ formats), ByteArrayConverter (Base64), KeyValuePairConverter, MapConverter, ListConverter and EnumConverter which handles C# enum, [Flags] enums, enums with values and custom Enum Styles.
C# TimeSpan properties are converted to Dart Duration using the XSD duration format which you can convert manually with:
fromTimeSpan("P11DT2H3M4.123S"); //= Duration(days:11,hours:2,minutes:3,seconds:4,milliseconds:123)
toTimeSpan(Duration(hours: 1)); //= PT1H
Comprehensive Test Suite​
To ensure a high quality implementation we've ported the TypeScript @servicestack/client test suite over to Dart which is itself a good resource for discovering different supported features and flexible HTTP Request options available, including:
| Test Suite | Description |
|---|---|
| client_test.dart | All HTTP Verbs, custom URLs, raw responses, batching, uploads |
| client_api_test.dart | api() / apiVoid() and ApiResult<T> error handling |
| client_auth_test.dart | JWT, Refresh Tokens, Token Cookies and re-authentication |
| test_auth_test.dart | Basic Auth and Credentials Auth |
| serization_test.dart | Serializing all C# Types, collections and inheritance |
| enum_serization_test.dart | Enums, [Flags] Enums, Enum Lists and Maps |
| converter_test.dart | DateTime and XSD Duration conversions |
| techstacks_test.dart | AutoQuery requests with runtime args |
| aiserver_client_test.dart | AI Server file uploads, e.g. Speech to Text |
| inspect_test.dart | Inspect.dump() and Inspect.dumpTable() utils |
Which you can run after cloning the repo with:
dart test
HelloFlutter App​
To showcase popular API Requests in action we've created a basic HelloFlutter App that mimics functionality in the HelloMobile App used to provide working examples of the same ServiceStack Service Client features running in the different supported Mobile and Desktop platforms.
HelloFlutter was created in VS Code with the DartCode extension using the Flutter: New Project action in VS Code's Command Palette.
This creates a basic Flutter App which you can run in your Android Device or Android Emulator where it's automatically picked and made visible in the bottom right of VS Code's status bar.
Then to use JsonServiceClient add the servicestack dependency to your apps pubspec.yaml:
dependencies: servicestack: ^3.0.3
Saving pubspec.yaml automatically runs flutter packages get to install any new dependencies in your App.
Our App will be making API calls to 2 different ServiceStack instances which we'll need to get typed DTOs for using the get-dtos script:
cd lib
npx get-dtos dart https://techstacks.io
npx get-dtos dart https://test.servicestack.net test
Which will save the DTOs for each endpoint in different files:
Saved to: dtos.dart
Saved to: test.dtos.dart
Incidentally you can get the latest version for all Dart Service References by running npx get-dtos dart without arguments:
npx get-dtos dart
Which updates all Dart references in the current directory, including any customization options available in the header of each file:
Updated: test.dtos.dart
Updated: dtos.dart
This gives us everything we need to call Web Services in our Flutter App, by importing package:servicestack/client.dart containing JsonServiceClient as well as any generated DTOs.
Then create new JsonServiceClient instances initialized with the BaseUrl for each of the remote endpoints we want to call:
import 'package:servicestack/client.dart';
import 'test.dtos.dart';
import 'dtos.dart';
const TestBaseUrl = "https://test.servicestack.net";
const TechStacksBaseUrl = "https://techstacks.io";
var testClient = JsonServiceClient(TestBaseUrl);
var techstacksClient = JsonServiceClient(TechStacksBaseUrl);
HelloFlutter UI​
Flutter works similarly to React and React Native where you need to return the entire UI layout for your Widget in its Widget build(BuildContext context) method, akin to React's render() method. For complete reference the app is contained in lib/main.dart, but for clarity we'll just highlight the relevant parts in each section.
Our widget requires some state to render its UI so our widget needs to inherit StatefulWidget. Stateful widgets require an additional supporting class for reasons explained in this Thread:
INFO
With a stateful widget, it's common to make closures whose life cycle are tied to the state's life cycle, which lasts through multiple widgets. With a stateless widget, it's common to make closures whose life cycle are tied to the widget's life cycle, which doesn't cause a problem
Ultimately this results in following the same dual class pattern below where the HelloFlutterState defines the state it needs as instance fields, this state is preserved across Hot Module reloads which is how Dart can update a live running App despite the implementation of the class changing.
class HelloFlutter extends StatefulWidget {
@override
State<StatefulWidget> createState() => HelloFlutterState();
}
class HelloFlutterState extends State<HelloFlutter> {
//State for this widget
String result = "";
Uint8List imageBytes = Uint8List(0);
@override
Widget build(BuildContext context) {
//...
RaisedButton(
child: Text("Async"),
onPressed: () async {
var r = await testClient.get(Hello(name: "Async"));
setState(() {
result = r.result;
});
},
),
//...
result != null && result != ""
? Text(result)
: Image.memory(imageBytes, width:500.0, height:170.0),
}
}
HelloFlutter's UI consists of 6 buttons across the top of the screen and an area to display the results of each call in the Widget's body. Each of the example Requests will populate either the result String for standard JSON responses or imageBytes for the HelloImage Service returning binary data.
Standard API Requests​
The first Async example shows an example of the most popular API Request for calling ServiceStack Services, simply by sending a populated Request DTO that returns a populated Response DTO, in this case sending a Hello Request DTO that returns a HelloResponse DTO:
var r = await testClient.get(Hello(name: "Async"));
setState(() {
result = r.result;
});
To update the UI any modified State needs to be done within the setState((){ }) closure which triggers re-rendering of the Widget with the new state.
This results in displaying the contents of the result String in a Text widget that was returned by the remote Hello Service:

Authenticated Requests​
This example shows how to make Authenticated Requests where first the JsonServiceClient instance is authenticated by sending a Authenticate request with valid Username/Password credentials which is validated against the servers configured CredentialsAuthProvider. If successful this will return Session Cookies containing a reference to the Authenticated UserSession stored on the server. The Cookies are automatically saved on the JsonServiceClient instance and re-sent on subsequent requests which is how it's able to make an Authenticated request to the protected HelloAuth Service:
RaisedButton(
child: Text("Auth"),
onPressed: () async {
var auth = await testClient.post(Authenticate(
provider: "credentials",
userName: "test",
password: "test"));
var r = await testClient.get(HelloAuth(name: "Auth"));
setState(() {
result = "${r.result} your JWT is: ${auth.bearerToken}";
});
},
),
If the Username and Password were valid it will display the result of the HelloAuth Service along with the encapsulated JWT Token returned in the initial AuthenticateResponse call.

JWT's encapsulate a signed, stateless Authenticated UserSession which is able to Authenticate with remote Services that have an JwtAuthProvider registered with the same AES or RSA Key used to sign the JWT Token. As they enable Authentication with stateless Services they're ideal for use in Microservices.
JWT RefreshToken Requests​
The JWT sample shows an example of authenticating via JWT, but instead of configuring the JsonServiceClient instance with the JWT BearerToken above (and what's needed to make JWT Authenticated Requests), it's only populating the long-lived RefreshToken which the client automatically uses behind the scenes to fetch a JWT Bearer Token from the remote ServiceStack endpoint, which if the User is still allowed to Sign In will populate the instance with a new JWT Bearer Token encapsulated with the latest UserSession.
RaisedButton(
child: Text("JWT"),
onPressed: () async {
var auth = await testClient.post(Authenticate(
provider: "credentials",
userName: "test",
password: "test"));
var newClient = JsonServiceClient(TestBaseUrl)
..refreshToken = auth.refreshToken;
var r = await newClient.get(HelloAuth(name: "JWT"));
setState(() {
result = "${r.result} your RefreshToken is: ${auth.refreshToken}";
});
},
),
The RefreshToken is smaller than a JWT Bearer Token as it just contains a signed token with permission to fetch new JWT Tokens and not the actual UserSession contained in the JWT Bearer Token.

AutoQuery Requests​
AutoQuery lets us effortlessly creating queryable high-performance RDBMS APIs with just a Request DTO class definition, e.g:
[Route("/technology/search")]
public class FindTechnologies : QueryDb<Technology>
{
public string Name { get; set; }
public string NameContains { get; set; }
}
ServiceStack takes care of creating the implementation for this Service from this definition which queries the Technology RDBMS table.
Any properties added to the AutoQuery Request DTO will be generated in the Dart FindTechnologies Request DTO. However AutoQuery also lets you query any other property on the Technology table using any of the configured Implicit Conventions.
We can include any additional arguments that are not explicitly defined on the Request DTO using the optional args parameter available in each IServiceClient API.
This examples calls 2 different AutoQuery Services, first to retrieve the Flutter Technology in https://techstacks.io to retrieve its id which it uses to query the latest Announcement or Showcase posted in the Flutter organization:
RaisedButton(
child: Text("Query"),
onPressed: () async {
var techs = await techstacksClient.get(FindTechnologies(), args: {"slug": "flutter"});
var posts = await techstacksClient.get(QueryPosts(
anyTechnologyIds: [techs.results[0].id],
types: ['Announcement', 'Showcase'])
..take = 1);
setState(() {
result = "Latest Flutter Announcement:\n“${posts.results[0].title}”";
});
},
),
The 2nd Request calls the QueryPosts AutoQuery Service highlighting the Service Client's support for sending complex type arguments on the QueryString and an example of using Dart's method cascade operator to populate the take field in the inherited QueryBase class.

Auto Batched Requests​
The sendAll and sendAllOneWay APIs lets you use ServiceStack's Auto Batched Requests feature to batch multiple Requests DTOs of the same Type in a single Request that returns all Responses in a single Response, e.g:
RaisedButton(
child: Text("Batch"),
onPressed: () async {
var requests = ['foo', 'bar', 'qux']
.map((name) => Hello(name: name));
var responses = await testClient.sendAll(requests);
setState(() {
result = "Batch Responses:\n${responses.map((r) => r.result).join('\n')}";
});
},
),

Generating Unknown Types​
This is one area where we hit limitations of not being able to use Reflection in Dart which requires generating factories ahead-of-time for each type we need to create instances of at runtime. This is typically inferred by inspecting all Types referenced in each DTO, but as Auto Batched Requests lets you combine multiple requests for every Service, in the interest for reducing the amount of code-generation needed ServiceStack doesn't generate an explicit Service Contract for the Batched version of each API.
Instead you'll need to specify missing types needed, the easiest solution to do this is to create a Dummy Service containing properties of any missing Types needed, in this case we need to generate a factory for the List<HelloResponse> used to return the batched HelloResponse DTOs in:
public class UnknownTypes
{
public List<HelloResponse> HelloResponses { get; set; }
}
public class UnknownTypesService : Service
{
public object Any(UnknownTypes request) => request;
}
Binary Requests​
Most API Requests typically involve sending a populated Request DTO that returns a Typed Response DTO although ServiceStack Services can also return raw data like String, byte[] and Stream responses which the JsonServiceClient also seamlessly supports where instead of returning a Typed DTO it returns the raw HTTP Body as a String for Request DTOs implementing IReturn<String> or an Uint8List for any binary responses (e.g. Services implementing IReturn<byte[]> or IReturn<Stream>).
This example calls the HelloImage Service which dynamically creates and returns an image based on the different properties on the incoming HelloImage Request DTO. As it implements IReturn<byte[]> the JsonServiceClient returns the binary contents of the HTTP Response in a Uint8List - the preferred type for bytes in Dart.
RaisedButton(
child: Text("Image"),
onPressed: () async {
Uint8List bytes = await testClient.get(HelloImage(
name: "Flutter",
fontFamily: "Roboto",
background: "#0091EA",
width: 500,
height: 170));
setState(() {
result = "";
imageBytes = bytes;
});
},
),
//...
result != null && result != ""
? Text(result)
: Image.memory(imageBytes, width:500.0, height:170.0),
To display the image we assign the response to the imageBytes field within the stateful widget's setState() which triggers a re-render of the UI containing the generated Image displayed using the Image widget:

Angular Dart​
The HelloAngularDart project demonstrates the same functionality in an AngularDart Web App running inside a Web Browser.
The only difference is having to import web_client.dart containing the JsonWebClient:
import 'package:servicestack/web_client.dart';
and changing the clients to use the JsonWebClient instead, e.g:
var testClient = JsonWebClient(TestBaseUrl);
var techstacksClient = JsonWebClient(TechStacksBaseUrl);
But otherwise the actual client source code for all of the Typed API requests remains exactly the same.
The HelloAngularDart App is contained within the hello_world component with all Dart logic in:
hello_world.dart​
import 'dart:typed_data';
import 'dart:convert';
import 'package:angular/angular.dart';
import 'package:servicestack/web_client.dart';
import '../dtos/test.dtos.dart';
import '../dtos/dtos.dart';
@Component(
selector: 'hello-world',
styleUrls: const ['hello_world.css'],
templateUrl: 'hello_world.html',
)
class HelloWorldComponent {
var result = "";
var imageSrc = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; // 1x1 pixel
static const TestBaseUrl = "https://test.servicestack.net";
static const TechStacksBaseUrl = "https://techstacks.io";
var testClient = JsonWebClient(TestBaseUrl);
var techstacksClient = JsonWebClient(TechStacksBaseUrl);
doAsync() async {
var r = await testClient.get(Hello(name: "Async"));
result = r.result;
}
doAuth() async {
var auth = await testClient.post(Authenticate(
provider: "credentials", userName: "test", password: "test"));
var r = await testClient.get(HelloAuth(name: "Auth"));
result = "${r.result} your JWT is: ${auth.bearerToken}";
}
doJWT() async {
var auth = await testClient.post(Authenticate(
provider: "credentials", userName: "test", password: "test"));
var newClient = JsonWebClient(TestBaseUrl)
..refreshToken = auth.refreshToken;
var r = await newClient.get(HelloAuth(name: "JWT"));
result = "${r.result} your RefreshToken is: ${auth.refreshToken}";
}
doQuery() async {
var techs = await techstacksClient
.get(FindTechnologies(), args: {"slug": "flutter"});
var posts = await techstacksClient.get(QueryPosts(
anyTechnologyIds: [techs.results[0].id],
types: ['Announcement', 'Showcase'])
..take = 1);
result = "Latest Flutter Announcement:\n“${posts.results[0].title}”";
}
doBatch() async {
var requests = ['foo', 'bar', 'qux'].map((name) => Hello(name: name));
var responses = await testClient.sendAll(requests);
result = "Batch Responses:\n${responses.map((r) => r.result).join('\n')}";
}
doImage() async {
Uint8List bytes = await testClient.get(HelloImage(
name: "Flutter",
fontFamily: "Roboto",
background: "#0091EA",
width: 500,
height: 170));
result = "";
imageSrc = "data:image/png;base64," + base64.encode(bytes);
}
}
hello_world.html​
Which uses this template markup to render its UI:
<div>
<button (click)="doAsync()">Async</button>
<button (click)="doAuth()">Auth</button>
<button (click)="doJWT()">JWT</button>
<button (click)="doQuery()">Query</button>
<button (click)="doBatch()">Batch</button>
<button (click)="doImage()">Image</button>
</div>
<div id="result">{{result}}</div>
<img src="{{imageSrc}}">
Where it runs a functionally equivalent App in a browser:

DTO Customization Options​
In most cases you'll just use the generated Dart DTOs as-is, however you can further customize how the DTOs are generated by overriding the default options.
Customize DTO Type generation​
Additional Dart specific customization can be statically configured like PreTypeFilter, InnerTypeFilter & PostTypeFilter (available in all languages) can be used to inject custom code in the generated DTOs output.
Use the PreTypeFilter to generate source code before and after a Type definition, e.g. this will append the [Validate] attribute on non enum & interface types:
DartGenerator.PreTypeFilter = (sb, type) => {
if (!type.IsEnum.GetValueOrDefault() && !type.IsInterface.GetValueOrDefault())
{
sb.AppendLine("@validate()");
}
};
The InnerTypeFilter gets invoked just after the Type Definition which can be used to generate common members for all Types and interfaces, e.g:
TypeScriptGenerator.InnerTypeFilter = (sb, type) => {
sb.AppendLine("String id = '${Random().nextDouble()}'.substring(2);");
};
There's also PrePropertyFilter & PostPropertyFilter for generating source before and after properties, e.g:
TypeScriptGenerator.PrePropertyFilter = (sb , prop, type) => {
if (prop.Name == "Id")
{
sb.AppendLine("@PrimaryKey()");
}
};
Emit custom code​
To enable greater flexibility when generating complex Typed DTOs, you can use [Emit{Language}] attributes to generate code before each type or property.
These attributes can be used to generate different attributes or annotations to enable client validation for different validation libraries in different languages, e.g:
[EmitDart("@validate")]
[EmitCode(Lang.Dart | Lang.CSharp | Lang.Kotlin, "// App User")]
public class User : IReturn<User>
{
[EmitDart("@isNotEmpty", "@isEmail")]
[EmitCode(Lang.TypeScript | Lang.Kotlin, new[]{ "@IsNotEmpty()", "@IsEmail()" })]
public string Email { get; set; }
}
Which will generate [EmitDart] code in Dart DTOs:
@validate
// App User
class User implements IReturn<User>, IConvertible
{
@isNotEmpty
@isEmail
String email;
User({this.email});
User.fromJson(Map<String, dynamic> json) { fromMap(json); }
fromMap(Map<String, dynamic> json) {
email = json['email'];
return this;
}
Map<String, dynamic> toJson() => {
'email': email
};
createResponse() { return User(); }
String getTypeName() { return "User"; }
TypeContext context = _ctx;
}
Whilst the generic [EmitCode] attribute lets you emit the same code in multiple languages with the same syntax.
Client Customization Options​
The header in the generated DTOs show the different options Dart native types support with their
defaults. Default values are shown with the comment prefix of //. To override a value, remove the //
and specify the value to the right of the :. Any uncommented value will be sent to the server to override
any server defaults.
The DTO comments allows for customizations for how DTOs are generated. The default options that were used
to generate the DTOs are repeated in the header comments of the generated DTOs, options that are preceded
by a Dart comment // are defaults from the server, any uncommented value will be sent to the server
to override any server defaults.
/* Options:
Date: 2025-06-04 09:48:37
Version: 8.80
Tip: To override a DTO option, remove "//" prefix before updating
BaseUrl: https://blazor-vue.web-templates.io
//GlobalNamespace:
//AddServiceStackTypes: True
//AddResponseStatus: False
//AddImplicitVersion:
//AddDescriptionAsComments: True
//IncludeTypes:
//ExcludeTypes:
//DefaultImports: package:servicestack/servicestack.dart
*/
We'll go through and cover each of the above options to see how they affect the generated DTOs:
Change Default Server Configuration​
The above defaults are also overridable on the ServiceStack Server by modifying the default config on the
NativeTypesFeature Plugin, e.g:
//Server example in CSharp
var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.GlobalNamespace = "dtos";
...
We'll go through and cover each of the above options to see how they affect the generated DTOs:
GlobalNamespace​
Specify a library for the generated Dart DTOs:
library dtos;
AddResponseStatus​
Automatically add a ResponseStatus property on all Response DTOs, regardless if it wasn't already defined:
class GetAnswers implements IReturn<GetAnswersResponse>
{
...
ResponseStatus responseStatus;
}
AddImplicitVersion​
Lets you specify the Version number to be automatically populated in all Request DTOs sent from the client:
class GetAnswers implements IReturn<GetAnswersResponse>
{
int version = 1;
...
}
This lets you know what Version of the Service Contract that existing clients are using making it easy to implement ServiceStack's recommended versioning strategy.
IncludeTypes​
Is used as a Whitelist to specify only the types you would like to have code-generated:
/* Options:
IncludeTypes: GetTechnology,GetTechnologyResponse
Will only generate GetTechnology and GetTechnologyResponse DTOs:
class class GetTechnology { ... }
class class GetTechnologyResponse { ... }
Include Request DTO and its dependent types​
You can include a Request DTO and all its dependent types with a .* suffix on the Request DTO, e.g:
/* Options:
IncludeTypes: GetTechnology.*
Which will include the GetTechnology Request DTO, the GetTechnologyResponse Response DTO and all Types that they both reference.
Include All Types within a C# namespace​
If your DTOs are grouped into different namespaces they can be all included using the /* suffix, e.g:
/* Options:
IncludeTypes: MyApp.ServiceModel.Admin/*
This will include all DTOs within the MyApp.ServiceModel.Admin C# namespace.
Include All Services in a Tag Group​
Services grouped by Tag can be used in the IncludeTypes where tags can be specified using braces in the format {tag} or {tag1,tag2,tag3}, e.g:
/* Options:
IncludeTypes: {web,mobile}
Or individually:
/* Options:
IncludeTypes: {web},{mobile}
ExcludeTypes​
Is used as a Blacklist to specify which types you would like excluded from being generated:
/* Options:
ExcludeTypes: GetTechnology,GetTechnologyResponse
Will exclude GetTechnology and GetTechnologyResponse DTOs from being generated.
DefaultImports​
Add additional import statements to the generated DTO's:
/* Options:
DefaultImports: package:servicestack/client.dart,package:flutter/material.dart
Will import the servicestack/client.dart and flutter/material.dart packages:
import 'package:servicestack/client.dart';
import 'package:flutter/material.dart';