Перейти к содержанию

API компиляции

Платформа содержит API для компиляции C# сценариев “на лету” на сервере приложений.

Компилятор

Низкоуровневым объектом, обеспечивающим связь с компилятором, является Tessa.Compilation.ICompiler. Он обеспечивает вызов C# компилятора и первичную обработку результата.

В общем случае рекомендуется использовать объект Tessa.Compilation.ITessaCompiler<T>. Он дополнительно предоставляет свойства для настройки параметров компиляции по умолчанию и вспомогательные методы для упрощения создания компилятора объекта заданного типа. Базовая реализация: TessaCompilerBase<T>.

Основные члены TessaCompilerBase<T>:

  • DefaultUsings – список подключаемых по умолчанию пространств имён.
  • DefaultStatics – список подключаемых по умолчанию статических пространств имён.
  • DefaultReferences – список подключаемых по умолчанию зависимостей.
  • DefaultIgnoreWarnings – список кодов игнорируемых по умолчанию предупреждений.
  • CreateContext – создаёт контекст компилятора.
  • CompileAsync – выполняет компиляцию объектов системы в соответствии с заданным контекстом.
  • PrepareCompilationContextAsync – подготавливает ICompilationContext к использованию.
  • CreateTessaCompilationResultAsync – создаёт объект ITessaCompilationResult с результатами компиляции.
  • PrepareValidationResultAsync – создаёт объект ValidationResult с результатами компиляции.

В платформе существует ряд компиляторов. Вот некоторые из них:

  • IKrCompiler – объект, выполняющий компиляцию объектов подсистемы маршрутов.
  • IKrVirtualFileCompiler – объект, выполняющий компиляцию скриптов виртуальных файлов.
  • IPlaceholderCompiler – объект, выполняющий компиляцию текста с плейсхолдерами.

Пример создания компилятора

Рассмотрим пример создания компилятора скриптов виртуальных файлов.

Создадим контекст компилятора:

Tessa.Extensions.Default.Server\Files\VirtualFiles\Compilation\IKrVirtualFileCompilationContext.cs

#nullable enable

using System;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <summary> /// Контекст компиляции для компилятора <see cref="IKrVirtualFileCompiler"/> /// </summary> public interface IKrVirtualFileCompilationContext { /// <summary> /// Идентификатор виртуального файла. /// </summary> Guid ID { get; set; }

/// <summary> /// Название виртуального файла. /// </summary> string Name { get; set; }

/// <summary> /// C# текст сценария инициализации виртуального файла. /// </summary> string? InitializationScenario { get; set; } } }

#nullable enable

using System;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <inheritdoc cref="IKrVirtualFileCompilationContext"/> public sealed class KrVirtualFileCompilationContext : IKrVirtualFileCompilationContext { #region Fields

private string name = string.Empty;

#endregion

#region IKrVirtualFileCompilationContext Members

/// <inheritdoc/> public Guid ID { get; set; }

/// <inheritdoc/> public string Name { get => this.name; set => this.name = NotNullOrThrow(value); }

/// <inheritdoc/> public string? InitializationScenario { get; set; }

#endregion } }

Определим интерфейс компилятора:

Tessa.Extensions.Default.Server\Files\VirtualFiles\Compilation\IKrVirtualFileCompiler.cs

#nullable enable

using Tessa.Compilation;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <summary> /// Объект, выполняющий компиляцию скриптов виртуальных файлов. /// </summary> public interface IKrVirtualFileCompiler : ITessaCompiler<IKrVirtualFileCompilationContext> { } }

Создадим реализацию компилятора:

Tessa.Extensions.Default.Server\Files\VirtualFiles\Compilation\KrVirtualFileCompiler.cs

#nullable enable

using System; using System.Threading; using System.Threading.Tasks; using Tessa.Compilation; using Tessa.Localization; using Tessa.Platform.Collections; using Tessa.Platform.Validation;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <inheritdoc cref="IKrVirtualFileCompiler"/> public sealed class KrVirtualFileCompiler : TessaCompilerBase<IKrVirtualFileCompilationContext>, IKrVirtualFileCompiler { #region Fields

private readonly ICompilationSourceProvider compilationSourceProvider;

/// <summary> /// Параметры скрипта. /// </summary> private static readonly Tuple<string, string>[] parameters = { new Tuple<string, string>(nameof(IKrVirtualFileScriptContext), "context") };

#endregion

#region Constructors

/// <summary> /// Инициализирует новый экземпляр класса. /// </summary> /// <param name="compiler"><inheritdoc cref="ICompiler" path="/summary"/></param> /// <param name="compilationSourceProvider"><inheritdoc cref="ICompilationSourceProvider" path="/summary"/></param> public KrVirtualFileCompiler( ICompiler compiler, ICompilationSourceProvider compilationSourceProvider) : base(compiler) { this.compilationSourceProvider = NotNullOrThrow(compilationSourceProvider);

this.DefaultStatics.AddRange( CompilationHelper.TessaDefaultStatics);

this.DefaultReferences.AddRange( CompilationHelper.TessaExtensionsDefaultReferences);

this.DefaultUsings.AddRange( CompilationHelper.TessaDefaultUsings); this.DefaultUsings.Add( "Tessa.Extensions.Default.Server.Files.VirtualFiles"); this.DefaultUsings.Add( "Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation"); }

#endregion

#region Base Overrides

/// <inheritdoc /> public override IKrVirtualFileCompilationContext CreateContext() => new KrVirtualFileCompilationContext();

/// <inheritdoc /> protected override ValueTask PrepareCompilationContextAsync( IKrVirtualFileCompilationContext tessaCompilationContext, ICompilationContext compilationContext, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(tessaCompilationContext.InitializationScenario)) { compilationContext.Sources.Add(this.PrepareSource( tessaCompilationContext.ID, tessaCompilationContext.Name, tessaCompilationContext.InitializationScenario)); }

return ValueTask.CompletedTask; }

/// <inheritdoc /> protected override async ValueTask<ValidationResult> PrepareValidationResultAsync( IKrVirtualFileCompilationContext tessaCompilerContext, ICompilationResult compilationResult, CancellationToken cancellationToken = default) { if (compilationResult.ValidationResult.IsSuccessful) { return await base.PrepareValidationResultAsync( tessaCompilerContext, compilationResult, cancellationToken); }

var result = new ValidationResultBuilder( compilationResult.CompilerOutput.Count);

foreach (var compilerOutputItem in compilationResult.CompilerOutput) { var source = NotNullOrThrow(compilerOutputItem.Source);

var errorText = await LocalizationManager.FormatAsync( "$KrVirtualFiles_CompilationErrorTemplate", await LocalizationManager.LocalizeAsync( source.Name, cancellationToken), compilerOutputItem.ToString(), cancellationToken);

var sourceCode = CompilationHelper.FormatErrorIntoMember( source, compilerOutputItem, CompilationHelper.GetMemberName( source, compilerOutputItem));

var validator = ValidationSequence .Begin(result) .SetObjectName(this);

if (compilerOutputItem.IsWarning) { validator.WarningDetails(errorText, sourceCode); } else { validator.ErrorDetails(errorText, sourceCode); }

validator.End(); }

return result.Build(); }

#endregion

#region Private Methods

/// <summary> /// Создаёт элемент компиляции. /// </summary> /// <param name="id">Идентификатор элемента компиляции.</param> /// <param name="name">Имя элемента компиляции.</param> /// <param name="source">C# сценарий.</param> /// <returns><inheritdoc cref="ICompilationSource" path="/summary"/></returns> private ICompilationSource PrepareSource( Guid id, string name, string source) { var syntaxTreeBuilder = this.compilationSourceProvider .AcquireSyntaxTree();

syntaxTreeBuilder .SetID(id) .SetName(name) .Namespace("Tessa.Extensions.Default.Servier.Files.VirtualFiles.Generated") .Class( $"VirtualFile_{id:N}", AccessModifier.Public, new[] { nameof(IKrVirtualFileScript) }, guid: id) .AddMethod( nameof(Task), nameof(IKrVirtualFileScript.InitializationScenarioAsync), parameters, source, isAsync: true);

return syntaxTreeBuilder.Build(); }

#endregion } }

Кэш компиляции

Кэш компиляции используется для:

  1. хранения результатов в памяти и/или базе данных;
  2. формирования контекста компиляции единообразным способом для всех объектов, содержащихся в кэше;
  3. выполнения компиляции, с помощью соответствующего компилятора, при отсутствии доступного результата компиляции объекта;
  4. создания фабрики экземпляров объектов сценариев (ITessaCompilationFactory<TKey, TInstance>).

Существует три основных интерфейса, описывающих кэши компиляции:

  1. ITessaCompilationObjectCacheCore<TKey, TInstance> – описывает базовый объект кэша, предоставляющий результаты компиляции. Базовая реализация: TessaCompilationObjectCacheCoreBase<TCompilerContext, TKey, TInstance>.
  2. ITessaCompilationObjectCache<TCompilerContext, TKey, TInstance> – кэш, содержащий объекты результатов компиляции (ITessaCompilationObject<TKey, TInstance>). Базовая реализация: TessaCompilationObjectCacheBase<TCompilerContext, TKey, TInstance>.
  3. ITessaCompilationObjectInMemoryCache<TCompilerContext, TKey, TInstance> – кэш, содержащий объекты результатов компиляции (ITessaCompilationObject<TKey, TInstance>) в памяти, источником для которых является внешний код. Базовая реализация: TessaCompilationObjectInMemoryCacheBase<TCompilerContext, TKey, TInstance>.

Кэш компиляции возвращает объект типа ITessaCompilationObject<TKey, TInstance>, где TKey – тип ключа, по которому можно получить доступ экземпляру объекта, TInstance – тип объекта.

  • Result – объект, содержащий результат компиляции объектов системы.
  • Factory – объект, предоставляющий экземпляры объектов типа TInstance по ключу типа TKey.

Фабрика экземпляров объектов (ITessaCompilationObject<TKey, TInstance>)

Фабрика предназначена для создания экземпляров объектов, содержащихся в заданной сборке. Базовым классом является: TessaCompilationFactoryBase<TKey, TInstance>, где TKey – тип ключа, по которому можно получить доступ экземпляру объекта. TInstance – тип объекта.

Поведение объекта определяется используемыми зависимостями:

  1. провайдером типов;
  2. провайдером идентификатора типа;
  3. стратегией создания экземпляров объектов;
  4. стратегией управления временем жизни созданного экземпляра объекта.

Рассмотрим пример создания фабрики экземпляров объектов:

// Массив байтов, который является образом в формате COFF, содержащим сборку. byte[]? assemblyBytes = ...;

// Получение зависимостей по умолчанию. var typeProvider = UnityContainer.Resolve<ITypeProvider>(); var typeIdentifierProvider = UnityContainer.Resolve<ITypeIdentifierProvider<Guid>>(); var instanceCreationStrategy = UnityContainer.Resolve<IInstanceCreationStrategy>(); var instanceLifetimeManager = UnityContainer.Resolve<IInstanceLifetimeManager>();

var factory = new TessaCompilationFactory<Guid, IKrVirtualFileScript>( assemblyBytes, typeProvider, typeIdentifierProvider, instanceCreationStrategy, instanceLifetimeManager);

// Получение экземпляра объекта. Guid keyID = ...; // Идентификатор `System.Type.GUID` типа объекта, экземпляр которого требуется создать. var instance = factory.TryGet(keyID);

Провайдер типов (ITypeProvider)

Предоставляет типы, удовлетворяющие некоторому условию. Реализация по умолчанию (TypeProvider) возвращает все не абстрактные типы, которые наследуются от заданного базового типа.

Провайдер идентификатора типа (ITypeIdentifierProvider<T>)

Предоставляет идентификатор для заданного типа. В типовом решении, есть две реализации:

  1. TypeGuidIdentifierProvider – объект, предоставляющий идентификатор типа (System.Type.GUID). Объект можно получить из Unity-контейнера по интерфейсу ITypeIdentifierProvider<Guid>.
  2. TypeNameIdentifierProvider – объект, предоставляющий имя типа (System.Reflection.MemberInfo.Name). Объект можно получить из Unity-контейнера по интерфейсу ITypeIdentifierProvider<string>.

Стратегия создания экземпляров объектов (IInstanceCreationStrategy)

Определяет способ создания экземпляра объекта заданного типа. Реализация по умолчанию (DefaultInstanceCreationStrategy) создаёт экземпляр, используя конструктор без параметров.

Tip

Типовые названия объектов, по которым они зарегистрированы в unity-контейнере, содержатся в Tessa.Compilation.InstanceCreationStrategyNames.

Стратегия управления временем жизни экземпляра объекта (IInstanceLifetimeManager)

Определяет время жизни созданного экземпляра объекта, при использовании в фабрике TessaCompilationFactoryBase<TKey, TInstance>.

В типовом решении есть две реализации:

  1. TransientInstanceLifetimeManager – объект, не управляющий временем жизни заданного значения.
  2. SingletonInstanceLifetimeManager – объект, управляющий временем жизни заданного значения с помощью стратегии синглтон.

Tip

Типовые названия объектов, по которым они зарегистрированы в unity-контейнере, содержатся в Tessa.Compilation.InstanceLifetimeManagerNames.

Пример создания кэша компиляции

В примере будет использован компилятор, созданный в п. Пример создания компилятора.

Определим интерфейс кэша компиляции:

Tessa.Extensions.Default.Server\Files\VirtualFiles\Compilation\IKrVirtualFileCompilationCache.cs

#nullable enable

using System; using Tessa.Compilation; using Tessa.Extensions.Default.Shared;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <summary> /// Кэш, содержащий результаты компиляции скриптов виртуальных файлов. /// </summary> /// <remarks>Категория кэша: <see cref="DefaultCompilationCacheNames.KrVirtualFile"/>.</remarks> public interface IKrVirtualFileCompilationCache : ITessaCompilationObjectCache<IKrVirtualFileCompilationContext, Guid, IKrVirtualFileScript> { } }

Реализуем его:

Tessa.Extensions.Default.Server\Files\VirtualFiles\Compilation\KrVirtualFileCompilationCache.cs

#nullable enable

using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tessa.Compilation; using Tessa.Extensions.Default.Shared; using Tessa.Platform; using Unity;

namespace Tessa.Extensions.Default.Server.Files.VirtualFiles.Compilation { /// <inheritdoc cref="IKrVirtualFileCompilationCache"/> public sealed class KrVirtualFileCompilationCache : TessaCompilationObjectCacheBase<IKrVirtualFileCompilationContext, Guid, IKrVirtualFileScript>, IKrVirtualFileCompilationCache { #region Fields

private readonly IKrVirtualFileCache krVirtualFileCache;

private readonly ITypeProvider typeProvider;

private readonly ITypeIdentifierProvider<Guid> typeIdentifierProvider;

private readonly IInstanceCreationStrategy instanceCreationStrategy;

private readonly IInstanceLifetimeManager instanceLifetimeManager;

#endregion

#region Constructors

/// <summary> /// Инициализирует новый экземпляр класса. /// </summary> /// <param name="tessaCompilationObjectGlobalCache"><inheritdoc cref="TessaCompilationObjectGlobalCache" path="/summary"/></param> /// <param name="compiler"><inheritdoc cref="IKrVirtualFileCompiler" path="/summary"/></param> /// <param name="tessaCompilationRepository"><inheritdoc cref="ITessaCompilationRepository" path="/summary"/></param> /// <param name="krVirtualFileCache"><inheritdoc cref="IKrVirtualFileCache" path="/summary"/></param> /// <param name="typeProvider"><inheritdoc cref="ITypeProvider" path="/summary"/></param> /// <param name="typeIdentifierProvider"><inheritdoc cref="ITypeIdentifierProvider{T}" path="/summary"/></param> /// <param name="instanceCreationStrategy"><inheritdoc cref="IInstanceCreationStrategy" path="/summary"/></param> /// <param name="instanceLifetimeManager"><inheritdoc cref="IInstanceLifetimeManager" path="/summary"/></param> /// <param name="unityDisposableContainer"><inheritdoc cref="IUnityDisposableContainer" path="/summary"/></param> public KrVirtualFileCompilationCache( TessaCompilationObjectGlobalCache tessaCompilationObjectGlobalCache, IKrVirtualFileCompiler compiler, ITessaCompilationRepository tessaCompilationRepository, IKrVirtualFileCache krVirtualFileCache, ITypeProvider typeProvider, ITypeIdentifierProvider<Guid> typeIdentifierProvider, IInstanceCreationStrategy instanceCreationStrategy, [Dependency(InstanceLifetimeManagerNames.Singleton)] IInstanceLifetimeManager instanceLifetimeManager, [OptionalDependency] IUnityDisposableContainer? unityDisposableContainer = null) : base( DefaultCompilationCacheNames.KrVirtualFile, tessaCompilationObjectGlobalCache, compiler, tessaCompilationRepository, unityDisposableContainer) { this.krVirtualFileCache = NotNullOrThrow(krVirtualFileCache); this.typeProvider = NotNullOrThrow(typeProvider); this.typeIdentifierProvider = NotNullOrThrow(typeIdentifierProvider); this.instanceCreationStrategy = NotNullOrThrow(instanceCreationStrategy); this.instanceLifetimeManager = NotNullOrThrow(instanceLifetimeManager); }

#endregion

#region Base Overrides

/// <inheritdoc/> protected override async Task<TessaCompilationContext<IKrVirtualFileCompilationContext>> GetCompilerContextAsync( Guid id, Func<IKrVirtualFileCompilationContext> getCompilerContextFunc, CancellationToken cancellationToken = default) { var krVirtualFile = await this.krVirtualFileCache.TryGetAsync( id, cancellationToken);

if (krVirtualFile is null) { return new TessaCompilationContext<IKrVirtualFileCompilationContext>( id) { ValidationResult = this.CreateSourceObjectNotFoundValidationResult( id), }; }

var context = getCompilerContextFunc();

context.ID = id; context.Name = krVirtualFile.Name; context.InitializationScenario = krVirtualFile.InitializationScenario;

return new TessaCompilationContext<IKrVirtualFileCompilationContext>( id) { CompilerContext = context, }; }

/// <inheritdoc/> protected override async Task<IList<TessaCompilationContext<IKrVirtualFileCompilationContext>>> GetCompilerContextAsync( Func<IKrVirtualFileCompilationContext> getCompilerContextFunc, CancellationToken cancellationToken = default) { var krVirtualFiles = await this.krVirtualFileCache.GetAllAsync( cancellationToken); var contexts = new List<TessaCompilationContext<IKrVirtualFileCompilationContext>>(krVirtualFiles.Length);

foreach (var krVirtualFile in krVirtualFiles) { var context = getCompilerContextFunc();

context.ID = krVirtualFile.ID; context.Name = krVirtualFile.Name; context.InitializationScenario = krVirtualFile.InitializationScenario;

contexts.Add( new TessaCompilationContext<IKrVirtualFileCompilationContext>( context.ID) { CompilerContext = context, }); }

return contexts; }

/// <inheritdoc/> protected override ValueTask<ITessaCompilationFactory<Guid, IKrVirtualFileScript>> CreateCompilationFactoryAsync( ITessaCompilationResult tessaCompilationResult, CancellationToken cancellationToken = default) { return ValueTask.FromResult<ITessaCompilationFactory<Guid, IKrVirtualFileScript>>( new TessaCompilationFactory<Guid, IKrVirtualFileScript>( tessaCompilationResult.AssemblyBytes, this.typeProvider, this.typeIdentifierProvider, this.instanceCreationStrategy, this.instanceLifetimeManager)); }

#endregion } }

Пример использования кэша компиляции

Рассмотрим пример использования кэша компиляции, созданного в предыдущем пункте:

var compilationCache = UnityContainer.Resolve<IKrVirtualFileCompilationCache>(); var validationResult = new ValidationResultBuilder(); Guid virtualFileID = ...;

// Получение объекта компиляции для виртуального файла с заданным идентификатором. var compilationResult = await compilationCache.GetAsync( virtualFileID, cancellationToken: cancellationToken);

// Обработка ошибок компиляции. validationResult.Add(compilationResult.Result.ValidationResult); if (!compilationResult.Result.ValidationResult.IsSuccessful) { return; }

// Создание экземпляра объекта скрипта. // В данном примере объект компиляции содержит только один объект и его идентификатор совпадает с идентификатором объекта компиляции. // В общем случае здесь может быть указан любой ключ, по которому запрашивается экземпляр объекта. IKrVirtualFileScript scriptInstance = compilationResult.Factory.TryGet(virtualFileID);

// Проверка успешности создания экземпляра сценария. if (scriptInstance is null) { validationResult.AddError(this, $"Class {virtualFileID} not found."); return; }

await scriptInstance.InitializationScenarioAsync(context);

Низкоуровневое API для управления хранением результатов компиляции в базе данных

Результаты компиляции всех объектов хранятся в базе данных в таблице CompilationCache.

Note

Можно безопасно удалять объекты напрямую из базы данных.

Для доступа к её объектам существует репозиторий ITessaCompilationRepository.

Основные методы:

  • GetAsync – возвращает результат компиляции, имеющий указанный идентификатор.
  • StoreAsync – сохраняет результат компиляции по указанному идентификатору.
  • DeleteAsync – удаляет результаты компиляции.
Back to top