Sometimes, we need a stored procedure to not only return data, but also to return actual and change-tracked entities. To make certain that happens, we need to do just a little bit more.
We will be using the NuGet
Package Manager to install the Entity Framework 4.1 assemblies.
The package installer can be found at http://nuget.org.
We will also be using a database for connecting to the data and updating it.
Open the Improving Entities with Stored Procedures solution in the included source code examples.
TransactionTests
to the test project. We make a test that connects to the database, adds an object, and then retrieves an entity with a stored procedure, by using the following code:using DataAccess; using DataAccess.Database; using DataAccess.Queries; using Microsoft.VisualStudio.TestTools.UnitTesting; using Test.Properties; namespace Test { [TestClass] public class StoredProcedureTests { [TestMethod] public void ShouldAllowReturnOfATrackedEntityFromStoredProcedure() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); var repo = new BlogRepository(context); //Act var blog = repo.GetBlog(1); //Assert Assert.AreEqual(1, blog.Id); } } }
DataAccess
project in the Database
folder with the following code to set up the data:using System; using System.Data.Entity; using BusinessLogic; namespace DataAccess.Database { public class Initializer : DropCreateDatabaseAlways<BlogContext> { public Initializer() { } protected override void Seed(BlogContext context) { context.Database.ExecuteSqlCommand(StoredProcedureDefinitions.GetBlogDefinition); context.Set<Blog>().Add(new Blog() { CreationDate = DateTime.Now, ShortDescription = "Testing", Title = "Test Blog" }); context.SaveChanges(); } } }
DataAccess
project, we add a new C# class named StoredProcedureDefinitions
with the following code, so we can recreate our stored procedures with each database creation:namespace DataAccess.Database { public static class StoredProcedureDefinitions { public static string GetBlogDefinition = @"CREATE PROCEDURE [dbo].[GetBlog] @BlogId int AS BEGIN SET NOCOUNT ON; SELECT b.BlogId as Id, b.Description as ShortDescription, b.Title, b.Rating, b.CreationDate FROM dbo.Blogs bEND "; } }
BusinessLogic
project, add a new C# class named Blog
with the following code:using System; namespace BusinessLogic { public class Blog { public int Id { get; set; } public DateTime CreationDate { get; set; } public string ShortDescription { get; set; } public string Title { get; set; } public double Rating { get; set; } } }
Mapping
folder to the DataAccess
project and add a BlogMapping
class to the folder with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogMapping : EntityTypeConfiguration<Blog> { public BlogMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("BlogId"); this.Property(x => x.Title).IsRequired().HasMaxLength(250); this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired(); this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description"); } } }
BlogContext
class to contain the new mappings, and a DbSet
property for Blog
with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.Mappings; namespace DataAccess { public class BlogContext : DbContext, IUnitOfWork { public BlogContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); base.OnModelCreating(modelBuilder); } public IQueryable<T> Find<T>() where T : class { return this.Set<T>(); } public void Refresh() { this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload()); } public void Commit() { this.SaveChanges(); } } }
DataAccess
project, we add a new folder named Queries
with a new C# class named StoredProcedures
with the following code:using System.Data.Entity; using System.Data.SqlClient; using System.Linq; using BusinessLogic; namespace DataAccess.Queries { public static class StoredProcedures { public static Blog GetBlog(this IBlogRepository repository, int blogId) { var items = repository.UnitOfWork.Context.Database.SqlQuery<Blog>(@"GetBlog @BlogId", new SqlParameter("BlogId",blogId) ); var blog = items.FirstOrDefault(); if (blog != null && repository.UnitOfWork.Context.Entry(blog) == null) repository.UnitOfWork.Context.Set<Blog>().Attach(blog); return blog; } } }
We start, as always, with a test that calls our stored procedure and returns a tracked entity, so we can ensure that we have accomplished the goal that we set forth.
We also make sure that the stored procedure is returning columns that match the names of the properties that we have in our entity. This is because there is no mapping between the stored procedure's return operation and the entity instantiation. This lack of mapping causes the framework to use reflection to try and directly correlate columns to properties, and we will need to make sure that correlation can happen seamlessly.
The stored procedure is called from the repository, but notice that it drills down to fire on the Database
object on DbContext
. This allows us to map anything from a simple integer return to an object, which are not tracked by default. So, we have to manually attach the object to the context. This will allow us to leverage stored procedures to load objects, but still have generated change statements and tracking on the object.
Loading the entity from a stored procedure is complex in the conventions that it relies on, and some runtime errors can occur if we do not prepare against them.
If a column name doesn't have a matching property, or vice versa, it will cause a runtime error. This is part of the magic making this happen. We have to make sure that our columns from the stored procedure are one-to-one with our objects. If this is not the case, then we will have to define a Data Transfer Object (DTO) that holds the result and then is parsed into the objects and attached.