<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[William Gross]]></title><description><![CDATA[Thoughts on simplifying web app development]]></description><link>https://thoughts.wgross.net</link><image><url>https://substackcdn.com/image/fetch/$s_!CU5W!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe106e180-d134-43bf-a26b-b2442cb7fccd_639x639.png</url><title>William Gross</title><link>https://thoughts.wgross.net</link></image><generator>Substack</generator><lastBuildDate>Mon, 18 May 2026 04:10:58 GMT</lastBuildDate><atom:link href="https://thoughts.wgross.net/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[William Gross]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[wgross@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[wgross@substack.com]]></itunes:email><itunes:name><![CDATA[William Gross]]></itunes:name></itunes:owner><itunes:author><![CDATA[William Gross]]></itunes:author><googleplay:owner><![CDATA[wgross@substack.com]]></googleplay:owner><googleplay:email><![CDATA[wgross@substack.com]]></googleplay:email><googleplay:author><![CDATA[William Gross]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Fixing the fundamental mistake in web form processing]]></title><description><![CDATA[Forms series&#8212;part 2]]></description><link>https://thoughts.wgross.net/p/form-processing-fundamental-mistake</link><guid isPermaLink="false">https://thoughts.wgross.net/p/form-processing-fundamental-mistake</guid><dc:creator><![CDATA[William Gross]]></dc:creator><pubDate>Tue, 28 Jan 2025 20:08:26 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4d72a7a0-586d-42a8-835d-f7526a13f1ab_1555x1274.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article is <strong>part two</strong> of the forms series:</p><ul><li><p>Intro and Part 1:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;0a890627-a878-468d-862f-a60a17369537&quot;,&quot;caption&quot;:&quot;My name is William Gross, and I am not a writer.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Forms in web apps: a new approach&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:4411369,&quot;name&quot;:&quot;William Gross&quot;,&quot;bio&quot;:null,&quot;photo_url&quot;:null,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-01-09T18:06:58.589Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe106e180-d134-43bf-a26b-b2442cb7fccd_639x639.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:151571194,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;William Gross&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe106e180-d134-43bf-a26b-b2442cb7fccd_639x639.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div></li><li><p>Part 2: Fixing the fundamental mistake in web form processing (this article)</p></li></ul><div><hr></div><p>There is a problem in the foundation of today&#8217;s most popular web frameworks. It&#8217;s been there all along, but maybe wasn&#8217;t apparent in the early days. It still doesn&#8217;t seem to be apparent to most developers. At least not directly, because what these frameworks&#8217; maintainers have done is address the symptoms of this problem without ever acknowledging the cause.</p><p>In this article and the entire series, I&#8217;m referring to traditional, server-side web frameworks. I&#8217;ve <a href="https://wgross.net/essays/spas-are-harder">known for a long time</a> that they&#8217;re the simplest choice for building forms. Here&#8217;s how most of these frameworks route a form&#8217;s HTTP requests<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cjuP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cjuP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cjuP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:170382,&quot;alt&quot;:&quot;Diagram showing traditional form processing, with initial GET routed to view and subsequent POST routed to action method&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing traditional form processing, with initial GET routed to view and subsequent POST routed to action method" title="Diagram showing traditional form processing, with initial GET routed to view and subsequent POST routed to action method" srcset="https://substackcdn.com/image/fetch/$s_!cjuP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!cjuP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76d6eca7-c736-4c24-bc26-314bc9461ad1_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">HTTP <code>GET</code> requests are routed to a view, which renders the form. <code>POST</code> requests are routed to an action method, which processes the submitted data.</figcaption></figure></div><p>The problem with routing form submissions directly to action methods is lack of context. The action method receives a raw list of form data in the request body and needs to make sense of it. And making sense of it requires <em>duplicating knowledge that is already in the view</em>, namely which data entities and fields exist on the form and what modifications are permitted. Frameworks have partially addressed this with features like structured naming of form controls, which enables automatic mapping to trees of model objects.</p><p>But there are essential problems with this architecture that cannot be overcome. Developers must <a href="https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters">restate</a> <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/forms#mitigate-overposting-attacks">the</a> <a href="https://docs.djangoproject.com/en/5.1/topics/forms/#the-django-form-class">list</a> <a href="https://laravel.com/docs/11.x/eloquent#mass-assignment">of</a> <a href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-initbinder.html#mvc-ann-initbinder-model-design">form</a> <a href="https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/crud#security-note-about-overposting">fields</a> in the action to defend against <a href="https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html">mass assignment attacks</a>. For any drop-down widget, they must reestablish the list of options that was shown to the user, to ensure that a malicious, handcrafted <code>POST</code> request cannot succeed in selecting a choice meant to be unavailable. Data types and formats must match those used by the rendered form. The list goes on, and complexity multiplies when the form expands with new features. It&#8217;s all an unnecessary maintenance burden.</p><p>Why did we do this? Why did we throw <a href="https://media.pragprog.com/titles/tpp20/dry.pdf">Don&#8217;t Repeat Yourself</a> under the bus? Because of the popular belief that spending the time to fully rebuild the view before even beginning to process the submission <em>can&#8217;t possibly</em> be a good idea. That would mean&#8230;building two views (or the same view twice) on a single request! How could such a flow ever perform well enough for production use?</p><p>Try to set this concern aside for a minute. Think about a humbler design. What if we simplify things, reduce our maintenance costs, and, yes, admit that we don&#8217;t need absolute maximum performance?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-ekk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-ekk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-ekk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg" width="1456" height="966" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:966,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1271039,&quot;alt&quot;:&quot;Two-track railroad bridge with one track removed&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two-track railroad bridge with one track removed" title="Two-track railroad bridge with one track removed" srcset="https://substackcdn.com/image/fetch/$s_!-ekk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-ekk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfa04ee8-e098-485f-888a-bdb60a07eb8e_1920x1274.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>ASP.NET Web Forms: The Good Part</h2><p>I began my career building apps with the ASP.NET Web Forms framework from Microsoft. The critics are right: it got a lot wrong. But one thing it got right was routing form <code>POST</code> requests back to the page that created the form, and enabling the page to &#8220;rebuild itself&#8221; before receiving the field values.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!m-fZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!m-fZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!m-fZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:155661,&quot;alt&quot;:&quot;Diagram showing the way form processing should be, with GET and POST requests routed to page, and POST requests then executing action method&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing the way form processing should be, with GET and POST requests routed to page, and POST requests then executing action method" title="Diagram showing the way form processing should be, with GET and POST requests routed to page, and POST requests then executing action method" srcset="https://substackcdn.com/image/fetch/$s_!m-fZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!m-fZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0157a5c6-a9c0-4834-a721-946ed5158c68_2560x1440.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">All requests <em>should</em> be routed to the page. The form should always be created, and the action method should have it available as context. It doesn&#8217;t matter that it won&#8217;t be rendered.</figcaption></figure></div><p>The Enterprise Web Framework (EWF) in <a href="https://enterpriseweblibrary.org/">EWL</a> borrows this concept; you can see how it translates to code <a href="https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach">in the previous article</a>. And back to performance: while it might seem expensive to rebuild the page on <code>POST</code>, in my experience it&#8217;s never been a problem. I&#8217;ve also learned that a framework can use deferred execution (as EWF does) to avoid <em>fully</em> rebuilding the page all the way through to a finished HTML document. It can merely create enough of a skeleton to know what and where the form fields are.</p><h2>Zooming in on fields</h2><p>Take the expression for the movie price control from the previous article:</p><pre><code>movieInsert.GetPriceFormItem( minValue: 0, maxValue: 100 )</code></pre><p>As I mentioned there, this is all you need for an HTML <code>input type=number</code> control that is bound to the movie price field. It handles the control, the label, the validation rules, the display of error messages. Mass assignment attacks are not possible, so there&#8217;s no other place where you need to whitelist the price field, or blacklist other movie fields. And again, as I stated in that article, there is no model class anywhere that supports this page. There is only the database, and automatically generated code.</p><p>The formatting of the decimal number as a string, for HTML, and the parsing of a submitted string back into a number, are both encapsulated within this expression. It is not possible to have any type of a formatting mismatch. If you needed to use some type of custom widget with its own formatting requirements, you would likewise have both translations (number &#8594; string and string &#8594; number) defined in the same place.</p><p>And take the drop-down list with movie ratings:</p><pre><code>movieInsert.GetRatingDropDownFormItem(
  DropDownSetup.Create(
    getRatings().Select( r =&gt; SelectListItem.Create( r, r ) )
  )
)

IReadOnlyCollection&lt;string&gt; getRatings() =&gt; [ "G", "PG", "PG-13" ];</code></pre><p>The same list of ratings is used for rendering and for validation. It&#8217;s impossible for a value that is not listed to make it into the database via this form, even if such a value were valid according to database integrity constraints. This is not just a maintenance time saver; it&#8217;s also the complete elimination of a security risk.</p><p>Imagine having a drop-down list for setting a user&#8217;s role, where the two choices are &#8220;editor&#8221; and &#8220;normal user.&#8221; What if there were a third role, &#8220;admin,&#8221; defined in the database but not accessible on this particular page? Do you see the value of knowing, for sure, that the &#8220;admin&#8221; value cannot be set maliciously through your page? The value of never having to open up Postman or another HTTP manipulation tool to test for such an attack vector?</p><p>Do you see the power, and superiority, of integrating validation/<code>POST</code> logic into the form-rendering code?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://thoughts.wgross.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. To receive new posts in your inbox, please subscribe.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>In MVC frameworks like <a href="https://rubyonrails.org/">Rails</a>, all requests are routed to controller actions, but <code>GET</code>s are typically forwarded immediately to a view, where an HTML-based template will render the form. <code>POST</code>s are routed to a different action within the same controller that persists the submitted data.</p><p>In a component-based framework such as Blazor (in <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes">static server-side rendering mode</a>), requests are routed to pages. <code>GET</code> causes the Razor/HTML portion (i.e. the view) of the page to render, and <code>POST</code> executes a C# method in the page&#8217;s code block. But the method has no awareness of or dependency on the view.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Forms in web apps: a new approach]]></title><description><![CDATA[Forms series - part 1]]></description><link>https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach</link><guid isPermaLink="false">https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach</guid><dc:creator><![CDATA[William Gross]]></dc:creator><pubDate>Thu, 09 Jan 2025 18:06:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe106e180-d134-43bf-a26b-b2442cb7fccd_639x639.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My name is William Gross, and I am not a writer.</p><p>I build and maintain web applications. Specifically, <em>enterprise</em> web applications, according to <a href="https://martinfowler.com/bliki/EnterpriseApplication.html">Martin Fowler&#8217;s definition</a>. And I&#8217;d still be building and maintaining them right now too, if it weren&#8217;t for the white-hot ball of fire inside me that won&#8217;t let me rest until I trap myself in this Substack editor, kicking and screaming, and tell you what I&#8217;ve learned.</p><p>My development experience lies wholly in the Microsoft universe, beginning with Visual Basic and Active Server Pages, continuing into .NET, C#, and Web Forms, and finally moving to .NET 6/7/8/9. But that&#8217;s not the interesting part. The interesting part is that along that journey, in my foolish youth, I decided to bushwhack off the Microsoft-recommended trail and ended up in the wilderness of custom web frameworks. I worked with a few others to build what we call the Enterprise Web Framework (EWF), which is a part of the larger <a href="https://enterpriseweblibrary.org/">EWL</a>.</p><p>After two decades of iteration and improvement, this is likely a good place to be. Our small group has shepherded several applications from their Web Forms inception to their modern life with .NET 9. We have protected their essential business logic from the destructive &#8220;total rewrites&#8221; that are common in the industry. But what&#8217;s certain is that this web framework is unique, and as the only steward of it at the moment, I feel obligated to get the word out.</p><h2>With that said</h2><p>This article is <strong>part one </strong>of the forms series:</p><ul><li><p>Part 1: Forms in web apps: a new approach (this article)</p></li><li><p>Part 2:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;cb3e7a32-dd28-4968-9e7b-a56def092dc3&quot;,&quot;caption&quot;:&quot;This article is part two of the forms series:&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Fixing the fundamental mistake in web form processing&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:4411369,&quot;name&quot;:&quot;William Gross&quot;,&quot;bio&quot;:null,&quot;photo_url&quot;:null,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-01-28T20:08:26.928Z&quot;,&quot;cover_image&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4d72a7a0-586d-42a8-835d-f7526a13f1ab_1555x1274.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://thoughts.wgross.net/p/form-processing-fundamental-mistake&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:155034853,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;William Gross&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe106e180-d134-43bf-a26b-b2442cb7fccd_639x639.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div></li></ul><div><hr></div><p>Let&#8217;s start with the framework&#8217;s approach to forms, which are a key part of these applications. Consider some questions:</p><ul><li><p>What if you could build forms in an entirely programmatic way, with no separation of logic between HTML and code?</p></li><li><p>What if your validation/post logic was inline with the rendering of the form?</p></li><li><p>What if your forms could talk directly to an automatically-generated data access layer, at least in the simpler parts of your application where separate domain modeling isn&#8217;t necessary?</p></li></ul><p>Here&#8217;s what that looks like, in the context of a movie database app that is intentionally similar to the one in the <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app">Blazor tutorial</a>. Take a page that lets users add new movies:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cfow!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cfow!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 424w, https://substackcdn.com/image/fetch/$s_!cfow!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 848w, https://substackcdn.com/image/fetch/$s_!cfow!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 1272w, https://substackcdn.com/image/fetch/$s_!cfow!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cfow!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png" width="1456" height="744" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:744,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76294,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cfow!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 424w, https://substackcdn.com/image/fetch/$s_!cfow!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 848w, https://substackcdn.com/image/fetch/$s_!cfow!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 1272w, https://substackcdn.com/image/fetch/$s_!cfow!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa623a01b-4280-4fb8-8ce5-706449f94821_1577x806.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is the C# code for that page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MFsi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MFsi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MFsi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:409912,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MFsi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 424w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 848w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 1272w, https://substackcdn.com/image/fetch/$s_!MFsi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7cfa240-27d3-436e-86f3-9d648f2627cf_2560x1440.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Curious? <a href="https://youtu.be/4PQeMA-8TiI">View a one-minute video</a> of how this was built.</figcaption></figure></div><p>Aside from database schema (a <code>Movie</code> table with a few columns), <em>what you see above is all there is</em>. No model class, no scaffolded code, no HTML. Just a small amount of application-level configuration (commensurate with what you&#8217;d see in Blazor or other frameworks), some generated code, and a URL pattern defined at the parent level:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8Fbj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8Fbj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 424w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 848w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 1272w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8Fbj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png" width="1456" height="109" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:109,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:78724,&quot;alt&quot;:&quot;AddMovie URL source code&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="AddMovie URL source code" title="AddMovie URL source code" srcset="https://substackcdn.com/image/fetch/$s_!8Fbj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 424w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 848w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 1272w, https://substackcdn.com/image/fetch/$s_!8Fbj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b754495-ef2e-4300-8129-c8b27a5fcce6_3809x284.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://youtu.be/4PQeMA-8TiI&quot;,&quot;text&quot;:&quot;See this built in a 1-minute video&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://youtu.be/4PQeMA-8TiI"><span>See this built in a 1-minute video</span></a></p><h2>But why?</h2><p>Why abstract away the HTML?<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> Fully-programmatic UI is certainly not a good choice for every page in every application. It&#8217;s not good particularly if some of the people making changes are web designers, who may feel quite comfortable editing HTML-based templates but would never touch C# code.</p><p>But it can be an amazing time saver for full-stack developers. Notice that <em>all</em> of the logic for each form control is located together. The control itself, the label, the validation rules. You aren&#8217;t forced to jump around to different files, or scroll to different parts of a file, to add a new control to the form.</p><p>You may wonder how the validation logic on this form even executes, or more generally how a <code>POST</code> request is processed. Answer: the page UI is built again, just as it was on the initial render. And during this process, the validation methods for each control are registered with the <code>PostBack</code> object. After the framework finishes building the UI, it executes the validations in the <code>PostBack</code> followed by its <code>modificationMethod</code>, which in this case performs the SQL insert of the new movie. Finally, it executes the post-modification action, which returns the user to the <code>Movies</code> list page.</p><p>It would be fair to consider this rebuilding-on-post a slight performance compromise. In practice it&#8217;s not a problem due to other optimizations in the framework. On the flip side, it elegantly prevents overposting (i.e. mass assignment) attacks, because the full context of the form is available during validation. In other words, the framework knows which fields the user has available to edit, and will only accept submitted values for those same fields.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Know anyone hungry for a new approach to web app development?</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>For next time</h2><p>This is the first part in a series about forms, and I&#8217;d love to expand on these brief explanations and also cover the more advanced features of the framework. Please let me know if you have questions or if there are specific topics you&#8217;d like me to write about.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://thoughts.wgross.net/p/forms-in-web-apps-a-new-approach/comments"><span>Leave a comment</span></a></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>The framework does support placing blocks of raw HTML on a page, but they can&#8217;t be used for controls that need to send data back to the server. They are mostly intended for regions of the page that contain &#8220;administrator-editable&#8221; content such as the terms and conditions of a purchase, which may change every few months or from year to year.</p></div></div>]]></content:encoded></item></channel></rss>