How do I map a MySql user defined function with ASP.NET EF Core Pomelo - entity-framework

I'm trying to map a custom MySQL function to a method on my dbcontext as shown in this article. As suggested in the article I've stubbed out a method with the same signature of my user-defined function.
public int CalcOffset(DateTime input_date, int id)
=> throw new InvalidOperationException();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(typeof(DbContext).GetMethod(nameof(CalcOffset), new[] { typeof(DateTime), typeof(int) }),
b =>
{
b.HasName("fn_test");
b.HasParameter("input_date");
b.HasParameter("id");
});
}
The MySql function I'm attempting to map to:
CREATE DEFINER=`myuser`#`localhost` FUNCTION `fn_test`(input_date datetime, project_id int) RETURNS int(11)
BEGIN
RETURN 1;
END
finally I call the function via the context
var offset = _context.CalcOffset(DateTime.Now, id);
When I run the code above I'm getting the error message thrown by the function I stubbed out. However per the article the method should be mapped the database function, not the actual method:
The body of the CLR method is not important. The method will not be
invoked client-side, unless EF Core can't translate its arguments. If
the arguments can be translated, EF Core only cares about the method
signature
Versions:
ASP.NET Core 3.1
Pomelo Entity Framework Core 3.2
MySql 5.7

UDFs work fine in my tests. Here is a fully working console program:
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Pomelo.EntityFrameworkCore.MySql.Storage;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
}
public class Context : DbContext
{
public virtual DbSet<IceCream> IceCreams { get; set; }
public int IncrementInteger(int value)
=> throw new InvalidOperationException();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So66857783";
optionsBuilder.UseMySql(
connectionString,
options => options.CharSetBehavior(CharSetBehavior.NeverAppend)
.ServerVersion(ServerVersion.AutoDetect(connectionString)))
.UseLoggerFactory(
LoggerFactory.Create(
configure => configure
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
modelBuilder.HasDbFunction(
typeof(Context).GetMethod(nameof(IncrementInteger)),
b => b.HasName("udf_increment")
.HasParameter("value"));
entity.HasData(
new IceCream {IceCreamId = 1, Name = "Vanilla"},
new IceCream {IceCreamId = 2, Name = "Chocolate"},
new IceCream {IceCreamId = 3, Name = "Matcha"}
);
});
}
}
internal static class Program
{
private static void Main(string[] args)
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
SetupDatabase(context);
var iceCreams = context.IceCreams
.OrderBy(i => i.IceCreamId)
.Select(
i => new
{
IceCreamId = i.IceCreamId,
IcrementedValue = context.IncrementInteger(i.IceCreamId)
})
.ToList();
Trace.Assert(iceCreams.Count == 3);
Trace.Assert(iceCreams[0].IceCreamId == 1);
Trace.Assert(iceCreams[0].IcrementedValue == 2);
}
private static void SetupDatabase(DbContext context)
{
context.Database.OpenConnection();
var connection = context.Database.GetDbConnection();
using var command = connection.CreateCommand();
command.CommandText = #"CREATE FUNCTION `udf_increment`(value int) RETURNS int
DETERMINISTIC
BEGIN
RETURN value + 1;
END";
command.ExecuteNonQuery();
}
}
}
It generates the following SQL query:
SELECT `i`.`IceCreamId`, `udf_increment`(`i`.`IceCreamId`) AS `IcrementedValue`
FROM `IceCreams` AS `i`
ORDER BY `i`.`IceCreamId`
However, just calling the function without an entity context, as you do in your OP, will not work because you would just execute your function body, instead of letting EF Core translating an expression tree to SQL:
// This does not work, because Context.IncrementInteger(int) is being directly called
// here, instead of being part of an expression tree, that is then translated to SQL
// by EF Core.
var answer = context.IncrementInteger(41);
Trace.Assert(answer == 42);
Of course you can always execute some good old SQL yourself, if you need to:
// Executing the UDF using god old SQL.
// Use parameters for input values in the real world.
context.Database.OpenConnection();
using var command = context.Database.GetDbConnection().CreateCommand();
command.CommandText = #"SELECT `udf_increment`(41)";
var answerToEverything = (int)command.ExecuteScalar();
Trace.Assert(answerToEverything == 42);

Related

How do I call stored procedures in EF Core 6 using named parameters?

A lot of code examples use either named parameters or execute stored procedures, but not both. How do I do so when I don't have a pre-defined entity type being selected by the stored proc? (Which means that .FromSqlRaw is out.)
The code below allows you to call a stored procedure and generate a list of named parameters, just from the list of SqlParameters.
var sqlParams = new SqlParameter[] {
new SqlParameter("p1", valOfP1),
new SqlParameter("p2", valOfP2),
new SqlParameter("pOut", SqlDbType.Int)
{
Direction = System.Data.ParameterDirection.Output
}
};
// OK to use the same names as for the SqlParameter identifiers. Does not interfere.
var sql = "myStoredProc " + String.Join(", ", sqlParams.Select(x =>
$"#{x.ParameterName} = #{x.ParameterName}" +
(x.Direction == ParameterDirection.Output ? " OUT" : "")
));
myDbContext.Database.ExecuteSqlRaw(sql, sqlParams);
var outputId = (int)(sqlParams.First(p => p.Direction == ParameterDirection.Output).Value);
Try Below example code which lets you call sp and store result in a
datatable.
using (var command = db.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "sp_name";
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(new SqlParameter("key", "Value"));
db.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
var dataTable = new DataTable();
dataTable.Load(result);
return dataTable;
}
}
Here Product is class where you can define property whatever you want to retrieve from procedure
public class DataBaseContext : DbContext
{
public DataBaseContext() : base("name=DataBaseContext")
{
}
public DbSet<Product> Products { get; set; }
}
-- // This below code you need to write where you want to execute
var context = new DataBaseContext();
var products = context.Database.SqlQuery<Product>("EXEC GetProductsList #ProductId",
new SqlParameter("#ProductId", "1")
).ToList();
Add DbSet as below code
public DbSet ChartModels { get; set; }
Set Dbset AS a HasNoKey() if it is use only for Query
builder.Entity< ChartModel >().HasNoKey();
Call Sp as below Code
string sqlQuery = "EXECUTE dbo.GetDashboardChart";
SqlParameter p = new SqlParameter("#name", "test");
var lst = await ChartModels.FromSqlRaw(sqlQuery,p).ToListAsync();
Pretty much the same as SAEED said above. Add the code below to your DbContext class:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Berk>().HasNoKey();
}
[NotMapped]
public DbSet<Berk> Berks { get; set; }
public virtual List<Berk> CallSP(string berkberk) =>
Berks.FromSqlRaw("exec dbo.berk #berkBerk = {0}", berkberk).ToList();
called with:
List<Berk> berk = = _whateverYouCalledTheDbContext.CallSP("berk berk berk!");
Will return a DbSet where Berk is just an object that matches the return values from the stored procedure. There is no Berks table in the database, but you have your stored procedure return values to play with as you wish.

Mount SQL query to EXTRACT postgresql function with EF Core 3.1

I'm using Postgresql with EF Core 3.1 on my ASP.NET project
I want to generate a sql fragment like this:
EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE f."MyDateTimeField")"
So, I have writed this code on OnModelCreating method of the DbContext:
modelBuilder
.HasDbFunction(typeof(MyDbFunctions).GetMethod(
nameof(MyDbFunctions.ExtractEpochFromTimestampWithTimezone)))
.HasTranslation(args
=> SqlFunctionExpression.Create(
"EXTRACT",
new []
{
new SqlFragmentExpression(
$"EPOCH FROM TIMESTAMP WITH TIME ZONE"),
args.First()
}, typeof(double), null));
But the generated SQL fragment was:
EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE, f."MyDateTimeField")
Note that SQL has one "," after word "ZONE".
How can I fix my code to generate the SQL fragment without this ","?
Any one that faced same issues as above. I have solve above issue with the following code
Test entity
public class Employee
{
public int Id { get; set; }
public DateTime DOB { get; set; }
public string Name { get; set; }
}
EF Core Dbcontext with custom postgres EXTRACT function
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(ExtractMethod)
.HasTranslation(args =>
{
var arguments = args.ToList();
//{((Microsoft.EntityFrameworkCore.Query.SqlExpressions.ColumnExpression)arguments[1]).TypeMapping.StoreType}
arguments[0] = new SqlFragmentExpression($"{((Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlConstantExpression)arguments[0]).Value} from {((Microsoft.EntityFrameworkCore.Query.SqlExpressions.ColumnExpression)arguments[1]).Table.Alias}.\"{((Microsoft.EntityFrameworkCore.Query.SqlExpressions.ColumnExpression)arguments[1]).Name}\"");
arguments.RemoveAt(1);
return new SqlFunctionExpression("EXTRACT",
arguments,
false,
new bool[] { false, false},
typeof(decimal),
RelationalTypeMapping.NullMapping);
});
}
EF function which is mapped
private readonly MethodInfo ExtractMethod
= typeof(Context).GetRuntimeMethod(nameof(Extract), new[] { typeof(string), typeof(DateTime) });
public DateTime Extract(string field, DateTime source)
=> throw new NotSupportedException();
UPDATED CODE
The above method will fail when using nested functions. I have updated it with proper implementaion. I hope this will help others
Added a new class called ExtractExpression
public class ExtractExpression:SqlFunctionExpression
{
private readonly IReadOnlyCollection<SqlExpression> _params;
public ExtractExpression(IReadOnlyCollection<SqlExpression> parameters):base("EXTRACT", true, typeof(double), RelationalTypeMapping.NullMapping)
{
_params = parameters;
}
protected override Expression Accept(ExpressionVisitor visitor)
{
if (!(visitor is QuerySqlGenerator))
return base.Accept(visitor);
visitor.Visit(new SqlFragmentExpression("EXTRACT(")); //Postgres function name
visitor.Visit(_params.First()); //First paramenter
visitor.Visit(new SqlFragmentExpression(" from ")); //query clause
visitor.Visit(_params.Skip(1).First()); //2nd parameter
visitor.Visit(new SqlFragmentExpression(")")); //function ending brace
return this;
}
protected override void Print([NotNullAttribute] ExpressionPrinter expressionPrinter)
{
Console.WriteLine(expressionPrinter);
}
}
Add below code in OnModeCreating function
modelBuilder.HasDbFunction(ExtractMethod)
.HasTranslation(expressions =>
{
return new ExtractExpression(expressions);
});

How can I pass a LambdaExpression to an IncludeFilter?

I'm trying to pass a dynamically generated LambdaExpression to an IncludeFilter, as follows:
EDIT: I've changed my test code to the following, as (correctly) I wasn't implementing my "Where" statement. The correct where statement is being generated, but I can't pass the lambda statement into the IncludeFilter call:
DbSet<MyTestEntity> dbSet = db.Set<MyTestEntity>();
ParameterExpression parameter = Expression.Parameter(typeof(MyTestEntity), "t");
Expression idProperty = Expression.Property(parameter, "mytestentityid");
Expression delProperty = Expression.Property(parameter, "deleted");
Expression delTarget = Expression.Constant(false, typeof(bool));
Expression deletedMethod = Expression.Call(delProperty, "Equals", null, delTarget);
Expression<Func<MyTestEntity, bool>> lambda = Expression.Lambda<Func<MyTestEntity, bool>>(deletedMethod, parameter);
IQueryable<MyTestEntity> query = dbSet.Where(lambda);
Console.WriteLine("Current Query: {0}", query.ToString());
foreach (string include in includes)
{
Type subType = db.GetType().Assembly.GetTypes().SingleOrDefault(x => x.Name.EndsWith(include));
Assert.IsNotNull(subType);
ParameterExpression innerParam = Expression.Parameter(subType, subType.Name);
Assert.IsNotNull(innerParam);
MemberExpression inrDelProp = Expression.Property(innerParam, "deleted");
Assert.IsNotNull(inrDelProp);
ConstantExpression inrDelCstProp = Expression.Constant(false, typeof(bool));
Assert.IsNotNull(inrDelCstProp);
MethodCallExpression inrDelMthd = Expression.Call(inrDelProp, "Equals", null, inrDelCstProp);
Assert.IsNotNull(inrDelMthd);
var delegateType = typeof(Func<,>).MakeGenericType(subType, typeof(bool));
dynamic inrLmbdaExpr = Expression.Lambda(delegateType, inrDelMthd, innerParam);
Assert.IsNotNull(inrLmbdaExpr);
Console.WriteLine("inrLmbdaExpr: {0}", inrLmbdaExpr.ToString()); // Result: MyTestEntityChild => MyTestEntityChild.deleted.Equals(false)
query = query.IncludeFilter(inrLmbdaExpr); // ERROR HERE
Assert.IsNotNull(query);
Console.WriteLine("-------------------------------------------------");
Console.WriteLine("Current Query: {0}", query.ToString());
}
This is built into an abstract class allowing me to pass in an entity type, retrieve the records, and reuse the method irrespective of the entity type; however, I'm also trying to filter out child entities that are marked as deleted (thus the use of EF+).
How can I do this?
EDIT 2: So, I realized I also have Linq.Dynamic.Core (!) in my solution, so I already have access to parsing a LambdaExpression from string. However, the error I get says that IncludeFilter doesn't know which method it's trying to use. (I see in the Object Browser that one uses Expression> and one uses Expression>>. If I could just figure out how to get the IncludeFilter to recognize which method, I think I'd be done! Here's a sample of the code I've rewritten:
string myIncStr = String.Format("x => x.{0}.Where(s => s.deleted.Equals(false)).Where(x => x.MyEntityId.Equals(IncomingId)",includedEntityName);
IEnumerable<MyEntity> result = db.MyEntity.IncludeFilter(System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(typeof(MyChildEntity), myIncStr, null));
Is there a way to "force" (for lack of a better term) the IncludeFilter to use one method? Is it by passing a value instead of null in the Parser?
BTW, thanks for your help. Your EFP library is actually excellent.
Disclaimer: I'm the owner of the project Entity Framework Plus
Yes, it's possible but only if you can specify the generic argument type required by the method explicitly for the QueryFilter (As you mentioned in your comment).
Otherwise, you will need to also call the QueryFilter via the expression to make everything generic.
However, your current expression seems to have some error such as not calling the Where methods.
What you want to achieve is probably something similar to this:
query = query.IncludeFilter(x => x.Childs.Where(y => !y.Deleted));
Disclaimer: I'm the owner of the project Eval-Expression.NET
This library is not free but makes working with a dynamic expression easier and faster.
Once you get used, you can quickly create a dynamic expression in only a few lines as you normally write LINQ. Here is a code that could handle a similar scenario as your:
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Windows.Forms;
using Z.Expressions;
namespace Z.EntityFramework.Plus.Lab.EF6
{
public partial class Form_Request_IncludeFilter_Dynamic : Form
{
public Form_Request_IncludeFilter_Dynamic()
{
InitializeComponent();
// CLEAN
using (var context = new EntityContext())
{
context.MyEntityClasses.RemoveRange(context.MyEntityClasses);
context.MyEntityClassToFilters.RemoveRange(context.MyEntityClassToFilters);
context.SaveChanges();
}
// SEED
using (var context = new EntityContext())
{
var entity1 = context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 1, Childs = new List<MyEntityClassToFilter>()});
entity1.Childs.Add(new MyEntityClassToFilter {ColumnInt = 1, Deleted = true});
entity1.Childs.Add(new MyEntityClassToFilter {ColumnInt = 2, Deleted = false});
context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 2});
context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 3});
context.SaveChanges();
}
// TEST
using (var context = new EntityContext())
{
// You must register extension method only once
// That should not be done here, but for example purpose
EvalManager.DefaultContext.RegisterExtensionMethod(typeof(QueryIncludeFilterExtensions));
// That could be also dynamic. I believe you already handle this part
IQueryable<MyEntityClass> query = context.MyEntityClasses;
// The path to include
var include = "Childs";
// The dynamic expression to execute
var dynamicExpression = "IncludeFilter(x => x." + include + ".Where(y => !y.Deleted));";
query = query.Execute<IQueryable<MyEntityClass>>(dynamicExpression);
// The result
var list = query.ToList();
}
}
public class EntityContext : DbContext
{
public EntityContext() : base("CodeFirstEntities")
{
}
public DbSet<MyEntityClass> MyEntityClasses { get; set; }
public DbSet<MyEntityClassToFilter> MyEntityClassToFilters { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Types().Configure(x =>
x.ToTable(GetType().DeclaringType != null
? GetType().DeclaringType.FullName.Replace(".", "_") + "_" + x.ClrType.Name
: ""));
base.OnModelCreating(modelBuilder);
}
}
public class MyEntityClass
{
public int ID { get; set; }
public int ColumnInt { get; set; }
public List<MyEntityClassToFilter> Childs { get; set; }
}
public class MyEntityClassToFilter
{
public int ID { get; set; }
public int ColumnInt { get; set; }
public bool Deleted { get; set; }
}
}
}
EDIT: Answer question
Please review my changed code
You are still missing the where clause.
What you have is something similar to this as you commented
// Result: MyTestEntityChild => MyTestEntityChild.deleted.Equals(false)
What you want is something similar to this
// Result: MyTestEntityChild => MyTestEntityChild.Where(x => x.deleted.Equals(false))
EDIT: Answer question
Oh sorry, I now understand the problem with it.
If you don't know the type, you will need to call the IncludeFilter in an expression as well to make everything generic. It cannot be called explicitely like you are trying to do.

How to execute a full text search using entity framework 6

I have the query:
var query = DataContext.Fotos.Where(x => x.Pesquisa.Contais("myTerm")
The SQL generated is:
SELECT
...
FROM Fotos AS [Extent1]
WHERE [Extent1].[Pesquisa] LIKE N'%mytem%'
But I need to use:
SELECT
...
FROM Fotos AS [Extent1]
WHERE CONTAINS ([Extent1].[Pesquisa], 'my term')
How to execute a full text search using entity framework 6?
Seems that Entity Framework 6 does not support full text search, but there is a workaround with interceptors.
http://www.entityframework.info/Home/FullTextSearch
Update Link doesn't work so here is the original content:
Microsoft TSQL supports full-text query by means of predicates
(CONTAINS and FREETEXT)
For example, you have table Notes
Create table Notes (
Id int Identity not null,
NoteText text
)
CREATE FULLTEXT CATALOG [Notes Data]
When you search this table for records containing word 'John', you
need to issue
SELECT TOP (10)
* from gps.NOTES
WHERE contains(NoteText, '(john)')
Unfortunately, Enity framework does not support full-text search
predicates still. For EFv6, you can make a workaround using
interception.
The idea is to wrap search text with some magic word during inside
plain String.Contains code and use interceptor to unwrap it right
before sql is executed in SqlCommand.
To start, lets create the interceptor class:
public class FtsInterceptor : IDbCommandInterceptor
{
private const string FullTextPrefix = "-FTSPREFIX-";
public static string Fts(string search)
{
return string.Format("({0}{1})", FullTextPrefix, search);
}
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
}
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
}
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
RewriteFullTextQuery(command);
}
public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
RewriteFullTextQuery(command);
}
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
}
public static void RewriteFullTextQuery(DbCommand cmd)
{
string text = cmd.CommandText;
for (int i = 0; i < cmd.Parameters.Count; i++)
{
DbParameter parameter = cmd.Parameters[i];
if (parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength, DbType.AnsiStringFixedLength))
{
if (parameter.Value == DBNull.Value)
continue;
var value = (string)parameter.Value;
if (value.IndexOf(FullTextPrefix) >= 0)
{
parameter.Size = 4096;
parameter.DbType = DbType.AnsiStringFixedLength;
value = value.Replace(FullTextPrefix, ""); // remove prefix we added n linq query
value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE
parameter.Value = value;
cmd.CommandText = Regex.Replace(text,
string.Format(
#"\[(\w*)\].\[(\w*)\]\s*LIKE\s*#{0}\s?(?:ESCAPE
N?'~')",parameter.ParameterName),
string.Format(#"contains([$1].[$2], #{0})",parameter.ParameterName));
if (text == cmd.CommandText)
throw new Exception("FTS was not replaced on: " + text);
text = cmd.CommandText;
}
}
}
}
}
I used extension function In that can be defined like this:
static class LanguageExtensions
{
public static bool In<T>(this T source, params T[] list)
{
return (list as IList<T>).Contains(source);
}
}
Now lets compose a sample how to use it. We need entity class Note:
public class Note
{
public int Id { get; set; }
public string NoteText { get; set; }
}
Mapping configuration for it:
public class NoteMap : EntityTypeConfiguration<Note>
{
public NoteMap()
{
// Primary Key
HasKey(t => t.Id);
}
}
And our DbContext ancestor:
public class MyContext : DbContext
{
static MyContext()
{
DbInterception.Add(new FtsInterceptor());
}
public MyContext(string nameOrConnectionString) : base(nameOrConnectionString)
{
}
public DbSet<Note> Notes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new NoteMap());
}
}
Now we ready to use it. Lets search for 'john':
class Program
{
static void Main(string[] args)
{
var s = FtsInterceptor.Fts("john");
using (var db = new MyContext("CONNSTRING"))
{
var q = db.Notes.Where(n => n.NoteText.Contains(s));
var result = q.Take(10).ToList();
}
}
}
You can use raw SQL queries with EF. So, there is another easy workaround.
using (DBContext context = new DBContext())
{
string query = string.Format("Select Id, Name, Description From Fotos Where CONTAINS(Pesquisa, '\"{0}\"')", textBoxStrToSearch.Text);
var data = context.Database.SqlQuery<Fotos>(query).ToList();
dataGridView1.DataSource = data;
}
Input validation etc. is omitted.
Edit: Code is modified according to OP's query.

Using the same dbcontext for different models

I have a DbContext that is empty. Mappings are created dynamically and the DbContext is used generically using Set();
The following is my generic DbContext.
/// <summary>
/// Object context
/// </summary>
public class MethodObjectContext : DbContext, IDbContext
{
private readonly IEventPublisher _eventPublisher;
public MethodObjectContext(string nameOrConnectionString, IEventPublisher eventPublisher)
: base(nameOrConnectionString)
{
_eventPublisher = eventPublisher;
}
public MethodObjectContext(DbConnection existingConnection, bool contextOwnsConnection, IEventPublisher eventPublisher)
: base(existingConnection, contextOwnsConnection)
{
_eventPublisher = eventPublisher;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
_eventPublisher.Publish(new ModelCreating(modelBuilder));
base.OnModelCreating(modelBuilder);
}
public new IDbSet<TEntity> Set<TEntity>() where TEntity : class
{
return base.Set<TEntity>();
}
}
I am trying write a unit test that will assert that the database is out of sync if I change the mappings (from the ModelCreating event).
The following is my test code.
[TestClass]
public class MigrationTests
{
private string _connectionString = string.Empty;
private string _testDb = string.Empty;
public MigrationTests()
{
_testDb = Path.Combine("C:\\", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name.Replace(".", "") + ".sdf");
if (File.Exists(_testDb))
File.Delete(_testDb);
_connectionString = string.Format("Data Source={0};Persist Security Info=False;", _testDb);
Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
}
[TestMethod]
public void ThrowsErrorForOutOfDateDatabase()
{
// The initializer will handle migrating the database.
// If ctor param is false, auto migration is off and an error will be throw saying the database is out of date.
Database.SetInitializer(new MigrationDatabaseInitializer<MethodObjectContext>(false));
// Create the initial database and do a query.
// This will create the database with the conventions of the Product1 type.
TryQueryType<Product1>("Product");
// The next query will create a new model that has conventions for the product2 type.
// It has an additional property which makes the database (created from previous query) out of date.
// An error should be thrown indicating that the database is out of sync.
ExceptionAssert.Throws<InvalidOperationException>(() => TryQueryType<Product2>("Product"));
}
private void TryQueryType<T>(string tableName) where T : class
{
using (var context = new MethodObjectContext(_connectionString, new FakeEventPublisher(x => x.ModelBuilder.Entity<T>().ToTable(tableName))))
{
var respository = new EfRepository<T>(context);
var items = respository.Table.ToList();
}
}
}
My Product1 class is a POCO object, and my Product2 class is the same object with an additional db field.
My problem is that when I new() up the MethodObjectContext the second time and do a query, the ModelCreating method isn't called, causing me to get the following error.
The entity type Product2 is not part of the model for the current context.
Product2 would be a part of the context of the ModelCreating event was being called, but it is not. Any ideas?
NOTE: I am expecting errors since we are using the same connection string (sdf) and the db being created didn't create the additional field that my second call (Product2) requires.
My DbCompiledModel was being cached. The following flushed the cache.
private void ClearDbCompiledModelCache()
{
var type = Type.GetType("System.Data.Entity.Internal.LazyInternalContext, EntityFramework");
var cmField = type.GetField("CachedModels",System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
var cachedModels = cmField.GetValue(null);
cachedModels.GetType().InvokeMember("Clear", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.InvokeMethod, null, cachedModels, null);
}