A Lightweight Library to generate quickly Databases from entities. Concepts: - 0 Migration. No "MigrationsHistory" table. Avoids a directory with hundred of migrations. - Scaffold command to generate/synchronize entities, DbModel and Snapshot with Database - Generate script or update database command to update the database Db Supported: SQL Server, SQLite, MySQL|MariaDB, PostgreSQL
License
—
Deps
8
Install Size
—
Vulns
✓ 0
Published
Jul 30, 2022
$ dotnet add package CodeFirstDbGenerator.SqlServerMotivation: allow to create migrations and update database without installing Entity Framework, for libraries like Dapper.
Db Supported:
Languages supported:
install-package CodeFirstDbGenerator.SqlServer
CodeFirstDbGenerator is installed as dependency
install-package CodeFirstDbGenerator
And CodeFirstDbGenerator.Tools (Visual Studio Package Manager Console)
install-package CodeFirstDbGenerator.Tools
.. or dotnet-cf (dotnet tool)
dotnet tool install --global dotnet-cf
Tip: To uninstall a previous version of the tool and list the tools
dotnet tool uninstall -g dotnet-cf
dotnet tool list -g
Or add package references to project
<PackageReference Include="CodeFirstDbGenerator" Version="5.2.5" />
<PackageReference Include="CodeFirstDbGenerator.SqlServer" Version="5.2.5" />
<PackageReference Include="CodeFirstDbGenerator.Tools" Version="5.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Recommendation : Create a class Library .NET 5 or .NET 6 for Migrations
Generate entities from existing/ updated database with Entity Generator (dotnet tool)
With On Configuring
public class ApplicationDbModel : DbModel
{
public override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>();
modelBuilder.Entity<Author>();
modelBuilder.Entity<Post>();
}
public override void OnConfiguring(DbModelOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Sample;Trusted_Connection=True;MultipleActiveResultSets=true");
}
}With ServiceCollection Extensions
public class ApplicationDbModel : DbModel
{
public ApplicationDbModel(DbModelOptions<ApplicationDbModel> options)
: base(options)
{ }
public override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>();
modelBuilder.Entity<Author>();
modelBuilder.Entity<Post>();
}
}CF autodetects data annotations, primary keys, foreign keys and relations by default. Also Fluent Api can be used.
Data Annotations Attributes
// [Table("tbl_Companies")] allows to define the name of the table
public class Company
{
// [Key] or auto discovered if property name equals ClassName + Id (One Key attribute per class)
public int CompanyId { get; set; } // key
[StringLength(100)]
public string Name { get; set; } //required
[Required]
//[Column("MyPostalCode")] allows to rename the column
public string PostalCode { get; set; } // required with data annotations
[Column(TypeName ="ntext")] // allows to change the type
public string? Address { get; set; }
[MaxLength(50)] // or [StringLength(50)]
public string? City { get; set; } // not required
[Timestamp]
public byte[] RowVersion { get; set; }
public List<Employee> Employees { get; set; } = new();
}
public class Employee
{
[Key] // or not identity [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int EmployeeId { get; set; } // recommendation: make Key unique, dont use names like "Id" for all primary keys
public string FirstName { get; set; }
public string LastName { get; set; }
public int CompanyId { get; set; } // foreign key auto detected
public Company Company { get; set; }
[NotMapped] // ignored
public string FullName
{
get { return $"{FirstName} {LastName}"; }
}
}Auto detected if property name equals "Class name + Id"
public class Author
{
public int AuthorId { get; set; }
}Foreign key. Auto detected if property equals "Principal" class name + Id
public class Author
{
public int AuthorId { get; set; }
}
public class Post
{
public int PostId { get; set; }
public int AuthorId { get; set; }
}Else use Foreign key Attribute
public class Author
{
public int AuthorId { get; set; }
}
public class Post
{
public int PostId { get; set; }
[ForeignKey(nameof(TheAuthor))]
public int TheAuthorId { get; set; }
public Author TheAuthor { get; set; }
}Or
public class Post
{
public int PostId { get; set; }
public int TheAuthorId { get; set; }
[ForeignKey(nameof(TheAuthorId))]
public Author TheAuthor { get; set; }
}Many to Many relations : a table "PostCategory" is created
public class Post
{
public int PostId { get; set; }
// etc.
public List<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public List<Post> Posts { get; set; }
}Fluent Api
Allows to define composite key for example
modelBuilder.Entity<Category>().HasKey(x => new { x.CategoryId1, x.CategoryId2 });Methods
Samples :
modelBuilder.Entity<Author>().ToTable("MyAuthors");
modelBuilder.Entity<Category>().Property(x => x.Name).HasColumnName("CategoryName");
modelBuilder.Entity<Post>().Property(x => x.Content).HasColumnType("text");
modelBuilder.Entity<Post>().HasForeignKey(x => x.TheAuthorId, "Authors", "AuthorId", onDelete: ReferentialStrategy.Cascade);
modelBuilder.Entity<Post>().HasIndex(x => x.Title);OnConfiguring DbModel methodServiceCollection. Example for SqlServer
services.AddDbModel<ApplicationDbModel>(options =>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));Add a constructor with DbModelOptions like this
public class ApplicationDbModel : DbModel
{
public ApplicationDbModel(DbModelOptions<ApplicationDbModel> options)
: base(options)
{ }
}Example for Sqlite
services.AddDbModel<SqliteDbModel>(options => options.UseSqlite(@"Data Source=C:\Db\Sample.db; Cache = Shared")); // dont use relative pathOnConfiguring
public class ApplicationDbModel : DbModel
{
public override void OnConfiguring(DbModelOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Sample;Trusted_Connection=True;MultipleActiveResultSets=true");
}
// etc.
}3 choices :
Package Manager Console . First select the project with the DbModel in the Package Manager Console then
Add-Migration InitialCreate
| Option | Description |
|---|---|
| -Project | The name of the project |
| -StartupProject | The name of the Startup project |
| -DbModel | The name of the DbModel to use if the project has more than one DbModel |
Add-Migration InitialCreate -StartupProject SampleWeb -DbModel ApplicationDbModel
A migration (20220119222431_InitialCreate for example) and a model snapshot (ApplicationDbModelSnapshot for example) are generated in a directory Migrations.
Note: It's possible to remove last migrations and ModelSnapshot.
... Or dotnet cf tool
dotnet cf migrations add InitialCreate -a path/to/assembly.dll
It's possible to generate a migration and define a SQL Query for example
public partial class AddGetCompanySP : Migration
{
protected override void Down(MigrationBuilder migrationBuilder)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE PROC usp_GetCompany
@CompanyId int
AS
BEGIN
SELECT *
FROM Companies
WHERE CompanyId = @CompanyId
END
GO
");
}
}Package Manager Console
Update-Database
| Option | Description |
|---|---|
| -Project | The name of the project |
| -StartupProject | The name of the Startup project |
| -Migration | The migration id. Used to Rollback |
| -DbModel | The name of the DbModel to use if the project has more than one DbModel |
| -ConnectionString | To define another connection string to use |
| -Verbose | To show SQL queries |
Update-Database -DbModel ApplicationDbModel -Verbose
Sample to rollback to a migration
Update-Database -Migration 20220119222431_InitialCreate
... Or dotnet cf tool
dotnet cf database update -a path/to/assembly.dll
With a library that contains migrations and a startup assembly (Application Web Asp.Net Core) for example :
dotnet cf database update -a path/to/assembly.dll -s path/to/startup-assembly.dll
Tip: create a bash file to execute multiple commands. Example test.sh and use GIT Bash 'sh test.sh'
echo '> Sample1'
dotnet cf database update -a "C:\Samples\Sample1\bin\Debug\net5.0\Sample1.dll"
echo '> Sample2'
dotnet cf database update -a "C:\Samples\Sample2\bin\Debug\net6.0\Sample2.dll"
By default a table "__CFMigrationsHistory" is created.
Sample to use JsonMigrationsHistory:
services.AddDbModel<ApplicationDbModel>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
.WithDropDatabase()
.UseHistory(new JsonMigrationsHistory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CodeFirstDbGenerator\\SampleDb__MigrationsHistory.json")));
});It's possible to create a custom MigrationsHistory. Just implement IMigrationsHistory and change the history.
Install packages
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
</ItemGroup>
Add a CreateHostBuilder function to the entry point of the application
using CodeFirstDbGenerator;
using CodeFirstDbGenerator.SqlServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Sample.Data;
using System;
using System.Windows;
namespace WpfDi
{
public partial class App : Application
{
private IHost host;
public App()
{
host = CreateHostBuilder().Build();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
host.Start();
var shell = host.Services.GetRequiredService<MainWindow>();
shell.Show();
}
public static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddScoped<MainWindow>();
services.AddDbModel<MyDbModel>(options => options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=TestWpfDi;Trusted_Connection=True;"));
});
}
}Sample. Use Faker.Net to create fake data and SDE System.Data.Extensions to execute commands.
Create a DbInitializer
public interface IDbInitializer
{
void Initialize();
}
public class DbInitializer : IDbInitializer
{
private string _connectionString;
public DbInitializer(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public void Initialize()
{
try
{
using (var scope = new TransactionScope())
{
using (var connection = new SqlConnection(_connectionString))
{
if ((int)connection.ExecuteScalar("SELECT COUNT(*) FROM [Companies]") == 0)
{
// companies
var newIds = new List<int>();
for (int i = 0; i < Faker.RandomNumber.Next(5, 10); i++)
newIds.Add(AddCompany(connection, new Company
{
Name = Faker.Company.Name(),
Address = Faker.Address.StreetAddress(),
City = Faker.Address.City(),
PostalCode = Faker.Address.ZipCode()
}));
// employees
foreach (var id in newIds)
{
for (int x = 0; x < Faker.RandomNumber.Next(2, 20); x++)
{
AddEmployee(connection, new Employee
{
Name = Faker.Name.FullName(),
Email = Faker.Internet.Email(),
Title = "Employee",
CompanyId = id
});
}
}
}
scope.Complete();
}
}
}
catch (Exception ex)
{ }
}
private static void AddEmployee(SqlConnection connection, Employee employee)
{
connection.ExecuteNonQuery("INSERT INTO [Employees](Name,Email,Phone,Title,CompanyId)VALUES(@Name,@Email,@Phone,@Title,@CompanyId)", employee);
}
private static int AddCompany(SqlConnection connection, Company company)
{
return (int)connection.ExecuteScalar("INSERT INTO [Companies](Name,Address,City,State,PostalCode) output inserted.CompanyId VALUES(@Name,@Address,@City,@State,@PostalCode)", company);
}
}Call the db initializer on database update complete.
services.AddDbModel<ApplicationDbModel>(options => options
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
.WithDropDatabase()
.WithDefaultDeleteStrategy(ReferentialStrategy.Cascade)
.WithOnDatabaseUpdated(() => new DbInitializer(Configuration).Initialize())); // <=Create a libray for a database is easy.