📜 ⬆️ ⬇️

ServiceStack & .NET Core

A month and a half ago, ServiceStack, a popular framework for creating web services and applications, finally released a .NET Core release. In this review, I want to tell you a little about the platform packages themselves and what will have to be redone in my projects using ServiceStack in order to port them to the .NET Core platform.

Introduction


It should immediately say that the project team tried their best. I was able to adapt my mediocre research project with a couple of dozens of services to new realities in almost one day.

What was the profit from the transfer? Definitely - objectively increased the speed of responses and query processing. For individual methods, the response time decreased by 3 times. On average, the performance increase was 30% percent. Of course, the main point is that the overhead projector has decreased due to the lack of the System.Web library and the whole ASP.NET subsystem on the whole, on which the main project ServiceStack is based.

Main libraries


The authors did not do .NET Core support directly in the main package, and released a separate set of packages under the new version 1.0- * ( 1.0.25 at the time of this writing)
')
The set of packages is quite extensive:


and is the same code base as in the main project, with small differences in individual points. Perhaps, for most of the tasks presented by the pacts, it will be more than enough to port your code with minimal cost.

The code of the current packages matches, subjectively, by 98-99%. There are differences in places that require support for the .net core infrastructure. To be perfectly accurate, these packages fully support .NET Standard 1.6. Those. can actually be used under all platforms that implement it, and since Currently 1.6 is the latest version of the standard, then these are all modern platforms. Learn more about the .NET Standard Library .

Features of the current version


Containers

ServiceStack has used its own DI container, Funq , for centuries. ASP.NET infrastructure did not provide any other. Of course, you could replace the current Funq with another favorite container, but in general, the functionality of the built-in was more than enough .

In the Core version, you can continue to use the built-in Funq , but now ServiceStack builds it into the .NET Core infrastructure DI container. In fact, you can now configure the container from 2 places:

From the method:

internal class Startup { public void ConfigureServices(IServiceCollection services) { } ... } 

As the platform proposes and from the method:

 internal sealed class ServicesHost : AppHostBase { public override void Configure(Container container) { } ... } 

As it was all the time. However, it is necessary to take into account the main thing - with this approach, a “visibility zone” of services now appears. Services declared in the Startup class can be enabled from all points of the application. Services that will be configured inside the Configure overload will only be visible inside the ServiceStack. Infrastructure. NET Core will not see them.

Logging

Now all ServiceStack logging is proxied to the standard .NET Core logger. Those. now you do not need to use third-party libraries to extend SS logging. It is enough to use such to expand logging of the built-in LoggerFactory .

ServiceStack.Authentication

Since DotNetOpenAuth is not ported to .NET Core, then everything that was associated with this dependency is not working now. I do not use this functionality, so I don’t have the details of how it is now “broken”.

SOAP

All that is associated with it in the current version is not fully supported.

Mini profiler

The useful thing on the main platform, in the Core does not work, because requires a dependency on System.Web .

On the other changes and supported and not supported features can be found here .

How to start porting


Convert the project

There are 2 main ways to port to .NET Core:

1. Convert the current folder to a project by adding project.json files, almost standard for all * .xproj , a file containing the main function and a Starupt.cs file (in fact, the name can be anything)

2. Create an empty ASP.NET Core App and simply add files from the old project.

The second option is preferable, because requires a little less effort to transform and fit the current project.

Add links to

There is no need to invent anything, just add the standard ServiceStack packages:

 "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" } 

I have also added an extension for logging NLog, because The project used it earlier. Configuration is done through the nlog.config file, which is no different from older versions.

Change 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); } } 

I use the approach when everything is in a container, so even the main ServicesHost class also fits in the container. This allows later in the constructor to get the necessary dependencies without resorting to resolving them on their own.

In many projects earlier, Singleton from the System.Web space was used to access the current Http context. Now this class is not available in .Net Core. You need to use the following service:

 services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); 

Here you need to know that the value in HttpContextAccessor.HttpContext will be different from null , only when there is any request from outside. In other cases, it always contains null .

Methods for connecting configuration files can be used not only for appsettings , but also for any other lines, for example, for the nlog configuration

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

In the main container, all dependencies that are in the project (data access layers, external services, etc.) are also configured. Inside the ServiceStack you should leave only a very limited number, which no one outside SS will ever use.

ServicesHost Changes

This is the main container class for configuring the ServiceStack environment and is usually already in the old project. In each project, it can be called differently, but the essence is that this class is inherited from the class AppHostBase , if in a ported project it is not, then it is necessary to inherit from this class:

 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} } }); } ..... } 

In my case, the standard functionality of request validation is configured inside the ServiceStack, since it is not used anywhere else.

The complexity of transferring this file to .NET Core depends on the project. If you use only standard SS functionality and other third-party packages that are already ported to .NET Core, then changes to the file may be minimal.

System.Configuration

This is one of the “sickest” places when porting from classic ASP.NET to .NET Core. ServiceStack, in its implementation of abstraction, IAppSettings used and used work with old app / web.config files. In order to enable it to work with appsettings.config, you just need to implement your version of IAppSettings . The class is actually quite simple and uses part of the infrastructure of the ServiceStack itself:

  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 } } 

Now just add the dependency to the container:

 services.AddSingleton<IAppSettings, AppSettings>(); 

And that's it - the settings from the appSettings section in the appsettings.config file are in your pocket.

Conclusion


What I described above is all (!) Changes (little things with renaming some interfaces and forwarding them to other classes are a matter of a short time), which I added to the project written on the full version of ServiceStack, what would it work with its Core version .

All that concerns OrmLite, Redis, Text, Client and other useful packages didn’t require any changes at all. And it is very beneficial distinguishes, for example, the same OrmLite from EF. In order to port a project written in EF from a full .NET to .Core, you will have to expend quite a few efforts. There is no need for any manipulations at all - everything just works.

If you used ServiceStack in your projects on full .NET, the transition to .NET Core can take quite a bit of time. The SS team tried to make everything as compatible as possible and hey, it was quite possible. Of course, the ServiceStack is paid, but no one forbids you to use the open source code of the project for your personal purposes.

And at the end of a small life hack, how to remove restrictions on use during development (those that are - never enough):

1. Download to your github.com/ServiceStack/ServiceStack.Text
2. Change the limits in the file LicenseUtils.cs or disable the call to checks
3. We collect Core project
4. Copy files from bin \ Release \ netstandart1.3 to% HOME% \. Nuget \ packages \ ServiceStack.Text.Core \ {your version} \ lib \ netstandart1.3
5. Profit!

If there is a new version, you will have to do all the steps again. And do not be afraid, until you delete the entire cache - "your" assembly does not get stuck.

PS: oh yeah ... now everything works not on iis, but in the docker container.

UPD: In the commentary, Scratch proposed a less invasive option to bypass the restrictions, but for the Core version it will be slightly different:

 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 }; } } } 

Source: https://habr.com/ru/post/314202/


All Articles