How to change entry values for DbContext.SaveChanges() - entity-framework

We are trying to implement a multi tenant architecture in our Web API based application. We are using RLS in SQL Server, and Subscription_Id is, what is given to each subscriber. We have set the default value for Subscription_Id in SQL Server, so while I am calling db.SaveChanges(), I just want to ignore the Subscription_Id going to the SQL Server from the API.
I tried setting the value of Subscription_Id in the SaveChanges() override method but got stuck here.
public override int SaveChanges()
{
var objectType = selectedEntity.CurrentValues.ToObject();
Guid value = new Guid("54E720FC-616B-44C6-8485-5F2185FD7B4C");
PropertyInfo propertyInfo =
objectType.GetType().GetProperty("Subscription_Id");
ChangeTracker.Entries().FirstOrDefault()
.CurrentValues.ToObject().GetType()
.GetProperty("Subscription_Id")
.SetValue(objectType, Convert.ChangeType(value, propertyInfo.PropertyType), null);
return base.SaveChanges();
}

My advice is that you shouldn't modify your SaveChanges() code for this.
A recommended way of using RLS is making the TenantId columns transparent to your EF model and your code, so you don't need to define Tenant ID or navigation properties in your entities. This way you don't need to change your SaveChanges() code, or to explicitly manage and set Subscription_Id values anywhere in your code other that when opening the DB connection.
What you need to do is manually setting a default value constraint in the Subscription_Id columns in your database, with a default value based on the current session Subscription_Id parameter. The value will be set when inserting the records, and implicitly used to filter any subsequent queries and commands at database level.
In case of a new column:
ALTER TABLE SomeEntityTable ADD Subscription_Id nvarchar(128)
DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))
In case of an existing column:
ALTER TABLE SomeEntityTable
ADD DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128)
FOR Subscription_Id
If the column had a previous different DEFAULT value it would be good to also delete its associated obsolete DEFAULT constraint. More info about updating default values in existing columns can be found here.
These columns should not be included in your model. You should not have properties for them in your entity classes. If you are using Database First you should make sure you exclude/ignore these columns when updating your model from your database.
How to do this if you are using EF Code First: you can manually include AlterColumn (or CreateColumn) instructions in a code migration after you generate it with Add-Migration. Do it for every entity table:
public override void Up()
{
AlterColumn("dbo.SomeEntityTable", "Subscription_Id",
c => c.String(
nullable: false,
maxLength: 128,
defaultValueSql: "CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))"));
}
(It would be good to add also a Down() method removing the column.)
Warning: Be careful when running this migration if you already have existing records in the tables with an empty Subscription_Id column value (or if you are adding a new Subscription_Id column to a table that already have records). The empty column will be filled with the value of the Subscription_Id in the connection that is executing the migration, which probably will be wrong (you probably don't want all the existing records to be associated to that specific subscription). In that case you may want to include explicit UPDATE instructions with the right Subscription_Id values in your Up() method, with the Sql() method. Something like this:
Sql("UPDATE SomeEntitiesTable SET Subscription_Id= '19bc9b0d-28dd-4510-bd5e-d6b6d445f511' WHERE Id IN (1, 2, 5)");
With Code First you should also remove the Subscription_Id properties from your model classes. If you can't, at least add explicit Ignore() instructions in your configuration code for the Subscription_Id columns, you don't want them in your EF mappings.
Note: I'm assuming here that you created a RLS policy in your DB that uses UserId parameter in SESSION_CONTEXT, and that your application code is setting that value when opening the DB connection, via a DbConnectionInterceptor or something similar.
This page contains more info.

Related

How to run a subquery when inserting into a table in EFCore

I have an orderid integer column in a Postgres table called Orders and it's not the primary key. And there's a logic to automatically increment it considering the max value of the orderid and adding 1 to it.
One solution to this is creating a function in the database layer and set it as the default value to this column. But since that couples us to the database layer, we are thinking of something that's related to EF. We can't pass a SQL as the default value to orderid because postgres gives an error. We can also make it an auto-increment value but we don't wanna do that because the logic could change.
What we want is a way to run this subquery and automatically generate the value to orderid.
I presume what you want to achieve here is to maintain a separate non-primary key, auto-incremented unique id which is number. You can this by using a sequence.
1) Create the sequence via fluent API
2) Set the column to use the next value from the sequence
3) Make sure to create and run migrations before testing.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers")
.StartsAt(1000)
.IncrementsBy(1);
modelBuilder.Entity<Order>()
.Property(o => o.OrderNo)
.HasDefaultValueSql("nextval('\"OrderNumbers\"')");
}

Inserting only assigned properties with EF6

Is it possible to get SaveChanges to produce an INSERT statement containing only columns that have assigned values for the associated properties.
For example:
administrator.Login = login
administrator.PasswordSalt = salt
administrator.Password = hashed
administrator.CreatedBy = "xxx"
db.Administrators.Add(administrator)
db.SaveChanges()
Should only have four fields in the INSERT statement. Right now, SaveChanges is adding all the fields, setting the unassigned properties to have a value of NULL, which prevents any default value being used. Example: CreatedDate has a default of getdate().
There are at least two different ways to exclude property from INSERT/UPDATE statement in compile-time:
1) EDMX: go to entity and set StoreGeneratedPattern option for a property to Computed or Identity.
If CodeFirst is used: try [DatabaseGenerated(DatabaseGeneratedOption.Computed)] attribite instead.
2) Add AFTER INSERT trigger to SQL table to update an inserted record with default values. How to update inserted field in trigger
I'm not sure it is possible to do in run-time in pure EF.

When I add a column in the database, under what conditions do I need to update my EDMX?

When I add a column in the database, under what conditions do I need to update my EDMX?
To elaborate:
I know if I add a non-nullable field, I need to update the model if I want to write to the database. What if just I want to read?
What if it's a nullable field? Can I both read and write?
What if I were to change the primary key to the new column but the edmx still has the old column as primary?
1) If you want to port an old database, you need to make sure that every table in your database must have a primary key. This is the only requirement for creating the EDMX.
2) If you've added a column in a table at database side, and have not updated edmx, then you'll simply not be able to use that column though EntityFramework.
If you create a non nullable column with no default value, the insert operation will fail with exception "Cannot insert null into column , statement terminated". And the you'll not be able to read values of that column using entityframeowrk, unless you update the edmx.
3) If you've changed the primary key of any table at database side, and if the edmx is not aware of that, your application might create a runtime exception when performing operations with that table.
Remember, Entity Framework creates SQL queries depending upon its knowledge of database(which is defined in EDMX). So if EDMX is incorrect, the resulting SQL queries so generated might lead to problems at runtime.

EF 6 Migration: How to execute sql SELECT?

In our project we have necessity of adding some predefined data to DB. I think the best way and concept is using for that EF Migrations (not Seed method).
But we have a big troubles with adding related data to DB:
For Example:
Suppose we have 2 tables:
Users:
Id (PK auto increment)
Name
RoleId
Roles:
Id (PK auto increment)
Name
Let's suppose that we need to add User(Name = 'John', RoleId = (Id of role that name is 'Admin')).
How can we do it? It would be great if we find a solution that allows us to execute pure SQL SELECT script which not uses Entities of Code First because they can be modified or removed.
For DELETE, INSERT, UPDATE can be used Sql(...) method but what about SELECT?
You cannot have a context into the migration.
Logically first are ran the migrations to Update the DB Schema, then you can have a context to work with the data via it. If your DB does not match the model, or even the table is still not there, you cannot use it in EF.
I had to look into the EF code (and also because was curious). Practically the Sql() method in the DbMigration class in several levels below just adds the SQL string into a list of queries that should be executed into the transaction and moves on. It does not executes it when it is called. So in short EF just fills in a list of codes lines that should be executed in the end at once. And it seems correct if you try to walk in all paths of what you can do with the C# code in the migration code.
The question is quite good actually, unfortunately still I didn't found any better solution rather than using pure ADO.
Another option is to generate more custom SQL queries, and use T-SQL more widely.
For your case as you want to insert the user and set the groupId looking by the name, it can be used with inner select:
INSERT INTO Users (Name, GroupId)
VALUES ('John', RoleId = (SELECT Id FROM Roles WHERE Name = 'Admin')).
For my issue, I had to a bit do more sophisticated execution - the following does the same as the AddOrUpdate method of the DbSet, using the IF statement:
IF EXISTS (SELECT * FROM Table1 WHERE Column1='SomeValue')
UPDATE Table1 SET (...) WHERE Column1='SomeValue'
ELSE
INSERT INTO Table1 VALUES (...)
I found it here: http://blogs.msdn.com/b/miah/archive/2008/02/17/sql-if-exists-update-else-insert.aspx
I'm using good old LINQ for this:
public override void Up()
{
using (var dc = new DbContext("your connection string or name"))
{
var ids = dc.Database.SqlQuery<int>("SELECT id FROM sometable WHERE somefield={0}", 42).ToArray();
...
}
}
Using LINQ is better, even for usual migrations, because, there is a bug in DbMigration.Sql method, it ignores arguments: How to pass parameters to DbMigration.Sql() Method

Adding new parent table with Entity Framework Migrations

I'm trying to use migrations to add a parent table to an existing child table. For eg. I currently have User table, now I want to add a Department table that has a 1 to many relationship: Department has many User.
My questions, in automatic update, can I somehow seed the parent table before adding the FK so I can update all the children to this default seeded Department? If automatic update cannot do this, how do I accomplish this in code?
What I currently did: Made the FK nullable, created the Parent and seeded it, then update all child User FK to the parent. But now I cant change the FK not nullable because throws this error: Automatic migration was not applied because it would result in data loss.
Switching from nullable to non-nullable is considered data loss because after the migration, there is no way to tell which rows (if any) were null. If you are ok with this, you can call Update-Database with the -Force flag.
Another option would be to add a code-based migration that would:
Add the Departments table
Insert a default department
Add the required FK column to User with a default value of the inserted department