Sphere10.Framework.Web.AspNetCore.MVC 3.2.0-ci.163.43664782937.1

This is a prerelease version of Sphere10.Framework.Web.AspNetCore.MVC.
dotnet add package Sphere10.Framework.Web.AspNetCore.MVC --version 3.2.0-ci.163.43664782937.1
                    
NuGet\Install-Package Sphere10.Framework.Web.AspNetCore.MVC -Version 3.2.0-ci.163.43664782937.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sphere10.Framework.Web.AspNetCore.MVC" Version="3.2.0-ci.163.43664782937.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sphere10.Framework.Web.AspNetCore.MVC" Version="3.2.0-ci.163.43664782937.1" />
                    
Directory.Packages.props
<PackageReference Include="Sphere10.Framework.Web.AspNetCore.MVC" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sphere10.Framework.Web.AspNetCore.MVC --version 3.2.0-ci.163.43664782937.1
                    
#r "nuget: Sphere10.Framework.Web.AspNetCore.MVC, 3.2.0-ci.163.43664782937.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sphere10.Framework.Web.AspNetCore.MVC@3.2.0-ci.163.43664782937.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sphere10.Framework.Web.AspNetCore.MVC&version=3.2.0-ci.163.43664782937.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sphere10.Framework.Web.AspNetCore.MVC&version=3.2.0-ci.163.43664782937.1&prerelease
                    
Install as a Cake Tool

Sphere10.Framework.Web.AspNetCore.MVC

This library extends ASP.NET Core MVC applications that use Sphere10 Framework with action results, Razor HTML helpers, AJAX Bootstrap forms and action filters. It builds on the shared ASP.NET Core integration and does not register controllers, routes or client scripts by itself.

Features

Area APIs
Action results XmlResult for serialized objects or existing XML, and the Windows-only ImageResult for System.Drawing images.
Views Bootstrap HTML helpers, enum select lists and Controller.RenderViewAsync.
AJAX forms FormModelBase, BootstrapFormScope<TModel>, FormScopeOptions, FormResult and the embedded forms script.
Validation and filters ModelState extensions, CleanupAttribute, FilterFactoryBase<TFilter> and FormActionAttribute.
Routing and URLs UrlExtensions.Content and CustomRouteAttribute.

Installation

Reference the package from an ASP.NET Core project that targets .NET 10 or later:

dotnet add package Sphere10.Framework.Web.AspNetCore.MVC

Pass --version to match the other Sphere10.Framework packages in the application, and --source to restore from a local folder feed. The root README explains how to build and publish the packages, including to a private folder feed.

To build against the source instead, reference the project. From the repository root, these commands create the host for the examples below, beside the repository:

dotnet new web --name MvcDemo --output ../MvcDemo --framework net10.0
dotnet add ../MvcDemo/MvcDemo.csproj reference src/Sphere10.Framework.Web.AspNetCore.MVC/Sphere10.Framework.Web.AspNetCore.MVC.csproj

Quick start: serve a sitemap

Replace MvcDemo/Program.cs with:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
app.Run();

Add Controllers/SitemapController.cs:

using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Controllers;

public class SitemapController : Controller {
	[HttpGet("sitemap.xml")]
	public IActionResult Index() {
		var sitemap = new SitemapXml();
		sitemap.Add("https://example.com/", frequency: SitemapXml.Frequency.Weekly, priority: 1.0);
		return new XmlResult(sitemap);
	}
}

Run the host from the repository root, then open /sitemap.xml at the address that it prints:

dotnet run --project ../MvcDemo -p:BuildRevision=0

XmlResult is a ContentResult with the application/xml content type and status 200 unless you pass another status code:

Constructor Response body
XmlResult(object), XmlResult(object, statusCode) The object serialized by Tools.Xml.WriteToString, with the namespace of its [XmlRoot] as the default namespace.
XmlResult(string), XmlResult(string, statusCode) The string as given, so a string argument is never serialized.
XmlResult(), XmlResult(statusCode) Empty.

XML results do not need the Framework to be started. Add the registration and start calls from the shared hosting quick start when the application needs Framework modules and lifecycle services.

Domains

Every folder of Sphere10.Framework.Web.AspNetCore.MVC is a domain with its own README, which lists all of its public types.

Domain What it provides Source
Extensions Extension methods for MVC controllers, Razor HTML helpers, URL helpers, select lists and model state. source
Filters MVC action filter attributes for per-action cleanup, service-resolved filters and form-tag flags. source
Forms Bootstrap AJAX form models, rendering scope, options and JSON results for the embedded forms script. source
Processing A Windows-only action result that writes a System.Drawing image to the response. source
Routing A controller attribute that declares conventional routes and registers them from an assembly. source
XML An action result that returns serialized objects or existing XML as application/xml. source

Project root

Types declared in the project folder itself:

Type Kind Purpose
Tools.Web.Mvc tools Builds an MVC SelectList from an enum type, labelled by each member's description, with an optional selected value and sort direction.

Select lists

using System;
using Sphere10.Framework;

var weekdays = Tools.Web.Mvc.ToSelectList<DayOfWeek>(DayOfWeek.Monday, SortDirection.Ascending);

Pass the returned SelectList to Html.DropDownListFor or Html.BootstrapDropDownListFor. Each option's value is the enum member's name, and its label comes from Tools.Enums.GetDescription, which reads a [Description] attribute and falls back to the name. The optional sort orders the labels and keeps the selected value. ToSelectList(Type, ...) takes the enum type at run time instead.

For other collections, the ToSelectList(text, value, defaultOption, selected) extension on IEnumerable<T> builds SelectListItem objects from delegates. A defaultOption becomes the first item, with the value -1.

Bootstrap AJAX forms

BootstrapFormScope<TModel> renders a form that the embedded hydrogen-bootstrap-forms-1.0.0.js script submits with jQuery AJAX. The action answers with a FormResult, and the script then shows a message, redirects, or replaces the page or the form. The steps below add a contact form to the sitemap host.

Prerequisites

The application supplies the client libraries, because this package neither downloads nor serves them:

  • jQuery, loaded before Bootstrap. The script calls Bootstrap's jQuery tooltip and popover plugins, which Bootstrap registers only when jQuery is present.
  • Bootstrap 5, whose data-bs-* attributes and form-control, form-select and btn-close classes the helpers and the script use.
  • Choices.js for any drop-down that BootstrapDropDownListFor renders with its default useChoicesJS: true. Without the library, the form's initialization stops at that element and the form is not converted to AJAX, so pass useChoicesJS: false for a plain select element.
  • Font Awesome, optionally, for the fa icons in result alerts.

The example's view loads them from wwwroot/lib/jquery/jquery.min.js, wwwroot/lib/bootstrap/bootstrap.bundle.min.js and wwwroot/lib/bootstrap/bootstrap.min.css.

Serve the embedded script

The script is an embedded resource of the MVC assembly, not a static web asset. Add these imports at the top of Program.cs and the endpoint before app.Run():

using Microsoft.AspNetCore.Http;
using Sphere10.Framework;
using Sphere10.Framework.Web.AspNetCore.MVC;

app.MapGet("/sphere10/forms.js", () => {
	var script = typeof(FormModelBase).Assembly.GetManifestResourceStream(
		"Sphere10.Framework.Web.AspNetCore.MVC.Forms.hydrogen-bootstrap-forms-1.0.0.js"
	);
	Guard.Ensure(script != null, "The embedded forms script was not found.");
	return Results.Stream(script, "text/javascript");
});

Results.Stream disposes the resource stream once the response is written. Load the script in the page head, without defer or async, because each form calls the script's F_Init function from an inline script.

Define the model and controller

Save Models/ContactForm.cs:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Models;

public sealed class ContactForm : FormModelBase {
	[Required]
	[DisplayName("Your name")]
	public string Name { get; set; } = string.Empty;
}

FormModelBase gives each model a random, DOM-safe ID, which becomes the form's element ID, and a SubmitCount that the script increments on every submission.

Save Controllers/ContactController.cs:

using Microsoft.AspNetCore.Mvc;
using MvcDemo.Models;
using Newtonsoft.Json;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Controllers;

[Route("contact")]
public class ContactController : Controller {
	[HttpGet]
	public IActionResult Index() => View(new ContactForm());

	[HttpPost]
	[ValidateAntiForgeryToken]
	public IActionResult Submit(ContactForm model) {
		var result = new FormResult {
			Result = ModelState.IsValid,
			ResultType = FormResultType.ShowMessage,
			Message = ModelState.IsValid ? "Demo submission accepted. No data was stored." : "Enter your name."
		};
		return Content(JsonConvert.SerializeObject(result), "application/json");
	}
}

FormResult carries Newtonsoft.Json attributes, so the action serializes it with JsonConvert to produce the result, message, type, url and content fields that the script reads. ASP.NET Core's default System.Text.Json output would name the type field resultType and write it as a number, so returning Json(result) does not work. No MVC Newtonsoft integration package is needed, because this library already references Newtonsoft.Json.

Render the form

Save Views/Contact/Index.cshtml:

@using Microsoft.AspNetCore.Mvc.Rendering
@using Sphere10.Framework.Web.AspNetCore.MVC
@model MvcDemo.Models.ContactForm
@{
	Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>Contact form demo</title>
	<link rel="stylesheet" href="/lib/bootstrap/bootstrap.min.css" />
	<script src="/lib/jquery/jquery.min.js"></script>
	<script src="/lib/bootstrap/bootstrap.bundle.min.js"></script>
	<script src="/sphere10/forms.js"></script>
</head>
<body>
	<main class="container py-4">
		@using (Html.BeginBootstrapForm("/contact", Model, options: FormScopeOptions.UseLoadingOverlay | FormScopeOptions.AddAntiForgeryToken)) {
			@Html.BootstrapLabelFor(model => model.Name)
			@Html.BootstrapTextBoxFor(model => model.Name)
			@Html.BootstrapFormButton(Model, "Submit")
		}
	</main>
</body>
</html>

Stop the host, run it again and open /contact. Submitting the form posts it with AJAX and shows the action's message at the end of the form. The field keeps its value after submission, because the options omit ClearOnSuccess.

The scope writes the <form> tag with the model's ID, the hidden ID and SubmitCount fields, the antiforgery token and, with UseLoadingOverlay, a loading overlay. Disposing the scope writes a {formId}_message_marker element, the F_Init call and the closing tag.

The embedded script shows each result in an alert after that marker, and later results replace the alert. A page can instead supply an element whose ID is {formId}_result, which the script replaces with the alert. Call MessageMarker() to place the scope's marker earlier in the form, and the scope then omits it on disposal.

BootstrapFormScope implements both Dispose and DisposeAsync. DisposeAsync writes the closing markup with asynchronous writes only.

Results and options

FormResultType type value Script behavior
ShowMessage message Shows message in a success alert when result is true and in a danger alert otherwise.
Redirect redirect Navigates to url.
ReplacePage replace_page Replaces the whole document with content.
ReplaceForm replace_form Replaces the form with content, then shows message when it is set.
FormScopeOptions flag Effect
UseLoadingOverlay Renders an overlay inside the form tag, which the script shows during each submission to block input. Without the flag, the scope renders no overlay and the script shows none.
ClearOnSuccess Restores the form's original markup after a successful result.
OmitFormTag Writes only the marker and the F_Init call, so the view supplies a <form> element whose ID is the model's ID.
AddAntiForgeryToken Writes an antiforgery token inside the form.
BotProtect Base64-encodes the action URL, which the script decodes before posting. It requires the URL overload of BeginBootstrapForm, because the action and controller overload throws with this flag.
UseGet Declares get on the form tag, and the script sends the fields in the query string of a GET request instead of posting them. The action must accept GET.
SmallerBlockerOverlay Sizes the overlay at 90% of the form instead of 100%.
Default UseLoadingOverlay, AddAntiForgeryToken, BotProtect and ClearOnSuccess.
DefaultRetainInput Default without ClearOnSuccess.

BotProtect deters generic form bots only. It does not replace server-side validation or the antiforgery check.

Bootstrap input helpers

The Bootstrap*For helpers wrap the MVC *For helpers with Bootstrap 5 markup. Each takes its label or placeholder text from the member's [DisplayName] attribute and appends * for a [Required] member. When ModelState holds errors for the member, the input gets the is-invalid class and a Bootstrap tooltip that lists the errors.

Helper Renders
BootstrapLabelFor A label.
BootstrapTextBoxFor, BootstrapPasswordFor A form-control input. The text box can be read-only.
BootstrapTextAreaFor A form-control text area whose height follows approxRows.
BootstrapDropDownListFor A form-select element with the display name as its empty option, enhanced by Choices.js unless useChoicesJS is false.
BootstrapCheckBoxAndLabelFor A form-check check box and its label.
BootstrapFormButton The submit button of a form scope, with a spinner that the script shows during submission.
ValidationMessageForEx A validation message styled as a Bootstrap warning badge, with the badge bg-warning text-dark classes, optionally floated to the end.

Render a view to a string

Controller.RenderViewAsync(viewName, model, partial) renders an MVC view into a string, for example as the content of a ReplaceForm result. An empty viewName selects the current action's view, and partial: true renders it as a partial view. When the view engine finds no view, the method returns the text A view with the name <viewName> could not be found instead of throwing, so check for a missing view before you send the result.

To replace the contact form with a thank-you fragment, save Views/Contact/Thanks.cshtml:

@model MvcDemo.Models.ContactForm
<div class="alert alert-success">Thank you, @Model.Name.</div>

Then change the Submit action of ContactController to the version below, and add using System.Threading.Tasks; and using Sphere10.Framework; to the file:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(ContactForm model) {
	var nameCheck = model.Name == "admin" ? Result.Error("Choose another name.") : Result.Success;
	ModelState.AddModelError(nameof(ContactForm.Name), nameCheck);

	var result = ModelState.IsValid
		? new FormResult {
			Result = true,
			ResultType = FormResultType.ReplaceForm,
			Content = await this.RenderViewAsync("Thanks", model, partial: true)
		}
		: new FormResult {
			Result = false,
			ResultType = FormResultType.ShowMessage,
			Message = "Correct the highlighted fields."
		};
	return Content(JsonConvert.SerializeObject(result), "application/json");
}

RenderViewAsync is an extension method, so a controller calls it through this. AddModelError(key, Result) adds each error message of a Sphere10.Framework.Result under the key, and a successful Result adds nothing. The other overloads add a Result or a formatted message under the empty key, and Remove(params string[]) removes several keys at once.

Images

ImageResult writes a System.Drawing.Image to the response as JPEG, which is the default, or as PNG or GIF, and throws for any other ImageFormat. It is marked Windows-only and disposes the image after writing, so do not reuse the image. MVC executes it asynchronously: it encodes the image in memory, sets the content length and copies the bytes to the response body with asynchronous writes, so AllowSynchronousIO is not needed. Only a direct call to the synchronous ExecuteResult saves straight to the response body.

This controller draws a small PNG badge on each request:

using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Controllers;

[SupportedOSPlatform("windows")]
public class BadgeController : Controller {
	[HttpGet("badge.png")]
	public IActionResult Badge() {
		var bitmap = new Bitmap(120, 24);
		using (var graphics = Graphics.FromImage(bitmap)) {
			graphics.Clear(Color.SteelBlue);
			graphics.DrawString("Sphere10", SystemFonts.DefaultFont, Brushes.White, 4, 4);
		}
		return new ImageResult(bitmap, ImageFormat.Png);
	}
}

The result disposes the bitmap after writing it, so the action does not dispose it. The one-argument constructor writes JPEG.

Filters, routing and URLs

API Behavior
CleanupAttribute Gives the action a ViewBag.Cleanup list of Action delegates and runs them after the result executes. A failing delegate is logged through SystemLog, and the next one still runs.
FilterFactoryBase<TFilter> Base attribute that resolves TFilter from the request services on every request. Register TFilter in the service collection, because an unregistered filter throws InvalidOperationException.
FormActionAttribute Sets ViewData[FormActionAttribute.OmitFormTag] to true for the view to read. Nothing in the library reads the flag.
ModelStateDictionaryExtensions Adds the error messages of a Result, or a formatted message, as model errors, and removes several keys at once.
UrlExtensions.Content(path, toAbsolute) Extends IUrlHelper, so views call it as Url.Content(path, true). It resolves a content path and, with toAbsolute, makes it absolute for the helper's current request. When the host carries no port, the absolute URL omits it.
CustomRouteAttribute Declares routes on a controller class. CustomRouteAttribute.RegisterCustomRoutesFromAssembly(app, assembly) maps each attribute of every controller in the assembly as a conventional controller route, ordered by Priority, with the controller and DefaultAction as defaults. A route may not contain {controller}. Attribute routing such as [Route] and [HttpGet("...")] remains the usual ASP.NET Core choice.

Filters example

This controller exports a temporary file, deletes it after the response is written and logs each call through a filter that the service provider creates. Save it as Controllers/ReportsController.cs:

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Controllers;

public sealed class AuditFilter : IActionFilter {
	private readonly ILogger<AuditFilter> _logger;

	public AuditFilter(ILogger<AuditFilter> logger) {
		_logger = logger;
	}

	public void OnActionExecuting(ActionExecutingContext context)
		=> _logger.LogInformation("Running {Action}", context.ActionDescriptor.DisplayName);

	public void OnActionExecuted(ActionExecutedContext context) {
	}
}

public sealed class AuditAttribute : FilterFactoryBase<AuditFilter> {
}

[Route("reports")]
public class ReportsController : Controller {
	[HttpGet("export")]
	[Audit]
	[Cleanup]
	public IActionResult Export() {
		var path = Path.GetTempFileName();
		System.IO.File.WriteAllText(path, "Name,Visits\nAlice,3\n");
		((List<Action>)ViewBag.Cleanup).Add(() => System.IO.File.Delete(path));
		return PhysicalFile(path, "text/csv", "report.csv");
	}
}

Register the filter before builder.Build() in Program.cs, because FilterFactoryBase resolves it from the request services:

builder.Services.AddScoped<MvcDemo.Controllers.AuditFilter>();

ViewBag.Cleanup is dynamic, so cast it to List<Action> before you add a lambda. CleanupAttribute runs the delegates after the file result has been sent, so /reports/export downloads the file before it is deleted.

Custom routes example

CustomRouteAttribute maps conventional routes without attribute routing on the controller. Save Controllers/HelpController.cs:

using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web.AspNetCore.MVC;

namespace MvcDemo.Controllers;

[CustomRoute("help/{topic}", DefaultAction = "Topic", Defaults = "topic=index")]
public class HelpController : Controller {
	public IActionResult Topic(string topic) => Content($"Help topic: {topic}");
}

Then register the routes of every controller in the assembly, before app.Run() in Program.cs:

using Sphere10.Framework.Web.AspNetCore.MVC;

CustomRouteAttribute.RegisterCustomRoutesFromAssembly(app, typeof(MvcDemo.Controllers.HelpController).Assembly);

/help/paging then returns Help topic: paging, and /help uses the default topic index. Defaults takes key=value pairs joined with &, and DefaultAction adds the action default.

Structure and dependencies

Folder or file Contents
Extensions/ Controller, HTML helper, URL, select-list and model-state extensions.
Forms/ Form model, options, scope and result types, and the embedded forms script.
Filters/ CleanupAttribute, FilterFactoryBase<TFilter> and FormActionAttribute.
Routing/ CustomRouteAttribute.
XML/ XmlResult.
Processing/ ImageResult.
MvcTool.cs Tools.Web.Mvc.ToSelectList.

The project targets net10.0, and also net11.0 when a .NET 11 SDK builds it. It has a Microsoft.AspNetCore.App framework reference, project references to the shared Web.AspNetCore layer and to Drawing, and a Newtonsoft.Json package reference for the form types. The Drawing reference serves only ImageResult. The host-independent Web library suffices when an application needs only the web models.

Upgrading from the combined library

Releases up to 3.1.2 shipped these types inside Sphere10.Framework.Web.AspNetCore, in the Sphere10.Framework.Web.AspNetCore namespace. Reference this package and import Sphere10.Framework.Web.AspNetCore.MVC for the controller and view helpers, forms, filters, routing and results. Replace Tools.Web.AspNetCore.ToSelectList with Tools.Web.Mvc.ToSelectList. The hosting and HTTP types keep their namespaces in the shared package, and the sitemap, navigation and message models moved to Sphere10.Framework.Web.

The forms script is still an embedded resource rather than a static web asset. It moved from the Sphere10.Framework.Web.AspNetCore assembly, where its name was Sphere10.Framework.Web.AspNetCore.Forms.hydrogen-bootstrap-forms-1.0.0.js, to typeof(FormModelBase).Assembly under the name shown above.

Build and verification

From the repository root:

dotnet build src/Sphere10.Framework.Web.AspNetCore.MVC/Sphere10.Framework.Web.AspNetCore.MVC.csproj -p:BuildRevision=0
dotnet test tests/Sphere10.Framework.Web.AspNetCore.Tests -p:BuildRevision=0

The shared web tests check XmlResult status codes and sitemap output, enum list selection and sorting, the embedded script's resource name, asynchronous ImageResult output, FilterFactoryBase resolution, CustomRouteAttribute registration, and BootstrapFormScope completion, form method and overlay options, including a retry after a failed write and asynchronous disposal. The MVC tester is a minimal MVC host with one landing page for further controller and view demonstrations, and it does not include the contact form above. Package validation serves XmlResult output from an external consumer and checks it over HTTP. -p:BuildRevision=0 stops a local build from allocating the next build number.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.2.0-ci.163.43664782937.1 0 11/15/2026
3.2.0-ci.160.43660265392.1 0 11/15/2026
3.2.0-ci.159.43537748063.1 51 11/14/2026
3.2.0-ci.158.43536665157.1 49 11/14/2026
3.2.0-ci.157.43350632870.1 53 11/13/2026
3.2.0-ci.156.43233095581.1 48 11/12/2026