Entity Framework Code First custom migrations Transaction Exception - c#

I have defined a custom migration class, like so:
public class MyMigrate<TMigrationsConfiguration> : IDatabaseInitializer<MyContext>
where TMigrationsConfiguration : DbMigrationsConfiguration<MyContext>, new()
{
private readonly TMigrationsConfiguration _config;
public MyMigrate(string connectionString, string invariantProvider)
{
var typeInitialCreate = typeof(MyConfiguration);
var migrationsAssembly = Assembly.GetAssembly(typeInitialCreate);
var migrationsNamespace = typeInitialCreate.Namespace;
_config = new TMigrationsConfiguration
{
TargetDatabase = new DbConnectionInfo(connectionString, invariantProvider),
MigrationsAssembly = migrationsAssembly,
MigrationsNamespace = migrationsNamespace,
};
}
public void InitializeDatabase(MyContext context)
{
// Update the migrator with the config containing the right connection string
var dbMigrator = new DbMigrator(_config);
var dbm = dbMigrator.GetDatabaseMigrations();
var local = dbMigrator.GetLocalMigrations();
var pending = dbMigrator.GetPendingMigrations();
dbMigrator.Update();
}
}
This is the actual configuration class, that does some additional seeding stuff. This class is located in another assembly.
public sealed class MyMigrationConfiguration : DbMigrationsConfiguration<MyContext>
{
public MigrationConfiguration()
{
AutomaticMigrationsEnabled = false;
ContextKey = "MyContext";
}
}
This is the generated Configuration file you get after you enable migrations. It is used, to identify and load the assembly with the generated migration files.
internal sealed class MyConfiguration : DbMigrationsConfiguration<MyContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
ContextKey = "MyContext";
}
}
}
I execute it like so:
private static void MigrateToLatest()
{
var conString = System.Configuration.ConfigurationManager.ConnectionStrings["MyContext"].ConnectionString;
Database.SetInitializer(new MyMigrate<MigrationConfiguration>(conString, Migrate.SqlInvariantProvider));
using (var fctx = new MyContext(conString, "SYSTEM"))
{
if (!fctx.Database.CompatibleWithModel(false))
{
fctx.Database.Initialize(false);
}
}
}
I get the following error message when i call Initialize(false):
`An unhandled exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll
Additional information: The transaction passed in is not associated with the current connection. Only transactions associated with the current connection may be used.`
I have checked the connection strings, they seem fine. If i uncomment the Initializer line, or replace it with SetInitializer(null), i will get the "model has changed" error message, but transactions don't seem to be a problem. Somehow it must have to do with my custom migrator class. I used this code in EF 4.3 and it worked. In 6.1.3 it no longer executes.

Related

Including sql files when generating Migrations in EF Core - asp.net

We have a project ongoing at the moment which is using EF commands to automatically generate a migration script, i.e. dotnet ef migrations add InitialCreate
In addition to the auto-created entities, we need to run sql commands from files to generate stored procedures, which are required by external services.
It is simple enough to add the following manually to the migration, and this works locally:
var sqlFiles =
Directory.GetFiles(
$"{AppDomain.CurrentDomain.BaseDirectory}/sql", "*.sql");
foreach (var sqlFile in sqlFiles)
{
migrationBuilder.Sql(File.ReadAllText(sqlFile));
}
However this needs adding in again every time we run the migration builder (which is fairly frequently as there are often new models to add etc.), and this doesn't work when pushing to our development servers as they automatically run the migration builder and database update commands.
Is there a way to get EF to automatically include the sql files when generating the migration? Or a better way of doing this in general?
This is our implementation for executing SQL Scripts on app startup which might useful or at least guide you in some direction.
This is executed from program.cs as part of the Main() method:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
ExecuteAzureDatabaseScripts(host);
host.Run();
}
private static void ExecuteAzureDatabaseScripts(IWebHost host)
{
var scopeFactory = host.Services.GetService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var hostingEnvironment = scope.ServiceProvider.GetService<IHostingEnvironment>();
if (hostingEnvironment.IsDevelopment()) return;
var scriptRunner = scope.ServiceProvider.GetService<SqlScriptRunner>();
var storeContext = scope.ServiceProvider.GetService<IStoreDbContext>();
scriptRunner.Context = storeContext;
scriptRunner.Directory = #"SqlScripts\Azure";
scriptRunner.ExecuteScripts();
}
}
ScriptRunner class:
public class SqlScriptRunner
{
private IDbContext _context;
private readonly IHostingEnvironment _hosting;
private string _directory;
public SqlScriptRunner(IHostingEnvironment hosting)
{
_hosting = hosting;
}
public IDbContext Context
{
set => _context = value;
}
/// <summary>
/// Name of the directory containing json seed data e.g. Data\Store
/// </summary>
public string Directory
{
set => _directory = value;
}
public void ExecuteScripts()
{
if (_context == null) throw new NullReferenceException("Database context for SqlScriptRunner is null.");
if (string.IsNullOrEmpty(_directory)) throw new NullReferenceException("SqlScriptRunner directory is null or empty.");
var directoryInfo = new DirectoryInfo( Path.Combine(_hosting.ContentRootPath, _directory));
foreach (var file in directoryInfo.GetFiles().OrderBy(f => f.FullName))
{
var script = File.ReadAllText(file.FullName);
_context.Database.ExecuteSqlCommand(script);
}
}
}

How to use ApplicationDbContext in a Class / Separate Thread ASP.Net Core

I am attempting to have a separate thread start a looping process to collect and import data from an api call and insert it into a local db. I attempt to create the database connection the same way I do in my controllers but it keeps giving me various errors.
I have tried multiple ways. For the setup below it gives me this error:
System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.'
The setup is for testing purposes.
public class HomeController : Controller
{
public ApplicationDbContext db;
public HomeController(ApplicationDbContext context)
{
db = context;
}
public IActionResult Index()
{
// This executes the code and inserts the test row no problem.
BinanceData binanceData = new BinanceData();
binanceData.Symbol = "Test";
binanceData.RealTime = DateTime.Now;
db.BinanceData.Add(binanceData);
db.SaveChanges();
// End
// Where I create the new thread and start the process
Thread doThis = new Thread(delegate ()
{
DataCollection dataCollection = new DataCollection(db);
dataCollection.InsertData(DateTime.Now);
});
doThis.Start();
// End
return View();
}
}
This is the class where I attempt to create a connection (or pass existing connection) and to begin the loop and collect/insert data.
public class DataCollection
{
public ApplicationDbContext db;
public DataCollection(ApplicationDbContext context)
{
db = context;
}
public void InsertData(DateTime nextTime)
{
List<string> tokens = new List<string> { "ETHBTC", "LTCBTC", "BNBBTC", "NEOBTC", "GASBTC", "BTCUSDT", "MCOBTC", "WTCBTC", "LRCBTC", "QTUMBTC", "YOYOBTC", "OMGBTC", "ZRXBTC", "STRATBTC", "SNGLSBTC", "BQXBTC", "KNCBTC", "FUNBTC", "SNMBTC", "IOTABTC", "LINKBTC", "XVGBTC", "SALTBTC", "MDABTC", "MTLBTC", "SUBBTC", "EOSBTC", "SNTBTC", "ETCBTC", "MTHBTC", "ENGBTC", "DNTBTC", "ZECBTC", "BNTBTC", "ASTBTC", "DASHBTC", "OAXBTC", "BTGBTC", "EVXBTC", "REQBTC", "VIBBTC", "TRXBTC", "POWRBTC", "ARKBTC", "XRPBTC", "MODBTC", "ENJBTC", "STORJBTC", "KMDBTC", "RCNBTC", "NULSBTC", "RDNBTC", "XMRBTC", "DLTBTC", "AMBBTC", "BATBTC", "BCPTBTC", "ARNBTC", "GVTBTC", "CDTBTC", "GXSBTC", "POEBTC", "QSPBTC", "BTSBTC", "XZCBTC", "LSKBTC", "TNTBTC", "FUELBTC", "MANABTC", "BCDBTC", "DGDBTC", "ADXBTC", "ADABTC", "PPTBTC", "CMTBTC", "XLMBTC", "CNDBTC", "LENDBTC", "WABIBTC", "TNBBTC", "WAVESBTC", "GTOBTC", "ICXBTC", "OSTBTC", "ELFBTC", "AIONBTC", "NEBLBTC", "BRDBTC", "EDOBTC", "WINGSBTC", "NAVBTC", "LUNBTC", "APPCBTC", "VIBEBTC", "RLCBTC", "INSBTC", "PIVXBTC", "IOSTBTC", "STEEMBTC", "NANOBTC", "VIABTC", "BLZBTC", "AEBTC", "NCASHBTC", "POABTC", "ZILBTC", "ONTBTC", "STORMBTC", "XEMBTC", "WANBTC", "WPRBTC", "QLCBTC", "SYSBTC", "GRSBTC", "CLOAKBTC", "GNTBTC", "LOOMBTC", "REPBTC", "TUSDBTC", "ZENBTC", "SKYBTC", "CVCBTC", "THETABTC", "IOTXBTC", "QKCBTC", "AGIBTC", "NXSBTC", "DATABTC", "SCBTC", "NPXSBTC", "KEYBTC", "NASBTC", "MFTBTC", "DENTBTC", "ARDRBTC", "HOTBTC", "VETBTC", "DOCKBTC", "POLYBTC", "PHXBTC", "HCBTC", "GOBTC", "PAXBTC", "RVNBTC", "DCRBTC", "USDCBTC", "MITHBTC", "BCHABCBTC" };
foreach (string token in tokens)
{
BinanceData binance = new BinanceData();
using (var client = new BinanceClient())
{
var data = client.GetKlines(token, Binance.Net.Objects.KlineInterval.OneMinute, null, null, 2);
var kline = data.Data[0];
binance.Symbol = token;
binance.Close = kline.Close;
binance.CloseTime = kline.CloseTime;
binance.High = kline.High;
binance.Low = kline.Low;
binance.Open = kline.Open;
binance.OpenTime = kline.OpenTime;
binance.QuoteAssetVolume = kline.QuoteAssetVolume;
binance.TakerBuyBaseAssetVolume = kline.TakerBuyBaseAssetVolume;
binance.TakerBuyQuoteAssetVolume = kline.TakerBuyQuoteAssetVolume;
binance.TradeCount = kline.TradeCount;
binance.Volume = kline.Volume;
binance.RealTime = DateTime.Now;
}
db.BinanceData.Add(binance);
db.SaveChanges();
}
CountUntilNextMin(nextTime);
}
public void CountUntilNextMin(DateTime nextTime)
{
while (DateTime.Now < nextTime)
{
System.Threading.Thread.Sleep(10000);
}
DateTime passTime = DateTime.Now.AddMinutes(1);
InsertData(passTime);
}
}
Here is my ApplicationDbContext Class
public class ApplicationDbContext : IdentityDbContext
{
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
public DbSet<BinanceData> BinanceData { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public ApplicationDbContext()
: base()
{
}
}
And my StartUp.cs ConfigureServices Method
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
By default your DI container is scoping the DbContext to the request. If you want to use one in a long-running background process, just create it directly. EG:
Thread doThis = new Thread(delegate ()
{
using (var db = new ApplicationDbContext())
{
DataCollection dataCollection = new DataCollection();
dataCollection.InsertData(DateTime.Now);
}
});
The DbContext will need to be configured, and the simplest way to do that is to just override OnConfiguring to connect to your database. Minimally, like this:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=YourServer;Database=YourDatabase;Trusted_Connection=True;MultipleActiveResultSets=true");
}
Although you'll probably want to read the connection string from your configuration.
i using this by calling CreateScope try it
public static async Task InitializeDatabaseAsync(IServiceProvider serviceProvider, IHostingEnvironment env)
{
var result = false;
using (var scope1 = serviceProvider.CreateScope())
using (var db1 = scope1.ServiceProvider.GetService<MainContext>())
{
result = await db1.Database.EnsureCreatedAsync();
if (result)
{
InsertTestData(serviceProvider, env);
}
}
}
then
using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
using (var db = scope.ServiceProvider.GetService<MainContext>())
{
existingData = db.Set<TEntity>().ToList();
}

"No connection string named 'Entities' could be found in the application config file." error with Moq

I'm using Moq to provide a mocking context for my Oracle db. But when I call _context.Entry with the mocked context, I get an InvalidOperationException.
"No connection string named 'Entities' could be found in the application config file."
I'm already providing a mocked context, so not sure why it's still trying to read connection string to create the context.
// generated code for oracle db
public partial class Entities : DbContext
{
public Entities()
: base("name=Entities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<ACTIVITY_CODE> ACTIVITY_CODE { get; set; }
}
// my code
public partial class Entities : System.Data.Entity.DbContext
{
public Entities(string scon) : base(scon) { }
}
// my code
public partial class ActivityCodeService
{
private Entities _context;
public ActivityCodeService(Entities context)
{
this._context = context;
}
public ACTIVITY_CODE Update(ACTIVITY_CODE item)
{
ACTIVITY_CODE ret = null;
var found = Read(item.ACT_ID);
if (found != null)
{
_context.Entry<ACTIVITY_CODE>(found).CurrentValues.SetValues(item); // throws InvalidOperationException "No connection string named 'Entities' could be found in the application config file."
_context.SaveChanges();
ret = item;
}
return ret;
}
}
// test code
[TestMethod]
public void activity_code_update_test()
{
// arrange
var mockSet = new Mock<DbSet<ACTIVITY_CODE>>();
mockSet.As<IQueryable<ACTIVITY_CODE>>().Setup(o => o.Provider).Returns(testData.Provider);
mockSet.As<IQueryable<ACTIVITY_CODE>>().Setup(o => o.Expression).Returns(testData.Expression);
mockSet.As<IQueryable<ACTIVITY_CODE>>().Setup(o => o.ElementType).Returns(testData.ElementType);
mockSet.As<IQueryable<ACTIVITY_CODE>>().Setup(o => o.GetEnumerator()).Returns(testData.GetEnumerator());
var mockContext = new Mock<Entities>();
mockContext.Setup(c => c.ACTIVITY_CODE).Returns(mockSet.Object);
var expected = new ACTIVITY_CODE() { ACT_ID = 1, ACT_CODE = "code 2", ACT_DESC = "desc 2" };
var target = new ActivityCodeService(mockContext.Object);
// act
target.Update(expected);
}
But if I don't use _context.Entry, then the test runs fine which is expected. So does that mean _context.Entry is creating another internal context and ignoring my mocked context?
// my code
public ACTIVITY_CODE Update(ACTIVITY_CODE item)
{
var ret = _context.ACTIVITY_CODE.FirstOrDefault(o => o.ACT_ID == item.ACT_ID);
if (ret != null)
{
ret.ACT_CODE = item.ACT_CODE;
ret.ACT_DESC = item.ACT_DESC;
_context.SaveChanges(); // this will work fine with Moq's mocked context
}
return ret;
}
Entry isn't, and can't be, mocked by Moq as it's not virtual so it is still going to try to use the database that it believes is there. That's why it's looking for a connection string.
What I have been able to do which has worked well is to abstract that function call into a virtual method that I had enough control over to actually mock.
Alternatives:
There are some tools based on answers to other questions that have the same base problem. Looks like TypeMock and JustMock may be able to work around the issue.
Additionally, it looks like MS Fakes should be able to shim it. After a little investigation it looks like it'd work something like this:
ShimDbEntityEntry<TestModel> entryMock = new ShimDbEntityEntry<TestModel>();
ShimDbPropertyValues mockValues = new ShimDbPropertyValues();
mockValues.SetValuesObject = (newValues) => { }; // Manually do something here
entryMock.CurrentValuesGet = () => mockValues;
ShimDbContext.AllInstances.EntryOf1M0<TestModel>((ctx, target) => entryMock);

Override DbContext EF6

I'm trying to understand how to dynamically create the connection string for my DbContext, but my application says it has no connection string in the app.config (and that's correct because I don't want to use it in the app.config or web.config). This is what I have:
In my solution I have a project called InterfaceApp. It is a ASP.NET MVC 5 application. When I put my connection string in the web.config all seems to be working fine.
In my solution I have an other project called InterfaceApp.Connector.Erp1. Here I want to connect to an ERP application and fetch some items. So in my repository I have:
namespace InterfaceApp.Connector.Erp1.Repository
{
internal class ItemRepository : IItemRepository
{
public IEnumerable<Item> Items
{
get
{
List<Item> items = new List<Item>();
using (Models.Entities context = new Models.Entities())
{
var itemList = context.Items.ToList();
foreach(var item in itemList)
{
items.Add(new Item() { Id = item.ID, Description = item.Description, ItemCode = item.ItemCode });
}
}
return items.ToList();
}
}
}
}
I've created a partial class to connect to the database:
namespace InterfaceApp.Connector.Erp1.Models
{
public partial class Entities
{
public Entities(string connectionString)
: base(ConnectionString())
{
}
private static string ConnectionString()
{
SqlConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder
{
DataSource = "MyServer", //When this works it will be dynamic
InitialCatalog = "XXX", //When this works it will be dynamic
PersistSecurityInfo = true,
IntegratedSecurity = true,
MultipleActiveResultSets = true,
};
var entityConnectionStringBuilder = new EntityConnectionStringBuilder
{
Provider = "System.Data.SqlClient",
Metadata = "res://*/Models.Erp1Model.csdl|res://*/Models.Erp1Model.ssdl|res://*/Erp1Model.msl",
ProviderConnectionString = sqlBuilder.ConnectionString
};
return entityConnectionStringBuilder.ConnectionString;
}
}
}
The Context class that is auto-generated by EF6 (Db First) looks like this:
namespace InterfaceApp.Connector.Erp1.Models
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
public partial class Entities : DbContext
{
public Entities()
: base("name=Entities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<Items> Items { get; set; }
}
}
When I run my application, the debugger stops at the auto-generated class, but not at my partial class. Because it cannot find the connection string Entities in my app.config and web.config it generates an error saying that the connection string is not found in the application config file. What am I doing wrong?
When you are calling the DbContext, you're calling the empty constructor (new Models.Entities()). Thus, it will call the auto-generated DbContext. If you want to call your partial class, you need to call it explicitly with the parameter.
Remember when you create a partial class, the compiler merges them, so you have this when compiled :
public partial class Entities : DbContext
{
public Entities()
: base("name=Entities")
{
}
public Entities(string connectionString)
: base(ConnectionString())
{
}
private static string ConnectionString()
{
SqlConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder
{
DataSource = "MyServer", //When this works it will be dynamic
InitialCatalog = "XXX", //When this works it will be dynamic
PersistSecurityInfo = true,
IntegratedSecurity = true,
MultipleActiveResultSets = true,
};
var entityConnectionStringBuilder = new EntityConnectionStringBuilder
{
Provider = "System.Data.SqlClient",
Metadata = "res://*/Models.Erp1Model.csdl|res://*/Models.Erp1Model.ssdl|res://*/Erp1Model.msl",
ProviderConnectionString = sqlBuilder.ConnectionString
};
return entityConnectionStringBuilder.ConnectionString;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<Items> Items { get; set; }
}
What you probably need a a method to create your DbContext and call it instead of calling a new DbContext.
public static Entities Create()
{
return new Entities(ConnectionString());
}
Then you can use it this way :
using (var context = Entities.Create())
{
//...
}

EntityFramework 6 Code-based configuration of connection string for database-first

I'm attempting to make an existing application work without an app.config (it is required due to a very specific environment). Problem is that it's heavily relying on EntityFramework 6 to work with an SQL-Server.
I'm trying to use a code-based configuration, but I can't figure out how to provide a correct connection string through my configuration class.
I made a configuration class:
public class MyConfiguration : DbConfiguration
{
public MyConfiguration()
{
SetDefaultConnectionFactory(new MyConnectionFactory());
SetProviderServices("System.Data.SqlClient", System.Data.Entity.SqlServer.SqlProviderServices.Instance);
}
}
Then provided it to my DbContext (Generated by EF automatically from bd):
[DbConfigurationType(typeof(MyConfiguration))]
public partial class TestModelEntities
{
}
With a custom connection factory:
public class MyConnectionFactory : IDbConnectionFactory
{
public DbConnection CreateConnection(string nameOrConnectionString)
{
var newConnStringBuilder = new SqlConnectionStringBuilder
{
UserID = "user",
Password = "pass",
InitialCatalog = "databaseName",
DataSource = "serverName"
};
var entityConnectionBuilder = new EntityConnectionStringBuilder
{
Provider = "System.Data.SqlClient",
ProviderConnectionString = newConnStringBuilder.ToString(),
Metadata = #"res://*/TestModel.csdl|
res://*/TestModel.ssdl|
res://*/TestModel.msl"
};
var newDbConnect = new EntityConnection(entityConnectionBuilder.ToString());
return newDbConnect;
}
}
However. When I test it, I get an UnintentionalCodeFirstException. Why? What am I missing?
You should provide connection string to your context via :base(connectionString). Create a class as below:
public class ConnectionStringBuilder
{
public static string Construct()
{
var newConnStringBuilder = new SqlConnectionStringBuilder
{
UserID = "user",
Password = "pass",
InitialCatalog = "databaseName",
DataSource = "serverName"
};
var entityConnectionBuilder = new EntityConnectionStringBuilder
{
Provider = "System.Data.SqlClient",
ProviderConnectionString = newConnStringBuilder.ToString(),
Metadata = #"res://*/TestModel.csdl|
res://*/TestModel.ssdl|
res://*/TestModel.msl"
};
return entityConnectionBuilder.ToString();
}
}
Then modify your Context constructor to look like this:
public DbContext()
: base(ConnectionStringBuilder.Construct())
{
}
It should work fine now. (source)

Categories