Last week we reached our first milestone by accessing the database with our T4 template and create classes that match our tables (entities). We can now build on top of that knowledge and generate repositories to work with the data in our database.
Limitations and the *.ttincludes files
Be aware of the limitations of the code generators we create in this blog series. The reason for that is the limited space in a blog post and not due to T4.
You can find the code for the _DbAccess.ttinclude and the _Imports.ttincludein this post.
Generate the repositories and the interfaces
To make testing simpler, I like to create an interface for each repository. We will not unit test the repository itself, then the generated SQL code only shows us errors if we run it against a real database. But for all the code that uses our repositories, the interface will allow us to test the logic in isolation, and thus removes the need for a database full of test data.
For our code generator, we create a new template named RepositoryGeneratorFromDb.tt, add our *.ttincludes and loop twice through our list of tables. The first time to create the interface, the second time to generate the repository itself:
<#@templatedebug="false"hostspecific="true"language="C#"#><#@includefile="$(TargetDir)\_Imports.ttinclude"#><#@outputextension=".txt"#><#@includefile="$(TargetDir)\T4.FileManager.VisualStudio.ttinclude"#><#@includefile="$(TargetDir)\_DbAccess.ttinclude"#><#varschemaReader=newSchemaReader();varfileManager=newT4FileManager(this);foreach(varnameinschemaReader.ReadTables()){fileManager.StartNewFile("I"+name+"Repository.g.cs","","FromDb/Repositories");#>usingSystem;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.Repositories{publicpartialinterfaceI<#=name#>Repository{intCreate(<#=name#><#=name.ToLower()#>);<#=name#>FindById(intid);voidUpdate(<#=name#><#=name.ToLower()#>);voidDelete(<#=name#><#=name.ToLower()#>);}}<#fileManager.Process();};#><#foreach(varnameinschemaReader.ReadTables()){fileManager.StartNewFile(name+"Repository.g.cs","","FromDb/Repositories");#>usingSystem;usingSystem.Data;usingSystem.Linq;usingDapper;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.Repositories{publicpartialclass<#=name#>Repository:I<#=name#>Repository{privatereadonlyIDbConnectionconnection;public<#=name#>Repository(IDbConnectionconnection){this.connection=connection;}publicintCreate(<#=name#><#=name.ToLower()#>){varsql=@"<#= GenerateInsert(name, schemaReader.ReadColumns(name))#>";returnthis.connection.Query<int>(sql,<#=name.ToLower()#>).Single();}public<#=name#>FindById(intid){varsql=@"<#= GenerateSelect(name, schemaReader.ReadColumns(name))#>";returnthis.connection.Query<<#=name#>>(sql,new{Id=id}).FirstOrDefault();}publicvoidUpdate(<#=name#><#=name.ToLower()#>){varsql=@"<#= GenerateUpdate(name, schemaReader.ReadColumns(name))#>";this.connection.Execute(sql,<#=name.ToLower()#>);}publicvoidDelete(<#=name#><#=name.ToLower()#>){varsql="DELETE FROM <#= name #> WHERE Id = @Id";this.connection.Execute(sql,new{Id=<#=name.ToLower()#>.Id});}}}<#fileManager.Process();};#><#+publicstringGenerateInsert(stringtable,List<Column>columns){varsb=newStringBuilder();sb.AppendLine();sb.AppendLine("INSERT INTO dbo."+table+"(");varindex=0;foreach(varcolincolumns){if(col.Name=="Id"){continue;}if(index>0){sb.Append(",");}sb.AppendLine($"[{col.Name}]");index++;}sb.AppendLine(") VALUES (");index=0;foreach(varcolincolumns){if(col.Name=="Id"){continue;}if(index>0){sb.Append(",");}sb.AppendLine($"@{col.Name}");index++;}sb.AppendLine(")");sb.AppendLine("SELECT CAST(SCOPE_IDENTITY() as INT);");returnsb.ToString();}publicstringGenerateSelect(stringtable,List<Column>columns){varsb=newStringBuilder();sb.AppendLine();sb.AppendLine("SELECT");varindex=0;foreach(varcolincolumns){if(index>0){sb.Append(",");}sb.AppendLine($"[{col.Name}]");index++;}sb.AppendLine("FROM dbo."+table);sb.AppendLine("WHERE Id = @Id");returnsb.ToString();}publicstringGenerateUpdate(stringtable,List<Column>columns){varsb=newStringBuilder();sb.AppendLine();sb.AppendLine("UPDATE dbo."+table);sb.AppendLine("SET");varindex=0;foreach(varcolincolumns){if(col.Name=="Id"){continue;}if(index>0){sb.Append(",");}sb.AppendLine($"[{col.Name}] = @{col.Name}");index++;}sb.AppendLine("WHERE Id = @Id");returnsb.ToString();}#>
The template is a bit longer, so let us dive into the different parts:
The interface offers us the 4 methods Create(), FindById(), Update() and Delete() to cover the basic CRUD operations.
The IDbConnection in the constructor for the repository is our access to the database. The caller of our repository manages the lifecycle of the connection, and our repository does not need to know any credentials to access the database.
In each of the CRUD methods we create a parametrised SQL statement, hand it to Dapper and run it against our database.
Since the queries for SELECT, UPDATE and INSERT are longer, we create them in dedicated methods at the end of our generator.
Run the generator
When we run the custom tool for our generator, it will create us two interfaces and two classes in the FromDb\Repositories folder:
Our generated ICustomerRepository.g.cs contains these method definitions:
usingSystem;usingSystem.Data;usingSystem.Linq;usingDapper;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.Repositories{publicpartialclassCustomerRepository:ICustomerRepository{privatereadonlyIDbConnectionconnection;publicCustomerRepository(IDbConnectionconnection){this.connection=connection;}publicintCreate(Customercustomer){varsql=@"INSERT INTO dbo.Customer([LastName],[FirstName],[Email],[IsActive],[CreatedOn]) VALUES (@LastName,@FirstName,@Email,@IsActive,@CreatedOn)SELECT CAST(SCOPE_IDENTITY() as INT);";returnthis.connection.Query<int>(sql,customer).Single();}publicCustomerFindById(intid){varsql=@"SELECT[Id],[LastName],[FirstName],[Email],[IsActive],[CreatedOn]FROM dbo.CustomerWHERE Id = @Id";returnthis.connection.Query<Customer>(sql,new{Id=id}).FirstOrDefault();}publicvoidUpdate(Customercustomer){varsql=@"UPDATE dbo.CustomerSET[LastName] = @LastName,[FirstName] = @FirstName,[Email] = @Email,[IsActive] = @IsActive,[CreatedOn] = @CreatedOnWHERE Id = @Id";this.connection.Execute(sql,customer);}publicvoidDelete(Customercustomer){varsql="DELETE FROM Customer WHERE Id = @Id";this.connection.Execute(sql,new{Id=customer.Id});}}}
The IProductRepository.g.cs offers us this contract:
usingSystem;usingSystem.Data;usingSystem.Linq;usingDapper;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.Repositories{publicpartialclassProductRepository:IProductRepository{privatereadonlyIDbConnectionconnection;publicProductRepository(IDbConnectionconnection){this.connection=connection;}publicintCreate(Productproduct){varsql=@"INSERT INTO dbo.Product([Name],[Price],[IsActive],[CreatedOn],[Description]) VALUES (@Name,@Price,@IsActive,@CreatedOn,@Description)SELECT CAST(SCOPE_IDENTITY() as INT);";returnthis.connection.Query<int>(sql,product).Single();}publicProductFindById(intid){varsql=@"SELECT[Id],[Name],[Price],[IsActive],[CreatedOn],[Description]FROM dbo.ProductWHERE Id = @Id";returnthis.connection.Query<Product>(sql,new{Id=id}).FirstOrDefault();}publicvoidUpdate(Productproduct){varsql=@"UPDATE dbo.ProductSET[Name] = @Name,[Price] = @Price,[IsActive] = @IsActive,[CreatedOn] = @CreatedOn,[Description] = @DescriptionWHERE Id = @Id";this.connection.Execute(sql,product);}publicvoidDelete(Productproduct){varsql="DELETE FROM Product WHERE Id = @Id";this.connection.Execute(sql,new{Id=product.Id});}}}
Next
It was not a giant leap from creating the entities to generating the repositories. Nevertheless, seeing that we can use a single generator to create multiple different but related files (like classes and interfaces) is a crucial step to understand the potential of code generators. Next week we go yet another step further and generate integration tests for our repositories.