Job Seek Development Journal - 2
Last week, I tried to fit this small job seek in DDD pattern. And this week, I am going to learn how to implement it.
Project Setup
// create DotNetJobSeek solution
dotnet new sln -o DotNetJobSeek
// src: source codes folder
// test: test codes folder
// documents: documents foler
mkdir src test documents
// README file
touch README.md
// add Domain projects and corresponding test
dotnet new console -o src/DotNetJobSeek.Domain
dotnet new xunit -o test/DotNetJobSeek.Domain.Tests
├── DotNetJobSeek.sln
├── DotNetJobSeek.userprefs
├── README.md
├── documents
│ ├── Job\ seek.xmind
│ ├── job\ seek.vpp
├── src
│ ├── DotNetJobSeek.Domain
└── test
└── DotNetJobSeek.Domain.Test
Add Constrains
The first need to be done is to add constrains for assets in domain to distinguish Entity, ValueObject, AggregateRoot and Repository.
// IEntity interface contains a object key
namespace DotNetJobSeek.Domain
{
public interface IEntity
{
// object key for string / int / Guid
object Key { get;}
}
}
// IAggretage interface
namespace DotNetJobSeek.Domain
{
public interface IAggregateRoot : IEntity
{
}
}
// IRepository
namespace DotNetJobSeek.Domain
{
public interface IRepository<T> where T : IAggregateRoot
{
}
}
ValueObjects
Any DAO without constrains in this project is valueObjects.
// Keyword.cs
namespace DotNetJobSeek.Domain
{
public class Keyword
{
public int Id { get; set; }
public string Name { get; set; }
[NotMapped]
public int Weight { get; set; }
public virtual ICollection<TagKeyword> TagKeywords { get; } = new List<TagKeyword>();
// [NotMapped]
// public IEnumerable<Tag> Tags => TagKeywords.Select(e => e.Tag);
}
}
// Tag.cs
namespace DotNetJobSeek.Domain
{
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
// Version for future AI usage
public int Version { get; set; }
public virtual ICollection<TagKeyword> TagKeywords { get; } = new List<TagKeyword>();
// [NotMapped]
// public IEnumerable<Keyword> Keywords => TagKeywords.Select(e => new Keyword {Id = e.Keyword.Id, Name = e.Keyword.Name, Weight = e.Weight });
}
}
// TagKeyword.cs
namespace DotNetJobSeek.Domain
{
public class TagKeyword
{
public int TagId { get; set; }
public Tag Tag { get; set; }
public int KeywordId { get; set; }
public Keyword Keyword { get; set; }
// weight for matching algorithm
public int Weight { get; set; }
}
}
At this stage, I need to test many to many relations, Version / Weight fields for matching algorithm. Thus I need to setup the basic EF Infrastructure services and unit test.
EF In Infrastructure
EF Dbcontext set up
# create EF project
dotnet new console -o src/Infrastructure/DotNetJobSeek.Infrastructure.E
cd src/Infrastructure/DotNetJobSeek.Infrastructure.EF/
# inside Domain add database connect for sqlserver or postgresql
# sqlite is essential here for unit test
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
# postgresql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# sqlserver
# dotnet add package Microsoft.EntityFrameworkCore.SqlServer #
# add
dotnet add package Microsoft.EntityFrameworkCore.Design
Then add
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
to DotNetJobSeek.Infrastructure.EF.csproj
Finally
dotnet restore
dotnet add reference ../../DotNetJobSeek.Domain/DotNetJobSeek.Domain.csproj
Define Mappers
// KeywordMapper
namespace DotNetJobSeek.Infrastructure.EF
{
public class KeywordMapper : IEntityTypeConfiguration<Keyword>
{
public void Configure(EntityTypeBuilder<Keyword> builder)
{
builder.HasIndex(k => k.Name)
.IsUnique();
}
}
}
// TagMapper
namespace DotNetJobSeek.Infrastructure.EF
{
public class TagMapper : IEntityTypeConfiguration<Tag>
{
public void Configure(EntityTypeBuilder<Tag> builder)
{
builder.HasIndex(t => t.Name)
.IsUnique();
builder.HasIndex(t => t.Version);
builder.Property(t => t.Version).HasDefaultValue(0);
}
}
}
// TagKeyword Mapper
namespace DotNetJobSeek.Infrastructure.EF
{
public class TagKeywordMapper : IEntityTypeConfiguration<TagKeyword>
{
public void Configure(EntityTypeBuilder<TagKeyword> builder)
{
builder.HasKey(t => new { t.TagId, t.KeywordId });
builder.HasOne(tk => tk.Tag).WithMany(t => t.TagKeywords).HasForeignKey(tk => tk.TagId);
builder.HasOne(tk => tk.Keyword).WithMany(k => k.TagKeywords).HasForeignKey(tk => tk.KeywordId);
builder.HasIndex(t => t.Weight);
builder.Property(t => t.Weight).HasDefaultValue(0);
}
}
}
Configure EFContext
- EFContext could load dynamic connection string for test and different data persistance purposes.
- Dynamic loading mappers
public class EFContext : DbContext
{
public EFContext()
{ }
public EFContext(DbContextOptions<EFContext> options)
: base(options)
{ }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// set default connection string
if(!optionsBuilder.IsConfigured)
{
optionsBuilder.UseNpgsql("Username=postgres;Password=hellopassword;Host=localhost;Port=5432;Database=jobseek;Pooling=true;", providerOptions=>providerOptions.CommandTimeout(60))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new TagMapper());
modelBuilder.ApplyConfiguration(new KeywordMapper());
modelBuilder.ApplyConfiguration(new TagKeywordMapper());
}
public DbSet<Keyword> Keywords { get; set; }
public DbSet<Tag> Tags { get; set; }
}
Then add migrations and init database if using pg or sqlserver
dotnet ef migrations add init
dotnet ef database update
Unit Test
TDD is essential for this project as I have to write many test cases to verify the thought and practice new knowledge during development.
# add xunit test
dotnet new xunit -o test/DotNetJobSeek.Domain.Test
# add reference to Domain and EFContext
cd test/DotNetJobSeek.Domain.Test
dotnet add reference ../../src/DotNetJobSeek.Domain/DotNetJobSeek.Domain.csproj
dotnet add reference ../../src/Infrastruture/DotNetJobSeek.Infrastructure.EF/DotNetJobSeek.Infrastructure.EF.csproj
According to Microsoft 20161, SQLite has an in-memory mode that allows you to use SQLite to write tests against a relational database, without the overhead of actual database operations.
eg:
public class TagKeywordMapperTest
{
Keyword k1, k2, k3;
Tag t1, t2, t3;
public TagKeywordMapperTest()
{
k1 = new Keyword { Id = 1, Name = "food" };
k2 = new Keyword { Id = 2, Name = "drink" };
k3 = new Keyword { Id = 3, Name = "hotel" };
t1 = new Tag { Id = 1, Name = "bar"};
t2 = new Tag { Id = 2, Name = "move"};
t3 = new Tag { Id = 3, Name = "live"};
}
[Fact]
public void TestTagAdd()
{
var connection = new SqliteConnection("DataSource=:memory:");
Tag test;
connection.Open();
try
{
var options = new DbContextOptionsBuilder<EFContext>()
.UseSqlite(connection)
.Options;
using(var context = new EFContext(options))
{
context.Database.EnsureCreated();
}
using(var context = new EFContext(options))
{
context.Tags.Add(t1);
try
{
context.SaveChanges();
}
catch (System.Exception)
{
throw;
}
}
using(var context = new EFContext(options))
{
test = context.Tags.Where(k => k.Id == 1).FirstOrDefault();
}
Assert.Equal("bar", test.Name);
Assert.Equal(0, test.Version);
}
finally
{
connection.Close();
}
}
}
CI / CD
As this project is hosted in gitlab, thus the CI / CD could be enabled by add .gitlab-ci.yml
image: microsoft/dotnet:latest
stages:
- test
before_script:
- "dotnet restore"
test:
stage: test
script:
- "dotnet test test/*"
only:
- master
- develop
Conclusion
In this journey, I spent most of my time in learning EF Core and C# Unit test. In the next journey, I will try to implement entities and repository.
Reference
-
Microsoft 2016, Testing with SQLite, https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite ↩