ServiceStack & .NET Core

Півтора місяці тому популярний фреймворк для створення веб-сервісів і додатків ServiceStack нарешті випустив реліз під .NET Core. У цьому огляді я хочу розповісти трохи про самих пакетах платформи і що доведеться переробити в своїх проектах використовують ServiceStack, що б перенести їх на платформу .NET Core.

Вступ

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

Який був отриманий профіт від переносу? Однозначно — об'єктивно збільшилася швидкість відповідей і обробки запитів. За окремим методам час відповіді зменшилася у 3 рази. В середньому приріст продуктивності склав 30% відсотків. Звичайно основний момент — це зменшився оверхед за рахунок відсутності бібліотеки System.Web і в цілому всієї підсистеми ASP.NET на якій базується основний проект ServiceStack.

Основні бібліотеки

Автори не стали робити підтримку .NET Core безпосередньо в основних пакету, і випустили окремий набір пакетів під новою версією 1.0-* (1.0.25 на момент написання статті)

Набір пакетів досить широкий:

  • ServiceStack.Text.Core
  • ServiceStack.Interfaces.Core
  • ServiceStack.Client.Core
  • ServiceStack.HttpClient.Core
  • ServiceStack.Common.Core
  • ServiceStack.Redis.Core
  • ServiceStack.OrmLite.Core
  • ServiceStack.OrmLite.Sqlite.Core
  • ServiceStack.OrmLite.SqlServer.Core
  • ServiceStack.OrmLite.PostgreSQL.Core
  • ServiceStack.Core
  • ServiceStack.Mvc.Core
  • ServiceStack.Server.Core
  • ServiceStack.ProtoBuf.Core
  • ServiceStack.Wire.Core
  • ServiceStack.Aws.Core
  • ServiceStack.RabbitMq.Core
  • ServiceStack.Stripe.Core
  • ServiceStack.Admin.Core
  • ServiceStack.Api.Swagger.Core
  • ServiceStack.Kestrel
і представляє з себе ту ж кодову базу, що і в основному проекті, з невеликими відмінностями в окремих моментах. Мабуть, для більшості завдань, представлених пактів буде більш ніж достатньо для портування свого коду з мінімальними витратами.

Код поточних пакетів збігається, суб'єктивно, на 98-99%. Відмінності є в місцях, які вимагають підтримки інфраструктури .net core. Якщо бути зовсім точним, то дані пакети повністю підтримують .NET Standart 1.6. Тобто фактично можуть використовуватися під усіма платформами, які його реалізують, а оскільки на поточний момент 1.6 є останньою версією стандарту, то це всі сучасні платформи. Детальніше про стандарти .NET Standard Library.

Особливості поточної версії

Контейнери
ServiceStack споконвіку використав у своєму складі власний DI-контейнер — Funq, т. к. інфраструктура ASP.NET не надавала ніяких інших. Звичайно можна було замінити поточний Funq на інший улюблений контейнер, але в цілому функціональності вбудованого було більш ніж достатньо.

У версії Core можна продовжувати використовувати вбудований Funq, однак тепер ServiceStack вбудовує її в DI-контейнер інфраструктури .NET Core. За фактом конфігурувати контейнер можна тепер з 2 місць:
з методу
internal class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}

...

}

як це пропонує платформа і з методу
internal sealed class ServicesHost : AppHostBase
{
public override void Configure(Container container)
{
}
...
}

як було весь час. Однак варто враховувати головне — при такому підході з'являється «зона видимості» сервісів. Сервіси, оголошені в класі Startup, можна дозволити з усіх точок програми. Сервіси, які будуть сконфигурированны всередині перевантаження Configure — будуть видні тільки всередині ServiceStack. Інфраструктура .NET Core їх бачити не буде.

Логування
Тепер всі логування ServiceStack проксирует в стандратный логер .NET Core. Тобто тепер не потрібно використовувати сторонні бібліотеки для розширення логування SS. Досить використовувати такі для розширення логування вбудованого LoggerFactory.

ServiceStack.Authentication
Т. к. DotNetOpenAuth не портированна .NET Соге, то все що було пов'язано з цією залежністю зараз не працює. Я не використовую цю функціональність, тому подробиць як це зараз «зламалося» у мене немає.

SOAP
Все що з ним пов'язано в поточній версії не підтримується повністю.

Mini Profiler
Корисна штучка, на основній платформі, в Core не працює, оскільки вимагає залежно від System.Web.

Про інші зміни і підтримуваних і не підтримуються функції можна ознайомитися на http://docs.servicestack.net/

З чого почати портування

Перетворимо проект
Є 2 основних шляхи портування на .NET Core:
1. Перетворити поточну папку проект додаванням файлів project.json, майже стандартного для всіх *.xproj, файл, який містить функцію main файл Starupt.cs (насправді ім'я може бути будь-яким)
2. Створити порожній ASP.NET Core App і просто додати файли зі старого проекту.

Другий варіант більш кращий, оскільки вимагає трохи менше сил на перетворення і підгонку поточного проекту.

Додаємо посилання на либы
Тут нічого вигадувати не потрібно, просто додаємо стандартні пакети ServiceStack:

"dependencies": {
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
"Microsoft.Extensions.Configuration": "1.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.Binder": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
},
"System.Diagnostics.Tools": "4.0.1",
"NLog.Extensions.Logging": "1.0.0-rtm-alpha4",

"ServiceStack.Api.Swagger.Core": "1.0.25",
"ServiceStack.Client.Core": "1.0.25",
"ServiceStack.Common.Core": "1.0.25",
"ServiceStack.Core": "1.0.25",
"ServiceStack.Interfaces.Core": "1.0.25",
"ServiceStack.ProtoBuf.Core": "1.0.25",
"ServiceStack.Redis.Core": "1.0.25",
"ServiceStack.Text.Core": "1.0.25",


"System.Reflection.TypeExtensions": "4.1.0"
}

у мене крім іншого додано розширення для логування NLog, т. к. проект використовував його раніше. Конфігурація здійснюється через файл nlog.config, який нічим не відрізняється від старих версій.

Міняємо Sturtup.cs
internal class Startup
{
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;

public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
{

_loggerFactory = loggerFactory;
_loggerFactory.AddNLog();
_logger = _loggerFactory.CreateLogger(GetType());
env.ConfigureNLog($"nlog.{env.EnvironmentName}.config");

var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();

Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
OrmLiteConfig.DialectProvider = SqlServer2012Dialect.Provider;
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton(Configuration);
services.AddScoped<IDbConnectionFactory>(p => new OrmLiteConnectionFactory(Configuration.GetConnectionString("DefaultConnection")));

services.AddScoped<IRedisClientsManager>(p => new BasicRedisClientManager(Configuration.GetConnectionString("RedisConnectionString")));

services.AddSingleton<ServicesHost>();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
_logger.LogInformation(new EventId(1), "Startup started");

var host = (AppHostBase)app.ApplicationServices.GetService(typeof(ServicesHost));
app.UseServiceStack(host);
}
}

Я використовую підхід, коли все знаходиться в контейнері, тому навіть головний клас ServicesHost так само поміщається в контейнер. Це дозволяє потім в конструкторі отримує потрібні залежності, не вдаючись до самостійного їх вирішення.

У багатьох проектах раніше для доступу до поточного Http контексту використовувався Singleton з простору System.Web, оскільки зараз цей клас не доступний .Net Core потрібно використовувати ось такий сервіс:
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

Тут треба знати, що значення в HttpContextAccessor.HttpContext буде відмінно від null, тільки коли буде який-небудь запит з поза. В інших випадках там завжди міститься null.

Прийоми підключення файлів конфігурацій можна використовувати не тільки для appsettings, але і для будь-яких інших рядків, наприклад для конфігурації nlog

env.ConfigureNLog($"nlog.{env.EnvironmentName}.config");


В основному контейнері так само налаштовані всі залежності, які є в проекті (шари доступу до даних, до зовнішніх сервісів і ін). Всередині ServiceStack варто залишити лише дуже обмежене число, якими ніхто поза SS користуватися ніколи не буде.

Зміни ServicesHost
Це основний клас-контейнер для конфігурування оточення ServiceStack він звичайно вже є в старому проекті. В кожному проекті він може називатися по-різному, але суть що це клас успадкований від класу AppHostBase, якщо в портируемом проекті це не так, то необхідно унаследоваться від цього класу:

internal sealed class ServicesHost : AppHostBase
{
private readonly IHttpContextAccessor _accessor;
private readonly IAppSettings _settings;
private readonly ILogFactory _loggerFactory;
private readonly IRedisClientsManager _redisClientsManager;

public ServicesHost(IHttpContextAccessor accessor,
IAppSettings settings,
ILogFactory loggerFactory,
IRedisClientsManager redisClientsManager): base(StringsConstants.SERVICES_NAME, typeof(AuthService).GetAssembly())
{
_accessor = accessor;
_settings = settings;
_loggerFactory = loggerFactory;
_redisClientsManager = redisClientsManager;
}

public override void Configure(Container container)
{
//Приватний варіант конфігурування оточення
ConfigurePlugins(); 
container.RegisterValidators(typeof(RegistrationRequestValidator).GetAssembly());
ConfigureBinders(); //Конфігуруємо окремі запити для коректного зв'язування
ConfigureGlobalRequestFilters(); //Конфігуруємо глобальні фільтри для запитів 

SetConfig(new HostConfig
{
ApiVersion = StringsConstants.API_VERSION,
#if !DEBUG
EnableFeatures = Feature.Json | Feature.ProtoBuf | Feature.Html,
WriteErrorsToResponse = false,
#else
EnableFeatures = Feature.Html | Feature.Json | Feature.RequestInfo | Feature.Metadata | Feature.ProtoBuf,
WriteErrorsToResponse = true,
#endif
DebugMode = _settings.Get("DebugMode", 'false'),
LogFactory = _loggerFactory,
Return204NoContentForEmptyResponse = true,
DefaultRedirectPath = "/swagger-ui/",
MapExceptionToStatusCode = {
{typeof(DomainException), (int) HttpStatusCode.InternalServerError}
}

});
}

.....

}

в моєму випадку стандартна функціональність валідації запитів конфігурується всередині ServiceStack, оскільки вона ніде не використовується.

Складність перенесення цього файлу .NET Core залежить від проекту. Якщо використовуються тільки стандартна функціональність SS та інші сторонні пакети, які вже портіровани на .NET Core, то зміни у файлі можуть бути мінімальними.

System.Configuration
Це одне із самих «хворих» місць при портування з класичного ASP.NET на .NET Core. ServiceStack у своїй реалізації абстракції IAppSettings використовував і використовує роботу зі старими файлами app/web.config. Для того що б дати йому можливість працювати з appsettings.config треба просто реалізувати свою версію IAppSettings. Клас насправді досить простий і використовує частину інфраструктури самого ServiceStack:

public class AppSettings : AppSettingsBase
{
public AppSettings (IConfigurationRoot root, string tier = null) : base(new ConfigurationManagerWrapper(root))
{
Tier = tier;
}

public override string GetString(string name)
{
return GetNullableString(name);
}

private class ConfigurationManagerWrapper : ISettings
{
private readonly IConfigurationRoot _root;
private Dictionary<string, string> _appSettings;

public ConfigurationManagerWrapper(IConfigurationRoot root)
{
_root = root;
}

private Dictionary<string, string> GetAppSettingsMap()
{
if (_appSettings == null)
{
var dictionary = new Dictionary<string, string>();
var appSettingsSection = _root.GetSection("appSettings");
if (appSettingsSection != null)
{
foreach (var child in appSettingsSection.GetChildren())
{
словник.Add(child.Key, child.Value);
}
}
_appSettings = dictionary;
}
return _appSettings;
}

#region Implementation of ISettings

public string Get(string key)
{
string str;
if (!GetAppSettingsMap().TryGetValue(key, out str))
return null;
return str;
}

public List < string> GetAllKeys()
{
return GetAppSettingsMap().Keys.ToList();
}

#endregion
}
}

тепер досить просто додати в контейнер залежність
services.AddSingleton<IAppSettings, AppSettings>();

і все — налаштування з секції appSettings у файлі appsettings.config у вас в кишені.

Висновок

Те що я описав вище — це всі(!) зміни (дрібниці з перейменуваннями деяких інтерфейсів і їх перенесення в інші класи — питання невеликого часу), які я додав в проект написаний ще на повній версії ServiceStack, що б він заробив з його Core версією.

Все що стосується OrmLite, Redis, Text, Client та інших корисних пакетів — не вимагає взагалі ніяких змін. І це дуже вигідно відрізняє, наприклад той же OrmLite від EF. Для того що б перенести проект написаний на EF з повного .NET на .Core доведеться витратити не мало зусиль. Тут не потрібно взагалі ніяких маніпуляцій — все просто працює.

Якщо ви використовували ServiceStack у своїх проектах на повному .NET, перехід на .NET Core може зайняти зовсім небагато часу. Команда SS постаралася зробити все якомога більш сумісним і їй це цілком вдалося. Звичайно ServiceStack платний, однак ніхто не забороняє вам використовувати відкритий код проекту в своїх особистих цілях.

І в кінці невеликий лайфхак, як прибрати обмеження на використання під час розробок (ті що є — ніколи не вистачає):

1. Викачуємо до себе github.com/ServiceStack/ServiceStack.Text
2. Міняємо ліміти у файлі LicenseUtils.cs або відключаємо виклик перевірок
3. Збираємо Core проект
4. Копіюємо файли з bin\Release\netstandart1.3 %HOME%\.nuget\packages\ServiceStack.Text.Core\{ваша версія}\lib\netstandart1.3
5. Profit!

Якщо з'являється нова версія доведеться проробляти всі кроки заново. І не варто боятися, поки ви не видалите весь кеш — «ваша» збірка не затреться.

p.s. ах так… тепер все працює не на iis, а в docker-контейнері.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.