Refactor domain layer to follow Clean Architecture

Updated the domain layer to align with Clean Architecture principles:
- Removed Entity Framework dependencies from the project.
- Updated AGENTS.md to document the new architecture.
- Introduced repository interfaces for data access abstraction.
- Added a generic IRepository interface for CRUD operations.
- Implemented entity-specific repositories for Profile, ProfileSqlJob, and ProfileHistory.
- Ensured the domain layer is infrastructure-independent with pure POCOs.
- Updated project structure and documentation for clarity.
This commit is contained in:
2026-07-09 13:33:39 +02:00
parent ce47653831
commit 3e007ccfeb
6 changed files with 230 additions and 75 deletions

View File

@@ -2,7 +2,12 @@
## Project Overview ## Project Overview
ECMJobRunner.Domain is a domain entity library for the ECM Job Runner system. This project contains entity models that represent the database schema for job runner configuration and execution history. ECMJobRunner.Domain is the **domain layer** for the ECM Job Runner system following **Clean Architecture** principles. This project contains:
- **Entity models** that represent the business domain
- **Repository interfaces** for data access abstraction
- **Domain logic** (currently none, pure data entities)
**Key Principle**: This layer has **NO** external dependencies - it's the core of the application.
## Target Frameworks ## Target Frameworks
@@ -11,11 +16,13 @@ ECMJobRunner.Domain is a domain entity library for the ECM Job Runner system. Th
The project is multi-targeted to support both legacy .NET Framework applications and modern .NET 8 applications. The project is multi-targeted to support both legacy .NET Framework applications and modern .NET 8 applications.
## Technologies ## Architecture
### Entity Framework This project follows **Clean Architecture** principles:
- **For .NET Framework 4.8**: Entity Framework 6.5.1 - **No infrastructure dependencies** (no Entity Framework, no database concerns)
- **For .NET 8.0**: Entity Framework Core 8.0.11 with SQL Server provider - **Pure domain entities** without ORM attributes
- **Repository pattern interfaces** for data access abstraction
- **Dependency inversion** - infrastructure depends on domain, not vice versa
## Project Structure ## Project Structure
@@ -25,6 +32,12 @@ ECMJobRunner.Domain/
│ ├── Profile.cs # Job configuration profile entity │ ├── Profile.cs # Job configuration profile entity
│ ├── ProfileSqlJob.cs # SQL job configuration entity │ ├── ProfileSqlJob.cs # SQL job configuration entity
│ └── ProfileHistory.cs # Job execution history entity │ └── ProfileHistory.cs # Job execution history entity
├── Interfaces/
│ ├── IRepository.cs # Generic repository interface
│ ├── IProfileRepository.cs # Profile-specific repository
│ ├── IProfileSqlJobRepository.cs
│ ├── IProfileHistoryRepository.cs
│ └── IUnitOfWork.cs # Unit of Work pattern interface
├── ECMJobRunner.Domain.csproj ├── ECMJobRunner.Domain.csproj
└── AGENTS.md # This file └── AGENTS.md # This file
``` ```
@@ -32,96 +45,105 @@ ECMJobRunner.Domain/
## Entities ## Entities
### Profile ### Profile
**Table:** `dbo.TBJR_CFG_PROFILE`
Represents a job runner profile configuration. Represents a job runner profile configuration.
**Key Properties:** **Properties:**
- `Id` (PK): Primary key, auto-generated - `Id` (long): Primary key
- `Active`: Enable/disable switch - `Active` (bool): Enable/disable switch
- `ProfileName`: Name of the profile - `ProfileName` (string, max 150): Name of the profile
- `TypeId`: Profile type (0=ADSync, 1=GraphQL, 2=SQL-Job, 3=SQL and REST-Job) - `TypeId` (byte): Profile type (0=ADSync, 1=GraphQL, 2=SQL-Job, 3=SQL and REST-Job)
- `Schedule`: Cron format schedule - `Schedule` (string, max 150): Cron format schedule
- `Comment`: Optional description - `Comment` (string?, max 500): Optional description
- `AddedWho`, `AddedWhen`, `ChangedWho`, `ChangedWhen`: Audit fields
**Relationships:** **Navigation Properties:**
- One-to-Many with `ProfileSqlJob` (SQL jobs) - `SqlJobs` (IEnumerable<ProfileSqlJob>?): Associated SQL jobs
- One-to-Many with `ProfileHistory` (execution history) - `ProfileHistories` (IEnumerable<ProfileHistory>?): Execution history
### ProfileSqlJob ### ProfileSqlJob
**Table:** `dbo.TBJR_CFG_PROFILE_SQLJOB`
Represents individual SQL jobs within a profile. Represents individual SQL jobs within a profile.
**Key Properties:** **Properties:**
- `Id` (PK): Primary key, auto-generated - `Id` (long): Primary key
- `ProfileId` (FK): Foreign key to `Profile` - `ProfileId` (long): Foreign key to Profile
- `Active`: Enable/disable switch - `Active` (bool): Enable/disable switch
- `Sequence`: Execution order within the profile - `Sequence` (short): Execution order within the profile
- `Name`: Optional job name - `Name` (string?, max 150): Optional job name
- `SqlCheckQuery`: SQL query for pre-check - `SqlCheckQuery` (string?): SQL query for pre-check
- `SqlMainQuery`: Main SQL query - `SqlMainQuery` (string?): Main SQL query
- `ApiCommand`: API command to execute - `ApiCommand` (string?): API command to execute
- `Comment` (string?, max 500): Optional description
- Audit fields
**Relationships:** **Navigation Properties:**
- Many-to-One with `Profile` - `Profile` (Profile?): Associated profile
### ProfileHistory ### ProfileHistory
**Table:** `dbo.TBJR_OUT_PROFILE_HISTORY`
Stores execution history and results of job profiles. Stores execution history and results of job profiles.
**Key Properties:** **Properties:**
- `Id` (PK): Primary key, auto-generated - `Id` (long): Primary key
- `ProfileId` (FK): Foreign key to `Profile` - `ProfileId` (long): Foreign key to Profile
- `ResultId`: Result status (0=OK, 1=ERROR, 2=WARNING) - `ResultId` (byte): Result status (0=OK, 1=ERROR, 2=WARNING)
- `ResultText`: Result message/details - `ResultText` (string): Result message/details
- Audit fields
**Relationships:** **Navigation Properties:**
- Many-to-One with `Profile` - `Profile` (Profile?): Associated profile
## Repository Interfaces
### IRepository<TEntity>
Generic repository interface providing CRUD operations:
- `GetById(id)`, `GetByIdAsync(id)`
- `GetAll()`, `GetAllAsync()`
- `Find(predicate)`, `FindAsync(predicate)`
- `SingleOrDefault(predicate)`, `SingleOrDefaultAsync(predicate)`
- `Add(entity)`, `AddRange(entities)`
- `Update(entity)`, `Remove(entity)`, `RemoveRange(entities)`
### Entity-Specific Repositories
- `IProfileRepository : IRepository<Profile>`
- `IProfileSqlJobRepository : IRepository<ProfileSqlJob>`
- `IProfileHistoryRepository : IRepository<ProfileHistory>`
### IUnitOfWork
Manages transactions and provides access to all repositories:
- `Profiles`: IProfileRepository
- `ProfileSqlJobs`: IProfileSqlJobRepository
- `ProfileHistories`: IProfileHistoryRepository
- `SaveChanges()`, `SaveChangesAsync()`
## Database Schema Source ## Database Schema Source
The entities are based on SQL Server tables located at: The entities map to SQL Server tables located at:
``` ```
M:\Datenbank\[DD_ECM]-Database\JobRunner\ M:\Datenbank\[DD_ECM]-Database\JobRunner\
``` ```
The following SQL files define the schema: **Table Mappings** (handled in Infrastructure layer):
- `[TBJR_CFG_PROFILE].sql` - Profile configuration table - `Profile``dbo.TBJR_CFG_PROFILE`
- `[TBJR_CFG_PROFILE_SQLJOB].sql` - SQL job configuration table - `ProfileSqlJob``dbo.TBJR_CFG_PROFILE_SQLJOB`
- `[TBJR_OUT_PROFILE_HISTORY].sql` - Execution history table - `ProfileHistory``dbo.TBJR_OUT_PROFILE_HISTORY`
- `[VWJR_CFG_PROFILE].sql` - Profile view (not implemented as entity)
- `[VWJR_CFG_PROFILE_SQLJOB].sql` - SQL job view (not implemented as entity)
## Naming Conventions ## Naming Conventions
- **Entity Classes**: Clean Pascal case (e.g., `Profile` instead of `TBJR_CFG_PROFILE`) - **Entity Classes**: Clean Pascal case (e.g., `Profile` instead of `TBJR_CFG_PROFILE`)
- **Properties**: Pascal case (e.g., `ProfileName` instead of `PROFILE_NAME`) - **Properties**: Pascal case (e.g., `ProfileName` instead of `PROFILE_NAME`)
- **Table Mapping**: Uses `[Table]` attribute to map to actual database table names - **No ORM attributes** - entities are pure POCOs
- **Column Mapping**: Uses `[Column]` attribute to map to actual column names - **Navigation properties** are nullable `IEnumerable<T>?` (loaded only when explicitly included)
- Entity names are simplified without technical prefixes (Jr/Cfg/Out)
## Attributes Used ## Dependencies
- `[Table]` - Specifies the database table name and schema **NONE** - This is a core domain layer with zero external dependencies.
- `[Column]` - Specifies the database column name
- `[Key]` - Marks the primary key
- `[Required]` - Marks non-nullable properties
- `[MaxLength]` - Specifies maximum string length
- `[ForeignKey]` - Specifies foreign key relationships
- `[DatabaseGenerated]` - Specifies identity/auto-generated columns
## Building the Project ## Building the Project
To build the project:
```bash ```bash
dotnet build ECMJobRunner.Domain.csproj dotnet build ECMJobRunner.Domain.csproj
``` ```
To build for a specific framework: For specific framework:
```bash ```bash
dotnet build ECMJobRunner.Domain.csproj -f net8.0 dotnet build ECMJobRunner.Domain.csproj -f net8.0
dotnet build ECMJobRunner.Domain.csproj -f net480 dotnet build ECMJobRunner.Domain.csproj -f net480
@@ -129,11 +151,16 @@ dotnet build ECMJobRunner.Domain.csproj -f net480
## Development Notes ## Development Notes
- All entities include audit fields: `AddedWho`, `AddedWhen`, `ChangedWho`, `ChangedWhen` - **Clean Architecture**: Domain layer is independent of infrastructure concerns
- Navigation properties are marked as `virtual` to support lazy loading - **Navigation properties**: Nullable IEnumerable - populated only when explicitly loaded via `.Include()`
- Nullable reference types are enabled for better null-safety - **No ORM attributes**: Pure POCOs - mapping is done in Infrastructure layer
- XML documentation is generated for IntelliSense support - **Repository pattern**: All data access through interfaces
- Default values are set according to database constraints - **Unit of Work pattern**: Transaction management abstraction
## Related Projects
- **ECMJobRunner.Infrastructure**: Implements repositories and DbContext
- **ECMJobRunner.Application**: Application layer with business logic
## Company Information ## Company Information

View File

@@ -15,15 +15,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net480'"> <ItemGroup Condition="'$(TargetFramework)' == 'net480'">
<!-- Entity Framework 6 for .NET Framework 4.8 --> <!-- System.ComponentModel.DataAnnotations for .NET Framework 4.8 -->
<PackageReference Include="EntityFramework" Version="6.5.1" /> <Reference Include="System.ComponentModel.DataAnnotations" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<!-- Entity Framework Core 8 for .NET 8 -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,12 @@
using ECMJobRunner.Domain.Entities;
namespace ECMJobRunner.Domain.Interfaces
{
/// <summary>
/// Repository interface for CfgProfile entity
/// </summary>
public interface ICfgProfileRepository : IRepository<CfgProfile>
{
// Add custom CfgProfile-specific methods here if needed
}
}

View File

@@ -0,0 +1,12 @@
using ECMJobRunner.Domain.Entities;
namespace ECMJobRunner.Domain.Interfaces
{
/// <summary>
/// Repository interface for ProfileHistory entity
/// </summary>
public interface IProfileHistoryRepository : IRepository<ProfileHistory>
{
// Add custom ProfileHistory-specific methods here if needed
}
}

View File

@@ -0,0 +1,12 @@
using ECMJobRunner.Domain.Entities;
namespace ECMJobRunner.Domain.Interfaces
{
/// <summary>
/// Repository interface for ProfileSqlJob entity
/// </summary>
public interface IProfileSqlJobRepository : IRepository<ProfileSqlJob>
{
// Add custom ProfileSqlJob-specific methods here if needed
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace ECMJobRunner.Domain.Interfaces
{
/// <summary>
/// Generic repository interface for data access operations
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
public interface IRepository<TEntity> where TEntity : class
{
/// <summary>
/// Get entity by ID asynchronously
/// </summary>
Task<TEntity?> GetByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// Get all entities asynchronously
/// </summary>
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Find entities by predicate asynchronously
/// </summary>
Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Get single entity by predicate asynchronously
/// </summary>
Task<TEntity?> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Add new entity from DTO asynchronously
/// Maps DTO to entity and adds it
/// </summary>
/// <typeparam name="TDto">DTO type</typeparam>
/// <param name="dto">DTO containing values for new entity</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Created entity</returns>
Task<TEntity> AddAsync<TDto>(TDto dto, CancellationToken cancellationToken = default) where TDto : class;
/// <summary>
/// Add multiple entities from DTOs asynchronously
/// Maps DTOs to entities and adds them
/// </summary>
/// <typeparam name="TDto">DTO type</typeparam>
/// <param name="dtos">DTOs containing values for new entities</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Number of entities added</returns>
Task<int> AddRangeAsync<TDto>(IEnumerable<TDto> dtos, CancellationToken cancellationToken = default) where TDto : class;
/// <summary>
/// Update entities matching predicate with DTO values asynchronously
/// Maps DTO properties onto matching entities
/// </summary>
/// <typeparam name="TDto">DTO type</typeparam>
/// <param name="predicate">Predicate to find entities</param>
/// <param name="dto">DTO containing values to update</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Number of entities updated</returns>
Task<int> UpdateAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class;
/// <summary>
/// Update single entity matching predicate with DTO values asynchronously
/// Throws exception if multiple entities match
/// </summary>
/// <typeparam name="TDto">DTO type</typeparam>
/// <param name="predicate">Predicate to find entity</param>
/// <param name="dto">DTO containing values to update</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if entity was found and updated, false otherwise</returns>
Task<bool> UpdateSingleAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class;
/// <summary>
/// Delete entities matching predicate asynchronously (hard delete)
/// </summary>
/// <param name="predicate">Predicate to find entities to delete</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Number of entities deleted</returns>
Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Delete single entity matching predicate asynchronously (hard delete)
/// Throws exception if multiple entities match
/// </summary>
/// <param name="predicate">Predicate to find entity to delete</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if entity was found and deleted, false otherwise</returns>
Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Save all changes asynchronously
/// </summary>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
}