Light Architecture of a Target Project
D3ORM has minimal influence on an application architecture thanks to choice of own or generated entities and adaptable repositories...
- This article explains basics of D3ORM architecture and integration to your project infrastructures.
- You will learn how to cooperate D3ORM with your layers.
- You will learn how to integrate Code Generators in all layers.
- You will learn how to write queries and how to use bulk operations in the infrastructure layer.
Design Constraints and Possibilities
- Single Column Database Table PK/FK Keys: Ids must be a single column string or 32 bit integer or 64 bit integer. Id must be auto-increment in all tables or you have to set ID3Context.UseAutoIncrement to false for all tables.
- United Format of IDs: United format database key column names and entity key property names.
- The formats are set by the parameters DbIdColumnFormat and EntityIdPropFormat when instantiating ID3Context, see example.
// Base Constructor
public D3BaseContext(Dictionary<Type, EntityMapping> EntityMappings, string EntityIdPropFormat, string DbIdColumnFormat, string DtoPatternFormat, string NotEqualOperator, char JoinPrefixChar, string DateFormat)
//
// Implementations
//
public MSSQLD3Context(Dictionary<Type, EntityMapping> EntityMappings, string EntityIdPropFormat = "{0}Id", string DbIdColumnFormat = "{0}Id", string DtoPatternFormat = "{0}DTO", char joinPrefixHelperChar = 'x', string DateFormat = "yyyyMMdd") : base(EntityMappings, DbIdColumnFormat, DtoPatternFormat, EntityIdPropFormat, "!=", joinPrefixHelperChar, DateFormat)
// or
public MySQLD3Context(Dictionary<Type, EntityMapping> EntityMappings, string EntityIdPropFormat = "{0}Id", string DbIdColumnFormat = "{0}_id", string DtoPatternFormat = "{0}DTO", char joinPrefixHelperChar = 'x', string DateFormat = "yyyy-MM-dd") : base(EntityMappings, DbIdColumnFormat, DtoPatternFormat, EntityIdPropFormat, "!=", joinPrefixHelperChar, DateFormat)
// or
public SQLiteD3Context(Dictionary<Type, EntityMapping> EntityMappings, string EntityIdPropFormat = "{0}Id", string DbIdColumnFormat = "{0}Id", string DtoPatternFormat = "{0}DTO", char joinPrefixHelperChar = 'x', string DateFormat = "yyyy-MM-dd") : base(EntityMappings, DbIdColumnFormat, DtoPatternFormat, EntityIdPropFormat, "!=", joinPrefixHelperChar, DateFormat)
//
// or using your own factory
public class MySQLD3ContextFactory : ID3ContextFactory
{
public ID3Context Create()
{
return new MySQLD3Context(EntityPropMappings.Dict, DtoPatternFormat: Constants.DtoPatternFormat);
}
}
...
builder.Services.AddSingleton<ID3ContextFactory, MySQLD3ContextFactory>();
builder.Services.AddScoped<ID3Context>(x => x.GetService<ID3ContextFactory>()!.Create());
- The formats are set by the parameters DbIdColumnFormat and EntityIdPropFormat when instantiating ID3Context, see example.
- Use the same names with the same conventions of camel case or underscores in the database and the application.
- i.e. (Using MySQL): The database names are used with the underscore convention everywhere but .NET entities and properties are used with the camel case convention everywhere.
- If you need to have some name exceptions (i.e.: GUIDExpirationDate), you need to define them globally, see ID3Context.ExceptionalDbColumnNames.
-- MySQL `
`tournament_team_stat`.`tournament_team_stat_id` `
`tournament_team_stat`.`tournament_team_id` `
`tournament_team_stat`.`phase_points`
...
-- .NET
TournamentTeamStat.TournamentTeamStatId
TournamentTeamStat.TournamentTeamId
TournamentTeamStat.PhasePoints
...
- Use the corresponding names for navigation properties: Name the navigation properties according to foreign keys (without Id suffix). This also applies for "one to many" relations (collections) where you should use the plural form of the navigation property (not necessarily) (i.e.: many to one: Match.HomeTeam; one to many: Team.HomeTeams or Team.MatchHomeTeams or Team.HomeTeam or Team.MatchHomeTeam).
- D3ORM can recognize any plurality of English noun including irregular plurals. As you can see in the test below.
//
/// Tests for transferring singular names to plural names.
/// Mappings of irregular plural noun endings: us -> ((us)|i)s?; on -> ((on)|a); is -> ((es)|(is))s?; [.]s -> [.]ss?; [.]z -> [.]zz? else replaces only last char with anything.
/// It is OK to name the collection navigation property with same name, see Criterion and Criterion.
//
[TestMethod]
[DataRow("TournamentTeam", "TournamentTeamStat", "TournamentTeamStats", @"^(TournamentTeam)?TournamentTeamSta.(i|e)?s?$")]
[DataRow("TournamentTeam", "TournamentTeamStats", "TournamentTeamStats", @"^(TournamentTeam)?TournamentTeamSta.ss?(i|e)?s?$")]
[DataRow("Forrest", "Wolf", "Wolves", @"^(Forrest)?Wol.(i|e)?s?$")]
[DataRow("Expression", "Parenthesis", "Parentheses", @"^(Expression)?Parenthes((es)|(is))s?(i|e)?s?$")]
[DataRow("TournamentTeam", "Status", "Statuses", @"^(TournamentTeam)?Stat((uss?)|i)(i|e)?s?$")]
[DataRow("PetrolStation", "Gas", "Gasses", @"^(PetrolStation)?G.ss?(i|e)?s?$")]
[DataRow("HatShop", "Fez", "Fezzes", @"^(HatShop)?F.zz?(i|e)?s?$")]
[DataRow("Constraint", "Criterion", "Criterion", @"^(Constraint)?Criteri((on)|a)(i|e)?s?$")]
[DataRow("Constraint", "Criterion", "Criteria", @"^(Constraint)?Criteri((on)|a)(i|e)?s?$")]
[DataRow("School", "Study", "Studies", @"^(School)?Stud.(i|e)?s?$")]
[DataRow("Desert", "Cactus", "Cacti", @"^(Desert)?Cact((uss?)|i)(i|e)?s?$")]
public void Helpers_GetPluralEntityNamePattern(string entityName, string manyToOneProp, string oneToManyProp, string expectedPattern)
{
var pattern = Helpers.GetPluralEntityNamePattern(entityName, manyToOneProp);
Assert.AreEqual(expectedPattern, pattern);
Assert.IsTrue(Regex.IsMatch(oneToManyProp, pattern));
}
- D3ORM can recognize any plurality of English noun including irregular plurals. As you can see in the test below.
- DTO Constraints:
- DTOs do not have to contain all properties, but must contain id properties - you can use JsonIgnoreAttribute not to include them in a HTTP response or you can omit them completely using a different DTO in the function object.ToDTO() on the server side.
- All DTOs of a single instance (with aggregates) must be placed into the same namespace or defined in some namespace from ID3Context.OtherModelNamespaces.
- Id property names must remain the same as the entity name or at least should follow the id property pattern with a DTO name containing the original entity name instead of the entity name itself (the same for navigation properties).
- DTO names must contain its entity name.
- The namespace and DTO name constraints do not have to applied for DTOs created at the server side (after fetching all the data from a database).
- see more
Layers
Minimal and optional influence of infrastructure-higher layers, you will see...
Domain Layer (or some Data Layer):
- Models, interfaces for repositories and eventually interfaces for core data services are defined here.
- Entity Models can be generated by EntitiesFromEFCore.tt or written by your own following the Design Constraints - own entities are not so often supported across existing ORMs. All works thanks EntityPropMappings (continue reading).
- Repository Interfaces:
- You have a choice to define your own repository interfaces and wrap (adapt) D3Repositories in the infrastructure implementations...
- or you can decorate (extend) D3ORM repository template interfaces - just make your repository interfaces inherit from ICommonRepository (for non-root entities) or IAggRootRepository (for root entities). The repository interfaces' functions would be also compatible with NoSQL design.
- Using repository interfaces, you only have to include KLO128.D3ORM.Common.Abstract.dll in your domain layer.
- You do not have to implement any logic about the repositories in the infrastructure layer - use D3Repository implementations. Non-atomic complex data operations should be handled by core data services.
- Use Repositories-Ifaces.tt to generate repository interfaces.
//
// This is an interface example of a root entity repository when the decoration (inheritance) pattern is chosen.
//
// Minimal architecture impact, no coding...
//
using Domain.Models.Entities;
using KLO128.D3ORM.Common.Abstract;
namespace Domain.Repositories
{
public interface ITeamRepository : IAggRootRepository
{
}
}
//
// This is an interface example of a root entity repository when the adaptation (wrapping) pattern is chosen.
//
// No architecture impact, some coding...
//
using Domain.Models.Entities;
using KLO128.D3ORM.Common.Abstract;
namespace Domain.Repositories
{
public interface ITeamRepository
{
void MyAddAsChild<TParent>(TParent parent, Expression<Func<TParent, IEnumerable<Team>>> prop, Team entity) where TParent : class;
void MyAddAsChild<TParent>(TParent parent, Expression<Func<TParent, Team>> prop, Team entity) where TParent : class;
void MyBulkDelete(ISpecification<Team> specification, int delayAfterTaskStart = 300, int? commandTimeout = null);
...
Team? MyFindBy(ISpecification<Team> specification);
Team? MyFindByIdSingle(int id);
List<Team> MyFindManyBy(ISpecification<Team> specification, int? commandTimeout = null);
List<Team> MyFindManyBy(ISpecification<Team> specification, bool countAllItems, out int? allItemsCount, int? skip, int? take, int? commandTimeout = null);
...
}
}
Infrastructure Layer
- The only layer which should be adapted to D3ORM.Common and D3ORM.Common.<DbName>.
- Entities: As you have defined entity models in the domain layer, your infrastructure layer will work the entities or with any entities with the same properties after running EntityPropMappings.tt, that generates .cs class that defines all the relations and all the db name <-> entity name mappings. For example: Entity Framework has more restrictive rules how generated entities should look like and does not support different entities with the same structure, because they does not take a part in the database context model builder - only generated entities does.
- Specifications: Define as much repetitive queries (specifications) as you can as static instances and prebuild them. More prebuilt static queries (specifications) means faster data processing. Filter arguments can be attached by SetFilterParams() and SetFilterParamsFor() see Filters.
- Query Container: You can access the static queries by your own QueryContainer implementing your own IQueryContainer that has scoped life time in the IoC Container. see QueryContainer
- Repositories: The specific repository implementations will be atomic and clean using D3ORM repository heritage. It can also be generated by Repositories-Impl.tt or you can use your own custom adaptation as mentioned.
- Remember, that all complex (non-atomic) data logic should be moved to core data services.
- i.e.: Processing specific (non-general) query in order to execute additional CRUD operation will require to call an atomic repository twice.
- Additionally, D3ORM repositories also supports Bulk Operations.
//
// This is all the code you need for one repository!
//
using Domain.Repositories;
using Domain.Models.Entities;
using KLO128.D3ORM.Common;
using KLO128.D3ORM.Common.Impl;
using System.Data;
namespace Infrastructure.Repositories
{
public class TeamRepository : D3AggRootRepository<Team>, ITeamRepository
{
public TeamRepository(IDbConnection DbConnection, ID3Context D3Context) : base(DbConnection, D3Context)
{
}
}
}
- Remember, that all complex (non-atomic) data logic should be moved to core data services.
//
// Or you can use wrapping (adaptation) if you do not want to include KLO128.D3ORM.Abstract in your domain interfaces.
// But that is more lines code...
//
using Domain.Repositories;
using Domain.Models.Entities;
using KLO128.D3ORM.Common;
using KLO128.D3ORM.Common.Impl;
using System.Data;
namespace Infrastructure.Repositories
{
public class TeamRepository : ITeamRepository
{
private IAggRootRepository<Team> D3Repository { get; }
public TeamRepository(IAggRootRepository<Team> D3Repository)
{
this.D3Repository = D3Repository;
}
public void MyAddAsChild<TParent>(TParent parent, Expression<Func<TParent, IEnumerable<Team>>> prop, Team entity) where TParent : class
{
D3Repository.AddAsChild(parent, prop, entity);
}
public void MyAddAsChild<TParent>(TParent parent, Expression<Func<TParent, Team>> prop, Team entity) where TParent : class
{
D3Repository.AddAsChild(parent, prop, entity);
}
public void MyBulkDelete(ISpecification<Team> specification, int delayAfterTaskStart = 300, int? commandTimeout = null)
{
D3Repository.BulkDelete(specification, delayAfterTaskStart, commandTimeout);
}
...
public Team? MyFindBy(ISpecification<Team> specification)
{
return D3Repository.FindBy(specification);
}
public Team? MyFindByIdSingle(int id)
{
return D3Repository.FindByIdSingle<int>(id);
}
public List<Team> MyFindManyBy(ISpecification<Team> specification, int? commandTimeout = null)
{
return D3Repository.FindManyBy(specification, commandTimeout);
}
public List<Team> MyFindManyBy(ISpecification<Team> specification, bool countAllItems, out int? allItemsCount, int? skip, int? take, int? commandTimeout = null)
{
return List<Team> FindManyBy(specification, countAllItems, out allItemsCount, skip, take, commandTimeout);
}
...
}
}
Application Layer (Business Logic Services)
- DTO Objects: Entities' data transfer objects (DTOs) can be generated from entities using EntityDTOs.tt using **EntityDTOs.json configuration, where you can define multiple DTOs for single entity specifying which properties to include using regular expressions. see more...
- Insufficient Security: Do not expose plain entities in service results! Defining which properties to include or exclude for a query by using D3Specification.ExcludeSelectPropMasks() or D3Specification.IncludeSelectProps() will remain the property name with a default or null value.
- Sufficient Security: So, please use DTOs as you can easily specify which data to expose by a service to be secure...
- DTOExtensions: Extension for creating DTO instances dynamically during the runtime using DTOExt.ToDTO<TDTO>(this object entity). see more...
- Usage Example: team.ToDTO<TeamDTO>()
- It is applicable to any object (not only entities). TDTO can represent any type (not only generated DTO classes).
- It prevents cyclic entity reference infinite loops.
Presentation Layer (Front-End)
- D3ORM does not include any extensions or tools for a server side or a client presentation layer.
Example of Dependency Injection
using KLO128.D3ORM.Sample.Application.Contracts.Services;
using KLO128.D3ORM.Sample.Application.Web;
using KLO128.D3ORM.Sample.Domain;
using KLO128.D3ORM.Sample.Domain.Repositories;
using KLO128.D3ORM.Sample.Domain.Services;
using KLO128.D3ORM.Sample.Domain.Services.Impl;
using KLO128.D3ORM.Sample.Domain.Shared;
using KLO128.D3ORM.Sample.Infra.D3ORM;
using KLO128.D3ORM.Sample.Infra.D3ORM.MySQL;
using KLO128.D3ORM.Sample.Infra.D3ORM.Repositories;
using KLO128.D3ORM.Sample.Presentation.WebApi.Extensions;
using Microsoft.Extensions.Localization;
using MySql.Data.MySqlClient;
using System.Data;
// D3ORM Infrastructures
builder.Services.AddScoped<IDbConnection, MySqlConnection>(x => new MySqlConnection(builder.Configuration.GetConnectionString(Constants.AppSettingKeys.DefaultConnectionString)));
builder.Services.AddSingleton<ID3ContextFactory, MySQLD3ContextFactory>();
builder.Services.AddScoped<ID3Context>(x => x.GetService<ID3ContextFactory>()!.Create());
// Repositories
builder.Services.AddScoped<IAddressRepository, D3AddressRepository>();
builder.Services.AddScoped<IMatchRepository, D3MatchRepository>();
...
// Other Infrastructures
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IQueryContainer, QueryContainer>();
//Application Services
builder.Services.AddScoped<IStringLocalizer, MyLocalizer>(x => new MyLocalizer(Translations.ResourceManager));
builder.Services.AddScoped<ISettingsService, SettingsService>();
builder.Services.AddScoped<IAccountService, AccountWebService>();
...