In previous versions of EF it was only possible to map Code First entities directly to tables. It is fairly easy to use SqlQuery to select data using stored procedures but there was no feasible way to use stored procedures for insert, update and delete.
Open Issues/Limitations
- Stored procedure mapping can currently only be done with the Fluent API. We will also be looking at an attribute based (Data Annotation) alternative.
- Code First Migrations currently doesn’t have native support for stored procedures. You can manually add a call to Sql(string) to create the procedures but we will also be adding a first class API and support for scaffolding these calls during Add-Migration.
Default Code First Conventions
This feature has no impact on the default Code First Conventions. We will always map directly to tables by default.
Basic Entity Mapping
You can opt into using stored procedures for insert, update and delete using the Fluent API. You cannot use a mixture of SPROCs and direct table access for a given entity. The insert, update and delete operations must all use direct table access or stored procedures.
modelBuilder .Entity<Blog>() .MapToStoredProcedures();
Doing this will cause Code First to use some conventions to build the expected shape of the stored procedures in the database.
- Three stored procedures named <type_name>_Insert, <type_name>_Update and <type_name>_Delete (e.g. Blog_Insert, Blog_Update and Blog_Delete).
- Parameter names correspond to the property names. If you use HasColumnName() or the Column attribute to rename the column for a given property then this name is used for parameters instead of the property name.
- The insert stored procedure will have a parameter for every property, except for those marked as store generated (identity or computed). The stored procedure should return a result set with a column for each store generated property.
- The update stored procedure will have a parameter for every property, except for those marked as computed. The stored procedure should return a result set with a column for each computed property.
- The delete stored procedure should have a parameter for the key value of the entity (or multiple parameters if the entity has a composite key).
Using the following class as an example:
publicclass Blog { publicint BlogId { get; set; } publicstring Name { get; set; } publicstring Url { get; set; } }
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
}
The default stored procedures would be:
CREATE PROCEDURE [dbo].[Blog_Insert]
@Name varchar(max),
@Url varchar(max)
AS
INSERT INTO [dbo].[Blogs] ([Name], [Url])
VALUES (@Name, @Url)
SELECT SCOPE_IDENTITY() AS BlogId
CREATE PROCEDURE [dbo].[Blog_Update]
@BlogId int,
@Name varchar(max),
@Url varchar(max)
AS
UPDATE [dbo].[Blogs]
SET [Name] = @Name, [Url] = @Url
WHERE BlogId = @BlogId;
CREATE PROCEDURE [dbo].[Blog_Delete]
@BlogId int
AS
DELETE FROM [dbo].[Blogs]
WHERE BlogId = @BlogId
Overriding the Defaults
You can override part or all of what was configured by default.
You can change the name of one or more stored procedures. This example renames the update stored procedure only.
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog")));
This example renames all three stored procedures.
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog"))
.Delete(d => d.HasName("delete_blog"))
.Insert(i => i.HasName("insert_blog")));
In these examples the calls are chained together, but you can also use lambda block syntax.
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
{
s.Update(u => u.HasName("modify_blog"));
s.Delete(d => d.HasName("delete_blog"));
s.Insert(i => i.HasName("insert_blog"));
});
This example renames the parameter for the BlogId property on the update stored procedure:
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.Parameter(b => b.BlogId, "blog_id")));
These calls are all chainable and composable. Here is an example that renames many stored procedures and parameters.
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog")
.Parameter(b => b.BlogId, "blog_id")
.Parameter(b => b.Name, "blog_name")
.Parameter(b => b.Url, "blog_url"))
.Delete(d => d.HasName("delete_blog")
.Parameter(b => b.BlogId, "blog_id"))
.Insert(i => i.HasName("insert_blog")
.Parameter(b => b.Name, "blog_name")
.Parameter(b => b.Url, "blog_url")));
Relationships Without a Foreign Key in the Class
When a foreign key property is included in the class definition, the corresponding parameter can be renamed in the same way as any other property. When a relationship exists without a foreign key property in the class, the default parameter name is <navigation_property_name>_<primary_key_name>.
For example, the following class definitions would result in a Blog_BlogId parameter being expected in the stored procedures to insert and update Posts.
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
Overriding the Defaults
You can change parameters for foreign keys that are not included in the class by supplying the path to the primary key property to the Parameter method.
modelBuilder
.Entity<Post>()
.MapToStoredProcedures(s =>
s.Insert(i => i.Parameter(p => p.Blog.BlogId, "blog_id")));
If you don’t have a navigation property on the dependent entity (i.e no Post.Blog property) then you can use the Association method to identify the other end of the relationship and then configure the parameters that correspond to each of the key property(s).
modelBuilder
.Entity<Post>()
.MapToStoredProcedures(s =>
s.Insert(i => i.Association<Blog>(
b => b.Posts,
c => c.Parameter(b => b.BlogId, "blog_id"))));
Concurrency Tokens
Insert and update stored procedures may also need to deal with concurrency:
- If the entity contains any concurrency tokens, the stored procedure should have an output parameter named RowsAffected that returns the number of rows updated/deleted.
- For each concurrency token there will be a parameter named <property_name>_Original (i.e. Timestamp_Original). This will be passed the original value of this property – the value when queried from the database.
- Concurrency tokens that are computed by the database – such as timestamps – will only have an original value parameter.
- Non-computed properties that are set as concurrency tokens will also have a parameter for the new value in the update procedure. This uses the naming conventions already discussed for new values. An example of such a token would be using SocialSecurityNumber as a concurrency token, the new value is required because this can be updated to a new value by your code (unlike a Timestamp token which is only updated by the database).
This is an example class and update stored procedure with a timestamp concurrency token:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
CREATE PROCEDURE [dbo].[Person_Update]
@PersonId int,
@Name varchar(max),
@Timestamp_Original rowversion,
@RowsAffected int OUTPUT
AS
UPDATE [dbo].[People]
SET [Name] = @Name
WHERE PersonId = @PersonId AND Timestamp = @Timestamp_Original
SET @RowsAffected = @@RowCount
Here is an example class and update stored procedure with non-computed concurrency token:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
[ConcurrencyCheck]
public string SocialSecurityNumber { get; set; }
}
CREATE PROCEDURE [dbo].[Person_Update]
@PersonId int,
@Name varchar(max),
@SocialSecurityNumber varchar(max),
@SocialSecurityNumber_Original varchar(max),
@RowsAffected int OUTPUT
AS
UPDATE [dbo].[People]
SET [Name] = @Name, [SocialSecurityNumber] = @SocialSecurityNumber
WHERE PersonId = @PersonId AND SocialSecurityNumber = @SocialSecurityNumber_Original;
SET @RowsAffected = @@RowCount
Overriding the Defaults
If you are using concurrency tokens you can change the name of the rows affected parameter:
modelBuilder
.Entity<Person>()
.MapToStoredProcedures(s =>
s.Update(u => u.RowsAffectedParameter("rows_affected")));
For database computed concurrency tokens – where only the original value is passed – you can just use the standard parameter renaming mechanism to rename the parameter for the original value.
modelBuilder
.Entity<Person>()
.MapToStoredProcedures(s =>
s.Update(u => u.Parameter(p => p.Timestamp, "person_timestamp")));
For non-computed concurrency tokens – where both the original and new value are passed – you can use an overload of Parameter that allows you to supply a name for each parameter.
modelBuilder
.Entity<Person>()
.MapToStoredProcedures(s =>
s.Update(u => u.Parameter(p => p.SocialSecurityNumber, "person_ssn", "person_original_ssn")));
Many to Many Relationships
We’ll use the following classes as an example in this section.
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Tag> Tags { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string TagName { get; set; }
public List<Post> Posts { get; set; }
}
Many to many relationships can be mapped to stored procedures with the following syntax.
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.MapToStoredProcedures();
If no other configuration is supplied then the following stored procedure shape is used by default.
- Two stored procedures named <type_one>_<type_two>_Insert and <type_one>_<type_two>_Delete.
Open Issue: In current builds the second type name is pluralized, we should consider disabling this (i.e. Post_Tags_Insert and Post_Tags_Delete).
Open Issue: We need to determine clear guidance around which type will make up the first part of the name and which will be second. - The parameters will be the key value(s) for each type. The name of each parameter being <type_name>_<property_name> (i.e. Post_PostId and Tag_TagId).
Here are example insert and update stored procedures.
CREATE PROCEDURE [dbo].[Post_Tags_Insert]
@Post_PostId int,
@Tag_TagId int
AS
INSERT INTO [dbo].[Post_Tags] (Post_PostId, Tag_TagId)
VALUES (@Post_PostId, @Tag_TagId)
CREATE PROCEDURE [dbo].[Post_Tags_Delete]
@Post_PostId int,
@Tag_TagId int
AS
DELETE FROM [dbo].[Post_Tags]
WHERE Post_PostId = @Post_PostId AND Tag_TagId = @Tag_TagId
Overriding the Defaults
The procedure and parameter names can be configured in a similar way to entity stored procedures.
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.MapToStoredProcedures(s =>
s.Insert(i => i.HasName("add_post_tag")
.LeftKeyParameter(p => p.PostId, "post_id")
.RightKeyParameter(t => t.TagId, "tag_id"))
.Delete(d => d.HasName("remove_post_tag")
.LeftKeyParameter(p => p.PostId, "post_id")
.RightKeyParameter(t => t.TagId, "tag_id")));