О применении 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 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/


Интересные статьи

Интересные статьи

В данной статье мы рассмотрим систему аутентификации пользователей и внешних систем в личном кабинете через сервер аутентификации Blitz Identity Provider. Согласно требованиям проекта, который мы...
Всем привет. Текст состоит из двух частей:1. Небольшая шпаргалка по параметрам настроек по умолчанию;2. Текст о том, почему вообще существование такой шпаргалки может кому-то понадобится.
Доброго времени суток, друзья! В этой небольшой заметке я хочу поделиться с Вами информацией о некоторых консольных командах, которые можно использовать наряду с console.log(). Полный спи...
Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...
Одной из «киллер-фич» 12й версии Битрикса была объявлена возможность отдавать статические файлы из CDN, тем самым увеличивая скорость работы сайта. Попробуем оценить практический выигрыш от использова...