Skip to content

negotiate idea#6

Merged
lewisrenfrew merged 5 commits into
mainfrom
better-negotiate
May 12, 2026
Merged

negotiate idea#6
lewisrenfrew merged 5 commits into
mainfrom
better-negotiate

Conversation

@lewisrenfrew
Copy link
Copy Markdown
Collaborator

@lewisrenfrew lewisrenfrew commented May 12, 2026

adds a new overload of .Negotiate that takes a Func<Task> instead of an already-evaluated model. means endpoints that serve both the app shell and api data can just do one line instead of the manual accept header check we sometimes implement..

how it differs from the existing negotiate:

right now Negotiate(T model) needs the result already computed by the time you call it. so for endpoints that return both html and json you either hit the database for nothing (html just renders Index.cshtml and chucks the IResult away), or you stick that if (text/html) guard in every single endpoint to avoid the wasted call.

the new Negotiate(Func<Task>) looks at the accept header first. if the client wants text/html it just serves the shell straight away - delegate never runs, no database hit. if it wants json/csv/whatever it invokes the delegate and goes through the normal handler pipeline as before.

also moves the HtmlNegotiator, ViewLoader, and the common models (ApplicationSettings, ViewModel, ViewResponse) into this library so we stop copying them into every project.

why its better:

  • no more double dipping the database when a browser hits an endpoint - for one this causes needless traffic / db load, but its also horrible for slow queries - you end up waiting twice which is doubly painful if slow (huge reqs, big reports, etc)
  • no more boilerplate in every endpoint to check accept headers and bail early for html
  • no more relying on DI registration order actually being correct (the old way only worked because HtmlNegotiator happened to be registered first - UniversalResponseNegotiator says CanHandle=true for everything so if it wins the race you get a 406)
  • harder to mess up - just call res.Negotiate(() => service.DoThing()) and forget about it

@lewisrenfrew lewisrenfrew requested a review from a team May 12, 2026 13:24
Copy link
Copy Markdown

@richardiphillips richardiphillips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like it . is there a way we can still pass in the application settings.
I wonder if that will be limiting having them stuck in library code in the future.

@lewisrenfrew
Copy link
Copy Markdown
Collaborator Author

like it . is there a way we can still pass in the application settings. I wonder if that will be limiting having them stuck in library code in the future.

yeah, I've just added an options class for that - ApplicationSettings now uses a dictionary under the hood with the standard keys as defaults. You can override any of them or add new ones at registration time via HtmlNegotiatorOptions:

services.AddSingleton<IResponseNegotiator>(sp =>
    new HtmlNegotiator(
        sp.GetRequiredService<IViewLoader>(),
        sp.GetRequiredService<ITemplateEngine>(),
        new HtmlNegotiatorOptions
        {
            ExtraSettings =
            {
                ["MyCustomKey"] = ConfigurationManager.Configuration["BLAH"]
            }
        }));

anything in ExtraSettings overwrites the default if the key matches, or just gets added if its new. so if a project doesn't need anything custom they just register HtmlNegotiator as normal and get the standard settings, but nothing is locked in

@lewisrenfrew lewisrenfrew merged commit bd0d59b into main May 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants