Z>По сути, веб-приложение — это на самом деле два отдельных приложения: веб-клиент и веб-сервис, которые обмениваются между собой запросами-сообщениями.
Эээ... интересная постановка вопроса

Ну как бы да, серверная и клиентская части — это совершенно разные приложения.
Единственно, что их связывает — это протокол прикладного уровня. Стандартный — HTTP(s), FTP, SMTP, SSH и т.п., либо какой-то кастомный — REST over HTTP(s), SOAP over HTTP(s), GRPC over HTTP2 и т.п.
И пишут их зачастую разные люди и даже на разных языках программирования.
Z>Значит, в начальной точке веб-сервиса должно быть создано несколько объектов:
Z>- объект синтаксического разбора (распаковки) запроса
Z>- объект-контроллер для обработки запроса
Z>- объект упаковки результата
Z>- и много других объектов, которые создаются в корне компоновки, которые необходимы для работы сервиса
Собственно, нет. В том смысле, что эти объекты, конечно, нужны, но создавать
каждый из них в явном виде в точке сборки не надо.
Для серверной части main может быть вот таким простым:
// Собственно, вот и весь composition root.
var config = ReadConfigFromSomeWhere();
var webServer = new WebServer(new WebServerOptions { BindAddress = config.ListenAddress, Port = config.ListenPort }, new MyRouter());
webServer.Start();
// А уже в конструкторе WebServer может быть что-то вроде:
// this._router = router; // IRouter, который приходит как параметр конструктора
// this._socket = new Socket(); // сокет, на котором слушаются входящие подключения
// this._inputBuffer = new Buffer(); // Буфер для входящих данных
// this._outputBuffer = new Buffer(); // Буфер для исходящих данных
// this._parser = new HttpProtocolParser(this._inputBuffer); // Парсер http-протокола
// и т.д. и т.п.
//
// WebServer будет оперировать этими внутренними объектами, получать сырые байты, разбирать их в объекты HttpRequest и отдавать на обработку в заданный снаружи IRouter.Handle(httpRequest, httpResponse).
Далее в реализации роутера может быть, например, так:
public class MyRouter: IRouter {
private JsonSerializer _json = new(); // Вот здесь объекты тоже создаются уже "внутри" реализации, нет необходимости тащить их в composition root
private QueryStringParser _qsParser = new();
public void Process(HttpRequest httpRequest, HttpResponse httpResponse) {
object responseObj = ((httpRequest.Method, httpRequest.Path)) switch {
// Здесь снова конкретный обработчик (контроллер) создается по месту, а не в composition root
case ("POST", "api/users") => new UsersController().CreateUser(_json.Deserialize<CreateUserRequestDto>(request.Body));
case ("GET", "api/users") => new UsersController().ListUsers(_qsParser.Parse<UsersFilterDto>(request.QueryString));
case ("POST", "api/orders") => new OrdersController().PlaceOrder(_json.Deserialize<PlaceOrderRequest>(request.Body));
_ => throw new NotFoundException();
}
httpResponse.SetStatusCode(200);
httpResponse.Write(_json.Serialize(responseObj));
}
}