Blazor Diffusion

The goal of our increasing Blazor investments is to enable a highly productive and capable platform for rapidly developing a majority of internal Apps CRUD functionality as well as enabling a hybrid development model where the management of Back office supporting tables can be quickly implemented using custom AutoQueryGrid components freeing up developers to be able to focus a majority of their efforts where they add the most value - in the bespoke Blazor UI's optimized customer-facing UX.

To best demonstrate its potential we've embarked on development of a new project we're excited to announce that does exactly this!

blazordiffusion.com

blazordiffusion.com is a new ServiceStack.Blazor App front-end for Stable Diffusion - a deep learning text-to-image model that can generate quality images from a text prompt whose ability to run on commodity GPU hardware makes it one of the most exciting Open Source AI projects ever released. If you haven't experienced Stable Diffusion yet, we welcome you to create an account and start building your Stable Diffusion portfolio for FREE!

Effortless Admin Pages

It's a great example of Hybrid Development in action where the entire user-facing UI is a bespoke Blazor App that's optimized for creating, searching, cataloging and discovering Stable Diffusion generated images, whilst all its supporting admin tasks to manage the back office tables that power the UI were effortlessly implemented with custom AutoQueryGrid components.

To get a glimpse of this in action we've created a video showing how quick it was to build the first few Admin Pages:

Blazor Diffusion is an example of a real-world App leveraging a number of different ServiceStack features to achieve its functionality that we're using to "dog food" new ServiceStack features to help identify any friction points or missing functionality that we can feedback into the design and improvements of new and existing features, which it has done for most of the new features in this release.

Blazor Server or Blazor WASM

To ensure all new ServiceStack.Blazor features continue to work in both Blazor Server and Blazor WASM we're maintaining identical versions of Blazor Diffusion running in both of Blazor's hosting modes:

Where it's initially developed from a Blazor Server project template to take advantage of its fast iterative dev model then uses a script to export all Pages and Server functionality to a Blazor WASM project template that's optimal for Internet deployments.

Blazor Diffusion Features

To help discovery we'll link to where new features in this release are used.

Dark Mode

The decision to build blazordiffusion.com was in large part due to choosing an App that would look best in Dark Mode, as-is often preferred when viewing images and video. The public UI uses JS.init() to force Dark Mode whilst the Admin Pages uses a different AdminLayout.razor that allows dark mode to be toggled on/off as seen in the BlazorDiffusion Video.

AutoComplete

The Create.razor uses the new <Autocomplete> to quickly select Artists and Modifiers.

Admin Pages

The /admin pages we're all built using AutoQueryGrid for its data management and uses NavList and Breadcrumbs for its navigation.

EditForm

The following components make use of <EditForm> AutoQueryGrid extensibility to display unique forms for their custom workflow requirements:

<AutoQueryGrid @ref=@grid Model="Creative" Apis="Apis.AutoQuery<QueryCreatives,UpdateCreative,HardDeleteCreative>()"
               ConfigureQuery="ConfigureQuery">
    <EditForm>
        <div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
            <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
                <CreativeEdit Creative="context" OnClose="grid!.OnEditDone" />
            </div>
        </div>
    </EditForm>
</AutoQueryGrid>

SelectInput

The Modifiers.razor admin page uses SelectInput EvalAllowableValues feature to populate its options from a C# AppData property:

public class CreateModifier : ICreateDb<Modifier>, IReturn<Modifier>
{
    [ValidateNotEmpty, Required]
    public string Name { get; set; }
    [ValidateNotEmpty, Required]
    [Input(Type="select", EvalAllowableValues = "AppData.Categories")]
    public string Category { get; set; }
    public string? Description { get; set; }
}

TagInput

The Artists.razor admin page uses declarative TagInput to render its AutoQueryGrid Create and Edit Forms:

public class UpdateArtist : IPatchDb<Artist>, IReturn<Artist>
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public int? YearDied { get; set; }
    [Input(Type = "tag"), FieldCss(Field = "col-span-12")]
    public List<string>? Type { get; set; }
}

We're excited to be able to leverage our support for Litestream and showcase an example of architecting a production App at minimal cost which avoids paying for expensive managed hosted RDBMS's by effortlessly replicating its SQLite databases to object storage.

Reduce Complexity & Save Costs

Avoid expensive managed RDBMS servers, reduce deployment complexity, eliminate infrastructure dependencies & save order of magnitude costs vs production hosting

To make it easy for Blazor Tailwind projects to take advantage of our first-class Litestream support, we've created a new video combining these ultimate developer experience & value combo solutions that walks through how to deploy a new Blazor Tailwind SQLite + Litestream App to any Linux server with SSH access, Docker and Docker Compose:

Custom SQLite functions

Using SQLite also gives us access to features not available in other RDBMS's, e.g. for the "Explore Similar Artifacts" feature we're using a custom registered C# function that we can use in SQL to find other Artifacts with the nearest background colors in SearchService.cs:

public static class DbFunctions
{
    public static void RegisterBgCompare(this IDbConnection db)
    {
        var sqliteConn = (SqliteConnection)db.ToDbConnection();
        sqliteConn.CreateFunction("bgcompare", (string a, string b) => ImageUtils.BackgroundCompare(a, b));
    }
}

After registering the function with the db connection we can reference it in our typed SQL Expression with OrmLite's Sql.Custom() API:

db.RegisterBgCompare();
q.SelectDistinct<Artifact, Creative>((a, c) => new {
    a,
    c.UserPrompt,
    c.ArtistNames,
    c.ModifierNames,
    c.PrimaryArtifactId,
    Similarity = Sql.Custom($"bgcompare('{similarToArtifact.Background}',Background)"),
});

This same technique is also used for finding similar images by PerceptualHash, AverageHash & DifferenceHash functions provided by the ImageHash library.

The SearchService.cs itself is a great example of a complex custom AutoQuery implementation which is solely responsible for providing the entire search functionality on the home page.

Hetzner US Cloud

Our analysis of US Cloud Hosting Providers led us to moving to Hetzner Cloud for hosting where it costs vastly less than equivalent specs at a major cloud provider. But this also meant we also had to look elsewhere to also avoid AWS's expensive egress costs for S3 for image storage which can easily get out of control for a highly traffic image host.

R2 Virtual Files Provider

Fortunately we were in time to take advantage of Cloudflare's inexpensive R2 Object Storage solution with $0 egress fees, together with their generous free tier and ability to serve R2 assets behind their free CDN, we ended up with great value and performance managed cloud storage solution with the only cost expected in the near future to be R2's $0.015 / GB storage cost.

R2 is mostly S3 compatible however it needed a custom S3VirtualFiles provider to workaround missing features which is being maintained in the new R2VirtualFiles VFS provider.

Files Upload Transformer

The Managed Files Upload Feature is configured in Configure.AppHost.cs and used for all website File Uploads:

var appFs = VirtualFiles = new R2VirtualFiles(s3Client, appConfig.ArtifactBucket);
Plugins.Add(new FilesUploadFeature(
    new UploadLocation("artifacts", appFs,
        readAccessRole: RoleNames.AllowAnon,
        maxFileBytes: AppData.MaxArtifactSize),
    new UploadLocation("avatars", appFs, allowExtensions: FileExt.WebImages, 
        // Use unique URL to invalidate CDN caches
        resolvePath: ctx => X.Map((CustomUserSession)ctx.Session, x => $"/avatars/{x.RefIdStr[..2]}/{x.RefIdStr}/{ctx.FileName}")!,
        maxFileBytes: AppData.MaxAvatarSize,
        transformFile:ImageUtils.TransformAvatarAsync)
    ));

It utilizes the new transformFile: option to transform an uploaded file and save a reference to the transformed file instead. This is used to only save a reference to the 128x128 resized avatar used by the App, whilst still persisting the original uploaded image in a Background MQ task in case a higher resolution of their avatar is needed later.

public class ImageDetails
{
    public static async Task<IHttpFile?> TransformAvatarAsync(FilesUploadContext ctx)
    {
        var originalMs = await ctx.File.InputStream.CopyToNewMemoryStreamAsync();

        // Offload persistance of original image to background task
        using var mqClient = HostContext.AppHost.GetMessageProducer(ctx.Request);
        mqClient.Publish(new DiskTasks {
            SaveFile = new() {
                FilePath = ctx.Location.ResolvePath(ctx),
                Stream = originalMs,
            }
        });

        var resizedMs = await CropAndResizeAsync(originalMs, 128, 128, PngFormat.Instance);

        return new HttpFile(ctx.File)
        {
            FileName = $"{ctx.FileName.LastLeftPart('.')}_128.{ctx.File.FileName.LastRightPart('.')}",
            ContentLength = resizedMs.Length,
            InputStream = resizedMs,
        };
    }

    public static async Task<MemoryStream> CropAndResizeAsync(Stream inStream, int width, int height, IImageFormat format)
    {
        var outStream = new MemoryStream();
        var image = await Image.LoadAsync(inStream);
        using (image)
        {
            var clone = image.Clone(context => context
                .Resize(new ResizeOptions {
                    Mode = ResizeMode.Crop,
                    Size = new Size(width, height),
                }));
            await clone.SaveAsync(outStream, format);
        }
        outStream.Position = 0;
        return outStream;
    }
}

Background MQ

Background MQ is utilized to improve API response times by offloading a number of non-essential background tasks in BackgroundMqServices.cs to perform functions like:

  • Saving JSON metadata snapshot of Stable Diffusion generated images alongside the images themselves
  • Write Files to R2
  • Recalculating temporal scores and ranking of Artifacts and Albums
  • Recording Analytics