ServiceStack v6.4

RAD Blazor

Due to the popularity of our Blazor Jamstack templates we've continued investing in Blazor and improving upon their already optimal Development Workflow in which we're excited to announce exciting new Blazor Components enabling a compelling a Rapid Application Development platform for Blazor Apps delivering many of the productivity benefits previously limited to locode.dev UI's.

Seamless Upgrade from Locode

Our new native Blazor Components allows for a beautiful progression story where you can start with a Database-First Locode solution to instantly generate Data Models and CRUD APIs around your existing databases that's able to utilize Locode's built-in UI for an instant modern UI for managing your App's databases whose declarative dev model can be used to define API Authorization & Validation rules or to customize the Behaviour & Appearance of Locode's Auto UI.

But to provide a compelling UX you'll want to adopt a Hybrid solution where after the necessary RAD dev cycles to gather and solidify business requirements you can start implementing their important unique workflows with a bespoke Blazor App that can reuse existing typed AutoQuery APIs and Data Models to speed up development whilst other supporting back-office functionality can continue to be managed by Locode.

Rapid Application with Blazor

Thanks to the new AutoQueryGrid Blazor component, being able to implement CRUD UIs is now trivial where you'll be able to quickly replace Locode and develop your entire App in Blazor! In addition the new Blazor components also support customization using existing UI & Metadata Attributes - preserving any efforts spent customizing UI's Behavior and Appearance.

Introducing Blazor Server

We're also happy to report our Blazor Tailwind Components are no longer limited to just Blazor WASM and arguably work even better in Blazor Server Apps which benefits from fast startup and exceptional responsiveness in low latency environments thanks to its architecture of running your App in a server session that only needs to propagate thin UI Virtual DOM updates to clients.

Blazor Server Tailwind Template

Blazor Server Tailwind Template

Ultimate dev model & UX ideal for low-latency Intranet environments

We're happy to announce our new Blazor Server App template offering a number compelling advantages over Blazor WASM, including:

  • A superior dev model and debugging experience
  • Improved live-reload and faster iterative dev cycles
  • Full access to .NET Server functionality
  • Better start times & UI responsiveness
  • Less complexity from unnecessary client project or pre-rendering solutions

Although the limitations of its highly-coupled stateful server rendering session architecture does make it a poor fit for most high latency Internet sites which we continue to recommend our Blazor WASM project template for.

Lightweight Blazor Tailwind Templates

To improve the utility of our Blazor WASM templates we've reduced their included functionality down to Simple CRUD and TODO MVC Examples pre-configured to use DB Migrations, integrated Auth inc. Sign In and Sign Up pages, support for Markdown docs and Top & Sidebar navigation which should reduce the effort to remove and replace functionality to make way for your new App.

To better showcase our growing Blazor functionality we've moved all other functionality to the new Blazor Gallery websites showcasing usage of available rich Blazor Components for rapidly develop beautiful Tailwind Web Apps:

As our components support both hosting models we're maintaining identical Gallery sites running on both Blazor Server and WASM:

For a closer look at ServiceStack.Blazor Components in action, download & run them to see how good they'll run in your Environment:

Blazor Tailwind Components

Blazor Tailwind Components

We encourage you to explore to the Blazor Gallery websites for the full preview, but we'll look at some of the Components here to give you some idea of the functionality available.

DataGrid

DataGrid is a versatile Component we expect to be heavily used for rendering any typed collection:

<DataGrid Model="Track" Items=@Track.Results />

Which by default renders results in a striped Tailwind Table:

Whose appearance can be styled to support many of the Tailwind Table Styles with the TableStyles Flag enum, e.g:

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.VerticalLines" />

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.WhiteBackground" />

<DataGrid Model="Track" Items=@Track.Results TableStyle="TableStyle.FullWidth" />

<DataGrid Model="Track" Items=@Track.Results 
          TableStyle="TableStyle.UppercaseHeadings | TableStyle.FullWidth | TableStyle.VerticalLines" />

It's a highly versatile component where you'll be able to control which columns are displayed and how they're formatted using <Column> definitions, e.g. here's how we can customize the table to look like Blazor's FetchData.cshtml tabular results:

<DataGrid Items=@forecasts class="max-w-screen-md" TableStyle="TableStyle.StripedRows | TableStyle.UppercaseHeadings">
    <Column Field="(WeatherForecast x) => x.Date" Format="dd/MM/yyyy" />
    <Column Title="Temp. (C)" Field="(WeatherForecast x) => x.TemperatureC" />
    <Column Title="Temp. (F)" Field="(WeatherForecast x) => x.TemperatureF" />
    <Column Field="(WeatherForecast x) => x.Summary" />
</DataGrid>

@code {
    List<WeatherForecast> forecasts = new();

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("data/weather.json") ?? new();
    }
}

Here's a more advanced example showing how to implement a responsive DataGrid by utilizing custom Header and Table Cell templates to define what columns and Headers are visible at different responsive breakpoints and how to enable different features like Row Selection and Filtering and examples of handling the Row and Header selected events App's can use for executing custom logic:

<DataGrid Model="Booking" Items=@Items AllowSelection="true" AllowFiltering="true"
          HeaderSelected="HandleSelectedHeader" RowSelected="HandleSelectedRow">
    <Column Field="(Booking x) => x.Id" class="text-gray-900" />
    <Column Field="(Booking x) => x.Name" VisibleFrom="Breakpoint.ExtraLarge" />
    <Column Field="(Booking x) => x.RoomType">
        <Header>
            <span class="hidden lg:inline">Room </span>Type
        </Header>
    </Column>
    <Column Field="(Booking x) => x.RoomNumber">
        <Header>
            <span class="hidden lg:inline">Room </span>No
        </Header>
    </Column>
    <Column Field="(Booking x) => x.Cost" Format="C" />
    <Column Field="(Booking x) => x.BookingStartDate" Formatter="FormatDate" VisibleFrom="Breakpoint.Small">
        <Header>
            Start<span class="hidden lg:inline"> Date</span>
        </Header>
    </Column>
    <Column Field="(Booking x) => x.BookingEndDate" Formatter="FormatDate" VisibleFrom="Breakpoint.ExtraLarge">
        <Header>
            End<span class="hidden lg:inline"> Date</span>
        </Header>
        <Template>@{ var booking = context as Booking; }@booking.BookingEndDate?.ToString("D")
        </Template>
    </Column>
    <Column Title="Employee" Field="(Booking x) => x.CreatedBy" VisibleFrom="Breakpoint.Medium" />
</DataGrid>

@code {
    public List<Booking> Items { get; set; } = new() {
        Create.Booking("First Booking!",  RoomType.Queen,  10, 100, "employee@email.com", "BOOK10"),
        Create.Booking("Booking 2",       RoomType.Double, 12, 120, "manager@email.com",  "BOOK25"),
        Create.Booking("Booking the 3rd", RoomType.Suite,  13, 130, "employee@email.com", "BOOK50"),
    };

    string FormatDate(object o) => o is DateTime d ? d.ToShortDateString() : "";

    public async Task HandleSelectedHeader(Column<Booking> item)
    {
        await JS.Log(item.Name);
    }

    public async Task HandleSelectedRow(Booking x)
    {
        await JS.Log(x);
    }
}

TIP

Resize webpage to preview its responsive appearance and different resolution breakpoints

AutoQueryGrid

The functionality and extensibility in DataGrid lays the foundation for higher-level components like AutoQueryGrid which makes use of it to enable its Auto UI around AutoQuery CRUD Services.

AutoQueryGrid Read Only

At a minimum AutoQueryGrid requires the AutoQuery APIs it should call to implement its functionality, so you can implement a read-only grid by only specifying the AutoQuery API to query a data model, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings>()" />

This one AutoQuery API is enough to power a functional read-only UI enabling multi flexible querying capabilities, paging, custom column selection and the ability to export the desired filtered resultset to .csv which can be open in Excel or copy the API URL Apps can use to consume the JSON API results:

AutoQueryGrid CRUD

Full CRUD functionality can be enabled by specifying the AutoQuery CRUD APIs for a specified data model, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings,CreateBooking,UpdateBooking,DeleteBooking>()" />

Customizable Columns

As AutoQueryGrid builds on DataGrid it also inherits its customizable option allowing for customizable responsive columns, e.g:

<AutoQueryGrid Model="Booking" Apis="Apis.AutoQuery<QueryBookings,CreateBooking,UpdateBooking,DeleteBooking>()"
               AllowSelection="true" AllowFiltering="true"
               HeaderSelected="OnSelectedHeader" RowSelected="OnSelectedRow">
    <Columns>
        <!-- Custom class -->
        <Column Field="(Booking x) => x.Id" class="text-gray-900" />
        <!-- Only show from Tailwind's xl responsive Breakpoint -->
        <Column Field="(Booking x) => x.Name" VisibleFrom="Breakpoint.ExtraLarge" />
        <!-- Custom Header collapsing 'Room' below 'lg' responsive breakpoint -->
        <Column Field="(Booking x) => x.RoomType">
            <Header>
                <span class="hidden lg:inline">Room </span>Type
            </Header>
        </Column>
        <!-- Custom Header collapsing 'Room' below 'lg' responsive breakpoint -->
        <Column Field="(Booking x) => x.RoomNumber">
            <Header>
                <span class="hidden lg:inline">Room </span>No
            </Header>
        </Column>
        <!-- Custom string Format -->
        <Column Field="(Booking x) => x.Cost" Format="C" />
        <!-- Custom C# Formatter -->
        <Column Field="(Booking x) => x.BookingStartDate" Formatter="FormatDate" VisibleFrom="Breakpoint.Small">
            <Header>
                Start<span class="hidden lg:inline"> Date</span>
            </Header>
        </Column>
        <!-- Custom Header and Cell Value -->
        <Column Field="(Booking x) => x.BookingEndDate" VisibleFrom="Breakpoint.ExtraLarge2x">
            <Header>
                End<span class="hidden lg:inline"> Date</span>
            </Header>
            <Template>
                @context.BookingEndDate?.ToString("D")
            </Template>
        </Column>
        <!-- Custom Title and Complex Type Cell with Reference Link -->
        <Column Title="Voucher" Field="(Booking x) => x.Discount" VisibleFrom="Breakpoint.ExtraLarge">
            <Template>
@if (context.Discount != null)
{
    <TextLink class="flex items-end" href=@($"/gallery/autoquerygrid/coupons?Id={context.Discount.Id}")>
        <PreviewFormat Value=@context.Discount />
    </TextLink>
}          </Template>
        </Column>
    </Columns>
</AutoQueryGrid>

Customizing how and when columns are rendered at different breakpoints using different formatting options and custom table header and cell templates:

Declarative Customizations

The columns can also be customized declaratively using the [Format] Metadata Attribute on the Model type:

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

    [Format(FormatMethods.IconRounded)]
    public string ProfileUrl { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    [Format(FormatMethods.Currency)]
    public int? SalaryExpectation { get; set; }

    [Format(FormatMethods.LinkEmail, Options = 
        @"{target:'_self',subject:'New Job Opportunity',
           body:'We have an exciting new opportunity...', cls:'text-green-600'}")]
    public string Email { get; set; }

    [Format(FormatMethods.LinkPhone)]
    public string Phone { get; set; }
}

Which can change how results are formatted in the data grid results:

Whilst the [Input] and [FieldCss] attributes on the AutoQuery CRUD DTOs:

public class UpdateContact : IPatchDb<Contact>, IReturn<Contact>
{
    public int Id { get; set; }

    [ValidateNotEmpty]
    public string? FirstName { get; set; }

    [ValidateNotEmpty]
    public string? LastName { get; set; }

    [Input(Type = "file"), UploadTo("profiles")]
    public string? ProfileUrl { get; set; }
    
    public int? SalaryExpectation { get; set; }

    [ValidateNotEmpty]
    public string? JobType { get; set; }

    public int? AvailabilityWeeks { get; set; }
    public EmploymentType? PreferredWorkType { get; set; }
    public string? PreferredLocation { get; set; }

    [ValidateNotEmpty]
    public string? Email { get; set; }
    public string? Phone { get; set; }
 
    [Input(Type="textarea")]
    [FieldCss(Field="col-span-12 text-center", Input="h-48", Label="text-xl text-indigo-700")]
    public string? About { get; set; }
}

Can customize how forms are rendered, e.g:

Changing AutoQueryGrid Defaults

A lot of AutoQueryGrid's UI is customizable allowing you to easily toggle on/off UI features as needed, if you have a consistent style you wish to enforce you can change the defaults of every AutoQueryGrid component with BlazorConfig, e.g. you can remove Copy URL button and change the default Table style to use Uppercase Headings with:

BlazorConfig.Set(new() {
    //...
    AutoQueryGridDefaults = new() {
        TableStyle = TableStyle.StripedRows | TableStyle.UppercaseHeadings,
        ShowCopyApiUrl = false,
    }
});

Which will change the appearance of every AutoQueryGrid Component used in the App unless overridden.

As AutoQueryGrid is a core component for the rapid development of Apps we're maintaining a dedicated section showcasing their different features at blazor-gallery.servicestack.net/grid:

To provide an optimal UX for relational fields AutoQueryGrid utilizes Modal Lookups for searching and selecting referential data that's automatically inferred from your OrmLite data model relationships, e.g:

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

    [References(typeof(Job))]
    public int JobId { get; set; }
    
    [References(typeof(Contact))]
    public int ContactId { get; set; }
    //...
}

Where it will display an enhanced LookupInput instead of a plain Text Input for the relational JobId and ContactId fields:

Which users can use to quickly search for the related record instead of manually inserting Foreign Key Ids:

File Uploads

Another feature showcased in the above screenshots is support for Managed File Uploads which can be declaratively added with the [Input(Type="file")] to render the FileInput Component and [UploadTo] attribute to specify which File Upload location it should use:

public class UpdateJobApplication : IPatchDb<JobApplication>, IReturn<JobApplication>
{
    public int Id { get; set; }
    public int? JobId { get; set; }
    public int? ContactId { get; set; }
    public DateTime? AppliedDate { get; set; }
    public JobApplicationStatus? ApplicationStatus { get; set; }

    [Input(Type = "file"), UploadTo("applications")]
    public List<JobApplicationAttachment>? Attachments { get; set; }
}

For a quick primer on using Managed File Uploads to Upload files from Blazor checkout:

Auto Forms

The Auto Form components are other high productivity components which can be used to create an automated form based from a Request DTO definition:

<AutoCreateForm Model="Booking" ApiType="typeof(CreateBooking)" />

AutoEditForm

Whilst AutoEditForm can be used to render an automated form based to update and delete an AutoQuery CRUD API:

<AutoEditForm Model="Booking" Edit="Model" ApiType="typeof(UpdateBooking)" DeleteApiType="typeof(DeleteBooking)" />

@code {
    Booking Model = Create.Booking("First Booking!", RoomType.Queen, 10, 100, "employee@email.com");
}

The forms behavior and appearance is further customizable with the API annotation, declarative validation and the custom Field and Input attributes, e.g:

[Description("Update an existing Booking")]
[Notes("Find out how to quickly create a <a class='svg-external' target='_blank' href='https://youtu.be/rSFiikDjGos'>C# Bookings App from Scratch</a>")]
[Route("/booking/{Id}", "PATCH")]
[ValidateHasRole("Employee")]
[AutoApply(Behavior.AuditModify)]
public class UpdateBooking : IPatchDb<Booking>, IReturn<IdResponse>
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public RoomType? RoomType { get; set; }
    [ValidateGreaterThan(0)]
    public int? RoomNumber { get; set; }
    [ValidateGreaterThan(0)]
    public decimal? Cost { get; set; }
    public DateTime? BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    [Input(Type = "textarea")]
    public string? Notes { get; set; }
    public bool? Cancelled { get; set; }
}

Both AutoForm components will render the Forms UI in a Slide Over dialog and includes built-in support for calling the API to update or edit the record with integrated contextual validation, reporting any field validation errors alongside their Input controls.

AutoFormFields

If more advanced customization of a Forms appearance and behavior is required, you can use AutoFormFields to just render the Form's fields (including Validation binding) that can be used to populate a Request DTO that your App can handle sending, e.g:

<form @onsubmit="submit" @onsubmit:preventDefault>
    <div class="shadow sm:overflow-hidden sm:rounded-md max-w-screen-lg">
        <div class="space-y-6 bg-white py-6 px-4 sm:p-6">
            <div>
                <h3 class="text-lg font-medium leading-6 text-gray-900">@(ApiType.GetDescription())</h3>
                <p class="notes mt-1 text-sm text-gray-500">
                    @((MarkupString)ApiType.GetNotes())
                </p>
            </div>

            <AutoFormFields Type="typeof(Booking)" Api="Api" FormLayout="FormLayout" ModelDictionary="ModelDictionary"/>

        </div>
        <div class="bg-gray-50 px-4 py-3 text-right sm:px-12">
            <PrimaryButton type="submit" onclick="submit">Save</PrimaryButton>
        </div>
    </div>
</form>

@code {
    [Inject] public JsonApiClient? Client { get; set; }
    IHasErrorStatus? Api { get; set; }
    Type ApiType = typeof(UpdateBooking);
    List<InputInfo>? FormLayout { get; set; }
    Dictionary<string, object> ModelDictionary { get; set; } = new();
    MetadataType MetadataType => ApiType.ToMetadataType();

    Booking Edit = Create.Booking("First Booking!", RoomType.Queen, 10, 100, "employee@email.com");

    protected override async Task OnParametersSetAsync()
    {
        await base.OnParametersSetAsync();
        Api = null;

        ModelDictionary = Edit.ToModelDictionary();
        FormLayout ??= MetadataType.CreateFormLayout<Booking>();
    }

    async Task submit()
    {
        var request = ModelDictionary.FromModelDictionary<UpdateBooking>();
        Api = await Client!.ApiAsync(request);
    }
}

PreviewFormat

The <PreviewFormat> component is useful for rendering Table Cell data into different customizable formats, e.g:

<PreviewFormat Value="50" Format=Formats.Currency />

<PreviewFormat Value="1000000" Format=Formats.Bytes />

<PreviewFormat Value=@Url Format=Formats.Icon IconClass="w-40 h-40" />

<PreviewFormat Value=@Url Format=Formats.Icon IconClass="w-40 h-40 rounded-full" />

<PreviewFormat Value=@Url Format=Formats.Attachment />

<PreviewFormat Value=@Path Format=Formats.Attachment />

<PreviewFormat Value=@Url Format=Formats.Link />

<PreviewFormat Value=@Email Format=Formats.LinkEmail />

<PreviewFormat Value=@Phone Format=Formats.LinkPhone />

HtmlFormat

Whilst the versatile <HtmlFormat> component can be used to render any Serializable object into a human-friendly HTML Format, e.g:

Single Model

<div class="max-w-screen-sm">
    <HtmlFormat Value=@Track.Results[0] />
</div>

Item Collections

<div class="max-w-screen-sm">
    <HtmlFormat Value=@Track.Results />
</div>

Nested Complex Types

<HtmlFormat Value=@Create.Players />

For more info about the Blazor Components available checkout the Component Gallery:

Blazor Config

A lot of the default conventions used by the Blazor Components are overridable with BlazorConfig initialized in Program.cs, where Blazor WASM projects configured with something like:

BlazorConfig.Set(new BlazorConfig
{
    IsWasm = true,
    Services = app.Services,
    FallbackAssetsBasePath = apiBaseUrl,
    EnableLogging = true,
    EnableVerboseLogging = builder.HostEnvironment.IsDevelopment(),
});

Asset and Fallback Paths

Where FallbackAssetsBasePath allows you to specify a fallback path for Images which is useful when there's a delay for syncing uploaded assets to the CDN that the Blazor WASM client is deployed to, as it can fallback to referencing the asset from the .NET App Server that handled the file upload.

Alternatively AssetsBasePath can be used for specifying a different primary CDN location that's different from the Blazor WASM App CDN or AssetsPathResolver and FallbackPathResolver can be used when more a advanced custom strategy is required.

Blazor WASM Tailwind Upgrade

The advanced functionality in the new high-level components required making some breaking changes to move Auth and Components previously defined in the templates to be included in the ServiceStack.Blazor library. To simplify upgrading we've wrapped most of these required changes in the mix scripts below which should be run from your blazor-tailwind WASM Client project:

x mix -delete blazor-upgrade-clean

x mix blazor-upgrade

Blazor Feedback

We'll be continuing to further develop and expand our Blazor Tailwind Component library in the coming releases that if you have any suggestions for new Components or new features on existing Components, please drop us a Feature Request at:

servicestack.net/ideas

DB Migrations

We've continued improving our story around Code-First DB Migrations and have created a new video demonstrating how it can be used to maintain DB Schema migrations under a typical development workflow:

Jamstack Templates Upgraded

As they encourage a structured workflow for incremental development of new App features we've upgraded all modern jamstacks.net templates to adopt DB Migrations for creating and populating their App DB.

Run or Debug Migrations from your IDE

A benefit of DB Migrations being implemented in a library instead of wrapped up behind an external tool, is that it's better integrated and more versatile in supporting more executable options like being able to run from code, a feature all Jamstack templates now benefit from with the new MigrationTasks Explicit TestFixture enabling DB Migrations to be run or debugged directly from within your IDE, implemented as:

[TestFixture, Explicit, Category(nameof(MigrationTasks))]
public class MigrationTasks
{
   IDbConnectionFactory ResolveDbFactory() => new ConfigureDb().ConfigureAndResolve<IDbConnectionFactory>();
   Migrator CreateMigrator() => new(ResolveDbFactory(), typeof(Migration1000).Assembly); 
   
   [Test]
   public void Migrate()
   {
       var migrator = CreateMigrator();
       var result = migrator.Run();
       Assert.That(result.Succeeded);
   }

   [Test]
   public void Revert_All()
   {
       var migrator = CreateMigrator();
       var result = migrator.Revert(Migrator.All);
       Assert.That(result.Succeeded);
   }

   [Test]
   public void Revert_Last()
   {
       var migrator = CreateMigrator();
       var result = migrator.Revert(Migrator.Last);
       Assert.That(result.Succeeded);
   }

   [Test]
   public void Rerun_Last_Migration()
   {
       Revert_Last();
       Migrate();
   }
}

Which uses your App's ConfigureDb Modular Startup configuration to resolve your App's configured OrmLiteConnectionFactory that the migrations are run against, that if needed can be run from Unit tests to debug through any schema migration issues.

Revert and Rerun Last Migration

The Rerun_Last_Migration task is especially useful during development of new features to easily revert and rerun the last migration before checking in a completed feature, allowing you to re-iterate and check in a completed and tested DB migration along with the new feature requiring it instead of multiple "micro migrations" for each DB change run at different times.

ServiceStack Reference

The pursuit of improving the discovery of Blazor Component features prompted us to generate library reference documentation for all core ServiceStack libraries which is being hosted at reference.servicestack.net that's configured with Real-time search using TypeSense that should let you quickly jump to the type or methods you're after with a few key strokes.

Along with the new Blazor Gallery websites it's a great resource to discover Blazor Component parameters and features which we'll continue improving over time at:

It covers most of ServiceStack's 69 Packages which given its size takes a couple hours to generate but has given us some insightful stats on the size and scale of ServiceStack libraries which have been in active development for over 10 years:

Lines of Code

Package Lines of Code
ServiceStack 358802
ServiceStack.Aws 22169
ServiceStack.Azure 4135
ServiceStack.Blazor 9098
ServiceStack.Logging 2214
ServiceStack.OrmLite 95075
ServiceStack.Redis 44765
ServiceStack.Text 61850

Total Namespaces / Classes / Interfaces

Symbol Count
Namespaces 116
Classes 2538
Interfaces 498

API QueryParams

ServiceStack's message-based design is centered around sending a single message which is all that's required to invoke any Typed API, however there may be times when you need to send additional params where you can't change the API's Request DTO definition or in AutoQuery's case its Implicit Conventions would require too many permutations to be able to type the entire surface area on each Request DTO.

Previously this would inhibit being able to invoke these Services from a typed Service Client API that would instead need to either use the untyped Get<T>(relativeUrl) ServiceClient APIs or HTTP Utils to construct the API Request path manually.

Now Request DTOs can implement IHasQueryParams where any entries will be sent as additional query params along with the typed DTO:

public interface IHasQueryParams
{
    Dictionary<string, string> QueryParams { get; set; }
}

Which is now available in all AutoQuery DTOs where it's added as a non-serializable property so it's only included in the QueryString:

[DataContract]
public abstract class QueryBase : IQuery, IHasQueryParams
{
    //...
    [IgnoreDataMember]
    public virtual Dictionary<string, string> QueryParams { get; set; }
}

Which now allows using existing ServiceClient typed APIs to send a combination of untyped queries in AutoQuery requests, e.g:

var api = await client.ApiAsync(new QueryContacts {
  IdsIn = new[]{ 1, 2, 3 },
  QueryParams = new() {
    ["LastNameStartsWith"] = "A"
  }
});

/api route supports multiple content types

The JSON /api pre-defined route was previously limited to calling JSON APIs with the pre-defined route:

/api/{Request}

This now supports returning API responses in multiple registered content types by using its format extension, e.g:

/api/{Request}.{ext}

  • /api/{Request}.csv
  • /api/{Request}.xml
  • /api/{Request}.jsv
  • /api/{Request}.html