diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0de63b0..b71f4a75 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -195,7 +195,11 @@ jobs: run: | cd ./examples/src/Topic dotnet run - - name: Run EF example + - name: Run Entity Framework Core example run: | - cd ./examples/src/EF + cd ./examples/src/EntityFrameworkCore.Ydb.QuickStart + dotnet tool install --global dotnet-ef + dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet ef migrations add InitialCreate + dotnet ef database update dotnet run diff --git a/examples/src/EF/EF.csproj b/examples/src/EF/EF.csproj deleted file mode 100644 index 489eb609..00000000 --- a/examples/src/EF/EF.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - enable - enable - EF - - - - - - diff --git a/examples/src/EF/Program.cs b/examples/src/EF/Program.cs deleted file mode 100644 index eebbcd21..00000000 --- a/examples/src/EF/Program.cs +++ /dev/null @@ -1,57 +0,0 @@ -using EntityFrameworkCore.Ydb.Extensions; -using Microsoft.EntityFrameworkCore; - -await using var db = new BloggingContext(); - -await db.Database.EnsureDeletedAsync(); -await db.Database.EnsureCreatedAsync(); - -Console.WriteLine("Inserting a new blog"); -db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); - -await db.SaveChangesAsync(); - -Console.WriteLine("Querying for a blog"); -var blog = await db.Blogs - .OrderBy(b => b.BlogId) - .FirstAsync(); - -Console.WriteLine("Updating the blog and adding a post"); -blog.Url = "https://devblogs.microsoft.com/dotnet"; -blog.Posts.Add(new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" }); -await db.SaveChangesAsync(); - -Console.WriteLine("Delete the blog"); -db.Remove(blog); -await db.SaveChangesAsync(); - -internal class BloggingContext : DbContext -{ - public DbSet Blogs { get; set; } - public DbSet Posts { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseYdb("Host=localhost;Port=2136;Database=/local") - .LogTo(Console.WriteLine); -} - -internal class Blog -{ - public int BlogId { get; init; } - - public string Url { get; set; } = string.Empty; - - // ReSharper disable once CollectionNeverQueried.Global - public List Posts { get; init; } = []; -} - -internal class Post -{ - public int PostId { get; init; } - - public string Title { get; init; } = string.Empty; - - public string Content { get; init; } = string.Empty; - - public Blog Blog { get; init; } = null!; -} \ No newline at end of file diff --git a/examples/src/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj b/examples/src/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj new file mode 100644 index 00000000..b20b9a8f --- /dev/null +++ b/examples/src/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + EntityFrameworkCore.Ydb + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/examples/src/EntityFrameworkCore.Ydb.QuickStart/Program.cs b/examples/src/EntityFrameworkCore.Ydb.QuickStart/Program.cs new file mode 100644 index 00000000..32018e4c --- /dev/null +++ b/examples/src/EntityFrameworkCore.Ydb.QuickStart/Program.cs @@ -0,0 +1,88 @@ +using EntityFrameworkCore.Ydb.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +await using var db = new BloggingContext(); + +// Create +Console.WriteLine("Inserting a new blog"); +db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); +await db.SaveChangesAsync(); + +// Read +Console.WriteLine("Querying for a blog"); +var blog = await db.Blogs + .OrderBy(b => b.BlogId) + .FirstAsync(); + +// Update +Console.WriteLine("Updating the blog and adding a post"); +blog.Url = "https://devblogs.microsoft.com/dotnet"; +blog.Posts.Add( + new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" }); +await db.SaveChangesAsync(); + +// Delete +Console.WriteLine("Delete the blog"); +db.Remove(blog); +await db.SaveChangesAsync(); + +internal class BloggingContextFactory : IDesignTimeDbContextFactory +{ + public BloggingContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + // IMPORTANT! + // Disables retries for the migrations context. + // Required because migration operations may use suppressed or explicit transactions, + // and enabling retries in this case leads to runtime errors with this provider. + // + // "System.NotSupportedException: User transaction is not supported with a TransactionSuppressed migrations or a retrying execution strategy." + // + // Bottom line: ALWAYS disable retries for design-time/migration contexts to avoid migration failures and errors. + return new BloggingContext( + optionsBuilder.UseYdb("Host=localhost;Port=2136;Database=/local", + builder => builder.DisableRetryOnFailure() + ).Options + ); + } +} + +internal class BloggingContext : DbContext +{ + internal BloggingContext() + { + } + + internal BloggingContext(DbContextOptions options) : base(options) + { + } + + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder options) => + options.UseYdb("Host=localhost;Port=2136;Database=/local"); +} + +internal class Blog +{ + public int BlogId { get; init; } + + public string Url { get; set; } = string.Empty; + + // ReSharper disable once CollectionNeverQueried.Global + public List Posts { get; init; } = []; +} + +internal class Post +{ + public int PostId { get; init; } + + public string Title { get; init; } = string.Empty; + + public string Content { get; init; } = string.Empty; + + public Blog Blog { get; init; } = null!; +} \ No newline at end of file diff --git a/examples/src/EntityFrameworkCore.Ydb.QuickStart/README.md b/examples/src/EntityFrameworkCore.Ydb.QuickStart/README.md new file mode 100644 index 00000000..0d72ab69 --- /dev/null +++ b/examples/src/EntityFrameworkCore.Ydb.QuickStart/README.md @@ -0,0 +1,16 @@ +# Entity Framework Core YDB Quick Start + +A sample application from the [official documentation](https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) +shows how to get started with EF, define a model, populate it with data and then query the database. + +## Running QuickStart + +1. Setup [YDB local](https://ydb.tech/docs/en/reference/docker/start). +2. Create the database: + ```bash + dotnet tool install --global dotnet-ef + dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet ef migrations add InitialCreate + dotnet ef database update + ``` +3. Run the app: `dotnet run` diff --git a/examples/src/YdbExamples.sln b/examples/src/YdbExamples.sln index df0bcce6..3f99e3df 100644 --- a/examples/src/YdbExamples.sln +++ b/examples/src/YdbExamples.sln @@ -17,7 +17,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YC", "YC\YC.csproj", "{753E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Topic", "Topic\Topic.csproj", "{0FB9A1C2-4B0C-4AE4-9FA2-E0ED37802A6E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF", "EF\EF.csproj", "{0CE9DF93-1411-4E73-BA88-A66018FAB948}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Ydb.QuickStart", "EntityFrameworkCore.Ydb.QuickStart\EntityFrameworkCore.Ydb.QuickStart.csproj", "{0CE9DF93-1411-4E73-BA88-A66018FAB948}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Ydb.Yandex.Cloud", "EntityFrameworkCore.Ydb.Yandex.Cloud\EntityFrameworkCore.Ydb.Yandex.Cloud.csproj", "{8F7266C7-75BB-4753-9FB2-BDF4678AF73B}" EndProject diff --git a/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs b/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs index 12047dae..34c258db 100644 --- a/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs +++ b/src/EFCore.Ydb/src/Infrastructure/Internal/YdbOptionsExtension.cs @@ -13,6 +13,8 @@ public class YdbOptionsExtension : RelationalOptionsExtension public X509Certificate2Collection? ServerCertificates { get; private set; } + public bool DisableRetryExecutionStrategy { get; private set; } + private DbContextOptionsExtensionInfo? _info; public YdbOptionsExtension() @@ -23,11 +25,13 @@ private YdbOptionsExtension(YdbOptionsExtension copyFrom) : base(copyFrom) { CredentialsProvider = copyFrom.CredentialsProvider; ServerCertificates = copyFrom.ServerCertificates; + DisableRetryExecutionStrategy = copyFrom.DisableRetryExecutionStrategy; } protected override RelationalOptionsExtension Clone() => new YdbOptionsExtension(this); - public override void ApplyServices(IServiceCollection services) => services.AddEntityFrameworkYdb(); + public override void ApplyServices(IServiceCollection services) => + services.AddEntityFrameworkYdb(!DisableRetryExecutionStrategy); public override DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); @@ -49,6 +53,15 @@ public YdbOptionsExtension WithServerCertificates(X509Certificate2Collection? se return clone; } + public YdbOptionsExtension DisableRetryOnFailure() + { + var clone = (YdbOptionsExtension)Clone(); + + clone.DisableRetryExecutionStrategy = true; + + return clone; + } + private sealed class ExtensionInfo(YdbOptionsExtension extension) : RelationalExtensionInfo(extension) { public override bool IsDatabaseProvider => true; diff --git a/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs b/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs index fa64290c..8a35327c 100644 --- a/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs +++ b/src/EFCore.Ydb/src/Infrastructure/YdbDbContextOptionsBuilder.cs @@ -14,4 +14,7 @@ public YdbDbContextOptionsBuilder WithCredentialsProvider(ICredentialsProvider? public YdbDbContextOptionsBuilder WithServerCertificates(X509Certificate2Collection? serverCertificates) => WithOption(optionsBuilder => optionsBuilder.WithServerCertificates(serverCertificates)); + + public YdbDbContextOptionsBuilder DisableRetryOnFailure() => + WithOption(optionsBuilder => optionsBuilder.DisableRetryOnFailure()); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs index 3d5a023b..23990cfe 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbRelationalConnection.cs @@ -13,6 +13,7 @@ public class YdbRelationalConnection : RelationalConnection, IYdbRelationalConne { private readonly ICredentialsProvider? _credentialsProvider; private readonly X509Certificate2Collection? _serverCertificates; + private readonly bool _disableRetryExecuteStrategy; public YdbRelationalConnection(RelationalConnectionDependencies dependencies) : base(dependencies) { @@ -21,6 +22,7 @@ public YdbRelationalConnection(RelationalConnectionDependencies dependencies) : _credentialsProvider = ydbOptionsExtension.CredentialsProvider; _serverCertificates = ydbOptionsExtension.ServerCertificates; + _disableRetryExecuteStrategy = ydbOptionsExtension.DisableRetryExecutionStrategy; } protected override DbConnection CreateDbConnection() @@ -40,6 +42,10 @@ public IYdbRelationalConnection Clone() { builder.WithCredentialsProvider(_credentialsProvider); builder.WithServerCertificates(_serverCertificates); + if (_disableRetryExecuteStrategy) + { + builder.DisableRetryOnFailure(); + } }).Options; return new YdbRelationalConnection(Dependencies with { ContextOptions = options });