feat(infrastructure): add EF Core context and repository implementations

This commit is contained in:
2026-10-10 11:18:27 +02:00
parent 1fbcc2d197
commit 88456e7f87
5 changed files with 287 additions and 1 deletions

View File

@@ -1,4 +1,8 @@
using Microsoft.Extensions.Configuration;
using DigitalData.TaskFlow.Application.Common.Interfaces;
using DigitalData.TaskFlow.Infrastructure.Persistence;
using DigitalData.TaskFlow.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DigitalData.TaskFlow.Infrastructure;
@@ -7,6 +11,21 @@ public static class DependencyInjection
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("TaskFlowDb");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("Connection string 'TaskFlowDb' is not configured.");
}
services.AddDbContext<TaskFlowDbContext>(options =>
{
options.UseSqlServer(connectionString);
});
services.AddScoped<IWorkflowRepository, WorkflowRepository>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
return services;
}
}

View File

@@ -0,0 +1,111 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace DigitalData.TaskFlow.Infrastructure.Persistence.Models;
/// <summary>
/// Represents a stored procedure row shape for workflow listing data.
/// </summary>
public sealed class WorkflowListingReadModel
{
/// <summary>
/// Gets or sets the process identifier.
/// </summary>
[Column("processid")]
public int ProcessId { get; set; }
/// <summary>
/// Gets or sets the profile identifier.
/// </summary>
[Column("profileid")]
public int ProfileId { get; set; }
/// <summary>
/// Gets or sets the voucher type.
/// </summary>
[Column("belegart")]
public string VoucherType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the voucher number.
/// </summary>
[Column("belegnr")]
public string VoucherNumber { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the voucher date.
/// </summary>
[Column("belegdatum")]
public DateTime VoucherDate { get; set; }
/// <summary>
/// Gets or sets the cost center.
/// </summary>
[Column("kostenstelle")]
public string CostCenter { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the cost center description.
/// </summary>
[Column("kostenstellenbezeichnung")]
public string CostCenterDescription { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the client number.
/// </summary>
[Column("mandantnr")]
public int ClientNumber { get; set; }
/// <summary>
/// Gets or sets the client name.
/// </summary>
[Column("mandantbez")]
public string ClientName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the supplier number.
/// </summary>
[Column("lieferantnr")]
public string SupplierNumber { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the supplier name.
/// </summary>
[Column("lieferantbez")]
public string SupplierName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the net amount.
/// </summary>
[Column("netto")]
public double NetAmount { get; set; }
/// <summary>
/// Gets or sets the gross amount.
/// </summary>
[Column("brutto")]
public double GrossAmount { get; set; }
/// <summary>
/// Gets or sets the discount amount.
/// </summary>
[Column("skontobetrag")]
public double DiscountAmount { get; set; }
/// <summary>
/// Gets or sets the currency.
/// </summary>
[Column("waehrung")]
public string Currency { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the priority.
/// </summary>
[Column("priority")]
public int Priority { get; set; }
/// <summary>
/// Gets or sets the source group text.
/// </summary>
[Column("GROUP_TEXT")]
public string GroupText { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,26 @@
using DigitalData.TaskFlow.Infrastructure.Persistence.Models;
using Microsoft.EntityFrameworkCore;
namespace DigitalData.TaskFlow.Infrastructure.Persistence;
/// <summary>
/// EF Core DbContext for TaskFlow infrastructure data access.
/// </summary>
public sealed class TaskFlowDbContext(DbContextOptions<TaskFlowDbContext> options) : DbContext(options)
{
/// <summary>
/// Gets the keyless result model used for workflow listing procedure reads.
/// </summary>
public DbSet<WorkflowListingReadModel> WorkflowListingReadModels => Set<WorkflowListingReadModel>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<WorkflowListingReadModel>(entity =>
{
entity.HasNoKey();
entity.ToView(null);
});
}
}

View File

@@ -0,0 +1,27 @@
using DigitalData.TaskFlow.Application.Common.Interfaces;
using DigitalData.TaskFlow.Domain.Documents;
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
/// <summary>
/// Temporary document repository implementation used as Infrastructure phase baseline.
/// </summary>
public sealed class DocumentRepository : IDocumentRepository
{
/// <summary>
/// Retrieves binary document content by identifier.
/// </summary>
/// <param name="id">Document identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Binary document payload.</returns>
public Task<DocumentBinaryResult> GetAsync(string id, CancellationToken cancellationToken)
{
return Task.FromResult(new DocumentBinaryResult
{
Content = [],
ContentType = "application/octet-stream",
FileName = $"{id}.bin",
Length = 0
});
}
}

View File

@@ -0,0 +1,103 @@
using DigitalData.TaskFlow.Application.Common.Interfaces;
using DigitalData.TaskFlow.Domain.Closing;
using DigitalData.TaskFlow.Domain.Workflows;
using DigitalData.TaskFlow.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
/// <summary>
/// Workflow repository implementation for reading workflow listing data and applying close operations.
/// </summary>
public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowRepository
{
/// <summary>
/// Retrieves grouped workflow data for a user.
/// </summary>
/// <param name="wisagId">WISAG user identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Grouped workflow response.</returns>
public async Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken)
{
const string resultType = "table";
const string objectType = "Ferdinand";
var rows = await dbContext
.WorkflowListingReadModels
.FromSqlInterpolated($"EXEC dbo.PRCUST_F19_GET_LISTING_DATA @pUSER_ID={wisagId}, @pRESULT_TYPE={resultType}, @pOBJECT_TYPE={objectType}")
.AsNoTracking()
.ToListAsync(cancellationToken);
var listing = rows
.Select(row => new WorkflowListingItem
{
ProcessId = row.ProcessId,
ProfileId = row.ProfileId,
VoucherType = row.VoucherType,
VoucherNumber = row.VoucherNumber,
VoucherDate = DateOnly.FromDateTime(row.VoucherDate),
CostCenter = row.CostCenter,
CostCenterDescription = row.CostCenterDescription,
ClientNumber = row.ClientNumber,
ClientName = row.ClientName,
SupplierNumber = row.SupplierNumber,
SupplierName = row.SupplierName,
NetAmount = Convert.ToDecimal(row.NetAmount),
GrossAmount = Convert.ToDecimal(row.GrossAmount),
DiscountAmount = Convert.ToDecimal(row.DiscountAmount),
Currency = row.Currency,
Priority = row.Priority
})
.ToList();
var ferdinandBlock = new FerdinandSourceBlock
{
Source = "Ferdinand",
Profiles = [],
Listing = listing,
Approvals = [],
Documents = [],
ClosingMethods = [],
RejectionReasons = []
};
var contractsBlock = new ContractsSourceBlock
{
Source = "Contracts",
Profiles = [],
Listing = [],
Approvals = [],
Documents = [],
ClosingMethods = [],
RejectionReasons = []
};
return new WorkflowSourcesResponse
{
Sources = [ferdinandBlock, contractsBlock]
};
}
/// <summary>
/// Applies a closing decision to a workflow item.
/// </summary>
/// <param name="wisagId">WISAG user identifier.</param>
/// <param name="source">Source system name.</param>
/// <param name="processId">Process identifier.</param>
/// <param name="decision">Closing decision payload.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result of the close operation.</returns>
public Task<CloseWorkflowResult> CloseAsync(
int wisagId,
string source,
int processId,
CloseWorkflowDecision decision,
CancellationToken cancellationToken)
{
return Task.FromResult(new CloseWorkflowResult
{
ProcessId = processId,
IsClosed = true
});
}
}