О применении RazorPages в консольных и десктопных приложениях

от автора

Иногда хочется автоматически создавать текстовые файлы, подставляя в шаблоны значения каких-то полей. Например, это могут быть исходники классов-хелперов на основе какого-то интерфейса, какие-то отчеты в XML, которые хотя и можно сгенерировать полностью программно, но на практике это может быть достаточно трудный для сопровождения код. Наверное, те, кто сталкивался с такой потребностью, смогут дополнить этот список. Приведу для примера задачу с хелперами.

Проблема

Предположим, мы создаём клиент-серверное приложение. На сервере у нас крутится MVC ASP.NET, основное клиентское приложение — WPF, но не исключено использование в будущем и других клиентов.

Чтобы при разработке модели как на клиенте, так и на сервере не заниматься сериализацией и десериализацией, мы решили клиент-серверное взаимодействие устроить по принципу коннектора, когда на клиенте вызывается некоторый метод, что приводит к вызову такого же (почти) на сервере.

Сама концепция здесь не важна, главное, что мы столкнулись с необходимостью строить исходники хелперов по интерфейсу клиентской части коннектора. Данный интерфейс можно считать недорогим вариантом Contract First парадигмы.

Пусть интерфейс такой:

using Net.Leksi.RestContract; using System.Collections.ObjectModel;  namespace DtoKit.Demo;  public interface IConnector {     [RoutePath("/shipCalls/{filter}/{amount:double}/{date}")]     [HttpMethodGet]     [Authorization(Roles = "1, 2, 3")]     Task GetShipCalls(DateTime date, double amount, ShipCallsFilter filter,                        ObservableCollection<IShipCallForList> list);      [RoutePath("/form")]     [HttpMethodPost]     Task Commit([Body] IShipCall shipCall);  } 

Здесь мы используем свои атрибуты, чтобы не включать в клиент зависимость от платформы ASP.NET, параметры, входящие в роутинг или являющиеся телом POST-запроса, должны быть в серверном методе, соответствующие атрибуты ASP.NET и пераметры из роутинга должны быть в контроллере. Остальные возможные параметры «остаются на клиенте». Всё должно однообразно сериализоваться/десериализоваться.

Мы хотим, чтобы при перестроении сборки, содержащей IConnector, автоматически консольным приложением создавались следующие исходники.

Родительский класс для клиентской части
//------------------------------ // Connector base // DtoKit.Demo.DemoConnectorBase // (Generated automatically) //------------------------------ using Microsoft.Extensions.DependencyInjection; using Net.Leksi.Dto; using Net.Leksi.RestContract; using System.Net.Http.Json; using System.Text.Json; using System.Web;  namespace DtoKit.Demo;  public class DemoConnectorBase {     private readonly HttpConnector _httpConnector;     public DemoConnectorBase(HttpConnector httpConnector)     {         _httpConnector = httpConnector;     }      public Task<HttpResponseMessage> GetShipCalls(DateTime date, Double amount,                                                    ShipCallsFilter filter)     {         DtoJsonConverterFactory getConverter = _httpConnector.Services           .GetRequiredService<DtoJsonConverterFactory>();         getConverter.KeysProcessing = KeysProcessing.OnlyKeys;         JsonSerializerOptions getOptions = new();         getOptions.Converters.Add(getConverter);         string _date = HttpUtility.UrlEncode(           JsonSerializer.Serialize(date, getOptions));         string _amount = HttpUtility.UrlEncode(           JsonSerializer.Serialize(amount, getOptions));         string _filter = HttpUtility.UrlEncode(           JsonSerializer.Serialize(filter, getOptions));         string route = $"/shipCalls/{_filter}/{_amount}/{_date}";         HttpRequestMessage httpRequest = new(HttpMethod.Get, route);          return _httpConnector.SendAsync(httpRequest);      }      public Task<HttpResponseMessage> Commit(IShipCall shipCall)     {         string route = $"/form";         HttpRequestMessage httpRequest = new(HttpMethod.Post, route);          DtoJsonConverterFactory postConverter = _httpConnector.Services           .GetRequiredService<DtoJsonConverterFactory>();         JsonSerializerOptions postOptions = new();         postOptions.Converters.Add(postConverter);         httpRequest.Content = JsonContent.Create(shipCall, typeof(IShipCall),                                                  default, postOptions);         return _httpConnector.SendAsync(httpRequest);      }  } 

которую мы будем использовать в самом

коннекторе:
using Net.Leksi.RestContract; using System.Collections.ObjectModel;  namespace DtoKit.Demo;  public class Connector : DemoConnectorBase,  IConnector {     public Connector(HttpConnector httpConnector) : base(httpConnector) { }     public async Task GetShipCalls(DateTime date, double amount,      ShipCallsFilter filter, ObservableCollection<IShipCallForList> list)     {     // Возможная логика до отправки запроса         HttpResponseMessage response = await base.GetShipCalls(date, amount,          filter);         Console.WriteLine(response.StatusCode);     // Возможная логика после получения ответа, включая запись результата в      // ObservableCollection<IShipCallForList> list для тображения в UI     // (этот параметр на сервере неизвестен)     }      public async Task Commit(IShipCall shipCall)     {         HttpResponseMessage response = await base.Commit(shipCall);         Console.WriteLine(response.StatusCode);     } } 

На сервере мы собираемся в

качестве контроллера разместить:
//------------------------------ // MVC Controller proxy  // DtoKit.Demo.DemoControllerProxy // (Generated automatically) //------------------------------ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Net.Leksi.Dto; using System.Text.Json;  namespace DtoKit.Demo;  public class DemoControllerProxy : Controller {      [Route("/shipCalls/{filter}/{amount:double}/{date}")]     [HttpGet]     [Authorize(Roles = "1, 2, 3")]     public async Task GetShipCalls(String date, Double amount, String filter)     {         DtoJsonConverterFactory converter = HttpContext.RequestServices         .GetRequiredService<DtoJsonConverterFactory>();         JsonSerializerOptions options = new();         options.Converters.Add(converter);         DateTime _date = JsonSerializer.Deserialize<DateTime>(date, options);         ShipCallsFilter _filter = JsonSerializer.Deserialize<ShipCallsFilter>(         filter, options);         Controller controller = (Controller)HttpContext.RequestServices         .GetRequiredService<IDemoController>();         controller.ControllerContext = ControllerContext;         await ((IDemoController)controller).GetShipCalls(_date, amount, _filter);     }      [Route("/form")]     [HttpPost]     public async Task Commit()     {         DtoJsonConverterFactory converter = HttpContext.RequestServices         .GetRequiredService<DtoJsonConverterFactory>();         JsonSerializerOptions options = new();         options.Converters.Add(converter);         IShipCall shipCall = await HttpContext.Request         .ReadFromJsonAsync<IShipCall>(options);         Controller controller = (Controller)HttpContext.RequestServices         .GetRequiredService<IDemoController>();         controller.ControllerContext = ControllerContext;         await ((IDemoController)controller).Commit(shipCall);     }  } 

и интерфейс реального контроллера, содержащего логику:

интерфейс реального контроллера, содержащего логику:
//------------------------------ // MVC Controller interface  // DtoKit.Demo.IDemoController // (Generated automatically) //------------------------------  namespace DtoKit.Demo;  public interface IDemoController {     Task GetShipCalls(DateTime date, Double amount, ShipCallsFilter filter);     Task Commit(IShipCall shipCall); } 

Нам бы могла подойти платформа RazorPages

Если бы можно было применить её в приложении командной строки, мы бы использовали для нашей цели следующие шаблоны.

Шаблон для родительского класса коннектора
@page @using Net.Leksi.RestContract @model Net.Leksi.RestContract.Pages.ConnectorBaseModel //------------------------------ // Connector base // @string.Join(".", new string[] { Model.NamespaceValue, Model.ClassName}) // (Generated automatically) //------------------------------ @foreach(string usng in Model.Usings) {     <text>using @usng; </text> }  namespace @Model.NamespaceValue;  public class @Model.ClassName {     private readonly HttpConnector _httpConnector;     public @Model.ClassName@{<text/>}(HttpConnector httpConnector)     {         _httpConnector = httpConnector;     }     @foreach (MethodModel mm in Model.Methods)     { <text>     public Task<HttpResponseMessage> @mm.Name@{<text/>}(@for(int i = 0; i < mm.Parameters.Count; ++i)         {                 if(i > 0){<text>, </text>}                 <text>@mm.Parameters[i].Type @mm.Parameters[i].Name</text>         })     {@if(mm.HasSerialized)         { <text>         DtoJsonConverterFactory @mm.GetConverterVariable = _httpConnector.Services.GetRequiredService<DtoJsonConverterFactory>();         @mm.GetConverterVariable@{<text/>}.KeysProcessing = KeysProcessing.OnlyKeys;         JsonSerializerOptions @mm.GetOptionsVariable = new();         @mm.GetOptionsVariable@{<text/>}.Converters.Add(@mm.GetConverterVariable);</text>             foreach(Tuple<string, string, string> tuple in mm.Deserializing)             { <text>         @tuple.Item1 @tuple.Item2 = HttpUtility.UrlEncode(JsonSerializer.Serialize(@tuple.Item3, @mm.GetOptionsVariable));</text>             }         } @* @if(mm.HasSerialized) *@                  string @mm.RouteVariable = @Html.Raw($"$\"{@mm.RouteValue}\"");         HttpRequestMessage @mm.HttpRequestVariable = new(HttpMethod.@mm.HttpMethod, @mm.RouteVariable);         @if(mm.PostConverterVariable is { })         { <text>         DtoJsonConverterFactory @mm.PostConverterVariable = _httpConnector.Services.GetRequiredService<DtoJsonConverterFactory>();         JsonSerializerOptions @mm.PostOptionsVariable = new();         @mm.PostOptionsVariable@{<text/>}.Converters.Add(@mm.PostConverterVariable);         @mm.HttpRequestVariable@{<text/>}.Content = JsonContent.Create(@mm.BodyVariable, typeof(@mm.BodyType), default, @mm.PostOptionsVariable);</text>         }          return _httpConnector.SendAsync(@mm.HttpRequestVariable);          }@* public async ... *@ </text>     }@* @foreach (MethodModel mm in Model.Methods) *@ }

Шаблон для прокси-контроллера
@page @using Net.Leksi.RestContract @model Net.Leksi.RestContract.Pages.ControllerProxyModel //------------------------------ // MVC Controller proxy  // @string.Join(".", new string[] { Model.NamespaceValue, Model.ClassName}) // (Generated automatically) //------------------------------ @foreach(string usng in Model.Usings) {     <text>using @usng; </text> }  namespace @Model.NamespaceValue;  public class @Model.ClassName: Controller {     @foreach (MethodModel mm in Model.Methods)     {         foreach(AttributeModel am in mm.Attributes)         { <text>         [@am.Name@if (am.Properties.Count > 0) {<text>(</text>             string[] keys = am.Properties.Keys.ToArray();             for(int i = 0; i < keys.Length; ++i)             {                 if(i > 0){<text>, </text>}                 if(!string.IsNullOrEmpty(keys[i])){<text>@keys[i] = </text>}<text>@Html.Raw(am.Properties[keys[i]])</text>             }             <text>)</text> }]</text>         } <text>     public async @mm.Type @mm.Name@{<text/>}(@for(int i = 0; i < mm.Parameters.Count; ++i)         {                 if(i > 0){<text>, </text>}                 <text>@mm.Parameters[i].Type @mm.Parameters[i].Name</text>         })     {@if(mm.HasSerialized)         { <text>         DtoJsonConverterFactory @mm.GetConverterVariable = HttpContext.RequestServices.GetRequiredService<DtoJsonConverterFactory>();         JsonSerializerOptions @mm.GetOptionsVariable = new();         @mm.GetOptionsVariable@{<text/>}.Converters.Add(@mm.GetConverterVariable);</text>             foreach(Tuple<string, string, string> tuple in mm.Deserializing)             {                 if(tuple.Item3 is null)                 { <text>         @tuple.Item1 @tuple.Item2 = await HttpContext.Request.ReadFromJsonAsync<@tuple.Item1>(@mm.GetOptionsVariable);</text>                 }                 else                 { <text>         @tuple.Item1 @tuple.Item2 = JsonSerializer.Deserialize<@tuple.Item1>(@tuple.Item3, @mm.GetOptionsVariable);</text>                 }             }         } @* @if(mm.HasSerialized) *@                  Controller @mm.ControllerVariable = (Controller)HttpContext.RequestServices.GetRequiredService<@mm.ControllerInterfaceClassName>();         @mm.ControllerVariable@{<text/>}.ControllerContext = ControllerContext;         await ((@mm.ControllerInterfaceClassName)@mm.ControllerVariable).@mm.Name@{<text/>}(@for(int i = 0; i < mm.ControllerParameters.Count; ++i)         {             if(i > 0){<text>, </text>}             <text>@mm.ControllerParameters[i]</text>         });     }@* public async ... *@ </text>     }@* @foreach (MethodModel mm in Model.Methods) *@ }

Шаблон для интерфейса контроллера
@page @using Net.Leksi.RestContract @model Net.Leksi.RestContract.Pages.ControllerInterfaceModel //------------------------------ // MVC Controller interface  // @string.Join(".", new string[] { Model.NamespaceValue, Model.ClassName}) // (Generated automatically) //------------------------------ @foreach(string usng in Model.Usings) {     <text>using @usng; </text> }  namespace @Model.NamespaceValue;  public interface @Model.ClassName  {     @foreach (MethodModel mm in Model.Methods)     {         <text>    @mm.Type @mm.Name</text><text>(</text>         @for(int i = 0; i < mm.Parameters.Count; ++i)         {             if(i > 0)             {                 <text>, </text>             }             <text>@mm.Parameters[i].Type @mm.Parameters[i].Name</text>         }         <text>); </text>     } } 

Инкапсуляция RazorPages в консольное приложение

Напишем простой класс, который будет делать это в принципе, без привязки к конкретной задаче.

public static class Generator { ...            public static async IAsyncEnumerable<KeyValuePair<string, object>>        Generate(IEnumerable<object> requisite, IEnumerable<string> requests)     {       ...     }

Здесь IEnumerable<object> requisite — всё, что понадобится внутри. Конкретно object может быть:

  • Экземпляр класса, который содержит информацию или предоставляет доступ к информации, необходимой какой-либо страничной модели. Этот объект будет зарегистрирован в службах внедрения зависимости как ServiceLifetime.Singleton под своим типом.

  • KeyValuePair<Type, object>, где Value— экземпляр, как описано в предыдущем пункте, который будет зарегистрирован как ServiceLifetime.Singleton под типом Key.

  • Type — будет зарегистрирован как ServiceLifetime.Transient .

  • KeyValuePair<Type, Type>Value будет зарегистрирован как ServiceLifetime.Transient под типом Key.

  • Assembly — сборка, содержащая в папке Pages сами страницы с моделями. Если другие объекты из предыдущих пунктов содержатся в этой сборке, то её можно не добавлять.

IEnumerable<string> requests — последовательность запросов к страницам, обычные пути. Можно при желании в зависимости от данных или предыдущих ответов решать, какой будет следующий запрос. Именно поэтому мы не используем здесь коллекцию или массив. То же самое с реквизитом, но он считывается до запросов, так что может зависеть только от исходных данных.

Возвращаемое значение IAsyncEnumerable<KeyValuePair<string, object>> — пары, где Key — строка запроса, а Value — либо строка содержимое ответа, либо Exception, если что-то пошло не так при выполнении этого запроса. Сам метод не падает, вызывающий код решает, что с этим делать.

Код класса находится здесь
using Microsoft.AspNetCore.Diagnostics; using System.Net.NetworkInformation; using System.Reflection;  namespace Net.Leksi.DocsRazorator;  public static class Generator {     private const string SecretWordHeader = "X-Secret-Word";     private const int MaxTcpPort = 65535;     private const int StartTcpPort = 5000;      public static async IAsyncEnumerable<KeyValuePair<string, object>> Generate(IEnumerable<object> requisite,          IEnumerable<string> requests)     {         ManualResetEventSlim appStartedGate = new();         WebApplication app = null!;         HttpClient client = null!;         string secretWord = Guid.NewGuid().ToString();          Exception? razorPageException = null;          appStartedGate.Reset();          Task loadTask = Task.Run(() =>         {             int port = MaxTcpPort + 1;             List<Assembly> assemblies = new();             List<object> services = new();             foreach (object obj in requisite)             {                 if (obj is Assembly asm)                 {                     if (!assemblies.Contains(asm))                     {                         assemblies.Add(asm);                     }                  }                 else if (obj is KeyValuePair<Type, Type> typeTypePair)                 {                     Assembly assembly = typeTypePair.Value.Assembly;                     if (!assemblies.Contains(assembly))                     {                         assemblies.Add(assembly);                     }                     if (services.Find(v => v is KeyValuePair<Type, object> p && p.Key == typeTypePair.Key                          && Object.ReferenceEquals(p.Value, typeTypePair.Value)) is null)                     {                         services.Add(obj);                     }                 }                 else if (obj is KeyValuePair<Type, object> typeObjectPair)                 {                     Assembly assembly = typeObjectPair.Value.GetType().Assembly;                     if (!assemblies.Contains(assembly))                     {                         assemblies.Add(assembly);                     }                     if (services.Find(v => v is KeyValuePair<Type, object> p && p.Key == typeObjectPair.Key                          && Object.ReferenceEquals(p.Value, typeObjectPair.Value)) is null)                     {                         services.Add(obj);                     }                 }                 else if (obj is Type type)                 {                     Assembly assembly = type.Assembly;                     if (!assemblies.Contains(assembly))                     {                         assemblies.Add(assembly);                     }                     if (!services.Contains(obj))                     {                         services.Add(obj);                     }                 }                 else                 {                     Assembly assembly = obj.GetType().Assembly;                     if (!assemblies.Contains(assembly))                     {                         assemblies.Add(assembly);                     }                     if (!services.Contains(obj))                     {                         services.Add(obj);                     }                 }             }             while (true)             {                 IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();                 int[] usedPorts = ipGlobalProperties.GetActiveTcpConnections()                         .Select(v => v.LocalEndPoint.Port).Where(v => v >= StartTcpPort).OrderBy(v => v).ToArray();                 for (int i = 1; i < usedPorts.Length; ++i)                 {                     if (usedPorts[i] > usedPorts[i - 1] + 1)                     {                         port = usedPorts[i] - 1;                         break;                     }                 }                 if (port > MaxTcpPort)                 {                     try                     {                         throw new Exception("No TCP port is available.");                     }                     finally                     {                         appStartedGate.Set();                     }                 }                  WebApplicationBuilder builder = WebApplication.CreateBuilder(new string[] { });                  builder.Logging.ClearProviders();                  builder.Services.AddRazorPages();                  IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews();                 foreach (Assembly assembly in assemblies)                 {                     mvcBuilder.AddApplicationPart(assembly);                 }                  foreach (object obj in services)                 {                     if(obj is KeyValuePair<Type, Type> typeTypePair)                     {                             builder.Services.AddTransient(typeTypePair.Key, typeTypePair.Value);                     }                     else if (obj is KeyValuePair<Type, object> typeObjectPair)                     {                         builder.Services.AddSingleton(typeObjectPair.Key, op =>                         {                             return typeObjectPair.Value;                         });                     }                     else if(obj is Type type)                     {                         builder.Services.AddTransient(type);                     }                     else                     {                                                  builder.Services.AddSingleton(obj.GetType(), op =>                         {                             return obj;                         });                     }                 }                  app = builder.Build();                  app.UseExceptionHandler(eapp =>                 {                     eapp.Run(async context =>                     {                         var exceptionHandlerPathFeature =                             context.Features.Get<IExceptionHandlerPathFeature>();                          razorPageException = exceptionHandlerPathFeature?.Error;                         context.Response.StatusCode = StatusCodes.Status500InternalServerError;                     });                 });                  app.Use(async (context, next) =>                 {                     if (!context.Request.Headers.ContainsKey(SecretWordHeader) || !context.Request.Headers[SecretWordHeader].Contains(secretWord))                     {                         context.Response.StatusCode = StatusCodes.Status403Forbidden;                         await context.Response.WriteAsync(String.Empty);                     }                     else                     {                         await next.Invoke(context);                     }                 });                  app.MapRazorPages();                  app.Lifetime.ApplicationStarted.Register(() =>                 {                     appStartedGate.Set();                 });                  app.Urls.Clear();                 app.Urls.Add($"http://localhost:{port}");                 try                 {                     app.Run();                     break;                 }                 catch (IOException ex) { }             }         });          appStartedGate.Wait();          if (loadTask.IsFaulted)         {             throw loadTask.Exception;         }         client = new HttpClient();         client.BaseAddress = new Uri(app.Urls.First());         client.DefaultRequestHeaders.Add(SecretWordHeader, secretWord);         foreach (string request in requests)         {             razorPageException = null;              HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, request);              HttpResponseMessage response = await client.SendAsync(requestMessage);              if(razorPageException is { })             {                 yield return new KeyValuePair<string, object>(request, razorPageException);             }             else if (response.IsSuccessStatusCode)             {                 yield return new KeyValuePair<string, object>(request, await response.Content.ReadAsStringAsync());             }                      }         await app.StopAsync();      } } 

Посмотрим, что происходит

{     ManualResetEventSlim appStartedGate = new();     WebApplication app = null!;     HttpClient client = null!;     string secretWord = Guid.NewGuid().ToString();      Exception? razorPageException = null;      appStartedGate.Reset();      ... }

Ворота ManualResetEventSlim appStartedGate откроются, когда загрузится локальный сервер WebApplication app. HttpClient client будет осуществлять запросы. string secretWord будем передавать в заголовках запросов, чтобы не пускать посторонних. В Exception? razorPageException будем искать Exception, если таковой возник при обработке запроса.

Запускаем WebApplication app

{         ...     // Задача запускает WebApplication Task loadTask = Task.Run(() =>         {         int port = MaxTcpPort + 1;         List<Assembly> assemblies = new();         List<object> services = new();         foreach (object obj in requisite)         {           // Сортируем реквизиты           ...         }         while (true)         {           // Ищем свободный TCP порт             IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();             int[] usedPorts = ipGlobalProperties.GetActiveTcpConnections()                     .Select(v => v.LocalEndPoint.Port).Where(v => v >= StartTcpPort).OrderBy(v => v).ToArray();             for (int i = 1; i < usedPorts.Length; ++i)             {                 if (usedPorts[i] > usedPorts[i - 1] + 1)                 {                     port = usedPorts[i] - 1;                     break;                 }             }             if (port > MaxTcpPort)             {                 try                 {                     throw new Exception("No TCP port is available.");                 }                 finally                 {                     appStartedGate.Set();                 }             } // Конфигурируем WebApplication             WebApplicationBuilder builder = WebApplication.CreateBuilder(new string[] { });              builder.Logging.ClearProviders();              builder.Services.AddRazorPages();            // Добавляем сборки со страницами             IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews();             foreach (Assembly assembly in assemblies)             {                 mvcBuilder.AddApplicationPart(assembly);             }              foreach (object obj in services)             {               // Регистрируем объекты и типы               ...             }              app = builder.Build();              // При исключении заносим его в razorPageException и возвращаем           // StatusCodes.Status500InternalServerError           app.UseExceptionHandler(eapp =>             {                 eapp.Run(async context =>                 {                     var exceptionHandlerPathFeature =                         context.Features.Get<IExceptionHandlerPathFeature>();                      razorPageException = exceptionHandlerPathFeature?.Error;                     context.Response.StatusCode = StatusCodes.Status500InternalServerError;                 });             });              // Отсекаем запросы не от нас           app.Use(async (context, next) =>             {                 if (!context.Request.Headers.ContainsKey(SecretWordHeader) || !context.Request.Headers[SecretWordHeader].Contains(secretWord))                 {                     context.Response.StatusCode = StatusCodes.Status403Forbidden;                     await context.Response.WriteAsync(String.Empty);                 }                 else                 {                     await next.Invoke(context);                 }             });              app.MapRazorPages();            // Открываем ворота appStartedGate, когда готовы обрабатывать запросы             app.Lifetime.ApplicationStarted.Register(() =>             {                 appStartedGate.Set();             });                      // Пытаемся запуститься на выбранном порту,             // но если ВДРУГ он уже занят, повторяем попытку с другим             app.Urls.Clear();             app.Urls.Add($"http://localhost:{port}");             try             {                 app.Run();                 break;             }             catch (IOException ex) { }         }     });  ... }

И начинаем слать запросы

{   ...     // Ждём сигнал на старт       appStartedGate.Wait();    // Если приложение не смогло запуститься, валимся     if (loadTask.IsFaulted)     {       throw loadTask.Exception;     }   // Настраиваем клиента     client = new HttpClient();     client.BaseAddress = new Uri(app.Urls.First());     client.DefaultRequestHeaders.Add(SecretWordHeader, secretWord);   // Шлём запросы     foreach (string request in requests)     {       // убираем прошлое исключение       razorPageException = null;        HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, request);        HttpResponseMessage response = await client.SendAsync(requestMessage);        if(razorPageException is { })       {         // Если получили исключение, возвращаем исключение         yield return new KeyValuePair<string, object>(request, razorPageException);       }       else        {         // возвращаем результат         yield return new KeyValuePair<string, object>(request, await response.Content.ReadAsStringAsync());       }      }   // Запросы кончились, занавес     await app.StopAsync();  } 

Использование для нашей задачи

Создадим проект:

Удалим всё ненужное, добавим нужное:

Модели у нас одинаковые, только строятся по разному, поэтому их унаследуем от BasePageModel:

using Microsoft.AspNetCore.Mvc.RazorPages;  namespace Net.Leksi.RestContract;  public class BasePageModel: PageModel {     internal string NamespaceValue { get; set; }     internal string ClassName { get; set; }     internal List<string> Usings { get; set; }     internal List<MethodModel> Methods { get; set; } } 

Сами модели заполняем извне с помощью билдера, полученного из внедрения зависимостей.

using Microsoft.AspNetCore.Mvc;  namespace Net.Leksi.RestContract.Pages;  public class ConnectorBaseModel : BasePageModel {     public void OnGet([FromServices] IConnectorBaseBuilder builder)     {         builder.BuildConnectorBase(this);     } } 
using Microsoft.AspNetCore.Mvc;  namespace Net.Leksi.RestContract.Pages;  public class ControllerInterfaceModel : BasePageModel {     public void OnGet([FromServices] IControllerInterfaceBuilder builder)     {         builder.BuildControllerInterface(this);     } } 
using Microsoft.AspNetCore.Mvc;  namespace Net.Leksi.RestContract.Pages {     public class ControllerProxyModel : BasePageModel     {         public void OnGet([FromServices] IControllerProxyBuilder builder)         {             builder.BuildControllerProxy(this);         }     } } 

Точкой входа является метод класса HelpersBuilder, который реализует все три интерфейса, которые ждут модели и сам является реквизитом:

public class HelpersBuilder : IControllerInterfaceBuilder, IControllerProxyBuilder, IConnectorBaseBuilder { ...     public async Task BuildHelpers<TConnector>(string controllerInterfaceFullName, string controllerProxyFullName,         string connectorBaseFullName)     {         CollectRequisites<TConnector>(controllerInterfaceFullName, controllerProxyFullName, connectorBaseFullName);         await foreach (KeyValuePair<string, object> result in Generator.Generate(             new object[] {                 new KeyValuePair<Type, object>(typeof(IConnectorBaseBuilder), this),                 new KeyValuePair<Type, object>(typeof(IControllerInterfaceBuilder), this),                 new KeyValuePair<Type, object>(typeof(IControllerProxyBuilder), this)             },             new string[] {                 "ConnectorBase",                 "ControllerInterface",                 "ControllerProxy",             }         ))         {             if (result.Value is Exception)             {                 Console.WriteLine($"// {result.Key}");             }             Console.WriteLine(result.Value);         }      } ...     public void BuildConnectorBase(ConnectorBaseModel model)     {     ... }     ...     public void BuildControllerProxy(ControllerProxyModel model)     { ...     }     ...     public void BuildControllerInterface(ControllerInterfaceModel model)     {     ...     }     ...     private void CollectRequisites<TConnector>(string controllerFullName,      string proxyFullName, string connectorBaseFullName)     { ...     }    ... }

Запустим в виде юнит-теста опять же для простоты

public class RestContract {     private static IHost _host;      static RestContract()     {         _host = Host.CreateDefaultBuilder()             .ConfigureServices(serviceCollection =>             {                 ...                 serviceCollection.AddTransient<HelpersBuilder>();             }).Build();         Trace.Listeners.Add(new ConsoleTraceListener());         Trace.AutoFlush = true;     }      [Test]     public async Task BuildRestHelpers()     {         HelpersBuilder codeGenerator = _host.Services.GetRequiredService<HelpersBuilder>();          await codeGenerator.BuildHelpers<IConnector>(           "DtoKit.Demo.IDemoController", "DtoKit.Demo.DemoControllerProxy",            "DtoKit.Demo.DemoConnectorBase");     } }

Разложим типы по файлам и разместим в сервере и клиенте

Все исходники:

https://github.com/Leksiqq/DocsRazorator/tree/v1.0.0/Library

https://github.com/Leksiqq/RestContract/tree/v1.0.0/HelpersBuilder

https://github.com/Leksiqq/DtoRestDemo


ссылка на оригинал статьи https://habr.com/ru/post/664712/