From 9e7715f206795dab660bb3f4270af6fd3506decf Mon Sep 17 00:00:00 2001 From: poma12390 Date: Wed, 17 Sep 2025 18:03:32 +0300 Subject: [PATCH 1/5] create slo lonq2db --- slo/src/Linq2db/Linq2db.csproj | 17 ++ slo/src/Linq2db/Program.cs | 4 + slo/src/Linq2db/SloLinq2DbContext.cs | 324 +++++++++++++++++++++++++++ slo/src/src.sln | 6 + 4 files changed, 351 insertions(+) create mode 100644 slo/src/Linq2db/Linq2db.csproj create mode 100644 slo/src/Linq2db/Program.cs create mode 100644 slo/src/Linq2db/SloLinq2DbContext.cs diff --git a/slo/src/Linq2db/Linq2db.csproj b/slo/src/Linq2db/Linq2db.csproj new file mode 100644 index 00000000..7bce67b3 --- /dev/null +++ b/slo/src/Linq2db/Linq2db.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + Linq2dbTest + + + + + + + + + diff --git a/slo/src/Linq2db/Program.cs b/slo/src/Linq2db/Program.cs new file mode 100644 index 00000000..41126e11 --- /dev/null +++ b/slo/src/Linq2db/Program.cs @@ -0,0 +1,4 @@ +using Internal; +using Linq2db; + +await Cli.Run(new SloLinq2DbContext(), args); \ No newline at end of file diff --git a/slo/src/Linq2db/SloLinq2DbContext.cs b/slo/src/Linq2db/SloLinq2DbContext.cs new file mode 100644 index 00000000..ef09f168 --- /dev/null +++ b/slo/src/Linq2db/SloLinq2DbContext.cs @@ -0,0 +1,324 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using System.Threading.RateLimiting; +using Grpc.Core; +using Internal; +using LinqToDB; +using LinqToDB.Async; +using LinqToDB.Data; +using LinqToDB.Mapping; +using LinqToDB.DataProvider; +using Microsoft.Extensions.Logging; +using Prometheus; +using Ydb.Sdk.Ado; + +namespace Linq2db; + +/// +/// SLO harness implemented on top of LINQ to DB provider for YDB. +/// Mirrors behavior of other SLO contexts (ADO.NET/EF/Topic) in this repo. +/// +public sealed class SloLinq2DbContext : ISloContext +{ + private static readonly ILogger Logger = ISloContext.Factory.CreateLogger(); + + // Prometheus metrics (shared labels: operation, status) + private static readonly Counter Requests = Metrics.CreateCounter( + "ydb_slo_requests_total", + "Total number of SLO operations processed.", + new CounterConfiguration { LabelNames = ["operation", "status"] }); + + private static readonly Histogram Duration = Metrics.CreateHistogram( + "ydb_slo_duration_seconds", + "Duration of SLO operations.", + new HistogramConfiguration { + LabelNames = ["operation", "status"], + Buckets = Histogram.ExponentialBuckets(start: 0.002, factor: 1.5, count: 20) + }); + + public async Task Create(CreateConfig config) + { + Logger.LogInformation("Create: connection={ConnectionString}, initialCount={InitialCount}, writeTimeout={Timeout}s", + config.ConnectionString, config.InitialDataCount, config.WriteTimeout); + + using var ydb = new YdbConnection(config.ConnectionString); + await ydb.OpenAsync(); + + var provider = ResolveYdbProvider(); + using var db = new DataConnection(provider, ydb); + db.AddMappingSchema(CreateMapping()); + + await EnsureTableAsync(db); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(config.WriteTimeout)); + var now = DateTime.UtcNow; + + const int batchSize = 500; + int total = config.InitialDataCount; + int inserted = 0; + + for (int i = 1; i <= total; i += batchSize) + { + var take = Math.Min(batchSize, total - i + 1); + var batch = new List(capacity: take); + for (int j = 0; j < take; j++) + { + var id = i + j; + batch.Add(new SloTable + { + Guid = MakeGuidFromInt(id), + Id = id, + PayloadStr = $"seed-{id}", + PayloadDouble = id * 1.0, + PayloadTimestamp = now + }); + } + + try + { + await db.BulkCopyAsync(new BulkCopyOptions { KeepIdentity = true }, batch, cts.Token); + inserted += batch.Count; + } + catch (NotSupportedException) + { + foreach (var e in batch) + { + await db.InsertAsync(e, token: cts.Token); + inserted++; + } + } + } + + Logger.LogInformation("Create finished. Seeded: {Inserted} rows.", inserted); + } + + public async Task Run(RunConfig config) + { + Logger.LogInformation( + "Run: conn={Conn}, pgw={Pgw}, period={Period}ms, readRps={ReadRps}, readTimeout={ReadTimeout}s, writeRps={WriteRps}, writeTimeout={WriteTimeout}s, time={Time}s", + config.ConnectionString, config.PromPgw, config.ReportPeriod, config.ReadRps, config.ReadTimeout, + config.WriteRps, config.WriteTimeout, config.Time); + + using var pusher = new MetricPusher(new MetricPusherOptions + { + Endpoint = config.PromPgw, + Job = "ydb_slo_linq2db", + Instance = Environment.MachineName, + ReplaceOnPush = true, + IntervalMilliseconds = config.ReportPeriod + }); + pusher.Start(); + + using var ydb = new YdbConnection(config.ConnectionString); + await ydb.OpenAsync(); + + var provider = ResolveYdbProvider(); + using var db = new DataConnection(provider, ydb); + db.AddMappingSchema(CreateMapping()); + + // Get current max Id + var maxId = await db.GetTable().Select(t => (int?)t.Id).MaxAsync() ?? 0; + var nextWriteId = maxId; + + var readLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = Math.Max(1, config.ReadRps), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0, + ReplenishmentPeriod = TimeSpan.FromSeconds(1), + TokensPerPeriod = Math.Max(1, config.ReadRps), + AutoReplenishment = true + }); + + var writeLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = Math.Max(1, config.WriteRps), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0, + ReplenishmentPeriod = TimeSpan.FromSeconds(1), + TokensPerPeriod = Math.Max(1, config.WriteRps), + AutoReplenishment = true + }); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(config.Time)); + + var readTask = Task.Run(() => LoopAsync("read", ReadOnceAsync), cts.Token); + var writeTask = Task.Run(() => LoopAsync("write", WriteOnceAsync), cts.Token); + + try + { + await Task.WhenAll(readTask, writeTask); + } + catch (Exception ex) + { + Logger.LogInformation(ex, "Run finished with cancellation or error."); + } + + pusher.Stop(); + Logger.LogInformation("Run task is finished."); + + return; + + async Task LoopAsync(string operation, Func action) + { + var limiter = operation == "read" ? readLimiter : writeLimiter; + var timeout = TimeSpan.FromSeconds(operation == "read" ? config.ReadTimeout : config.WriteTimeout); + + while (!cts.IsCancellationRequested) + { + using var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: cts.Token); + if (!lease.IsAcquired) continue; + + using var rpcCts = new CancellationTokenSource(timeout); + var sw = Stopwatch.StartNew(); + string status = "OK"; + + try + { + await action(rpcCts.Token); + } + catch (RpcException rpcEx) + { + status = $"GRPC_{rpcEx.Status.StatusCode}"; + Logger.LogWarning(rpcEx, "GRPC error in {Operation}", operation); + } + catch (Exception ex) when (TryExtractStatusLabel(ex, out var statusLabel)) + { + status = statusLabel; + Logger.LogWarning(ex, "Provider error in {Operation}", operation); + } + catch (Exception ex) + { + status = "EXCEPTION"; + Logger.LogWarning(ex, "Unhandled error in {Operation}", operation); + } + finally + { + sw.Stop(); + Requests.WithLabels(operation, status).Inc(); + Duration.WithLabels(operation, status).Observe(sw.Elapsed.TotalSeconds); + } + } + } + + async Task ReadOnceAsync(CancellationToken token) + { + var currentMax = Math.Max(1, Volatile.Read(ref nextWriteId)); + var id = Random.Shared.Next(1, currentMax + 1); + var guid = MakeGuidFromInt(id); + + _ = await db.GetTable() + .Where(t => t.Guid == guid && t.Id == id) + .FirstOrDefaultAsync(token); + } + + async Task WriteOnceAsync(CancellationToken token) + { + var id = Interlocked.Increment(ref nextWriteId); + var entity = new SloTable + { + Guid = MakeGuidFromInt(id), + Id = id, + PayloadStr = $"write-{id}", + PayloadDouble = id * 1.0, + PayloadTimestamp = DateTime.UtcNow + }; + + await db.InsertAsync(entity, token: token); + } + } + + private static MappingSchema CreateMapping() + { + var ms = new MappingSchema(); + var fb = new FluentMappingBuilder(ms); + + fb.Entity() + .HasTableName(SloTable.Name) + .Property(e => e.Guid).IsPrimaryKey().IsNullable(false) + .Property(e => e.Id).IsPrimaryKey().IsNullable(false) + .Property(e => e.PayloadStr).IsNullable(false) + .Property(e => e.PayloadDouble).IsNullable(false) + .Property(e => e.PayloadTimestamp).IsNullable(false); + + return ms; + } + + private static async Task EnsureTableAsync(DataConnection db) + { + try { await db.ExecuteAsync($"DROP TABLE {SloTable.Name};"); } catch { /* ignore */ } + + var create = $@" +CREATE TABLE {SloTable.Name} ( + Guid Uuid, + Id Int32, + PayloadStr Utf8, + PayloadDouble Double, + PayloadTimestamp Timestamp, + PRIMARY KEY (Guid, Id) +);"; + + await db.ExecuteAsync(create); + + foreach (var stmt in Internal.SloTable.Options.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + await db.ExecuteAsync(stmt + ";"); + } + + private static Guid MakeGuidFromInt(int id) + { + Span intBytes = stackalloc byte[4]; + BitConverter.TryWriteBytes(intBytes, id); + var hash = SHA1.HashData(intBytes); + Span guidBytes = stackalloc byte[16]; + hash.AsSpan(0,16).CopyTo(guidBytes); + return new Guid(guidBytes); + } + + private static bool TryExtractStatusLabel(Exception ex, out string label) + { + label = ""; + for (var e = ex; e != null; e = e.InnerException!) + { + var prop = e.GetType().GetProperty("StatusCode"); + if (prop != null && prop.PropertyType.IsEnum) + { + var val = prop.GetValue(e); + var typeName = prop.PropertyType.FullName ?? prop.PropertyType.Name; + if (typeName.Contains("Ydb", StringComparison.OrdinalIgnoreCase)) + { + label = $"YDB_{val}"; + return true; + } + if (typeName.Contains("Grpc", StringComparison.OrdinalIgnoreCase)) + { + label = $"GRPC_{val}"; + return true; + } + label = $"STATUS_{val}"; + return true; + } + } + return false; + } + + private static IDataProvider ResolveYdbProvider() + { + var asms = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var asm in asms) + { + foreach (var t in asm.GetTypes()) + { + if (typeof(IDataProvider).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface) + { + var name = t.FullName ?? t.Name; + if (name.Contains("Ydb", StringComparison.OrdinalIgnoreCase) || + name.Contains("YDB", StringComparison.OrdinalIgnoreCase)) + { + return (IDataProvider)Activator.CreateInstance(t)!; + } + } + } + } + throw new InvalidOperationException("YDB IDataProvider not found. Ensure your Linq2DB YDB provider assembly is referenced."); + } +} diff --git a/slo/src/src.sln b/slo/src/src.sln index 68acb0f7..e27a030f 100644 --- a/slo/src/src.sln +++ b/slo/src/src.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF", "EF\EF.csproj", "{291A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet.Dapper", "Dapper\AdoNet.Dapper.csproj", "{A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linq2db", "Linq2db\Linq2db.csproj", "{A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Release|Any CPU.Build.0 = Release|Any CPU + {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9a77e441ac987a84bf8f4ce5e775d2446741b353 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Wed, 17 Sep 2025 18:08:31 +0300 Subject: [PATCH 2/5] refactor --- slo/src/Linq2db/SloLinq2DbContext.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/slo/src/Linq2db/SloLinq2DbContext.cs b/slo/src/Linq2db/SloLinq2DbContext.cs index ef09f168..e490a6e4 100644 --- a/slo/src/Linq2db/SloLinq2DbContext.cs +++ b/slo/src/Linq2db/SloLinq2DbContext.cs @@ -14,15 +14,10 @@ namespace Linq2db; -/// -/// SLO harness implemented on top of LINQ to DB provider for YDB. -/// Mirrors behavior of other SLO contexts (ADO.NET/EF/Topic) in this repo. -/// public sealed class SloLinq2DbContext : ISloContext { private static readonly ILogger Logger = ISloContext.Factory.CreateLogger(); - // Prometheus metrics (shared labels: operation, status) private static readonly Counter Requests = Metrics.CreateCounter( "ydb_slo_requests_total", "Total number of SLO operations processed.", @@ -116,7 +111,6 @@ public async Task Run(RunConfig config) using var db = new DataConnection(provider, ydb); db.AddMappingSchema(CreateMapping()); - // Get current max Id var maxId = await db.GetTable().Select(t => (int?)t.Id).MaxAsync() ?? 0; var nextWriteId = maxId; From 6c2d3c0b2676974b92473deea91966036bfe76cd Mon Sep 17 00:00:00 2001 From: poma12390 Date: Wed, 17 Sep 2025 18:44:03 +0300 Subject: [PATCH 3/5] rename --- slo/src/Linq2db/{Linq2db.csproj => AdoNet.Linq2db.csproj} | 1 - slo/src/src.sln | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename slo/src/Linq2db/{Linq2db.csproj => AdoNet.Linq2db.csproj} (88%) diff --git a/slo/src/Linq2db/Linq2db.csproj b/slo/src/Linq2db/AdoNet.Linq2db.csproj similarity index 88% rename from slo/src/Linq2db/Linq2db.csproj rename to slo/src/Linq2db/AdoNet.Linq2db.csproj index 7bce67b3..fa52bd8b 100644 --- a/slo/src/Linq2db/Linq2db.csproj +++ b/slo/src/Linq2db/AdoNet.Linq2db.csproj @@ -9,7 +9,6 @@ - diff --git a/slo/src/src.sln b/slo/src/src.sln index e27a030f..79c39862 100644 --- a/slo/src/src.sln +++ b/slo/src/src.sln @@ -13,7 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF", "EF\EF.csproj", "{291A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet.Dapper", "Dapper\AdoNet.Dapper.csproj", "{A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linq2db", "Linq2db\Linq2db.csproj", "{A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet.Linq2db", "Linq2db\AdoNet.Linq2db.csproj", "{A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 03631d9aa5712310da94956c0c5646f3254a42b4 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Mon, 6 Oct 2025 22:51:36 +0300 Subject: [PATCH 4/5] linq2db QuickStart create --- examples/YdbExamples.sln | 6 + examples/linq2db.Ydb.QuickStart/Program.cs | 360 ++++++++++++++++++ .../linq2db.Ydb.QuickStart.csproj | 18 + 3 files changed, 384 insertions(+) create mode 100644 examples/linq2db.Ydb.QuickStart/Program.cs create mode 100644 examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj diff --git a/examples/YdbExamples.sln b/examples/YdbExamples.sln index 31c103ee..28c296ae 100644 --- a/examples/YdbExamples.sln +++ b/examples/YdbExamples.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database.Operations.Tutoria EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container", "Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container\Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container.csproj", "{77625697-498B-4879-BABA-046EE93E7AF7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "linq2db.Ydb.QuickStart", "linq2db.Ydb.QuickStart\linq2db.Ydb.QuickStart.csproj", "{FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,10 @@ Global {77625697-498B-4879-BABA-046EE93E7AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {77625697-498B-4879-BABA-046EE93E7AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {77625697-498B-4879-BABA-046EE93E7AF7}.Release|Any CPU.Build.0 = Release|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/linq2db.Ydb.QuickStart/Program.cs b/examples/linq2db.Ydb.QuickStart/Program.cs new file mode 100644 index 00000000..f20c0e52 --- /dev/null +++ b/examples/linq2db.Ydb.QuickStart/Program.cs @@ -0,0 +1,360 @@ +using Microsoft.Extensions.Logging; +using Polly; +using LinqToDB; +using LinqToDB.Async; +using LinqToDB.Data; +using LinqToDB.Mapping; + +using var factory = LoggerFactory.Create(b => b.AddConsole()); +await new AppContext(factory.CreateLogger()).Run(); + +#region LINQ2DB MODELS + +[Table("series")] +public sealed class Series +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("series_info")] + public string? SeriesInfo { get; set; } + + [Column("release_date"), DataType(DataType.Date)] + public DateTime ReleaseDate { get; set; } +} + +[Table("seasons")] +public sealed class Season +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [PrimaryKey, Column("season_id")] + public ulong SeasonId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("first_aired"), DataType(DataType.Date)] + public DateTime FirstAired { get; set; } + + [Column("last_aired"), DataType(DataType.Date)] + public DateTime LastAired { get; set; } +} + +[Table("episodes")] +public sealed class Episode +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [PrimaryKey, Column("season_id")] + public ulong SeasonId { get; set; } + + [PrimaryKey, Column("episode_id")] + public ulong EpisodeId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("air_date"), DataType(DataType.Date)] + public DateTime AirDate { get; set; } +} + +#endregion + +#region LINQ2DB DATACONTEXT + +internal sealed class MyYdb : DataConnection +{ + public MyYdb(string connectionString) : base("YDB", connectionString) {} + public MyYdb(DataOptions options) : base(options) {} + + public ITable Series => this.GetTable(); + public ITable Seasons => this.GetTable(); + public ITable Episodes => this.GetTable(); +} + +#endregion + +#region SETTINGS (без CmdOptions) + +internal sealed record Settings( + string Host, + int Port, + string Database, + bool UseTls, + int TlsPort) +{ + public string SimpleConnectionString => + $"Host={Host};Port={(UseTls ? TlsPort : Port)};Database={Database};UseTls={(UseTls ? "true" : "false")}"; +} + +internal static class SettingsLoader +{ + public static Settings Load() + { + string host = Environment.GetEnvironmentVariable("YDB_HOST") ?? "localhost"; + int port = TryInt(Environment.GetEnvironmentVariable("YDB_PORT"), 2136); + string db = Environment.GetEnvironmentVariable("YDB_DB") ?? "/local"; + bool useTls = TryBool(Environment.GetEnvironmentVariable("YDB_USE_TLS"), false); + int tls = TryInt(Environment.GetEnvironmentVariable("YDB_TLS_PORT"), 2135); + + return new Settings(host, port, db, useTls, tls); + + static int TryInt(string? s, int d) => int.TryParse(s, out var v) ? v : d; + static bool TryBool(string? s, bool d)=> bool.TryParse(s, out var v) ? v : d; + } +} + +#endregion + +internal class AppContext +{ + private readonly ILogger _logger; + private readonly Settings _settings; + + public AppContext(ILogger logger) + { + _logger = logger; + _settings = SettingsLoader.Load(); + } + + DataOptions BuildOptions(string? overrideConnectionString = null) + { + var cs = overrideConnectionString ?? _settings.SimpleConnectionString; + return new DataOptions().UseConnectionString("YDB", cs); + } + + public async Task Run() + { + _logger.LogInformation("Start app example"); + + await InitTables(); + await LoadData(); + await SelectWithParameters(); + await RetryPolicy(); + + _logger.LogInformation("Clearing all pools..."); + _logger.LogInformation("Cleared all pools"); + + await InteractiveTransaction(); + await TlsConnectionExample(); + await ConnectionWithLoggerFactory(); + + _logger.LogInformation("Finish app example"); + } + + + private async Task InitTables() + { + await using var db = new MyYdb(BuildOptions()); + + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("series exists"); } + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("seasons exists"); } + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("episodes exists"); } + + _logger.LogInformation("Created tables"); + } + + private async Task LoadData() + { + await using var db = new MyYdb(BuildOptions()); + + var series = new[] + { + new Series { SeriesId = 1, Title = "IT Crowd", ReleaseDate = new DateTime(2006,02,03), SeriesInfo="British sitcom..." }, + new Series { SeriesId = 2, Title = "Silicon Valley",ReleaseDate = new DateTime(2014,04,06), SeriesInfo="American comedy..." } + }; + foreach (var s in series) await db.InsertAsync(s); + + var seasons = new List + { + new() { SeriesId=1, SeasonId=1, Title="Season 1", FirstAired=new DateTime(2006,02,03), LastAired=new DateTime(2006,03,03)}, + new() { SeriesId=1, SeasonId=2, Title="Season 2", FirstAired=new DateTime(2007,08,24), LastAired=new DateTime(2007,09,28)}, + new() { SeriesId=1, SeasonId=3, Title="Season 3", FirstAired=new DateTime(2008,11,21), LastAired=new DateTime(2008,12,26)}, + new() { SeriesId=1, SeasonId=4, Title="Season 4", FirstAired=new DateTime(2010,06,25), LastAired=new DateTime(2010,07,30)}, + new() { SeriesId=2, SeasonId=1, Title="Season 1", FirstAired=new DateTime(2014,04,06), LastAired=new DateTime(2014,06,01)}, + new() { SeriesId=2, SeasonId=2, Title="Season 2", FirstAired=new DateTime(2015,04,12), LastAired=new DateTime(2015,06,14)}, + new() { SeriesId=2, SeasonId=3, Title="Season 3", FirstAired=new DateTime(2016,04,24), LastAired=new DateTime(2016,06,26)}, + new() { SeriesId=2, SeasonId=4, Title="Season 4", FirstAired=new DateTime(2017,04,23), LastAired=new DateTime(2017,06,25)}, + new() { SeriesId=2, SeasonId=5, Title="Season 5", FirstAired=new DateTime(2018,03,25), LastAired=new DateTime(2018,05,13)}, + }; + await db.BulkCopyAsync(seasons); + + var eps = new List + { + new() { SeriesId=1, SeasonId=1, EpisodeId=1, Title="Yesterday's Jam", AirDate=new DateTime(2006,02,03)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=2, Title="Calamity Jen", AirDate=new DateTime(2006,02,03)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=3, Title="Fifty-Fifty", AirDate=new DateTime(2006,02,10)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=4, Title="The Red Door", AirDate=new DateTime(2006,02,17)}, + new() { SeriesId=1, SeasonId=2, EpisodeId=1, Title="The Work Outing", AirDate=new DateTime(2007,08,24)}, + new() { SeriesId=1, SeasonId=2, EpisodeId=2, Title="Return of the Golden Child", AirDate=new DateTime(2007,08,31)}, + new() { SeriesId=1, SeasonId=3, EpisodeId=1, Title="From Hell", AirDate=new DateTime(2008,11,21)}, + new() { SeriesId=1, SeasonId=3, EpisodeId=2, Title="Are We Not Men?", AirDate=new DateTime(2008,11,28)}, + new() { SeriesId=1, SeasonId=4, EpisodeId=1, Title="Jen The Fredo", AirDate=new DateTime(2010,06,25)}, + new() { SeriesId=1, SeasonId=4, EpisodeId=2, Title="The Final Countdown", AirDate=new DateTime(2010,07,02)}, + new() { SeriesId=2, SeasonId=2, EpisodeId=1, Title="Minimum Viable Product", AirDate=new DateTime(2014,04,06)}, + new() { SeriesId=2, SeasonId=2, EpisodeId=2, Title="The Cap Table", AirDate=new DateTime(2014,04,13)}, + new() { SeriesId=2, SeasonId=1, EpisodeId=3, Title="Articles of Incorporation", AirDate=new DateTime(2014,04,20)}, + new() { SeriesId=2, SeasonId=1, EpisodeId=4, Title="Fiduciary Duties", AirDate=new DateTime(2014,04,27)}, + }; + + await db.BulkCopyAsync(eps); + + _logger.LogInformation("Loaded data"); + } + + private async Task SelectWithParameters() + { + await using var db = new MyYdb(BuildOptions()); + + ulong seriesId = 1; + ulong seasonId = 1; + ulong limit = 3; + + var rows = await db.Episodes + .Where(e => e.SeriesId == seriesId && e.SeasonId > seasonId) + .OrderBy(e => e.SeriesId) + .ThenBy(e => e.SeasonId) + .ThenBy(e => e.EpisodeId) + .Take((int)limit) + .Select(e => new { e.SeriesId, e.SeasonId, e.EpisodeId, e.AirDate, e.Title }) + .ToListAsync(); + + _logger.LogInformation("Selected rows:"); + foreach (var r in rows) + _logger.LogInformation( + "series_id: {series_id}, season_id: {season_id}, episode_id: {episode_id}, air_date: {air_date}, title: {title}", + r.SeriesId, r.SeasonId, r.EpisodeId, r.AirDate, r.Title); + } + + private async Task RetryPolicy() + { + var policy = Polly.Policy + .Handle(_ => true) + .WaitAndRetryAsync(10, _ => TimeSpan.FromSeconds(1)); + + await policy.ExecuteAsync(async () => + { + await using var db = new MyYdb(BuildOptions()); + + var statsRaw = await db.Episodes + .GroupBy(e => new { e.SeriesId, e.SeasonId }) + .Select(g => new + { + SeriesId = g.Key.SeriesId, + SeasonId = g.Key.SeasonId, + Cnt = g.Count() + }) + .ToListAsync(); + + var stats = statsRaw + .OrderBy(x => x.SeriesId) + .ThenBy(x => x.SeasonId); + + foreach (var x in stats) + _logger.LogInformation("series_id: {series_id}, season_id: {season_id}, cnt: {cnt}", + x.SeriesId, x.SeasonId, x.Cnt); + }); + } + + private async Task InteractiveTransaction() + { + await using var db = new MyYdb(BuildOptions()); + using var tr = await db.BeginTransactionAsync(); + + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 13, + Title = "Test Episode", AirDate = new DateTime(2018, 08, 27) + }); + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 21, + Title = "Test 21", AirDate = new DateTime(2018, 08, 27) + }); + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 22, + Title = "Test 22", AirDate = new DateTime(2018, 08, 27) + }); + + await tr.CommitAsync(); + _logger.LogInformation("Commit transaction"); + + string title21 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 21) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("New episode title: {title}", title21); + + string title22 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 22) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("New episode title: {title}", title22); + + string title13 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 13) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("Updated episode title: {title}", title13); + } + + private async Task TlsConnectionExample() + { + if (!_settings.UseTls) + { + _logger.LogInformation("Tls example was ignored"); + return; + } + + var caPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "ca.pem"); + var tlsCs = $"Host={_settings.Host};Port={_settings.TlsPort};RootCertificate={caPath}"; + await using var db = new MyYdb(BuildOptions(tlsCs)); + + var rows = await db.Seasons + .Where(sa => sa.SeriesId == 1) + .Join(db.Series, sa => sa.SeriesId, sr => sr.SeriesId, + (sa, sr) => new { SeasonTitle = sa.Title, SeriesTitle = sr.Title, sr.SeriesId, sa.SeasonId }) + .OrderBy(x => x.SeriesId).ThenBy(x => x.SeasonId) + .ToListAsync(); + + foreach (var r in rows) + _logger.LogInformation("season_title: {SeasonTitle}, series_title: {SeriesTitle}, series_id: {SeriesId}, season_id: {SeasonId}", + r.SeasonTitle, r.SeriesTitle, r.SeriesId, r.SeasonId); + } + + private async Task ConnectionWithLoggerFactory() + { + await using var db = new MyYdb(BuildOptions( + $"Host={_settings.Host};Port={_settings.Port}")); + + db.OnTraceConnection = ti => + { + switch (ti.TraceInfoStep) + { + case TraceInfoStep.BeforeExecute: + _logger.LogInformation("BeforeExecute: {sql}", ti.SqlText); + break; + case TraceInfoStep.AfterExecute: + _logger.LogInformation("AfterExecute: {time} {records} recs", ti.ExecutionTime, ti.RecordsAffected); + break; + case TraceInfoStep.Error: + _logger.LogError(ti.Exception, "SQL error"); + break; + } + }; + + _logger.LogInformation("Dropping tables of examples"); + try { await db.DropTableAsync(); } catch { /* ignore */ } + try { await db.DropTableAsync(); } catch { /* ignore */ } + try { await db.DropTableAsync(); } catch { /* ignore */ } + _logger.LogInformation("Dropped tables of examples"); + } +} diff --git a/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj b/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj new file mode 100644 index 00000000..687ad40c --- /dev/null +++ b/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + From 1e3f019f468463265278c232930a9f2e4b18252a Mon Sep 17 00:00:00 2001 From: poma12390 Date: Mon, 6 Oct 2025 22:56:04 +0300 Subject: [PATCH 5/5] linq2db QuickStart create --- slo/src/Linq2db/AdoNet.Linq2db.csproj | 16 -- slo/src/Linq2db/Program.cs | 4 - slo/src/Linq2db/SloLinq2DbContext.cs | 318 -------------------------- slo/src/src.sln | 6 - 4 files changed, 344 deletions(-) delete mode 100644 slo/src/Linq2db/AdoNet.Linq2db.csproj delete mode 100644 slo/src/Linq2db/Program.cs delete mode 100644 slo/src/Linq2db/SloLinq2DbContext.cs diff --git a/slo/src/Linq2db/AdoNet.Linq2db.csproj b/slo/src/Linq2db/AdoNet.Linq2db.csproj deleted file mode 100644 index fa52bd8b..00000000 --- a/slo/src/Linq2db/AdoNet.Linq2db.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net8.0 - enable - enable - Linq2dbTest - - - - - - - - diff --git a/slo/src/Linq2db/Program.cs b/slo/src/Linq2db/Program.cs deleted file mode 100644 index 41126e11..00000000 --- a/slo/src/Linq2db/Program.cs +++ /dev/null @@ -1,4 +0,0 @@ -using Internal; -using Linq2db; - -await Cli.Run(new SloLinq2DbContext(), args); \ No newline at end of file diff --git a/slo/src/Linq2db/SloLinq2DbContext.cs b/slo/src/Linq2db/SloLinq2DbContext.cs deleted file mode 100644 index e490a6e4..00000000 --- a/slo/src/Linq2db/SloLinq2DbContext.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System.Diagnostics; -using System.Security.Cryptography; -using System.Threading.RateLimiting; -using Grpc.Core; -using Internal; -using LinqToDB; -using LinqToDB.Async; -using LinqToDB.Data; -using LinqToDB.Mapping; -using LinqToDB.DataProvider; -using Microsoft.Extensions.Logging; -using Prometheus; -using Ydb.Sdk.Ado; - -namespace Linq2db; - -public sealed class SloLinq2DbContext : ISloContext -{ - private static readonly ILogger Logger = ISloContext.Factory.CreateLogger(); - - private static readonly Counter Requests = Metrics.CreateCounter( - "ydb_slo_requests_total", - "Total number of SLO operations processed.", - new CounterConfiguration { LabelNames = ["operation", "status"] }); - - private static readonly Histogram Duration = Metrics.CreateHistogram( - "ydb_slo_duration_seconds", - "Duration of SLO operations.", - new HistogramConfiguration { - LabelNames = ["operation", "status"], - Buckets = Histogram.ExponentialBuckets(start: 0.002, factor: 1.5, count: 20) - }); - - public async Task Create(CreateConfig config) - { - Logger.LogInformation("Create: connection={ConnectionString}, initialCount={InitialCount}, writeTimeout={Timeout}s", - config.ConnectionString, config.InitialDataCount, config.WriteTimeout); - - using var ydb = new YdbConnection(config.ConnectionString); - await ydb.OpenAsync(); - - var provider = ResolveYdbProvider(); - using var db = new DataConnection(provider, ydb); - db.AddMappingSchema(CreateMapping()); - - await EnsureTableAsync(db); - - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(config.WriteTimeout)); - var now = DateTime.UtcNow; - - const int batchSize = 500; - int total = config.InitialDataCount; - int inserted = 0; - - for (int i = 1; i <= total; i += batchSize) - { - var take = Math.Min(batchSize, total - i + 1); - var batch = new List(capacity: take); - for (int j = 0; j < take; j++) - { - var id = i + j; - batch.Add(new SloTable - { - Guid = MakeGuidFromInt(id), - Id = id, - PayloadStr = $"seed-{id}", - PayloadDouble = id * 1.0, - PayloadTimestamp = now - }); - } - - try - { - await db.BulkCopyAsync(new BulkCopyOptions { KeepIdentity = true }, batch, cts.Token); - inserted += batch.Count; - } - catch (NotSupportedException) - { - foreach (var e in batch) - { - await db.InsertAsync(e, token: cts.Token); - inserted++; - } - } - } - - Logger.LogInformation("Create finished. Seeded: {Inserted} rows.", inserted); - } - - public async Task Run(RunConfig config) - { - Logger.LogInformation( - "Run: conn={Conn}, pgw={Pgw}, period={Period}ms, readRps={ReadRps}, readTimeout={ReadTimeout}s, writeRps={WriteRps}, writeTimeout={WriteTimeout}s, time={Time}s", - config.ConnectionString, config.PromPgw, config.ReportPeriod, config.ReadRps, config.ReadTimeout, - config.WriteRps, config.WriteTimeout, config.Time); - - using var pusher = new MetricPusher(new MetricPusherOptions - { - Endpoint = config.PromPgw, - Job = "ydb_slo_linq2db", - Instance = Environment.MachineName, - ReplaceOnPush = true, - IntervalMilliseconds = config.ReportPeriod - }); - pusher.Start(); - - using var ydb = new YdbConnection(config.ConnectionString); - await ydb.OpenAsync(); - - var provider = ResolveYdbProvider(); - using var db = new DataConnection(provider, ydb); - db.AddMappingSchema(CreateMapping()); - - var maxId = await db.GetTable().Select(t => (int?)t.Id).MaxAsync() ?? 0; - var nextWriteId = maxId; - - var readLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions - { - TokenLimit = Math.Max(1, config.ReadRps), - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - QueueLimit = 0, - ReplenishmentPeriod = TimeSpan.FromSeconds(1), - TokensPerPeriod = Math.Max(1, config.ReadRps), - AutoReplenishment = true - }); - - var writeLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions - { - TokenLimit = Math.Max(1, config.WriteRps), - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - QueueLimit = 0, - ReplenishmentPeriod = TimeSpan.FromSeconds(1), - TokensPerPeriod = Math.Max(1, config.WriteRps), - AutoReplenishment = true - }); - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(config.Time)); - - var readTask = Task.Run(() => LoopAsync("read", ReadOnceAsync), cts.Token); - var writeTask = Task.Run(() => LoopAsync("write", WriteOnceAsync), cts.Token); - - try - { - await Task.WhenAll(readTask, writeTask); - } - catch (Exception ex) - { - Logger.LogInformation(ex, "Run finished with cancellation or error."); - } - - pusher.Stop(); - Logger.LogInformation("Run task is finished."); - - return; - - async Task LoopAsync(string operation, Func action) - { - var limiter = operation == "read" ? readLimiter : writeLimiter; - var timeout = TimeSpan.FromSeconds(operation == "read" ? config.ReadTimeout : config.WriteTimeout); - - while (!cts.IsCancellationRequested) - { - using var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken: cts.Token); - if (!lease.IsAcquired) continue; - - using var rpcCts = new CancellationTokenSource(timeout); - var sw = Stopwatch.StartNew(); - string status = "OK"; - - try - { - await action(rpcCts.Token); - } - catch (RpcException rpcEx) - { - status = $"GRPC_{rpcEx.Status.StatusCode}"; - Logger.LogWarning(rpcEx, "GRPC error in {Operation}", operation); - } - catch (Exception ex) when (TryExtractStatusLabel(ex, out var statusLabel)) - { - status = statusLabel; - Logger.LogWarning(ex, "Provider error in {Operation}", operation); - } - catch (Exception ex) - { - status = "EXCEPTION"; - Logger.LogWarning(ex, "Unhandled error in {Operation}", operation); - } - finally - { - sw.Stop(); - Requests.WithLabels(operation, status).Inc(); - Duration.WithLabels(operation, status).Observe(sw.Elapsed.TotalSeconds); - } - } - } - - async Task ReadOnceAsync(CancellationToken token) - { - var currentMax = Math.Max(1, Volatile.Read(ref nextWriteId)); - var id = Random.Shared.Next(1, currentMax + 1); - var guid = MakeGuidFromInt(id); - - _ = await db.GetTable() - .Where(t => t.Guid == guid && t.Id == id) - .FirstOrDefaultAsync(token); - } - - async Task WriteOnceAsync(CancellationToken token) - { - var id = Interlocked.Increment(ref nextWriteId); - var entity = new SloTable - { - Guid = MakeGuidFromInt(id), - Id = id, - PayloadStr = $"write-{id}", - PayloadDouble = id * 1.0, - PayloadTimestamp = DateTime.UtcNow - }; - - await db.InsertAsync(entity, token: token); - } - } - - private static MappingSchema CreateMapping() - { - var ms = new MappingSchema(); - var fb = new FluentMappingBuilder(ms); - - fb.Entity() - .HasTableName(SloTable.Name) - .Property(e => e.Guid).IsPrimaryKey().IsNullable(false) - .Property(e => e.Id).IsPrimaryKey().IsNullable(false) - .Property(e => e.PayloadStr).IsNullable(false) - .Property(e => e.PayloadDouble).IsNullable(false) - .Property(e => e.PayloadTimestamp).IsNullable(false); - - return ms; - } - - private static async Task EnsureTableAsync(DataConnection db) - { - try { await db.ExecuteAsync($"DROP TABLE {SloTable.Name};"); } catch { /* ignore */ } - - var create = $@" -CREATE TABLE {SloTable.Name} ( - Guid Uuid, - Id Int32, - PayloadStr Utf8, - PayloadDouble Double, - PayloadTimestamp Timestamp, - PRIMARY KEY (Guid, Id) -);"; - - await db.ExecuteAsync(create); - - foreach (var stmt in Internal.SloTable.Options.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - await db.ExecuteAsync(stmt + ";"); - } - - private static Guid MakeGuidFromInt(int id) - { - Span intBytes = stackalloc byte[4]; - BitConverter.TryWriteBytes(intBytes, id); - var hash = SHA1.HashData(intBytes); - Span guidBytes = stackalloc byte[16]; - hash.AsSpan(0,16).CopyTo(guidBytes); - return new Guid(guidBytes); - } - - private static bool TryExtractStatusLabel(Exception ex, out string label) - { - label = ""; - for (var e = ex; e != null; e = e.InnerException!) - { - var prop = e.GetType().GetProperty("StatusCode"); - if (prop != null && prop.PropertyType.IsEnum) - { - var val = prop.GetValue(e); - var typeName = prop.PropertyType.FullName ?? prop.PropertyType.Name; - if (typeName.Contains("Ydb", StringComparison.OrdinalIgnoreCase)) - { - label = $"YDB_{val}"; - return true; - } - if (typeName.Contains("Grpc", StringComparison.OrdinalIgnoreCase)) - { - label = $"GRPC_{val}"; - return true; - } - label = $"STATUS_{val}"; - return true; - } - } - return false; - } - - private static IDataProvider ResolveYdbProvider() - { - var asms = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var asm in asms) - { - foreach (var t in asm.GetTypes()) - { - if (typeof(IDataProvider).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface) - { - var name = t.FullName ?? t.Name; - if (name.Contains("Ydb", StringComparison.OrdinalIgnoreCase) || - name.Contains("YDB", StringComparison.OrdinalIgnoreCase)) - { - return (IDataProvider)Activator.CreateInstance(t)!; - } - } - } - } - throw new InvalidOperationException("YDB IDataProvider not found. Ensure your Linq2DB YDB provider assembly is referenced."); - } -} diff --git a/slo/src/src.sln b/slo/src/src.sln index 79c39862..68acb0f7 100644 --- a/slo/src/src.sln +++ b/slo/src/src.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF", "EF\EF.csproj", "{291A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet.Dapper", "Dapper\AdoNet.Dapper.csproj", "{A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoNet.Linq2db", "Linq2db\AdoNet.Linq2db.csproj", "{A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,10 +39,6 @@ Global {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6B9B4F1-4C7C-42C1-A212-B71A9B0D67F7}.Release|Any CPU.Build.0 = Release|Any CPU - {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0AB76CF-A89C-43DE-99C8-0E1C3E539F98}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE