diff --git a/test/issue11/ArticleCollection.cs b/test/issue11/ArticleCollection.cs new file mode 100644 index 0000000..aaa2bc6 --- /dev/null +++ b/test/issue11/ArticleCollection.cs @@ -0,0 +1,10 @@ +using System; + +namespace Itg.Persistence.Secondary +{ + [Serializable] + public class ArticleCollection + { + public byte[] Data { get; set; } + } +} diff --git a/test/issue11/BackgroundTaskQueue.cs b/test/issue11/BackgroundTaskQueue.cs new file mode 100644 index 0000000..0092150 --- /dev/null +++ b/test/issue11/BackgroundTaskQueue.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace issue11 +{ + internal class BackgroundTaskQueue : IBackgroundTaskQueue + { + private readonly Channel _queue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + AllowSynchronousContinuations = false, + SingleReader = true, + SingleWriter = false, + }); + + public async Task EnqueueAsync(object order, CancellationToken ct) => await _queue.Writer.WriteAsync(order, ct); + + public async Task DequeueAsync(CancellationToken ct) => await _queue.Reader.ReadAsync(ct); + } +} diff --git a/test/issue11/Controllers/TestController.cs b/test/issue11/Controllers/TestController.cs new file mode 100644 index 0000000..8b72255 --- /dev/null +++ b/test/issue11/Controllers/TestController.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace issue11.Controllers +{ + [ApiController] + public class MainController : ControllerBase + { + private readonly IBackgroundTaskQueue _taskQueue; + + public MainController(IBackgroundTaskQueue taskQueue) + { + _taskQueue = taskQueue; + } + + [Route("[action]")] + public async Task Test() + { + await _taskQueue.EnqueueAsync(new CreateCollectionBackgroundWork.Order(), HttpContext.RequestAborted); + + return Ok(); + } + } +} diff --git a/test/issue11/CreateCollectionBackgroundWork.cs b/test/issue11/CreateCollectionBackgroundWork.cs new file mode 100644 index 0000000..9f1098a --- /dev/null +++ b/test/issue11/CreateCollectionBackgroundWork.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using Itg.Persistence.Secondary; +using Microsoft.Extensions.Logging; + +namespace issue11 +{ + public static class CreateCollectionBackgroundWork + { + public class Order : IBackgroundWorkOrder + { + /* contains parameters for it's worker*/ + } + + public class Worker : IBackgroundWorker + { + private readonly ILogger _logger; + private readonly ShopArticleService _articleService; + private readonly IKeyValueStore _collectionStore; + + public Worker( + ILoggerFactory loggerFactory, + ShopArticleService articleService, + /* ^ logging works inside this; into integra- ^ */ + IKeyValueStore collectionStore + /* ^ the logger in this (FileKeyValueStore) is the one not logging to the file, but does to console, and logs also appear when the config file is reloaded ^ */) + { + this._logger = loggerFactory.CreateLogger(nameof(CreateCollectionBackgroundWork)); + // ^ this is actually still logging to integra- file ^ + + _articleService = articleService; + _collectionStore = collectionStore; + } + + public async Task DoWork(Order order, CancellationToken ct) + { + var model = await _articleService.GetArticleCollectionAsync(); + + await _collectionStore.Set("test", model, ct); + } + } + } +} diff --git a/test/issue11/IBackgroundTaskQueue.cs b/test/issue11/IBackgroundTaskQueue.cs new file mode 100644 index 0000000..bdd0883 --- /dev/null +++ b/test/issue11/IBackgroundTaskQueue.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace issue11 +{ + public interface IBackgroundTaskQueue + { + Task EnqueueAsync(object order, CancellationToken ct); + Task DequeueAsync(CancellationToken ct); + } +} diff --git a/test/issue11/IBackgroundWorkOrder.cs b/test/issue11/IBackgroundWorkOrder.cs new file mode 100644 index 0000000..9c8c570 --- /dev/null +++ b/test/issue11/IBackgroundWorkOrder.cs @@ -0,0 +1,6 @@ +namespace issue11 +{ + internal interface IBackgroundWorkOrder + { + } +} diff --git a/test/issue11/IBackgroundWorker.cs b/test/issue11/IBackgroundWorker.cs new file mode 100644 index 0000000..f656675 --- /dev/null +++ b/test/issue11/IBackgroundWorker.cs @@ -0,0 +1,6 @@ +namespace issue11 +{ + public interface IBackgroundWorker + { + } +} diff --git a/test/issue11/Itg/Persistence/Secondary/FileKeyValueStore.cs b/test/issue11/Itg/Persistence/Secondary/FileKeyValueStore.cs new file mode 100644 index 0000000..4f2782d --- /dev/null +++ b/test/issue11/Itg/Persistence/Secondary/FileKeyValueStore.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Itg.Persistence.Secondary +{ + public class FileKeyValueStore : IKeyValueStore + { + private const string FILE_EXTENSION = ".store"; + private const int MAX_ATTEMPTS = 3; + + private readonly ILogger> _logger; + private readonly IFileProvider _fileProvider; + private readonly IFileInfo _fileInfo; + + private Dictionary> buffer; + private IChangeToken currentToken; + private int currentSaveAttempt; + private int currentLoadAttempt; + + public FileKeyValueStore( + FileKeyValueStoreOptions dataOptions, + IWebHostEnvironment environment, + ILogger> logger) + { + this._logger = logger; + this._fileProvider = environment.ContentRootFileProvider; + this._fileInfo = this.GetFileInfo(dataOptions); + System.Diagnostics.Debug.WriteLine("############################################"); + System.Diagnostics.Debug.WriteLine("############# CREATING FILE STORE #############"); + System.Diagnostics.Debug.WriteLine("############################################"); + } + + public async Task Get(string key, CancellationToken ct = default) + { + await this.LoadData(ct); + + if (this.buffer.Count == 0 || !this.buffer.ContainsKey(key)) + { + return default; + } + + return this.buffer[key].Record; + } + + public async Task Delete(string key, CancellationToken ct = default) + { + await this.LoadData(ct); + + if (ct.IsCancellationRequested) + { + return; + } + + if (this.buffer.Count == 0 || !this.buffer.ContainsKey(key)) + { + this._logger.RecordNotFound(key, this.buffer.Count); + return; + } + + this._logger.DeletingRecord(key, this.buffer[key].Stamp); + this.buffer.Remove(key); + + if (ct.IsCancellationRequested) + { + return; + } + + await this.SaveData(ct); + } + + public async Task Set(string key, TModel data, CancellationToken ct = default) + { + await this.LoadData(ct); + + if (ct.IsCancellationRequested) + { + return; + } + + var newRecord = new StampedRecord(data); + if (this.buffer is null) + { + this._logger.SettingNewRecord(key, newRecord.Stamp); + this.buffer = new Dictionary> + { + { key, newRecord } + }; + } + else if (this.buffer.ContainsKey(key)) + { + this._logger.OverwritingRecord(key, newRecord.Stamp); + this.buffer[key] = newRecord; + } + else + { + this._logger.SettingNewRecord(key, newRecord.Stamp); + /******************************************************** + * ^ Actual line I'd see on console and on reload ^ * + ********************************************************/ + this.buffer.Add(key, newRecord); + } + + if (ct.IsCancellationRequested) + { + return; + } + + await this.SaveData(ct); + } + + public async Task Clear(TimeSpan? olderThan = null, CancellationToken ct = default) + { + await this.LoadData(ct); + ct.ThrowIfCancellationRequested(); + + if (this.buffer is null) + { + this.buffer = new Dictionary>(); + return 0; + } + + if (olderThan is null) + { + var oldRecordsCount = this.buffer.Count; + this.buffer.Clear(); + await this.SaveData(ct); + this._logger.ClearingAllRecords(oldRecordsCount); + return 0; + } + + ct.ThrowIfCancellationRequested(); + var recordsToClear = this.buffer.Where(r => r.Value.Stamp > DateTime.Now - olderThan.Value).ToArray(); + foreach (var record in recordsToClear) + { + this.buffer.Remove(record.Key); + } + + ct.ThrowIfCancellationRequested(); + await this.SaveData(ct); + this._logger.ClearingOldRecords(recordsToClear.Length, olderThan.Value); + + return this.buffer.Count; + } + + protected virtual async Task LoadData(CancellationToken ct = default) + { + this.currentLoadAttempt = 1; + if (!this._fileInfo.Exists || ct.IsCancellationRequested) + { + if (this.buffer is null) + { + this.buffer = new Dictionary>(); + } + return; + } + + if (this.currentToken is null || this.currentToken.HasChanged) + { + this.currentToken = this._fileProvider.Watch(this._fileInfo.PhysicalPath); + try + { + await Task.Run(() => this.TryLoadData(ct), ct); + } + catch (Exception ex) + { + this._logger.LoadFailed(this._fileInfo.PhysicalPath, ex); + throw; + } + } + else + { + this._logger.ResourceDidNotChange(this._fileInfo.PhysicalPath); + } + } + + protected virtual async Task SaveData(CancellationToken ct = default) + { + this.currentSaveAttempt = 1; + if (ct.IsCancellationRequested) + { + return; + } + + try + { + await Task.Run(() => this.TrySaveData(ct), ct); + } + catch (Exception ex) + { + this._logger.SaveFailed(this._fileInfo.PhysicalPath, ex); + throw; + } + + this.currentToken = this._fileProvider.Watch(this._fileInfo.PhysicalPath); + } + + private async Task TrySaveData(CancellationToken ct) + { + try + { + if (ct.IsCancellationRequested) + { + return; + } + + var format = new BinaryFormatter(); + var resourceExists = this._fileInfo.Exists; + using var fileStream = new FileStream(this._fileInfo.PhysicalPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + this._logger.MediumIdentified(this._fileInfo.PhysicalPath, !resourceExists); + /******************************************************** + * ^ Actual line I'd see on console and on reload ^ * + ********************************************************/ + using var zipStream = new GZipStream(fileStream, CompressionMode.Compress); + format.Serialize(zipStream, this.buffer); + + // TODO: zipStream does not support reading! + //if (this._logger.IsEnabled(LogLevel.Debug)) + //{ + // using var ms = new MemoryStream(); + // zipStream.CopyTo(ms); + // this._logger.ResourceWritten(ms.Length, this._fileInfo.PhysicalPath); + //} + } + catch (IOException ex) + { + this._logger.SaveFailed(this._fileInfo.PhysicalPath, this.currentSaveAttempt, ex); + if (this.currentSaveAttempt <= MAX_ATTEMPTS) + { + ++this.currentSaveAttempt; + await Task.Delay(500, ct); + await this.TrySaveData(ct); + } + else + { + throw; + } + } + } + + private async Task TryLoadData(CancellationToken ct) + { + try + { + var format = new BinaryFormatter(); + using var fileStream = new FileStream(this._fileInfo.PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.None); + using var zipStream = new GZipStream(fileStream, CompressionMode.Decompress); + + // TODO: Messes up deserialization, and the zipStream cannot be seeked back to position 0. + //if (this._logger.IsEnabled(LogLevel.Debug)) + //{ + // using var ms = new MemoryStream(); + // zipStream.CopyTo(ms); + // this._logger.ResourceRead(ms.Length, this._fileInfo.PhysicalPath); + //} + + this.buffer = (Dictionary>)format.Deserialize(zipStream); + } + catch (IOException ex) + { + this._logger.LoadFailed(this._fileInfo.PhysicalPath, this.currentLoadAttempt, ex); + if (this.currentLoadAttempt <= MAX_ATTEMPTS) + { + ++this.currentLoadAttempt; + await Task.Delay(500, ct); + await this.TryLoadData(ct); + } + else + { + throw; + } + } + catch (InvalidCastException ex) + { + this._logger.LoadFailed(this._fileInfo.PhysicalPath, ex); + this.buffer = null; + /* swallowing */ + } + } + + private IFileInfo GetFileInfo(FileKeyValueStoreOptions dataOptions) + { + var filePath = Path.Combine( + dataOptions.FileBasePath ?? string.Empty, + dataOptions.FileNamePrefix + typeof(TModel).Name + FILE_EXTENSION); + + return this._fileProvider.GetFileInfo(filePath); + } + + #region Added for testing purposes + + public IFileInfo FileInfo => _fileInfo; + + #endregion + } +} diff --git a/test/issue11/Itg/Persistence/Secondary/FileKeyValueStoreOptions.cs b/test/issue11/Itg/Persistence/Secondary/FileKeyValueStoreOptions.cs new file mode 100644 index 0000000..ffc9866 --- /dev/null +++ b/test/issue11/Itg/Persistence/Secondary/FileKeyValueStoreOptions.cs @@ -0,0 +1,8 @@ +namespace Itg.Persistence.Secondary +{ + public class FileKeyValueStoreOptions + { + public string FileBasePath { get; set; } + public string FileNamePrefix { get; set; } + } +} diff --git a/test/issue11/Itg/Persistence/Secondary/IKeyValueStore.cs b/test/issue11/Itg/Persistence/Secondary/IKeyValueStore.cs new file mode 100644 index 0000000..4805f79 --- /dev/null +++ b/test/issue11/Itg/Persistence/Secondary/IKeyValueStore.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; + +namespace Itg.Persistence.Secondary +{ + public interface IKeyValueStore + { + IFileInfo FileInfo { get; } + + Task Get(string key, CancellationToken ct = default); + + Task Delete(string key, CancellationToken ct = default); + + Task Set(string key, TModel data, CancellationToken ct = default); + + Task Clear(TimeSpan? olderThan = null, CancellationToken ct = default); + } +} diff --git a/test/issue11/Itg/Persistence/Secondary/LoggerExtensions.cs b/test/issue11/Itg/Persistence/Secondary/LoggerExtensions.cs new file mode 100644 index 0000000..065046e --- /dev/null +++ b/test/issue11/Itg/Persistence/Secondary/LoggerExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Itg.Persistence.Secondary +{ + public static class LoggerExtensions + { + public static void RecordNotFound(this ILogger logger, string key, int count) => logger.LogError(new EventId(0, nameof(RecordNotFound)), "{KEY} {COUNT}", key, count); + public static void DeletingRecord(this ILogger logger, string key, DateTimeOffset stamp) => logger.LogInformation(new EventId(1, nameof(DeletingRecord)), "{KEY} {STAMP}", key, stamp); + public static void SettingNewRecord(this ILogger logger, string key, DateTimeOffset stamp) => logger.LogInformation(new EventId(2, nameof(SettingNewRecord)), "{KEY} {STAMP}", key, stamp); + public static void OverwritingRecord(this ILogger logger, string key, DateTimeOffset stamp) => logger.LogInformation(new EventId(3, nameof(OverwritingRecord)), "{KEY} {STAMP}", key, stamp); + public static void ClearingAllRecords(this ILogger logger, int count) => logger.LogInformation(new EventId(4, nameof(ClearingAllRecords)), "{COUNT}", count); + public static void ClearingOldRecords(this ILogger logger, int count, TimeSpan interval) => logger.LogInformation(new EventId(5, nameof(ClearingOldRecords)), "{COUNT} {INTERVAL}", count, interval); + public static void LoadFailed(this ILogger logger, string path, int attempt, Exception ex) => logger.LogCritical(new EventId(6, nameof(LoadFailed)), ex, "{PATH} {ATTEMPT}", path, attempt); + public static void LoadFailed(this ILogger logger, string path, Exception ex) => logger.LoadFailed(path, 0, ex); + public static void ResourceDidNotChange(this ILogger logger, string path) => logger.LogInformation(new EventId(7, nameof(ResourceDidNotChange)), "{PATH}", path); + public static void MediumIdentified(this ILogger logger, string path, bool notExists) => logger.LogInformation(new EventId(8, nameof(MediumIdentified)), "{PATH} {NOT_EXISTS}", path, notExists); + public static void SaveFailed(this ILogger logger, string path, int attempt, Exception ex) => logger.LogCritical(new EventId(9, nameof(SaveFailed)), ex, "{PATH} {ATTEMPT}", path, attempt); + public static void SaveFailed(this ILogger logger, string path, Exception ex) => logger.SaveFailed(path, 0, ex); + } +} diff --git a/test/issue11/Itg/Persistence/Secondary/StampedRecord.cs b/test/issue11/Itg/Persistence/Secondary/StampedRecord.cs new file mode 100644 index 0000000..86f104e --- /dev/null +++ b/test/issue11/Itg/Persistence/Secondary/StampedRecord.cs @@ -0,0 +1,14 @@ +using System; + +namespace Itg.Persistence.Secondary +{ + [Serializable] + public class StampedRecord + { + public StampedRecord(TModel record) => + (Record, Stamp) = (record, DateTimeOffset.UtcNow); + + public TModel Record { get; } + public DateTimeOffset Stamp { get; } + } +} diff --git a/test/issue11/Program.cs b/test/issue11/Program.cs new file mode 100644 index 0000000..f2e3a6f --- /dev/null +++ b/test/issue11/Program.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Itg.Persistence.Secondary; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace issue11 +{ + public class Program + { + public static async Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + + // we don't resolve FileKeyValueStore from the DI container + // because we don't want to pollute the logs at this point + var store = new FileKeyValueStore( + new FileKeyValueStoreOptions(), + scope.ServiceProvider.GetRequiredService(), + NullLogger>.Instance); + + var random = new Random(0); + + var article = new ArticleCollection + { + Data = new byte[1024 * 1024] + }; + + logger.LogInformation($"Preparing {store.FileInfo.PhysicalPath}..."); + + var fileInfo = new FileInfo(store.FileInfo.PhysicalPath); + if (fileInfo.Exists) + fileInfo.Delete(); + + for (int i = 0; !fileInfo.Exists || fileInfo.Length < 686 * 1024; fileInfo.Refresh(), i++) + { + random.NextBytes(article.Data); + await store.Set("key_" + i, article); + } + } + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .ConfigureLogging((context, logging) => + { + logging.AddConfiguration(context.Configuration.GetSection("Logging")); + logging.ClearProviders(); + logging.AddConsole(); +#if DEBUG + logging.AddDebug(); +#endif + logging.AddFile(o => o.RootPath = context.HostingEnvironment.ContentRootPath); + }); + } +} diff --git a/test/issue11/Properties/launchSettings.json b/test/issue11/Properties/launchSettings.json new file mode 100644 index 0000000..3e450a3 --- /dev/null +++ b/test/issue11/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "issue11": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/issue11/QueuedHostedService.cs b/test/issue11/QueuedHostedService.cs new file mode 100644 index 0000000..7cb37a3 --- /dev/null +++ b/test/issue11/QueuedHostedService.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace issue11 +{ + public class QueuedHostedService : BackgroundService + { + private readonly IServiceProvider _services; + private readonly IBackgroundTaskQueue _taskQueue; + private readonly ILogger _logger; + + public QueuedHostedService(IServiceProvider services, IBackgroundTaskQueue taskQueue, ILogger logger) + { + _services = services; + _taskQueue = taskQueue; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + this._logger.LogInformation("Queued Hosted Service is starting."); + // ^ this is logged properly in the integra- file from the category "Itg.Services.Tasks" ^ + await this.BackgroundProceessing(ct); + } + + private async Task BackgroundProceessing(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + var workOrder = await this._taskQueue.DequeueAsync(ct); + + using var scope = this._services.CreateScope(); + var workerType = workOrder + .GetType() + .GetInterfaces() + .First(t => t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(IBackgroundWorkOrder<,>)) + .GetGenericArguments() + .Last(); + + // Getting a specific worker based on the generic type defined for the received Order. + var worker = scope.ServiceProvider + .GetRequiredService(workerType); + + var task = (Task)workerType + .GetMethod("DoWork") + .Invoke(worker, new object[] { workOrder, ct }); + await task; + } + } + } +} diff --git a/test/issue11/ShopArticleService.cs b/test/issue11/ShopArticleService.cs new file mode 100644 index 0000000..1cf0a02 --- /dev/null +++ b/test/issue11/ShopArticleService.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Itg.Persistence.Secondary; +using Microsoft.Extensions.Logging; + +namespace issue11 +{ + public class ShopArticleService + { + private readonly ILoggerFactory _loggerFactory; + + public ShopArticleService(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public async Task GetArticleCollectionAsync() + { + var logger = _loggerFactory.CreateLogger("NHibernate.SomeNHibernateComponent"); + + // simulating heavy logging of DB access + for (int i = 0; i < 1_000; i++) + { + await Task.Yield(); // kind of a no-op simulating async control flow + + logger.LogInformation("#{COUNT} NHibernate message", i); + } + + var result = new ArticleCollection + { + Data = new byte[2 * 1024] // 2kb data + }; + + new Random(0).NextBytes(result.Data); + + return result; + } + } +} diff --git a/test/issue11/Startup.cs b/test/issue11/Startup.cs new file mode 100644 index 0000000..b71ad12 --- /dev/null +++ b/test/issue11/Startup.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Itg.Persistence.Secondary; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace issue11 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(new FileKeyValueStoreOptions { }); + + services.AddScoped, FileKeyValueStore>(); + services.AddSingleton(); + services.AddHostedService(); + + // Task implementations + services.AddScoped(); + + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + //app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/test/issue11/appsettings.json b/test/issue11/appsettings.json new file mode 100644 index 0000000..09d4770 --- /dev/null +++ b/test/issue11/appsettings.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "NHibernate": "Debug" + }, + + "File": { + "BasePath": "logs", + "FileAccessMode": "KeepOpenAndAutoFlush", + "MaxFileSize": 10485760, + "CounterFormat": "000", + "IncludeScopes": true, + "MaxQueueSize": 10000, + "Files": [ + { + "Path": "integra-persistence-.log", + "MinLevel": { + "Default": "None", + "NHibernate": "Information", + "NHibernate.Type": "Information", + "NHibernate.Cfg.XmlHbmBinding": "Warning", + "NHibernate.Cfg.XmlHbmBinding.Binder": "Information", + "Itg.Persistence": "Trace" + } + }, + { + "Path": "integra-.log", + "MinLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.AspNetCore.Authentication": "Debug", + "Microsoft.AspNetCore.Authorization": "Debug", + "Microsoft.AspNetCore.Server": "Debug", + "NHibernate": "None", + "Itg.Persistence": "None", + "Itg.Mailing": "Information" + } + } + ] + } + }, + "AllowedHosts": "*" +} diff --git a/test/issue11/issue11.csproj b/test/issue11/issue11.csproj new file mode 100644 index 0000000..38511e8 --- /dev/null +++ b/test/issue11/issue11.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/test/issue11/issue11.sln b/test/issue11/issue11.sln new file mode 100644 index 0000000..742423b --- /dev/null +++ b/test/issue11/issue11.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30128.74 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "issue11", "issue11.csproj", "{F9E14E7F-2474-4D5B-A9CE-D14E8636F6B1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F9E14E7F-2474-4D5B-A9CE-D14E8636F6B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9E14E7F-2474-4D5B-A9CE-D14E8636F6B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9E14E7F-2474-4D5B-A9CE-D14E8636F6B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9E14E7F-2474-4D5B-A9CE-D14E8636F6B1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD484071-C2FB-4B3D-9520-EC0EA85C1D71} + EndGlobalSection +EndGlobal