Entity Framework Core (EF Core) is stricter than Entity Framework 6 (EF 6).
To help identify potential issues, we've added several code analyzers that run when you save your BPM or Epicor Function (EF).
To assist with addressing these issues, we have added a Conversion Workbench task called EntityFrameworkCoreFix number 114, which during migration auto-fixes or provides additional diagnostics for many (but not all) compatibility issues across BPMs, Epicor Functions, Electronic Interfaces and Product Configurator.
You may see warnings related to the following issues.
Kinetic 2025.2 warns you about assignments of IDENTITY columns. 2026.1 will produce compiler errors as IDENTITY columns are read only.
Solution
The manual fix is to remove any code that attempts to set values for IDENTITY columns. Those are set by the database.
The EntityFrameworkCoreFix conversion will comment out simple assignment of an IDENTITY column. If the code is too complex to be auto-remediated the following diagnostic will be returned instead:
The value of the identity column {TableName}.{ColumnName} is set by the database. Do not set it in code.
EF Core does not support specifying a default value in the following methods:
DefaultIfEmptyFirstOrDefaultLastOrDefaultSingleOrDefault
For example, the following EF 6-style query will fail at runtime in EF Core due to the false default:
var hasActiveTip = Db.Tip
.Where(row => row.TipNum == 13)
.Select(row => row.Active)
.DefaultIfEmpty(false)
.FirstOrDefault();The EntityFrameworkCoreFix conversion will automatically fix the cases where the value specific is the default value for the data type. It will also handle the other default values for the different data types, such as null, false and 0. For this case, false is the default value for Boolean values, so the conversion will just remove false allowing it to run in EF Core.
Solution
For cases ther are not fixed by the conversion, you can fix this manually. Using AI tools like Copilot can solve most of these issue. For example, prompting Copilot with:
Fix the following query so that it does not pass false to DefaultIfEmpty:
followed by the above code will produced the following corrected version:
var hasActiveTip = Db.Tip
.Where(row => row.TipNum == 13)
.Select(row => (bool?)row.Active)
.FirstOrDefault() ?? false;Explanation
(bool?)row.Activecasts the result to a nullable Boolean. IfActiveis already nullable, this cast is unnecessary.FirstOrDefault()returnsnullif no rows match.?? falseprovides a default value offalsewhen no result is found.
Note on filtering
You can still use filters with methods like FirstOrDefault, LastOrDefault, and SingleOrDefault. For example:
Tip tipRow = this.Db.FirstOrDefault(row => row.TipNum == 13)EF Core will throw a Sequence contains no elements exception if the query you are calling, Min, Max or Sum on returns no rows. For example, if you are summing the quantity of something
in a query:
var result =
(from row in Db.Table
where row.Key == "ABC"
select row.Quantity)
.Max();This would fail if there were no rows returned. If you want the column's data types default value to be the default value for Max then you can do this:
var result =
(from row in Db.Table
where row.Key == "ABC"
select row.Quantity)
.DefaultIfEmpty()
.Max();If you need some other value than the column's data types default then you can do the following:
var result =
(from row in Db.Table
where row.Key == "ABC"
select (int?)row.Quantity)
.Max() ?? 999;For columns that are nullable, such as the integer Quantity column above, you have to cast the result to a nullable data type, int? in this case.
EF Core does not support DbFunctions. If you need to keep your code compatible with both 2025.2 and 2026.1 you can use conditional compilation. If you are running in 2026.1, you can just modify the code as suggested below without the conditional compiled code. If you are modifying code prior to 2026.1 and want the code to run in both your current and future version of Kinetic, then you can use USE_EF_CORE which is a conditional compilation value that is defined automatically when running in EF Core. After you have fully upgraded to 2026.1 or later, you can remove the conditional code and keep only the EF Core part. The TruncateTime conversion is shown with the conditional compile, the examples following that only show the final code for brevity.
The following methods can be modified to an EF Core-compatible alternative.
TruncateTimeAddDays,AddMonthsDiffDays,DiffHours,DiffMicroseconds,DiffMilliseconds,DiffMinutes,DiffMonths,DiffNanoseconds,DiffSeconds,DiffYears
Solution
The following changes can be made manually, or automatically using EntityFrameworkCoreFix.
var results =
from row in Db.ICEVer
where System.Data.Entity.DbFunctions.TruncateTime(row.DBSchemaDate) == new DateTime(2014, 7, 9)
select row;Should be changed to:
var results =
from row in Db.ICEVer
#if USE_EF_CORE
where row.DBSchemaDate.Value.Date == new DateTime(2014, 7, 9)
#else
where System.Data.Entity.DbFunctions.TruncateTime(row.DBSchemaDate) == new DateTime(2014, 7, 9)
#endif
select row;var results =
from row in Db.ICEVer
where System.Data.Entity.DbFunctions.AddDays(row.DBSchemaDate, 5) >= DateTime.Now
select row;Should be changed to:
var results =
from row in Db.ICEVer
where row.DBSchemaDate.Value.AddDays(5) >= DateTime.Now
select row;Diff methods listed above can be addressed by replacing with the appropriate Microsoft.EntityFrameworkCore.SqlServerDbFunctionsExtensions method. These methods begin with DateDiff. Below is an example of such replacement.
var results =
from row in Db.ICEVer
where System.Data.Entity.DbFunctions.DiffMonths(row.DBSchemaDate, DateTime.Now) > 5
select row;Should be changed to:
var results =
from row in Db.ICEVer
where Microsoft.EntityFrameworkCore.SqlServerDbFunctionsExtensions.DateDiffMonth(null, row.DBSchemaDate, DateTime.Now) > 5
select row;EF Core supports fewer overloads of standard C# functions in queries. We have added an analyzer to give compiler warnings for the most commonly used overloads. For the most part, these are string functions that specify different StringComparison parameters. For example:
var results =
(from row in Db.Menu
where string.Equals(row.MenuID, "ABC", StringComparison.OrdinalIgnoreCase)
select row);In most cases, you can just drop the StringComparison parameter. The comparison will use the comparison type defined by the database.
Solution
Using EntityFrameworkCoreFix the following methods can be auto-fixed in cases where the unsupported parameters (StringComparison, InvariantCase...) can simply be removed.
string.Comparestring.Containsstring.Equalsstring.StartsWithstring.EndsWith
EF Core only allows "simple" values in queries. For example, if you retrieve parentRow from a query and you need to use values for you current query:
var results =
from row in Db.ChildTable
where row.Key1 = parentRow.Key1
select row;Since parentRow.Key1 is not a "simple" value, you will need to do something like this:
var parentKey = parentRow.Key1;
var results =
from row in Db.ChildTable
where row.Key1 = parentKey
select row;Similar to EF Core requiring "simple" value, group can cause issues as well. In the query below, if you were to use menuGroup.Key in another query, it would throw an exception.
var results =
from menuRow in Db.Menu
group menuRow by menuRow.SystemCode into menuGroup
select new { menuGroup.Key, Count = menuGroup.Count() };MARS has been disabled. This means that a database connection can only have one query active at a time. For EF Core queries, you have to ensure that you have read through all the rows
in the result set before starting another query. A common solution is to add .ToList() to the end of the query. This will force all rows to be read and the reader closed. For example:
var tipRows =
from row in Db.Tip
select row;
foreach (var tipRow in tipRows)
{
var accessScopeRows = Db.AccessScope.FirstOrDefault();
}This will fail because the EF Core query for Tip rows is still active when the AccessScope query is executed in the foreach loop. The solution shown below is to force all Tip rows to be read so that query is closed before the foreach loop is entered.
var tipRows =
(from row in Db.Tip
select row)
.ToList();
foreach (var tipRow in tipRows)
{
var accessScopeRows = Db.AccessScope.FirstOrDefault();
}For raw SQL queries using a SqlDataReader, you have to dispose the reader before you execute another query. Commonly, you would add a using to the reader's variable declaration. This will
automatically close the reader.
Note: It is recommended that direct access to SQL be avoided altogether.
Here is an example where we start a second read before completing the first one:
using var command1 = connection.CreateCommand();
command1.CommandText = "SELECT TOP 1 TipTitle FROM Ice.Tip";
var reader1 = command1.ExecuteReader();
reader1.Read();
var userId = reader1.GetString(0);
using var command2 = connection.CreateCommand();
command2.CommandText = "SELECT TOP 1 Action FROM Ice.ChangeLog";
var reader2 = command2.ExecuteReader();
reader2.Read();
var companyId = reader2.GetString(0);And here is the fixed example where we close the first reader before opening the second:
using var command1 = connection.CreateCommand();
command1.CommandText = "SELECT TOP 1 TipTitle FROM Ice.Tip";
using (var reader1 = command1.ExecuteReader())
{
reader1.Read();
var userId = reader1.GetString(0);
}
using var command2 = connection.CreateCommand();
command2.CommandText = "SELECT TOP 1 Action FROM Ice.ChangeLog";
using (var reader2 = command2.ExecuteReader())
{
reader2.Read();
var companyId = reader2.GetString(0);
}You can use AI to help find and fix MARS issues. I have used Microsoft Copilot and it works well. Start a new "chat" and enter the following prompt:
Scan only the C# code below for MARS (Multiple Active Result Sets) issues when MARS is disabled in SQL Server. The application uses EntityFrameworkCore and SQLCommand.
CONTEXT:
- MARS disabled = only ONE active query per connection at a time
- MARS issues will produce error: "There is already an open DataReader associated with this Connection which must be closed first."
SAFE PATTERNS - DO NOT FLAG:
- If DBExpressionCompiler.Compile is used, that query is NOT a MARS issue because it automatically materializes results internally.
```csharp
Then copy the source code to fix. The AI should return confirmation of any issues found and suggest fixes.
If you only want to search the code for MARS issue then add the following before the source code:
OUPUT - DO NOT INCLUDE:
- Suggestion to apply fix
A number of EF 6 methods are no longer supported. For example:
const string sqlStatement = "SELECT * FROM Ice.Menu";
Db.ExecuteStoreCommand(sqlStatement);When Kinetic switches to using EF Core, you will need to replace that will supported method in EF Core.