diff --git a/ECMJobRunner.Domain/AGENTS.md b/ECMJobRunner.Domain/AGENTS.md index 836f962..7cf898b 100644 --- a/ECMJobRunner.Domain/AGENTS.md +++ b/ECMJobRunner.Domain/AGENTS.md @@ -2,7 +2,12 @@ ## 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 @@ -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. -## Technologies +## Architecture -### Entity Framework -- **For .NET Framework 4.8**: Entity Framework 6.5.1 -- **For .NET 8.0**: Entity Framework Core 8.0.11 with SQL Server provider +This project follows **Clean Architecture** principles: +- **No infrastructure dependencies** (no Entity Framework, no database concerns) +- **Pure domain entities** without ORM attributes +- **Repository pattern interfaces** for data access abstraction +- **Dependency inversion** - infrastructure depends on domain, not vice versa ## Project Structure @@ -25,6 +32,12 @@ ECMJobRunner.Domain/ │ ├── Profile.cs # Job configuration profile entity │ ├── ProfileSqlJob.cs # SQL job configuration 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 └── AGENTS.md # This file ``` @@ -32,96 +45,105 @@ ECMJobRunner.Domain/ ## Entities ### Profile -**Table:** `dbo.TBJR_CFG_PROFILE` - Represents a job runner profile configuration. -**Key Properties:** -- `Id` (PK): Primary key, auto-generated -- `Active`: Enable/disable switch -- `ProfileName`: Name of the profile -- `TypeId`: Profile type (0=ADSync, 1=GraphQL, 2=SQL-Job, 3=SQL and REST-Job) -- `Schedule`: Cron format schedule -- `Comment`: Optional description +**Properties:** +- `Id` (long): Primary key +- `Active` (bool): Enable/disable switch +- `ProfileName` (string, max 150): Name of the profile +- `TypeId` (byte): Profile type (0=ADSync, 1=GraphQL, 2=SQL-Job, 3=SQL and REST-Job) +- `Schedule` (string, max 150): Cron format schedule +- `Comment` (string?, max 500): Optional description +- `AddedWho`, `AddedWhen`, `ChangedWho`, `ChangedWhen`: Audit fields -**Relationships:** -- One-to-Many with `ProfileSqlJob` (SQL jobs) -- One-to-Many with `ProfileHistory` (execution history) +**Navigation Properties:** +- `SqlJobs` (IEnumerable?): Associated SQL jobs +- `ProfileHistories` (IEnumerable?): Execution history ### ProfileSqlJob -**Table:** `dbo.TBJR_CFG_PROFILE_SQLJOB` - Represents individual SQL jobs within a profile. -**Key Properties:** -- `Id` (PK): Primary key, auto-generated -- `ProfileId` (FK): Foreign key to `Profile` -- `Active`: Enable/disable switch -- `Sequence`: Execution order within the profile -- `Name`: Optional job name -- `SqlCheckQuery`: SQL query for pre-check -- `SqlMainQuery`: Main SQL query -- `ApiCommand`: API command to execute +**Properties:** +- `Id` (long): Primary key +- `ProfileId` (long): Foreign key to Profile +- `Active` (bool): Enable/disable switch +- `Sequence` (short): Execution order within the profile +- `Name` (string?, max 150): Optional job name +- `SqlCheckQuery` (string?): SQL query for pre-check +- `SqlMainQuery` (string?): Main SQL query +- `ApiCommand` (string?): API command to execute +- `Comment` (string?, max 500): Optional description +- Audit fields -**Relationships:** -- Many-to-One with `Profile` +**Navigation Properties:** +- `Profile` (Profile?): Associated profile ### ProfileHistory -**Table:** `dbo.TBJR_OUT_PROFILE_HISTORY` - Stores execution history and results of job profiles. -**Key Properties:** -- `Id` (PK): Primary key, auto-generated -- `ProfileId` (FK): Foreign key to `Profile` -- `ResultId`: Result status (0=OK, 1=ERROR, 2=WARNING) -- `ResultText`: Result message/details +**Properties:** +- `Id` (long): Primary key +- `ProfileId` (long): Foreign key to Profile +- `ResultId` (byte): Result status (0=OK, 1=ERROR, 2=WARNING) +- `ResultText` (string): Result message/details +- Audit fields -**Relationships:** -- Many-to-One with `Profile` +**Navigation Properties:** +- `Profile` (Profile?): Associated profile + +## Repository Interfaces + +### IRepository +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` +- `IProfileSqlJobRepository : IRepository` +- `IProfileHistoryRepository : IRepository` + +### IUnitOfWork +Manages transactions and provides access to all repositories: +- `Profiles`: IProfileRepository +- `ProfileSqlJobs`: IProfileSqlJobRepository +- `ProfileHistories`: IProfileHistoryRepository +- `SaveChanges()`, `SaveChangesAsync()` ## 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\ ``` -The following SQL files define the schema: -- `[TBJR_CFG_PROFILE].sql` - Profile configuration table -- `[TBJR_CFG_PROFILE_SQLJOB].sql` - SQL job configuration table -- `[TBJR_OUT_PROFILE_HISTORY].sql` - Execution history table -- `[VWJR_CFG_PROFILE].sql` - Profile view (not implemented as entity) -- `[VWJR_CFG_PROFILE_SQLJOB].sql` - SQL job view (not implemented as entity) +**Table Mappings** (handled in Infrastructure layer): +- `Profile` → `dbo.TBJR_CFG_PROFILE` +- `ProfileSqlJob` → `dbo.TBJR_CFG_PROFILE_SQLJOB` +- `ProfileHistory` → `dbo.TBJR_OUT_PROFILE_HISTORY` ## Naming Conventions - **Entity Classes**: Clean Pascal case (e.g., `Profile` instead of `TBJR_CFG_PROFILE`) - **Properties**: Pascal case (e.g., `ProfileName` instead of `PROFILE_NAME`) -- **Table Mapping**: Uses `[Table]` attribute to map to actual database table names -- **Column Mapping**: Uses `[Column]` attribute to map to actual column names -- Entity names are simplified without technical prefixes (Jr/Cfg/Out) +- **No ORM attributes** - entities are pure POCOs +- **Navigation properties** are nullable `IEnumerable?` (loaded only when explicitly included) -## Attributes Used +## Dependencies -- `[Table]` - Specifies the database table name and schema -- `[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 +**NONE** - This is a core domain layer with zero external dependencies. ## Building the Project -To build the project: - ```bash dotnet build ECMJobRunner.Domain.csproj ``` -To build for a specific framework: - +For specific framework: ```bash dotnet build ECMJobRunner.Domain.csproj -f net8.0 dotnet build ECMJobRunner.Domain.csproj -f net480 @@ -129,11 +151,16 @@ dotnet build ECMJobRunner.Domain.csproj -f net480 ## Development Notes -- All entities include audit fields: `AddedWho`, `AddedWhen`, `ChangedWho`, `ChangedWhen` -- Navigation properties are marked as `virtual` to support lazy loading -- Nullable reference types are enabled for better null-safety -- XML documentation is generated for IntelliSense support -- Default values are set according to database constraints +- **Clean Architecture**: Domain layer is independent of infrastructure concerns +- **Navigation properties**: Nullable IEnumerable - populated only when explicitly loaded via `.Include()` +- **No ORM attributes**: Pure POCOs - mapping is done in Infrastructure layer +- **Repository pattern**: All data access through interfaces +- **Unit of Work pattern**: Transaction management abstraction + +## Related Projects + +- **ECMJobRunner.Infrastructure**: Implements repositories and DbContext +- **ECMJobRunner.Application**: Application layer with business logic ## Company Information diff --git a/ECMJobRunner.Domain/ECMJobRunner.Domain.csproj b/ECMJobRunner.Domain/ECMJobRunner.Domain.csproj index 2101351..710947e 100644 --- a/ECMJobRunner.Domain/ECMJobRunner.Domain.csproj +++ b/ECMJobRunner.Domain/ECMJobRunner.Domain.csproj @@ -15,15 +15,8 @@ - - - - - - - - - + + diff --git a/ECMJobRunner.Domain/Interfaces/ICfgProfileRepository.cs b/ECMJobRunner.Domain/Interfaces/ICfgProfileRepository.cs new file mode 100644 index 0000000..06adc20 --- /dev/null +++ b/ECMJobRunner.Domain/Interfaces/ICfgProfileRepository.cs @@ -0,0 +1,12 @@ +using ECMJobRunner.Domain.Entities; + +namespace ECMJobRunner.Domain.Interfaces +{ + /// + /// Repository interface for CfgProfile entity + /// + public interface ICfgProfileRepository : IRepository + { + // Add custom CfgProfile-specific methods here if needed + } +} diff --git a/ECMJobRunner.Domain/Interfaces/IProfileHistoryRepository.cs b/ECMJobRunner.Domain/Interfaces/IProfileHistoryRepository.cs new file mode 100644 index 0000000..51bdc9f --- /dev/null +++ b/ECMJobRunner.Domain/Interfaces/IProfileHistoryRepository.cs @@ -0,0 +1,12 @@ +using ECMJobRunner.Domain.Entities; + +namespace ECMJobRunner.Domain.Interfaces +{ + /// + /// Repository interface for ProfileHistory entity + /// + public interface IProfileHistoryRepository : IRepository + { + // Add custom ProfileHistory-specific methods here if needed + } +} diff --git a/ECMJobRunner.Domain/Interfaces/IProfileSqlJobRepository.cs b/ECMJobRunner.Domain/Interfaces/IProfileSqlJobRepository.cs new file mode 100644 index 0000000..dfe5614 --- /dev/null +++ b/ECMJobRunner.Domain/Interfaces/IProfileSqlJobRepository.cs @@ -0,0 +1,12 @@ +using ECMJobRunner.Domain.Entities; + +namespace ECMJobRunner.Domain.Interfaces +{ + /// + /// Repository interface for ProfileSqlJob entity + /// + public interface IProfileSqlJobRepository : IRepository + { + // Add custom ProfileSqlJob-specific methods here if needed + } +} diff --git a/ECMJobRunner.Domain/Interfaces/IRepository.cs b/ECMJobRunner.Domain/Interfaces/IRepository.cs new file mode 100644 index 0000000..204414d --- /dev/null +++ b/ECMJobRunner.Domain/Interfaces/IRepository.cs @@ -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 +{ + /// + /// Generic repository interface for data access operations + /// + /// Entity type + public interface IRepository where TEntity : class + { + /// + /// Get entity by ID asynchronously + /// + Task GetByIdAsync(long id, CancellationToken cancellationToken = default); + + /// + /// Get all entities asynchronously + /// + Task> GetAllAsync(CancellationToken cancellationToken = default); + + /// + /// Find entities by predicate asynchronously + /// + Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); + + /// + /// Get single entity by predicate asynchronously + /// + Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancellationToken = default); + + /// + /// Add new entity from DTO asynchronously + /// Maps DTO to entity and adds it + /// + /// DTO type + /// DTO containing values for new entity + /// Cancellation token + /// Created entity + Task AddAsync(TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + /// + /// Add multiple entities from DTOs asynchronously + /// Maps DTOs to entities and adds them + /// + /// DTO type + /// DTOs containing values for new entities + /// Cancellation token + /// Number of entities added + Task AddRangeAsync(IEnumerable dtos, CancellationToken cancellationToken = default) where TDto : class; + + /// + /// Update entities matching predicate with DTO values asynchronously + /// Maps DTO properties onto matching entities + /// + /// DTO type + /// Predicate to find entities + /// DTO containing values to update + /// Cancellation token + /// Number of entities updated + Task UpdateAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + /// + /// Update single entity matching predicate with DTO values asynchronously + /// Throws exception if multiple entities match + /// + /// DTO type + /// Predicate to find entity + /// DTO containing values to update + /// Cancellation token + /// True if entity was found and updated, false otherwise + Task UpdateSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + /// + /// Delete entities matching predicate asynchronously (hard delete) + /// + /// Predicate to find entities to delete + /// Cancellation token + /// Number of entities deleted + Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); + + /// + /// Delete single entity matching predicate asynchronously (hard delete) + /// Throws exception if multiple entities match + /// + /// Predicate to find entity to delete + /// Cancellation token + /// True if entity was found and deleted, false otherwise + Task DeleteSingleAsync(Expression> predicate, CancellationToken cancellationToken = default); + + /// + /// Save all changes asynchronously + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); + } +}