Database gets updated before SaveChanges is called in Entity Framework - entity-framework

I am trying to use EF with an insert stored procedure as I have no direct access to the table. My understanding is the database should not get updated until SaveChanges() is called in the code but database is updated everything an insert happens. In this case, 4 database calls were made.
How do I make this to have just one database call and update multiple records?
This may be classed as database-first EF? The stored procedure is imported as a function to edmx in a normal way.
Code sample:
public ActionResult Index()
{
List<Product> products = new List<Product> {
new Product() { Title = "coca cola", Description = "good"},
new Product() { Title = "apple", Description = "fruit"},
new Product() { Title = "orange", Description = "fruit"},
new Product() { Title = "banana", Description = "my favourite"}
};
EFwithSPtest context = new EFwithSPtest();
foreach(var p in products)
{
context.Insert(p.Title, p.Description);
}
context.SaveChanges();
return View();
}
Stored procedure:
ALTER PROCEDURE [dbo].[Insert]
#Title nvarchar (50),
#Description nvarchar (max)
AS
INSERT INTO [Product] (Title, Description) VALUES (#Title, #Description)
RETURN 0
Auto generated DbContext class:
public partial class EFwithSPtest : DbContext
{
public EFwithSPtest()
: base("name=Entities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual int Insert(string title, string description)
{
var titleParameter = title != null ?
new ObjectParameter("Title", title) :
new ObjectParameter("Title", typeof(string));
var descriptionParameter = description != null ?
new ObjectParameter("Description", description) :
new ObjectParameter("Description", typeof(string));
return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction("Insert", titleParameter, descriptionParameter);
}
}

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.

Error with insertion in postgres Database with my ASP.NET CORE API

I'm creating an ASP.NET CORE API and I have a problem when I want to insert data in my postgres database.
The insert data's doesn't auto increment themself and it always start with id = 0 even if i insert an other status
My controller is :
[Route("api/[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly intranetApplicationAPIContext _context;
public StatusController(intranetApplicationAPIContext context)
{
_context = context;
}
[HttpGet]
public ActionResult<List<Status>> GetAll()
{
return _context.Status.AsNoTracking().ToList();
}
[HttpGet("{id}", Name = "GetStatus")]
public ActionResult<Status> GetById(long id)
{
var status = _context.Status.Find(id);
if (status == null)
{
return NotFound();
}
return status;
}
[HttpPost]
public IActionResult Create(Status status)
{
_context.Status.Add(status);
_context.SaveChanges();
return CreatedAtRoute("GetStatus", new { id = status.Id }, status);
}
Maybe the problem comes from .AsNoTracking() but I can't delete it because i will have this error :
The instance of entity type 'Status' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked
In my database, I see that I can do a sequence in order to auto increment but when I have to do my insert with this :
INSERT INTO status(state) VALUES ('test') RETURNING *;
How can I "translate" it in my statusController ?

EF5 Code First Enums and Lookup Tables

I'd like to define an enum for EF5 to use, and a corresponding lookup table. I know EF5 now supports enums, but out-of-the-box, it seems it only supports this at the object level, and does not by default add a table for these lookup values.
For example, I have a User entity:
public class User
{
int Id { get; set; }
string Name { get; set; }
UserType UserType { get; set; }
}
And a UserType enum:
public enum UserType
{
Member = 1,
Moderator = 2,
Administrator = 3
}
I would like for database generation to create a table, something like:
create table UserType
(
Id int,
Name nvarchar(max)
)
Is this possible?
Here's a nuget package I made earlier that generates lookup tables and applies foreign keys, and keeps the lookup table rows in sync with the enum:
https://www.nuget.org/packages/ef-enum-to-lookup
Add that to your project and call the Apply method.
Documentation on github: https://github.com/timabell/ef-enum-to-lookup
It is not directly possible. EF supports enums on the same level as .NET so enum value is just named integer => enum property in class is always integer column in the database. If you want to have table as well you need to create it manually in your own database initializer together with foreign key in User and fill it with enum values.
I made some proposal on user voice to allow more complex mappings. If you find it useful you can vote for the proposal.
I wrote a little helper class, that creates a database table for the enums specified in the UserEntities class. It also creates a foreign key on the tables that referencing the enum.
So here it is:
public class EntityHelper
{
public static void Seed(DbContext context)
{
var contextProperties = context.GetType().GetProperties();
List<PropertyInfo> enumSets = contextProperties.Where(p =>IsSubclassOfRawGeneric(typeof(EnumSet<>),p.PropertyType)).ToList();
foreach (var enumType in enumSets)
{
var referencingTpyes = GetReferencingTypes(enumType, contextProperties);
CreateEnumTable(enumType, referencingTpyes, context);
}
}
private static void CreateEnumTable(PropertyInfo enumProperty, List<PropertyInfo> referencingTypes, DbContext context)
{
var enumType = enumProperty.PropertyType.GetGenericArguments()[0];
//create table
var command = string.Format(
"CREATE TABLE {0} ([Id] [int] NOT NULL,[Value] [varchar](50) NOT NULL,CONSTRAINT pk_{0}_Id PRIMARY KEY (Id));", enumType.Name);
context.Database.ExecuteSqlCommand(command);
//insert value
foreach (var enumvalue in Enum.GetValues(enumType))
{
command = string.Format("INSERT INTO {0} VALUES({1},'{2}');", enumType.Name, (int)enumvalue,
enumvalue);
context.Database.ExecuteSqlCommand(command);
}
//foreign keys
foreach (var referencingType in referencingTypes)
{
var tableType = referencingType.PropertyType.GetGenericArguments()[0];
foreach (var propertyInfo in tableType.GetProperties())
{
if (propertyInfo.PropertyType == enumType)
{
var command2 = string.Format("ALTER TABLE {0} WITH CHECK ADD CONSTRAINT [FK_{0}_{1}] FOREIGN KEY({2}) REFERENCES {1}([Id])",
tableType.Name, enumProperty.Name, propertyInfo.Name
);
context.Database.ExecuteSqlCommand(command2);
}
}
}
}
private static List<PropertyInfo> GetReferencingTypes(PropertyInfo enumProperty, IEnumerable<PropertyInfo> contextProperties)
{
var result = new List<PropertyInfo>();
var enumType = enumProperty.PropertyType.GetGenericArguments()[0];
foreach (var contextProperty in contextProperties)
{
if (IsSubclassOfRawGeneric(typeof(DbSet<>), contextProperty.PropertyType))
{
var tableType = contextProperty.PropertyType.GetGenericArguments()[0];
foreach (var propertyInfo in tableType.GetProperties())
{
if (propertyInfo.PropertyType == enumType)
result.Add(contextProperty);
}
}
}
return result;
}
private static bool IsSubclassOfRawGeneric(Type generic, Type toCheck)
{
while (toCheck != null && toCheck != typeof(object))
{
var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
if (generic == cur)
{
return true;
}
toCheck = toCheck.BaseType;
}
return false;
}
public class EnumSet<T>
{
}
}
using the code:
public partial class UserEntities : DbContext{
public DbSet<User> User { get; set; }
public EntityHelper.EnumSet<UserType> UserType { get; set; }
public static void CreateDatabase(){
using (var db = new UserEntities()){
db.Database.CreateIfNotExists();
db.Database.Initialize(true);
EntityHelper.Seed(db);
}
}
}
I have created a package for it
https://www.nuget.org/packages/SSW.Data.EF.Enums/1.0.0
Use
EnumTableGenerator.Run("your object context", "assembly that contains enums");
"your object context" - is your EntityFramework DbContext
"assembly that contains enums" - an assembly that contains your enums
Call EnumTableGenerator.Run as part of your seed function. This will create tables in sql server for each Enum and populate it with correct data.
I have included this answer as I've made some additional changes from #HerrKater
I made a small addition to Herr Kater's Answer (also based on Tim Abell's comment). The update is to use a method to get the enum value from the DisplayName Attribute if exists else split the PascalCase enum value.
private static string GetDisplayValue(object value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
var descriptionAttributes = fieldInfo.GetCustomAttributes(
typeof(DisplayAttribute), false) as DisplayAttribute[];
if (descriptionAttributes == null) return string.Empty;
return (descriptionAttributes.Length > 0)
? descriptionAttributes[0].Name
: System.Text.RegularExpressions.Regex.Replace(value.ToString(), "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ");
}
Update Herr Katers example to call the method:
command = string.Format("INSERT INTO {0} VALUES({1},'{2}');", enumType.Name, (int)enumvalue,
GetDisplayValue(enumvalue));
Enum Example
public enum PaymentMethod
{
[Display(Name = "Credit Card")]
CreditCard = 1,
[Display(Name = "Direct Debit")]
DirectDebit = 2
}
you must customize your workflow of generation
1. Copy your default template of generation TablePerTypeStrategy
Location : \Microsoft Visual Studio 10.0\Common7\IDE\Extensions\Microsoft\Entity Framework Tools\DBGen.
2. Add custom activity who realize your need (Workflow Foundation)
3. Modify your section Database Generation Workflow in your project EF

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);
}

Update a foreign key in Entity Framework

I have created a partial class of an entity for getting a foreign key property on it.
public partial class Artikel
{
public int WarengruppenID
{
get
{
if (WarengruppeReference.EntityKey == null) return 0;
return (int)WarengruppeReference.EntityKey.EntityKeyValues[0].Value;
}
set
{
WarengruppeReference.EntityKey =
new EntityKey("ConsumerProtectionEntities.Warengruppe", "WarengruppenID", value);
}
}
}
Now I change the foreign key (property) but nothing happens?!
What do I have to do to update a foreign key for an entity?
I use EF 3.5 SP1 in ASP.NET MVC 2
EDIT
id = ArtikelID
updatedArtikel = Artikel properties
ArtikelWarengruppen = selected DropDownList value
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, Artikel updatedArtikel, string ArtikelWarengruppen)
{
try
{
int artikelWarengruppenID = int.Parse(ArtikelWarengruppen);
var originalArtikel = (from art in _db.Artikel.Include(typeof(Warengruppe).Name)
where art.ArtikelID == id
select art).FirstOrDefault();
_db.Attach(originalArtikel);
updatedArtikel.ArtikelID = id;
// Update FK
updatedArtikel.WarengruppenID = artikelWarengruppenID;
_db.ApplyPropertyChanges(typeof(Artikel).Name, updatedArtikel);
_db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
I've solved the problem.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, Artikel updatedArtikel, string ArtikelWarengruppen)
{
try
{
// update foreign key
int artikelWarengruppenID = int.Parse(ArtikelWarengruppen);
var originalArtikel = (from art in _db.Artikel.Include(typeof(Warengruppe).Name)
where art.ArtikelID == id
select art).FirstOrDefault();
originalArtikel.WarengruppenID = artikelWarengruppenID;
_db.Attach(originalArtikel);
// update properties
updatedArtikel.ArtikelID = id;
_db.ApplyPropertyChanges(typeof(Artikel).Name, updatedArtikel);
_db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
But is there a better way?
This should be the optimal way to achieve what you want:
int artikelWarengruppenID = int.Parse(ArtikelWarengruppen);
updatedArtikel.WarengruppenID = artikelWarengruppenID;
updatedArtikel.ArtikelID = id;
_db.Entry(updatedArtikel).State = EntityState.Modified;
_db.SaveChanges();
This does assume that your posted Artikel contains all the data that you want to keep in the entity.