Иногда хочется автоматически создавать текстовые файлы, подставляя в шаблоны значения каких-то полей. Например, это могут быть исходники классов-хелперов на основе какого-то интерфейса, какие-то отчеты в 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://habr.com/ru/post/664712/
Добавить комментарий