Pull to refresh

ServiceStack & .NET Core

Reading time 10 min
Views 14K
Полтора месяца назад популярный фреймворк для создания веб-сервисов и приложений 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 Сore, то все что было связанно с этой зависимостью сейчас не работает. Я не использую эту функциональность, поэтому подробностей как это сейчас «сломалось» у меня нет.

SOAP

Все что с ним связанно в текущей версии не поддерживается полностью.

Mini Profiler

Полезная штучка, на основной платформе, в Core не работает, т.к. требует зависимости от System.Web.

Об остальных изменениях и поддерживаемых и не поддерживаемых функциях можно ознакомиться тут.

С чего начать портирование


Преобразуем проект

Есть 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())
                        {
                            dictionary.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-контейнере.

UPD: В комментария Scratch предложил менее инвазивный вариант обойти ограничения, но для Core версии он будет чуток отличаться:

public static class ServiceStackHelper
    {
        static ServiceStackHelper()
        {
            var instance = new MyNetStandardPclExport();
            PclExport.Instance = instance;
            NetStandardPclExport.Provider = instance;
            Licensing.RegisterLicense(string.Empty);
        }

        public static void Help()
        {

        }

        private class MyNetStandardPclExport: NetStandardPclExport
        {
            public override LicenseKey VerifyLicenseKeyText(string licenseKeyText)
            {
                return new LicenseKey { Expiry = DateTime.MaxValue, Hash = string.Empty, Name = "Habr", Ref = "1", Type = LicenseType.Enterprise };
            }
        }
    }
Tags:
Hubs:
+18
Comments 4
Comments Comments 4

Articles