ASP.NET Core 10 AI features: Building an Azure OpenAI Image Generator with gpt-image-1.5

ASP.NET Core 10 continues the modern web development story for .NET developers by combining minimal hosting, dependency injection, Razor Pages, configuration, HTTP clients, validation, and static web assets in a clean application model. In this project, these ASP.NET Core 10 features are used to build an AI image generator named aspnet10aiimg. The application accepts a prompt, verifies the quality and safety of that prompt, sends it to Azure OpenAI, and displays the generated image in the browser.

The AI model used in this project is gpt-image-1.5, an Azure OpenAI image generation model. It accepts text prompts and returns generated image data. In the app, the server calls the Azure OpenAI image generations REST endpoint using the deployment name configured for your Azure OpenAI resource. The API version used is 2025-04-01-preview, and the project expects image output as base64 PNG data, which is converted into a browser-ready data URI.

Project implementation diagram

The following image explains the implementation flow used by the Razor Page, prompt validation service, Azure OpenAI service, JavaScript, and image preview area.


What the application does

  • Shows a Razor Pages UI with the prompt textbox and generated image panel side by side.
  • Validates the prompt before posting it to Azure OpenAI.
  • Blocks prompts containing restricted bad words, adult content words, violence words, mob/crime words, and other unsafe terms.
  • Uses JavaScript to submit the prompt without a full page refresh.
  • Shows a progress bar while the image generation request is running.
  • Loads the generated image into an image control when Azure OpenAI returns the image.
  • Provides a Save image button so the generated PNG can be downloaded to disk.

GPT model used: gpt-image-1.5

The gpt-image-1.5 model is used for image generation. In Azure OpenAI, you do not call the model name directly from the code. Instead, you create a deployment for the model in Azure AI Foundry or Azure OpenAI and configure the deployment name in the application. In this project, the setting is stored under AzureOpenAI:DeploymentName.

The image generation request is sent to this endpoint pattern:

/openai/deployments/{deployment-name}/images/generations?api-version=2025-04-01-preview

Azure OpenAI returns the completed generated image response. The browser does not receive partial pixels while the model is still generating. The JavaScript progress bar provides request progress feedback, and the image control is updated immediately after the generated image data is returned.

Application architecture

The app uses Razor Pages as the UI layer. Index.cshtml contains the prompt form, progress bar, image control, save button, and JavaScript. Index.cshtml.cs contains the page handler methods. The page model first validates the input and checks prompt quality. If the prompt is allowed, it calls AzureOpenAiImageService, which sends the request to Azure OpenAI. The result is returned as JSON to the browser.

Explanation of Program.cs

Program.cs configures the ASP.NET Core 10 application. It adds Razor Pages, registers PromptQualityService as a singleton, and registers AzureOpenAiImageService with HttpClient. The HTTP client timeout is set to five minutes because image generation can take longer than a normal web request.

Explanation of PromptQualityService

PromptQualityService performs prompt quality verification before the prompt is sent to Azure OpenAI. It checks the prompt against blocked term categories such as bad words, adult content, violence, mob or crime, and unsafe content. It also reads additional blocked words from configuration under PromptModeration:BlockedWords. The service uses regular expressions with word boundaries so that restricted terms are matched as complete words instead of accidentally matching parts of unrelated words.

Explanation of AzureOpenAiImageService

AzureOpenAiImageService is responsible for calling the Azure OpenAI image generation endpoint. It reads the endpoint, API key, deployment name, API version, and image size from configuration. It creates an HTTP POST request, sends the prompt to the image generations API, handles timeouts and HTTP errors, then reads the returned image. If Azure returns b64_json, the service converts it into a data:image/png;base64,... URI so the browser can assign it directly to the image control.

Explanation of Index.cshtml.cs

Index.cshtml.cs is the Razor Page model. It contains the bound Prompt property with validation attributes. The OnPostGenerateAsync handler is used by JavaScript to generate the image asynchronously. It first checks model validation, then calls PromptQualityService. If the prompt is blocked, the handler returns a JSON error and does not call Azure OpenAI. If the prompt is allowed, it calls AzureOpenAiImageService and returns the image data URI as JSON.

Explanation of Index.cshtml

Index.cshtml defines the UI. It places the prompt textbox on the left and the generated image area on the right. The right panel contains the progress bar, error message area, image placeholder, generated image control, and Save image button. The Save image button uses the HTML download attribute and receives the generated image URI after the image loads.

Explanation of the JavaScript code in Index.cshtml

The JavaScript intercepts the form submit event and prevents a full page reload. It validates the form, starts the progress animation, disables the Create image button, and posts the form data to ?handler=Generate using fetch. When the server responds, the script safely reads JSON responses and also handles non-JSON server errors. If image generation succeeds, it assigns the returned image data URI to the image control. When the browser finishes loading the image, the script hides the loading message, displays the image, enables the Save image button, and re-enables the Create image button.

Configuration and secret storage

The project keeps the API key value empty in appsettings.json. For local development, store the API key with user secrets instead of committing it to source code.

dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR-RESOURCE.openai.azure.com"
dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY"
dotnet user-secrets set "AzureOpenAI:DeploymentName" "YOUR_IMAGE_DEPLOYMENT_NAME"
dotnet user-secrets set "AzureOpenAI:ApiVersion" "2025-04-01-preview"

Complete source code

Project file: aspnet10aiimg.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSingleton<aspnet10aiimg.Services.PromptQualityService>();
builder.Services.AddHttpClient<aspnet10aiimg.Services.AzureOpenAiImageService>(client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();

Configuration: appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AzureOpenAI": {
    "Endpoint": "https://aspnetimageai.openai.azure.com/",
    "ApiKey": "",
    "DeploymentName": "gptimagegenerator",
    "ApiVersion": "2025-04-01-preview",
    "ImageSize": "1024x1024"
  },
  "PromptModeration": {
    "BlockedWords": [
      "damn",
      "hell",
      "crap",
      "shit",
      "fuck",
      "bitch",
      "bastard",
      "asshole",
      "sex",
      "sexual",
      "porn",
      "pornographic",
      "nude",
      "naked",
      "erotic",
      "xxx",
      "fetish",
      "kill",
      "murder",
      "blood",
      "gore",
      "torture",
      "weapon",
      "gun",
      "knife",
      "shoot",
      "stab",
      "bomb",
      "explosion",
      "beheading",
      "mob",
      "mafia",
      "gang",
      "gangster",
      "cartel",
      "drug trafficking",
      "kidnap",
      "ransom",
      "robbery",
      "terrorist",
      "hate",
      "racist",
      "suicide",
      "self-harm",
      "abuse",
      "harassment"
    ]
  },
  "AllowedHosts": "*"
}

Razor UI: Pages/Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "AI Image Generator";
}

<div class="text-center mb-4">
    <h1 class="display-5 fw-semibold">Azure OpenAI Image Generator</h1>
    <p class="lead text-muted">Enter a prompt and generate an image with your Azure OpenAI image deployment.</p>
</div>

<div class="row g-4 align-items-start">
    <div class="col-lg-5">
        <form method="post" class="card shadow-sm border-0" id="imageGenerationForm">
            <div class="card-body p-4">
                <div class="mb-3">
                    <label asp-for="Prompt" class="form-label fw-semibold">Prompt</label>
                    <textarea asp-for="Prompt" class="form-control" rows="12" placeholder="Example: A watercolor painting of a futuristic city at sunrise"></textarea>
                    <span asp-validation-for="Prompt" class="text-danger"></span>
                </div>

                <button type="submit" class="btn btn-primary px-4" id="createImageButton">Create image</button>
            </div>
        </form>
    </div>

    <div class="col-lg-7">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <div class="d-flex justify-content-between align-items-center gap-3 mb-3">
                    <h2 class="h4 mb-0">Generated image</h2>
                    <a class="btn btn-outline-success btn-sm d-none" id="saveImageButton" download="generated-image.png">Save image</a>
                </div>

                <div class="mb-4 d-none" id="generationProgressCard" aria-live="polite">
                    <div class="d-flex justify-content-between align-items-center mb-2">
                        <span class="fw-semibold" id="generationProgressStatus">Starting image generation...</span>
                        <span class="text-muted" id="generationProgressPercent">0%</span>
                    </div>
                    <div class="progress" role="progressbar" aria-label="Image generation progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
                        <div class="progress-bar progress-bar-striped progress-bar-animated" id="generationProgressBar" style="width: 0%"></div>
                    </div>
                    <div class="form-text mt-2">Image generation can take a few seconds. The image appears here as soon as Azure OpenAI returns it.</div>
                </div>

                <div class="alert alert-danger d-none" id="generationError" role="alert"></div>
                <div class="alert alert-info py-2 d-none" id="imageLoadStatus" role="status">Loading generated image...</div>

                <div class="border rounded bg-light d-flex align-items-center justify-content-center text-center p-4" id="imagePlaceholder" style="min-height: 420px;">
                    <span class="text-muted">Your generated image will appear here.</span>
                </div>

                <img class="img-fluid rounded border d-none" id="generatedImage" alt="AI generated image for the submitted prompt" />
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
    <script>
        (() => {
            const form = document.getElementById('imageGenerationForm');
            const submitButton = document.getElementById('createImageButton');
            const progressCard = document.getElementById('generationProgressCard');
            const progressBar = document.getElementById('generationProgressBar');
            const progressPercent = document.getElementById('generationProgressPercent');
            const progressStatus = document.getElementById('generationProgressStatus');
            const generationError = document.getElementById('generationError');
            const imagePlaceholder = document.getElementById('imagePlaceholder');
            const generatedImage = document.getElementById('generatedImage');
            const imageLoadStatus = document.getElementById('imageLoadStatus');
            const saveImageButton = document.getElementById('saveImageButton');
            let progressTimer;

            form?.addEventListener('submit', async (event) => {
                if (window.jQuery?.validator && !window.jQuery(form).valid()) {
                    return;
                }

                event.preventDefault();

                let progress = 0;
                window.clearInterval(progressTimer);
                progressCard?.classList.remove('d-none');
                progressBar?.classList.add('progress-bar-animated');
                submitButton?.setAttribute('disabled', 'disabled');
                generationError?.classList.add('d-none');
                imageLoadStatus?.classList.add('d-none');
                generatedImage?.classList.add('d-none');
                saveImageButton?.classList.add('d-none');
                saveImageButton?.removeAttribute('href');
                imagePlaceholder?.classList.remove('d-none');
                updateGenerationProgress(progress, 'Starting image generation...');

                progressTimer = window.setInterval(() => {
                    if (progress < 88) {
                        progress += progress < 60 ? 7 : 3;
                        updateGenerationProgress(progress, 'Generating image with Azure OpenAI...');
                        return;
                    }

                    if (progress < 96) {
                        progress += 1;
                        updateGenerationProgress(progress, 'Finalizing generated image...');
                    }
                }, 700);

                try {
                    const response = await window.fetch(`${form.action}?handler=Generate`, {
                        method: 'POST',
                        body: new FormData(form),
                        headers: {
                            'X-Requested-With': 'XMLHttpRequest'
                        }
                    });

                    const result = await readJsonResponse(response);

                    if (!response.ok || !result.success) {
                        throw new Error(result.error ?? 'Image generation failed.');
                    }

                    updateGenerationProgress(100, 'Image generated. Loading preview...');
                    loadGeneratedImage(result.imageDataUri);
                } catch (error) {
                    window.clearInterval(progressTimer);
                    progressBar?.classList.remove('progress-bar-animated');
                    submitButton?.removeAttribute('disabled');
                    updateGenerationProgress(0, 'Image generation failed.');

                    if (generationError) {
                        generationError.textContent = error instanceof Error ? error.message : 'Image generation failed.';
                        generationError.classList.remove('d-none');
                    }
                }
            });

            function updateGenerationProgress(value, status) {
                const progressValue = Math.min(value, 100);

                if (progressBar) {
                    progressBar.style.width = `${progressValue}%`;
                    progressBar.setAttribute('aria-valuenow', progressValue.toString());
                }

                if (progressPercent) {
                    progressPercent.textContent = `${progressValue}%`;
                }

                if (progressStatus) {
                    progressStatus.textContent = status;
                }
            }

            async function readJsonResponse(response) {
                const contentType = response.headers.get('content-type') ?? '';

                if (contentType.includes('application/json')) {
                    return await response.json();
                }

                const responseText = await response.text();
                return {
                    success: false,
                    error: responseText || `Unexpected response from server (${response.status}).`
                };
            }

            function loadGeneratedImage(imageDataUri) {
                if (!(generatedImage instanceof HTMLImageElement)) {
                    return;
                }

                imagePlaceholder?.classList.add('d-none');
                imageLoadStatus?.classList.remove('d-none');
                generatedImage.src = imageDataUri;
            }

            if (generatedImage instanceof HTMLImageElement) {
                generatedImage.addEventListener('load', () => {
                    window.clearInterval(progressTimer);
                    updateGenerationProgress(100, 'Image ready.');
                    progressBar?.classList.remove('progress-bar-animated');
                    imageLoadStatus?.classList.add('d-none');
                    progressCard?.classList.add('d-none');
                    generatedImage.classList.remove('d-none');
                    submitButton?.removeAttribute('disabled');

                    if (saveImageButton) {
                        saveImageButton.href = generatedImage.src;
                        saveImageButton.classList.remove('d-none');
                    }
                });

                generatedImage.addEventListener('error', () => {
                    window.clearInterval(progressTimer);
                    progressBar?.classList.remove('progress-bar-animated');
                    submitButton?.removeAttribute('disabled');

                    if (imageLoadStatus) {
                        imageLoadStatus.className = 'alert alert-danger py-2';
                        imageLoadStatus.textContent = 'The image was generated, but the browser could not load it.';
                    }
                });
            }
        })();
    </script>
}

Razor Page Model: Pages/Index.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using aspnet10aiimg.Services;

namespace aspnet10aiimg.Pages;

public class IndexModel(AzureOpenAiImageService imageService, PromptQualityService promptQualityService) : PageModel
{
    [BindProperty]
    [Required(ErrorMessage = "Enter a prompt to generate an image.")]
    [StringLength(4000, MinimumLength = 3, ErrorMessage = "Prompt must be between 3 and 4000 characters.")]
    public string Prompt { get; set; } = string.Empty;

    public string? GeneratedImageDataUri { get; private set; }

    public string? ErrorMessage { get; private set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (!IsPromptAllowed())
        {
            return Page();
        }

        try
        {
            GeneratedImageDataUri = await imageService.GenerateImageDataUriAsync(Prompt, HttpContext.RequestAborted);
        }
        catch (AzureOpenAiImageException ex)
        {
            ErrorMessage = ex.Message;
        }

        return Page();
    }

    public async Task<IActionResult> OnPostGenerateAsync()
    {
        if (!ModelState.IsValid)
        {
            return new JsonResult(new
            {
                success = false,
                error = ModelState[nameof(Prompt)]?.Errors.FirstOrDefault()?.ErrorMessage ?? "Enter a valid prompt."
            })
            {
                StatusCode = StatusCodes.Status400BadRequest
            };
        }

        var promptValidationError = GetPromptValidationError();

        if (promptValidationError is not null)
        {
            return new JsonResult(new
            {
                success = false,
                error = promptValidationError
            })
            {
                StatusCode = StatusCodes.Status400BadRequest
            };
        }

        try
        {
            var imageDataUri = await imageService.GenerateImageDataUriAsync(Prompt, HttpContext.RequestAborted);

            return new JsonResult(new
            {
                success = true,
                imageDataUri
            });
        }
        catch (AzureOpenAiImageException ex)
        {
            return new JsonResult(new
            {
                success = false,
                error = ex.Message
            })
            {
                StatusCode = StatusCodes.Status502BadGateway
            };
        }
        catch (OperationCanceledException) when (!HttpContext.RequestAborted.IsCancellationRequested)
        {
            return new JsonResult(new
            {
                success = false,
                error = "Image generation timed out. Try a simpler prompt or try again."
            })
            {
                StatusCode = StatusCodes.Status504GatewayTimeout
            };
        }
    }

    private bool IsPromptAllowed()
    {
        var error = GetPromptValidationError();

        if (error is null)
        {
            return true;
        }

        ModelState.AddModelError(nameof(Prompt), error);
        return false;
    }

    private string? GetPromptValidationError()
    {
        var result = promptQualityService.Validate(Prompt);

        return result.IsAllowed
            ? null
            : $"Prompt blocked. Remove restricted words or unsafe content before generating an image. Matched category: {result.Category}.";
    }
}

Azure OpenAI Client: Services/AzureOpenAiImageService.cs

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace aspnet10aiimg.Services;

public sealed class AzureOpenAiImageService(HttpClient httpClient, IConfiguration configuration)
{
    private const string PngMediaType = "image/png";

    public async Task<string> GenerateImageDataUriAsync(string prompt, CancellationToken cancellationToken)
    {
        var settings = configuration.GetSection("AzureOpenAI").Get<AzureOpenAiSettings>() ?? new AzureOpenAiSettings();
        settings.Validate();

        var endpoint = settings.Endpoint.TrimEnd('/');
        var requestUri = $"{endpoint}/openai/deployments/{Uri.EscapeDataString(settings.DeploymentName)}/images/generations?api-version={Uri.EscapeDataString(settings.ApiVersion)}";

        using var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
        request.Headers.Add("api-key", settings.ApiKey);

        var body = new
        {
            prompt,
            n = 1,
            size = settings.ImageSize
        };

        request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
        request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

        HttpResponseMessage response;
        string responseBody;

        try
        {
            response = await httpClient.SendAsync(request, cancellationToken);
            responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new AzureOpenAiImageException("Azure OpenAI image generation timed out. Try a simpler prompt or try again.");
        }
        catch (HttpRequestException ex)
        {
            throw new AzureOpenAiImageException($"Azure OpenAI request failed: {ex.Message}");
        }

        using (response)
        {
            if (!response.IsSuccessStatusCode)
            {
                throw new AzureOpenAiImageException($"Azure OpenAI image generation failed ({(int)response.StatusCode}): {ReadErrorMessage(responseBody)}");
            }

            var image = JsonSerializer.Deserialize<ImageGenerationResponse>(responseBody, JsonOptions)
                ?.Data
                ?.FirstOrDefault();

            if (!string.IsNullOrWhiteSpace(image?.B64Json))
            {
                return $"data:{PngMediaType};base64,{image.B64Json}";
            }

            if (!string.IsNullOrWhiteSpace(image?.Url))
            {
                return image.Url;
            }
        }

        throw new AzureOpenAiImageException("Azure OpenAI did not return image data.");
    }

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    private static string ReadErrorMessage(string responseBody)
    {
        if (string.IsNullOrWhiteSpace(responseBody))
        {
            return "No error details were returned.";
        }

        try
        {
            var errorResponse = JsonSerializer.Deserialize<AzureOpenAiErrorResponse>(responseBody, JsonOptions);
            return errorResponse?.Error?.Message ?? responseBody;
        }
        catch (JsonException)
        {
            return responseBody;
        }
    }

    private sealed class AzureOpenAiSettings
    {
        public string Endpoint { get; init; } = string.Empty;

        public string ApiKey { get; init; } = string.Empty;

        public string DeploymentName { get; init; } = string.Empty;

        public string ApiVersion { get; init; } = "2025-04-01-preview";

        public string ImageSize { get; init; } = "1024x1024";

        public void Validate()
        {
            if (string.IsNullOrWhiteSpace(Endpoint) || Endpoint.Contains("YOUR-RESOURCE-NAME", StringComparison.OrdinalIgnoreCase))
            {
                throw new AzureOpenAiImageException("Configure AzureOpenAI:Endpoint with your Azure OpenAI resource endpoint.");
            }

            if (string.IsNullOrWhiteSpace(ApiKey))
            {
                throw new AzureOpenAiImageException("Configure AzureOpenAI:ApiKey using user secrets, environment variables, or local appsettings.");
            }

            if (string.IsNullOrWhiteSpace(DeploymentName))
            {
                throw new AzureOpenAiImageException("Configure AzureOpenAI:DeploymentName with your Azure OpenAI image deployment name.");
            }

            if (string.IsNullOrWhiteSpace(ApiVersion))
            {
                throw new AzureOpenAiImageException("Configure AzureOpenAI:ApiVersion.");
            }

            if (string.IsNullOrWhiteSpace(ImageSize))
            {
                throw new AzureOpenAiImageException("Configure AzureOpenAI:ImageSize.");
            }
        }
    }

    private sealed class ImageGenerationResponse
    {
        public List<ImageGenerationData>? Data { get; init; }
    }

    private sealed class ImageGenerationData
    {
        [JsonPropertyName("b64_json")]
        public string? B64Json { get; init; }

        public string? Url { get; init; }
    }

    private sealed class AzureOpenAiErrorResponse
    {
        public AzureOpenAiError? Error { get; init; }
    }

    private sealed class AzureOpenAiError
    {
        public string? Message { get; init; }
    }
}

public sealed class AzureOpenAiImageException(string message) : Exception(message);

Prompt Quality Verification: Services/PromptQualityService.cs

using System.Text.RegularExpressions;

namespace aspnet10aiimg.Services;

public sealed class PromptQualityService(IConfiguration configuration)
{
    private static readonly Dictionary<string, string[]> DefaultBlockedTerms = new(StringComparer.OrdinalIgnoreCase)
    {
        ["bad words"] =
        [
            "damn", "hell", "crap", "shit", "fuck", "bitch", "bastard", "asshole"
        ],
        ["adult content"] =
        [
            "sex", "sexual", "porn", "pornographic", "nude", "naked", "erotic", "xxx", "fetish"
        ],
        ["violence"] =
        [
            "kill", "murder", "blood", "gore", "torture", "weapon", "gun", "knife", "shoot", "stab", "bomb", "explosion", "beheading"
        ],
        ["mob or crime"] =
        [
            "mob", "mafia", "gang", "gangster", "cartel", "drug trafficking", "kidnap", "ransom", "robbery", "terrorist"
        ],
        ["unsafe content"] =
        [
            "hate", "racist", "suicide", "self-harm", "abuse", "harassment"
        ]
    };

    public PromptQualityResult Validate(string prompt)
    {
        if (string.IsNullOrWhiteSpace(prompt))
        {
            return PromptQualityResult.Allowed;
        }

        foreach (var (category, terms) in GetBlockedTermsByCategory())
        {
            foreach (var term in terms)
            {
                if (ContainsBlockedTerm(prompt, term))
                {
                    return PromptQualityResult.Blocked(category);
                }
            }
        }

        return PromptQualityResult.Allowed;
    }

    private Dictionary<string, string[]> GetBlockedTermsByCategory()
    {
        var configuredTerms = configuration
            .GetSection("PromptModeration:BlockedWords")
            .Get<string[]>()
            ?.Where(term => !string.IsNullOrWhiteSpace(term))
            .Select(term => term.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        if (configuredTerms is null or { Length: 0 })
        {
            return DefaultBlockedTerms;
        }

        var mergedTerms = DefaultBlockedTerms.ToDictionary(
            pair => pair.Key,
            pair => pair.Value,
            StringComparer.OrdinalIgnoreCase);

        mergedTerms["configured restricted words"] = configuredTerms;

        return mergedTerms;
    }

    private static bool ContainsBlockedTerm(string prompt, string term)
    {
        var pattern = $@"(?<![\p{{L}}\p{{N}}]){Regex.Escape(term)}(?![\p{{L}}\p{{N}}])";

        return Regex.IsMatch(prompt, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
    }
}

public sealed record PromptQualityResult(bool IsAllowed, string? Category)
{
    public static readonly PromptQualityResult Allowed = new(true, null);

    public static PromptQualityResult Blocked(string category) => new(false, category);
}

Shared Layout: Pages/Shared/_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - aspnet10aiimg</title>
    <script type="importmap"></script>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/aspnet10aiimg.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">aspnet10aiimg</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

How to run the project

cd aspnet10aiimg
export PATH="$HOME/.dotnet:$PATH"
dotnet run

After the application starts, open the displayed local URL in the browser. Enter a safe image prompt, e.g. "Robots playing Boxing" click Create image, watch the progress bar, then save the generated image using the Save image button.

Code for this article can be downloaded from this link.



Popular posts from this blog

Azure AI Building RAG Application Solution: Using Azure AI Search Service and Creating Data Source, Index, and Indexer

ASP.NET Core 8 API: How to implement REPR Pattern Endpoints in ASP.NET Core 8 API

Azure AI Document Intelligence: Processing an Invoice and Saving it in Azure SQL Server Database using Azure Functions