diff --git a/ECMJobRunner.Infrastructure/AGENTS.md b/ECMJobRunner.Infrastructure/AGENTS.md new file mode 100644 index 0000000..c5c5770 --- /dev/null +++ b/ECMJobRunner.Infrastructure/AGENTS.md @@ -0,0 +1,205 @@ +# ECMJobRunner.Infrastructure + +## Project Overview + +ECMJobRunner.Infrastructure is the **infrastructure layer** for the ECM Job Runner system following **Clean Architecture** principles. This project contains: +- **DbContext implementation** with Entity Framework +- **Repository implementations** for data access +- **Entity configurations** (EF mappings) +- **Database connection management** + +**Key Principle**: This layer implements the interfaces defined in the Domain layer and handles all database concerns. + +## Target Frameworks + +- **.NET Framework 4.8** (`net480`) - Uses **Entity Framework 6.5.1** +- **.NET 8.0** (`net8.0`) - Uses **Entity Framework Core 8.0.11** + +The project uses conditional compilation to support both EF6 and EF Core with the same codebase. + +## Architecture + +This project follows **Clean Architecture** principles: +- **Depends on Domain layer** (implements domain interfaces) +- **No dependencies from Domain** (dependency inversion) +- **Conditional compilation** for EF6 vs EF Core differences +- **Repository pattern** implementation +- **Unit of Work pattern** implementation + +## Project Structure + +``` +ECMJobRunner.Infrastructure/ +├── Data/ +│ └── JobRunnerDbContext.cs # DbContext with conditional compilation +├── Repositories/ +│ ├── Repository.cs # Generic repository implementation +│ ├── ProfileRepository.cs # Profile-specific repository +│ ├── ProfileSqlJobRepository.cs +│ ├── ProfileHistoryRepository.cs +│ └── UnitOfWork.cs # Unit of Work implementation +├── ECMJobRunner.Infrastructure.csproj +└── AGENTS.md # This file +``` + +## Database Context + +### JobRunnerDbContext + +Multi-targeted DbContext supporting both EF6 and EF Core: + +**DbSets:** +- `Profiles`: DbSet +- `ProfileSqlJobs`: DbSet +- `ProfileHistories`: DbSet + +**Configuration:** +- Connection string via constructor parameter +- Conditional compilation directives (`#if NET48` / `#else`) +- Fluent API configurations in `OnModelCreating` + +**Table Mappings:** +- `Profile` → `dbo.TBJR_CFG_PROFILE` +- `ProfileSqlJob` → `dbo.TBJR_CFG_PROFILE_SQLJOB` +- `ProfileHistory` → `dbo.TBJR_OUT_PROFILE_HISTORY` + +**Column Mappings:** (Examples) +- `Profile.ProfileName` → `PROFILE_NAME` +- `ProfileSqlJob.SqlCheckQuery` → `SQL_CHECK_QUERY` +- All navigation properties configured with relationships + +## Repository Implementations + +### Repository +Generic repository implementing `IRepository` with full CRUD operations. + +**Key Methods:** +- Synchronous: `GetById`, `GetAll`, `Find`, `SingleOrDefault`, `Add`, `Update`, `Remove` +- Asynchronous: `GetByIdAsync`, `GetAllAsync`, `FindAsync`, `SingleOrDefaultAsync` + +### Entity-Specific Repositories +- `ProfileRepository : Repository, IProfileRepository` +- `ProfileSqlJobRepository : Repository, IProfileSqlJobRepository` +- `ProfileHistoryRepository : Repository, IProfileHistoryRepository` + +These can be extended with entity-specific query methods as needed. + +### UnitOfWork +Implements `IUnitOfWork` interface: +- Manages DbContext lifecycle +- Provides repository instances +- Handles transaction management via `SaveChanges`/`SaveChangesAsync` + +## Connection String + +Default connection string (configured in consuming applications): +``` +Server=SDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True; +``` + +## Database Schema Source + +SQL scripts and schema definitions are located at: +``` +M:\Datenbank\[DD_ECM]-Database\JobRunner\ +``` + +## Dependencies + +### .NET Framework 4.8 (`net480`) +- **ECMJobRunner.Domain** (project reference) +- **EntityFramework 6.5.1** (NuGet package) + +### .NET 8.0 (`net8.0`) +- **ECMJobRunner.Domain** (project reference) +- **Microsoft.EntityFrameworkCore 8.0.11** (NuGet package) +- **Microsoft.EntityFrameworkCore.SqlServer 8.0.11** (NuGet package) + +## Conditional Compilation + +The project uses `#if NET48` / `#else` directives to handle differences between EF6 and EF Core: + +**EF6 (.NET Framework 4.8):** +```csharp +#if NET48 +using System.Data.Entity; +public class JobRunnerDbContext : DbContext +#endif +``` + +**EF Core (.NET 8.0):** +```csharp +#if !NET48 +using Microsoft.EntityFrameworkCore; +public class JobRunnerDbContext : DbContext +#endif +``` + +## Building the Project + +```bash +dotnet build ECMJobRunner.Infrastructure.csproj +``` + +For specific framework: +```bash +dotnet build ECMJobRunner.Infrastructure.csproj -f net8.0 +dotnet build ECMJobRunner.Infrastructure.csproj -f net480 +``` + +## Entity Framework Differences + +### DbContext Constructor +- **EF6**: Accepts connection string directly +- **EF Core**: Requires `DbContextOptions` + +### Configuration +- **EF6**: `DbModelBuilder` in `OnModelCreating` +- **EF Core**: `ModelBuilder` in `OnModelCreating` + +### Querying +- **EF6**: `DbSet.AsNoTracking()` extension +- **EF Core**: Same API, built-in support + +### Async Operations +- **EF6**: Limited async support +- **EF Core**: Full async/await support + +## Development Notes + +- **Nullable navigation properties**: `IEnumerable?` to support defensive programming +- **No lazy loading**: Navigation properties loaded explicitly via `.Include()` +- **Transaction management**: Handled by Unit of Work pattern +- **Repository pattern**: Encapsulates EF-specific code +- **Clean Architecture**: Infrastructure depends on Domain, not vice versa + +## Usage Example + +```csharp +// Create DbContext (connection string from configuration) +var connectionString = ConfigurationManager.ConnectionStrings["JobRunner"].ConnectionString; +var context = new JobRunnerDbContext(connectionString); + +// Use Unit of Work +using var unitOfWork = new UnitOfWork(context); + +// Query profiles +var activeProfiles = await unitOfWork.Profiles + .FindAsync(p => p.Active); + +// Add new profile +var profile = new Profile { ProfileName = "Test", Active = true }; +unitOfWork.Profiles.Add(profile); +await unitOfWork.SaveChangesAsync(); +``` + +## Related Projects + +- **ECMJobRunner.Domain**: Contains entities and repository interfaces +- **ECMJobRunner.Application**: Application layer consuming repositories + +## Company Information + +**Author**: Digital Data GmbH +**Copyright**: 2026 +**Repository**: http://git.dd:3000/AppStd/ECMJobRunner.git diff --git a/ECMJobRunner.Infrastructure/DependencyExtension.cs b/ECMJobRunner.Infrastructure/DependencyExtension.cs new file mode 100644 index 0000000..2084571 --- /dev/null +++ b/ECMJobRunner.Infrastructure/DependencyExtension.cs @@ -0,0 +1,41 @@ +#if NET48 +using System.Data.Entity; +#else +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +#endif +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure.Data; +using ECMJobRunner.Infrastructure.Repositories; + +namespace ECMJobRunner.Infrastructure +{ +#if !NET48 + /// + /// Dependency injection extension methods for Infrastructure layer + /// + public static class DependencyExtension + { + /// + /// Add Infrastructure services to dependency injection container + /// + /// Service collection + /// Database connection string + /// Service collection for chaining + public static IServiceCollection AddInfrastructure(this IServiceCollection services, string connectionString) + { + // Register DbContext + services.AddDbContext(options => + options.UseSqlServer(connectionString)); + + // Register repositories + services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + } +#endif +} diff --git a/ECMJobRunner.Infrastructure/ECMJobRunner.Infrastructure.csproj b/ECMJobRunner.Infrastructure/ECMJobRunner.Infrastructure.csproj index 8f6f0fd..499d6ef 100644 --- a/ECMJobRunner.Infrastructure/ECMJobRunner.Infrastructure.csproj +++ b/ECMJobRunner.Infrastructure/ECMJobRunner.Infrastructure.csproj @@ -4,18 +4,24 @@ latest enable bin\$(Configuration)\$(TargetFramework)\$(MSBuildProjectName).xml - EECMJobRunner.Infrastructure + ECMJobRunner.Infrastructure Digital Data GmbH Digital Data GmbH - ECMJobRunner.Domain + ECMJobRunner.Infrastructure Copyright 2026 http://git.dd:3000/AppStd/ECMJobRunner.git - digital data ecm job runner Infrastructure + digital data ecm job runner infrastructure + + + + + + @@ -23,5 +29,7 @@ + + diff --git a/ECMJobRunner.Infrastructure/Repositories/CfgProfileRepository.cs b/ECMJobRunner.Infrastructure/Repositories/CfgProfileRepository.cs new file mode 100644 index 0000000..71e2375 --- /dev/null +++ b/ECMJobRunner.Infrastructure/Repositories/CfgProfileRepository.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using ECMJobRunner.Domain.Entities; +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure.Data; + +namespace ECMJobRunner.Infrastructure.Repositories +{ + /// + /// CfgProfile repository implementation + /// + public class CfgProfileRepository : Repository, ICfgProfileRepository + { + /// + /// Constructor + /// + public CfgProfileRepository(JobRunnerDbContext context, IMapper mapper) : base(context, mapper) + { + } + + // Entity-specific methods can be added here in the future + } +} diff --git a/ECMJobRunner.Infrastructure/Repositories/ProfileHistoryRepository.cs b/ECMJobRunner.Infrastructure/Repositories/ProfileHistoryRepository.cs new file mode 100644 index 0000000..4c311b5 --- /dev/null +++ b/ECMJobRunner.Infrastructure/Repositories/ProfileHistoryRepository.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using ECMJobRunner.Domain.Entities; +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure.Data; + +namespace ECMJobRunner.Infrastructure.Repositories +{ + /// + /// ProfileHistory repository implementation + /// + public class ProfileHistoryRepository : Repository, IProfileHistoryRepository + { + /// + /// Constructor + /// + public ProfileHistoryRepository(JobRunnerDbContext context, IMapper mapper) : base(context, mapper) + { + } + + // Entity-specific methods can be added here in the future + } +} diff --git a/ECMJobRunner.Infrastructure/Repositories/ProfileSqlJobRepository.cs b/ECMJobRunner.Infrastructure/Repositories/ProfileSqlJobRepository.cs new file mode 100644 index 0000000..0cc17b3 --- /dev/null +++ b/ECMJobRunner.Infrastructure/Repositories/ProfileSqlJobRepository.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using ECMJobRunner.Domain.Entities; +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure.Data; + +namespace ECMJobRunner.Infrastructure.Repositories +{ + /// + /// ProfileSqlJob repository implementation + /// + public class ProfileSqlJobRepository : Repository, IProfileSqlJobRepository + { + /// + /// Constructor + /// + public ProfileSqlJobRepository(JobRunnerDbContext context, IMapper mapper) : base(context, mapper) + { + } + + // Entity-specific methods can be added here in the future + } +} diff --git a/ECMJobRunner.Infrastructure/Repositories/Repository.cs b/ECMJobRunner.Infrastructure/Repositories/Repository.cs new file mode 100644 index 0000000..1849284 --- /dev/null +++ b/ECMJobRunner.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,202 @@ +#if NET48 +using System.Data.Entity; +#else +using Microsoft.EntityFrameworkCore; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using ECMJobRunner.Domain.Interfaces; +using ECMJobRunner.Infrastructure.Data; + +namespace ECMJobRunner.Infrastructure.Repositories +{ + /// + /// Generic repository implementation for Entity Framework + /// + /// Entity type + public class Repository : IRepository where TEntity : class + { + protected readonly JobRunnerDbContext _context; + protected readonly DbSet _dbSet; + protected readonly IMapper _mapper; + + /// + /// Constructor + /// + public Repository(JobRunnerDbContext context, IMapper mapper) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _dbSet = context.Set(); + } + + /// + public virtual async Task GetByIdAsync(long id, CancellationToken cancellationToken = default) + { +#if NET48 + return await Task.Run(() => _dbSet.Find(id), cancellationToken); +#else + return await _dbSet.FindAsync(new object[] { id }, cancellationToken); +#endif + } + + /// + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + { +#if NET48 + return await Task.Run(() => _dbSet.AsNoTracking().ToList(), cancellationToken); +#else + return await _dbSet.AsNoTracking().ToListAsync(cancellationToken); +#endif + } + + /// + public virtual async Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default) + { +#if NET48 + return await Task.Run(() => _dbSet.Where(predicate).AsNoTracking().ToList(), cancellationToken); +#else + return await _dbSet.Where(predicate).AsNoTracking().ToListAsync(cancellationToken); +#endif + } + + /// + public virtual async Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancellationToken = default) + { +#if NET48 + return await Task.Run(() => _dbSet.SingleOrDefault(predicate), cancellationToken); +#else + return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); +#endif + } + + /// + public virtual async Task AddAsync(TDto dto, CancellationToken cancellationToken = default) where TDto : class + { + if (dto == null) throw new ArgumentNullException(nameof(dto)); + + // Map DTO to new entity + var entity = _mapper.Map(dto); + +#if NET48 + await Task.Run(() => _dbSet.Add(entity), cancellationToken); +#else + await _dbSet.AddAsync(entity, cancellationToken); +#endif + + await SaveChangesAsync(cancellationToken); + return entity; + } + + /// + public virtual async Task AddRangeAsync(IEnumerable dtos, CancellationToken cancellationToken = default) where TDto : class + { + if (dtos == null) throw new ArgumentNullException(nameof(dtos)); + + var dtoList = dtos.ToList(); + if (!dtoList.Any()) + return 0; + + // Map DTOs to entities + var entities = _mapper.Map>(dtoList); + +#if NET48 + await Task.Run(() => _dbSet.AddRange(entities), cancellationToken); +#else + await _dbSet.AddRangeAsync(entities, cancellationToken); +#endif + + return await SaveChangesAsync(cancellationToken); + } + + /// + public virtual async Task UpdateAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class + { + if (dto == null) throw new ArgumentNullException(nameof(dto)); + + // Get entities with tracking enabled for update +#if NET48 + var entities = await Task.Run(() => _dbSet.Where(predicate).ToList(), cancellationToken); +#else + var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); +#endif + + if (!entities.Any()) + return 0; + + foreach (var entity in entities) + { + // Map DTO onto existing entity (only DTO properties are updated) + _mapper.Map(dto, entity); + } + + return await SaveChangesAsync(cancellationToken); + } + + /// + public virtual async Task UpdateSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class + { + if (dto == null) throw new ArgumentNullException(nameof(dto)); + + // Get entity with tracking enabled for update + var entity = await SingleOrDefaultAsync(predicate, cancellationToken); + + if (entity == null) + return false; + + // Map DTO onto existing entity (only DTO properties are updated) + _mapper.Map(dto, entity); + + await SaveChangesAsync(cancellationToken); + return true; + } + + /// + public virtual async Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + // Get entities with tracking enabled for delete +#if NET48 + var entities = await Task.Run(() => _dbSet.Where(predicate).ToList(), cancellationToken); +#else + var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); +#endif + + if (!entities.Any()) + return 0; + + _dbSet.RemoveRange(entities); + + return await SaveChangesAsync(cancellationToken); + } + + /// + public virtual async Task DeleteSingleAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + // Get entity with tracking enabled for delete + var entity = await SingleOrDefaultAsync(predicate, cancellationToken); + + if (entity == null) + return false; + + _dbSet.Remove(entity); + + await SaveChangesAsync(cancellationToken); + return true; + } + + /// + public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { +#if NET48 + return await _context.SaveChangesAsync(); +#else + return await _context.SaveChangesAsync(cancellationToken); +#endif + } + } +}