Создание веб-приложения ASP.NET Core, использующего API TESSA
Создание веб-приложения ASP.NET Core, использующего API TESSA¶
В этом примере мы рассмотрим создание независимого веб-приложения ASP.NET Core, которое может использоваться для обработки REST-запросов и вывода статических или динамических сайтов. Подход, описанный в этом разделе, может задействоваться для разработки кастомизированного веб-клиента или веб-приложения для мобильных устройств.
Откройте Visual Studio и создайте проект ASP.NET Core Web Application
.
Укажите реализацию для .NET Core 3.1
. В качестве шаблона проекта выберите Empty
.
Перейдите в диалог выбора NuGet-пакетов, для этого в контекстном меню на узле Solution '...'
в панели инструментов Solution Explorer
выберите пункт Manage NuGet Packages
.
Перейдите на вкладку Browse и последовательно установите пакеты Tessa.Linux
, Tessa.Server
(в сборке 3.4.0 или ранее назывался Tessa.Compilation
), Tessa.PostgreSql
и Tessa.Web
, выбрав версию пакета, соответствующую версии вашей сборки TESSA. Например, для версии сборки TESSA 3.5.0 укажите версию пакетов также 3.5.0. Не обновляйте пакеты до более новых версий до того, как будет обновлена инсталляция всей платформы (вместе с конфигурацией, базой данных и расширениями).
Для поддержки linux-сокетов рекомендуем установить пакет Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv
актуальной стабильной версии (в текущем релизе это 3.1.10).
Note
Для логирования ошибок ASP.NET Core, возникающих в запросах, не связанных с TESSA и её расширениями, таких как Swagger, добавьте NuGet-пакет
NLog.Web.AspNetCore актуальной версии. Это требуется для использования метода .UseNLog()
в коде ниже.
В файле Program.cs
в методе Main
добавьте вызовы методов инициализации перед тем, как будет вызван метод CreateWebHostBuilder()
.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;
using NLog.Web;
using Tessa.Platform;
using Tessa.Web;
namespace TessaWebApp
{
public class Program
{
public static async Task Main(string[] args)
{
try
{
await TessaPlatform.InitializeFromConfigurationAsync();
WebHelper.InitializeWebServer();
await CreateHostBuilder(args).Build().RunAsync();
}
catch (Exception ex)
{
TessaLoggers.ServiceHost.LogException("Service host failed to initialize", ex, NLog.LogLevel.Fatal);
throw;
}
finally
{
LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
WebHelper.ParseUrlsFromCommandLine(ref args, out string[] urls, out string[] sockets);
return Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.UseNLog()
.ConfigureWebHostDefaults(
webBuilder =>
{
bool linux = EnvironmentHelper.IsLinux;
if (sockets.Length > 0 && linux)
{
webBuilder
.UseLibuv();
}
webBuilder
.ConfigureKestrel(c =>
{
c.AllowSynchronousIO = true;
c.AddServerHeader = false;
WebServerLimits.Configuration.Apply(c.Limits);
if (linux)
{
c.UseSystemd();
if (sockets.Length > 0)
{
for (int i = 0; i < sockets.Length; i++)
{
c.ListenUnixSocket(sockets[i]);
}
}
}
});
if (urls.Length > 0)
{
webBuilder
.UseUrls(urls);
}
webBuilder
.UseTessaConfiguration(args)
.UseStartup<Startup>();
});
}
}
}
В файле Startup.cs
выполните конфигурацию сервисов TESSA в методе ConfigureServices
и конфигурацию приложения в методе Configure
.
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Tessa.Platform.Runtime;
using Tessa.Web;
using Tessa.Web.Services;
using Unchase.Swashbuckle.AspNetCore.Extensions.Extensions;
namespace TessaWebApp
{
public class Startup :
WebStartupBase
{
public Startup(IWebHostEnvironment hostEnvironment)
: base(hostEnvironment)
{
}
public void ConfigureServices(IServiceCollection services)
{
services
.AddTessaServices()
.AddTessaResponseCompression()
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.ConfigureTessaMvc();
// если используем проверку работоспособности сервиса (health checks)
services
.AddOptions()
.AddTessaHealthChecks()
.Configure<IISServerOptions>(x => x.AllowSynchronousIO = true)
.ConfigureWebOptions();
// если используем Swagger для документирования REST-методов
if (RuntimeHelper.SwaggerDocIsEnabled)
{
services
.AddSwaggerGen(c =>
{
c.SwaggerDocForTessa("My REST API", "My API Description");
// указываем одну или несколько сборок, в которых присутствуют описываемые контроллеры
c.IncludeXmlComments(Assembly.GetExecutingAssembly(), includeControllerXmlComments: true);
c.IncludeXmlCommentsFromInheritDocs();
c.UseAllOfToExtendReferenceSchemas();
c.AddEnumsWithValuesFixFilters(services, o =>
{
o.ApplySchemaFilter = true;
o.ApplyParameterFilter = true;
o.ApplyDocumentFilter = true;
o.IncludeDescriptions = true;
o.IncludeXEnumRemarks = true;
o.DescriptionSource = DescriptionSources.DescriptionAttributesThenXmlComments;
o.IncludeXmlCommentsForTessaClient();
});
})
.AddSwaggerGenNewtonsoftSupport();
}
}
public void Configure(
IApplicationBuilder app,
IHostApplicationLifetime applicationLifetime,
IOptions<WebOptions> options)
{
if (this.HostEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var forwardedHeaders = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
ForwardLimit = 10,
};
forwardedHeaders.KnownNetworks.Clear();
forwardedHeaders.KnownProxies.Clear();
app
.UseForwardedHeaders(forwardedHeaders)
.UsePathBaseIfSpecified(options.Value.PathBase)
.UseResponseCompression();
app.UseStaticFiles();
// если используем Swagger для документирования REST-методов
if (RuntimeHelper.SwaggerDocIsEnabled)
{
app
.UseSwagger()
.UseSwaggerUIForTessa("My custom API v1");
}
// вызовы UseRouting, UseAuthorization и UseEndpoints должны быть в указанном порядке
// разделены по разным операторам (точкой с запятой), чтобы анализатор .NET Core при сборке не выводил предупреждения
app
.UseTessaApplication()
.UseRouting();
app
.UseAuthentication()
.UseAuthorization();
app
.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/hcheck");
endpoints.MapControllers();
})
.ConfigureTessaApplication();
applicationLifetime.RegisterTessaLifetime(app);
app.Run(context => context.HandleNotFoundAsync());
}
}
}
Note
Метод app.UsePathBaseIfSpecified() доступен, начиная со сборки 3.5.0.
В файле TestController.cs
приведён пример REST-контроллера, который выполняет некоторые бизнес-требования.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tessa.Cards;
using Tessa.Platform;
using Tessa.Platform.Runtime;
using Tessa.Platform.Storage;
using Tessa.Roles;
using Tessa.Web;
using Tessa.Web.Services;
using Unity;
namespace TessaWebApp.Controllers
{
[Route("test"), AllowAnonymous]
public class TestController :
TessaControllerBase
{
public TestController(ITessaWebScope scope)
{
this.scope = scope;
}
private readonly ITessaWebScope scope;
private T Resolve<T>(string name = null) => this.scope.UnityContainer.Resolve<T>(name);
[HttpGet("system")]
public async Task<string> GetSystemUserText()
{
var serverSettings = this.Resolve<ITessaServerSettings>();
using (SessionContext.Create(Session.CreateSystemToken(SessionType.Server, serverSettings)))
{
return await this.GetSystemUserTextWithoutSession();
}
}
[HttpGet("method"), SessionMethod]
public async Task<string> GetSystemUserTextWithoutSession()
{
var cardRepository = this.Resolve<ICardRepository>();
CardGetResponse response = await cardRepository.GetAsync(
new CardGetRequest
{
CardID = Session.SystemID,
CardTypeID = RoleHelper.PersonalRoleTypeID,
});
string text = StorageHelper.PrintObject(response);
return text;
}
public sealed class Credentials
{
public string Login { get; set; }
public string Password { get; set; }
}
[HttpPost("login")]
public Task<string> PostLogin([FromBody] Credentials credentials) =>
this.Resolve<ILoginService>().OpenSessionAsync(
SessionClientParameters.CreateCurrent(),
ApplicationIdentifiers.Other,
credentials.Login,
credentials.Password);
[HttpPost("logout"), SessionMethod]
public Task PostLogout([FromBody, SessionToken] string token = null) =>
this.Resolve<ISessionService>().CloseSessionWithTokenAsync(token);
}
}
Компонент Swagger может использоваться для генерации страницы с автоматической документацией по REST-методам в ваших контроллерах. Для этого укажите регистрации Swagger в методах ConfigureServices
и Configure
(см. выше). В коде ваших контроллеров используйте символ тройного слэша ///
для добавления описаний для класса контроллера и его REST-методов. Также включите генерацию xml-файла документации в том проекте (или проектах), где расположены классы контроллеров, отредактировав файл проекта .csproj
и добавив строки:
<Project Sdk="...">
<PropertyGroup>
...
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
...
</Project>
Просмотреть документацию можно будет после публикации приложения, перейдя по адресу вида https://localhost/tessa/app/swagger
(где app
- имя вашего приложения).
Также в папке приложения нужен ряд конфигурационных файлов для настройки расширений, логирования и методов подключения к базе данных.
Добавьте файл extensions.xml
(Add -> New item -> XML File
), в свойствах файлах укажите Content
и Copy if newer
. Содержимое файла приведено ниже:
<?xml version="1.0" encoding="utf-8" ?>
<extensions xmlns="http://syntellect.ru/tessa/include">
<include file="Tessa.Extensions.Default.Shared.dll" />
<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.Shared.dll" />
<include file="Tessa.Extensions.Server.dll" serverOnly="true" />
<include file="Tessa.Extensions.PostgreSql.Server.dll" serverOnly="true" />
</extensions>
Добавьте файл NLog.config
(Add -> New item -> XML File
), в свойствах файлах укажите Content
и Copy if newer
. Содержимое файла приведено ниже:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets async="true">
<target name="file" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/log.txt" />
<target name="queries" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/queries.txt" layout="--${longdate}${newline}${message}${newline}GO${newline}" />
<target name="null" xsi:type="Null" formatMessage="false" />
</targets>
<rules>
<logger name="SqlQueries" minlevel="Off" writeTo="queries" final="true" />
<logger name="SqlQueries" minlevel="Trace" writeTo="null" final="true" />
<logger name="*" minlevel="Info" writeTo="file" />
</rules>
</nlog>
Добавьте файл app.json
(Add -> New item -> JSON File
), в свойствах файлах укажите Content
и Copy if newer
.
Содержимое вместе со строкой подключения и файлом лицензии мы рекомендуем скопировать из файла app.json
вашей инсталляции, удалив из него ключи: WebControllers
(обязательно!), GuyFawkesAuth
, WinAuth
, UserWallpaperName
, WallpaperSizeKb
, MultipartBodyLengthLimit
, SAML
, Themes
.
Пример содержимого файла приведён ниже (для тестового приложения можно скопировать его):
{
"ConnectionStrings": {
"default": [ "Host=localhost; Database=tessa; Integrated Security=false; User ID=postgres; Password=Master1234; Pooling=true; MaxPoolSize=100", "Npgsql" ]
},
"DataProviders": {
"Npgsql": "Npgsql.NpgsqlFactory, Npgsql"
},
".if": [
"linux",
{
"Settings": {
"PlatformDependencies": "Tessa.Platform.LinuxTessaPlatformDependencies, Tessa.Linux"
}
}
],
".include": [
"app-*.json",
"localization.json",
"patch*.json"
],
"Settings": {
// Autogenerated
"SignatureKey": "l5pthoEIB/s8LuiNnG1OUA+3FO1fSW5/nX4zilQnlLM013Z33Q/L7hErHKptUpfOQ6ZPG35mVkwMGu0oF8/JpA==",
"CipherKey": "SKW2d3hgQUeM61FRKV6TafWGjmqCCAQErVTk0g4YViY=",
// Basic options
"ServerCode": "tessa",
"LicenseFile": "@*.tlic",
"Redis": "",
"EnableInterprocessCache": true,
"ProbingPath": "extensions",
"ServerDependencies": "Tessa.Server.TessaServerDependencies, Tessa.Server",
"WebControllers": [ "extensions/Tessa.Extensions.Default.Server.Web.dll", "extensions/Tessa.Extensions.Server.Web.dll" ],
"WebRazorReferences": [ "extensions" ],
// Web client options
"PathBase": "",
"GuyFawkesAuth": "",
"WinAuthIsEnabled": false,
"WinAuth": "",
"WinAutoLogin": true,
"PreviewPdfEnabled": true,
"CryptoProPluginEnabled": false,
// Kerberos auth
"Kerberos.Enabled": false,
"Kerberos.Keytab": "*.keytab",
"Kerberos.DisableRealmCheck": false,
// Security options
"Configuration.Sealed": false,
"Configuration.StrictSecurity": false,
"HealthCheckIsEnabled": true,
"SwaggerDocIsEnabled": true,
"CookiesSameSite": "Strict",
"CheckPlatformVersion": true,
"SessionExpirationTimeSpan": "7.00:00:00",
"CipherKeyRotationInterval": "10.00:00:00",
"ViewAccessCacheTimeSpan": "0.01:00:00",
"AllowedRefererValues": [ ],
"ResponseHeaders": {
"X-Frame-Options": "sameorigin",
"X-XSS-Protection": "1; mode=block"
},
// Misc
"ExtensionTracingMode": "Off",
"RoleTimeoutTimeSpan": "0.00:30:00",
"RolesLockTimeoutSeconds": 300,
"LimitMaxThreads": true,
"MultipartBodyLengthLimit": -1,
"UserWallpaperName": "Wallpaper",
"WallpaperSizeKb": 600,
"WebServerLimits": {
"MaxResponseBufferSizeBytes": 65536,
"MaxRequestBufferSizeBytes": 1048576,
"MaxRequestLineSizeBytes": 8192,
"MaxRequestHeadersTotalSizeBytes": 32768,
"MaxRequestHeaderCount": 100,
"MaxRequestBodySizeBytes": 30000000,
"KeepAliveTimeoutSeconds": 120,
"RequestHeadersTimeoutSeconds": 30,
"MaxConcurrentConnections": null,
"MaxConcurrentUpgradedConnections": null,
"MinRequestBodyDataRateBytesPerSecond": 240.0,
"MinRequestBodyDataRateGraceSeconds": 5,
"MinResponseDataRateBytesPerSecond": 240.0,
"MinResponseDataRateGraceSeconds": 5
},
"LDAP": {
"Enabled": false,
"UseSsl": false,
"Url": "localhost",
"Port": 10389,
"TimeoutMilliseconds": null,
"BindDn": "uid=admin,ou=system",
"BindCredentials": "secret",
"SearchBase": "dc=example,dc=com",
"SearchFilter": "(&(objectClass=person)(cn={0}))"
}
}
}
Добавьте ваш файл лицензии .tlic
(Add -> Existing Item
), в свойствах файлах укажите Content
и Copy if newer
.
Также вам потребуется копировать в выходную папку файлы расширений, собранные для вашего проекта:
-
Tessa.Extensions.Server.dll
-
Tessa.Extensions.Shared.dll
-
Tessa.Extensions.Default.Server.dll
-
Tessa.Extensions.Default.Shared.dll
Все искомые файлы можно найти, например, в папке Services\web
внутри сборки TESSA вашей версии (в примере это 3.5.0). Добавьте эти файлы (Add -> Existing Item
), в свойствах файлов укажите None
и Copy if newer
.
Для сборок расширений должны быть установлены все их зависимости, которые обычно указываются в файлах проектов .csproj
для исходных файлах проектов расширений: Tessa.Extensions.Default.Server.csproj
, Tessa.Extensions.Default.Shared.csproj
, и любые зависимости в проектах вашего решения Tessa.Extensions.Server.csproj
и Tessa.Extensions.Shared.csproj
.
В платформе версии 3.5.0 это один дополнительный NuGet-пакет DocumentFormat.OpenXml v2.11.3
. Установим его:
Мы рекомендуем добавлять такое веб-приложение как проект в solution проектных расширений Tessa.Extensions.sln
. Тогда вместо ссылок на .dll
и дополнительных зависимостей (DocumentFormat.OpenXml
) можно добавить ссылку на проект, при этом выходной файл проектов с расширениями будет скопирован в сборку, а все зависимые NuGet-пакеты - автоматически установлены.
Веб-приложение перед использованием должно быть опубликовано. Для этого добавьте скрипт publish.bat
(Add -> New item -> Text File
), в свойствах файлах оставьте None
и Do Not Copy
.
Укажите следующее содержимое скрипта, причём в текстовом редакторе задайте кодировку UTF-8 без BOM (по умолчанию в Visual Studio создаётся файл в кодировке UTF-8 + BOM, что приводит к некорректному выполнению скрипта).
@echo off
set BuildPath=bin\Publish
rd /S /Q "%BuildPath%">nul 2>&1
dotnet publish -c Release -r win-x64 -o "%BuildPath%"
del /Q "%BuildPath%\*.Development.json"
for %%a in (cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant) do rd /S /Q "%BuildPath%\%%a"
pause
Note
Для публикации приложения для Linux замените win-x64
на linux-x64
в этом примере. Убедитесь, что вы предварительно установили NuGet-пакет Tessa.Linux
.
Запустите скрипт publish.bat
двойным кликом и дождитесь окончания его выполнения. Предварительно собирать проект в Visual Studio не требуется.
В папке bin\Publish
будет расположен собранный проект, который в дальнейшем можно будет настроить, например, в IIS по аналогии с настройкой веб-сервиса web
в Руководстве по установке.
Для проверки сервиса без установки, запустите exe-файл в папке публикации, например, TessaWebApp.exe
. Вы увидите окно с указанием локальных портов, по которым доступно приложение:
Перейдите в браузере по адресу http://localhost:5000/check
, и при корректном соединении с базой данных в app.json
, где уже установлена TESSA этой же версии, должно быть отображено окно следующего вида:
Теперь в веб-приложении можно добавлять любые зависимости как NuGet-пакеты и контроллеры по аналогии с тем, как они добавляются в проект Tessa.Extensions.Server.Web
.
В рамках примера создадим папку Controllers
и в ней класс TestController.cs
(Add -> New Item -> Class
).
Пример содержимого класса:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tessa.Cards;
using Tessa.Platform;
using Tessa.Platform.Runtime;
using Tessa.Platform.Storage;
using Tessa.Roles;
using Tessa.Web.Services;
using Unity;
namespace TessaWebApp.Controllers
{
[Route("test"), AllowAnonymous]
public class TestController :
Controller
{
public TestController(ITessaWebScope scope)
{
this.scope = scope;
}
private readonly ITessaWebScope scope;
[HttpGet("system")]
public async Task<string> GetSystemUserText()
{
var serverSettings = this.scope.UnityContainer.Resolve<ITessaServerSettings>();
var cardRepository = this.scope.UnityContainer.Resolve<ICardRepository>();
using (SessionContext.Create(Session.CreateSystemToken(SessionType.Server, serverSettings)))
{
CardGetResponse response = await cardRepository.GetAsync(
new CardGetRequest
{
CardID = Session.SystemID,
CardTypeID = RoleHelper.PersonalRoleTypeID,
});
string text = StorageHelper.PrintObject(response);
return text;
}
}
}
}
Опубликуем приложение (перед публикацией его надо закрыть, нажав Ctrl+C
в окне консоли, если приложение было запущено). Теперь запустим приложение TessaWebApp.exe
и откроем в браузере ссылку http://localhost:5000/test/system
При успешном выполнении должна быть загружена карточка сотрудника “System”, и для него выведена структура объекта с карточкой CardGetResponse
.
Теперь откройте ссылку http://localhost:5000/test/method
Поскольку этот метод с атрибутом [SessionMethod]
, то для его выполнения требуется аутентификация, и на экране будет отображён JSON для класса ValidationStorageResultBuilder
с сообщением об ошибке: Session is required to call operation /test/method
Для успешной работы такого метода сначала должен быть вызван POST-запрос по адресу /test/login
, потом результат вызова должен быть проброшен в HTTP-заголовок Tessa-Session
этого метода и всех последующих методов в пределах сессии. Для закрытия сессии выполняется POST-запрос по адресу /test/logout
(токен параметром передавать не требуется, он будет заполнен из HTTP-заголовка). Для реального приложения, конечно, адрес контроллера будет отличен от /test
.
Таким образом, мы создали, выполнили сборку и публикацию веб-приложения, которое может использоваться в сценариях обработки REST-запросов независимо от основного веб-приложения web
, при этом мы можем использовать полноценное серверное API платформы TESSA без ограничений.