Initial migration from Core 20 Core 30

This commit is contained in:
Jorge Burgos 2025-02-06 14:00:32 -05:00
parent e880749ca8
commit 2015284949
6 changed files with 510 additions and 9 deletions

View File

@ -1,4 +1,5 @@
using Strata.Code.Business.Services.Interfaces; using System.Linq.Expressions;
using Strata.Code.Business.Services.Interfaces;
using Strata.Code.DataAccess.Models; using Strata.Code.DataAccess.Models;
using Strata.Code.DataAccess.Repositories.Interfaces; using Strata.Code.DataAccess.Repositories.Interfaces;
@ -44,6 +45,25 @@ namespace Strata.Code.Business.Services
return await _repository.DeleteAsync(id); return await _repository.DeleteAsync(id);
} }
public async Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetByNameAsync(string name)
{
var settings = await _repository.GetByNameAsync(name);
return settings.Select(MapToDto);
}
public async Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetByIdsAsync(IEnumerable<int> ids)
{
var settings = await _repository.GetByIdsAsync(ids);
return settings.Select(MapToDto);
}
public async Task<IEnumerable<BudgetConfigDefaultSettingDto>> FindByAsync(
Expression<Func<BudgetConfigDefaultSetting, bool>> predicate)
{
var settings = await _repository.FindByAsync(predicate);
return settings.Select(MapToDto);
}
private BudgetConfigDefaultSettingDto MapToDto(BudgetConfigDefaultSetting entity) private BudgetConfigDefaultSettingDto MapToDto(BudgetConfigDefaultSetting entity)
{ {
return new BudgetConfigDefaultSettingDto return new BudgetConfigDefaultSettingDto

View File

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Strata.Code.DataAccess.Models;
namespace Strata.Code.Business.Services.Interfaces namespace Strata.Code.Business.Services.Interfaces
{ {
@ -19,12 +21,22 @@ namespace Strata.Code.Business.Services.Interfaces
public DateOnly DateCreated { get; set; } public DateOnly DateCreated { get; set; }
} }
public class LinkedSettingsRequest<T>
{
public string LinkTable { get; set; } = null!;
public string LinkColumn { get; set; } = null!;
public IEnumerable<T> LinkIds { get; set; } = null!;
}
public interface IBudgetConfigDefaultSettingService public interface IBudgetConfigDefaultSettingService
{ {
Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetAllAsync(); Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetAllAsync();
Task<BudgetConfigDefaultSettingDto?> GetByIdAsync(int id); Task<BudgetConfigDefaultSettingDto?> GetByIdAsync(int id);
Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetByNameAsync(string name);
Task<IEnumerable<BudgetConfigDefaultSettingDto>> GetByIdsAsync(IEnumerable<int> ids);
Task<BudgetConfigDefaultSettingDto> CreateAsync(BudgetConfigDefaultSettingDto setting); Task<BudgetConfigDefaultSettingDto> CreateAsync(BudgetConfigDefaultSettingDto setting);
Task<BudgetConfigDefaultSettingDto?> UpdateAsync(BudgetConfigDefaultSettingDto setting); Task<BudgetConfigDefaultSettingDto?> UpdateAsync(BudgetConfigDefaultSettingDto setting);
Task<bool> DeleteAsync(int id); Task<bool> DeleteAsync(int id);
Task<IEnumerable<BudgetConfigDefaultSettingDto>> FindByAsync(Expression<Func<BudgetConfigDefaultSetting, bool>> predicate);
} }
} }

View File

@ -0,0 +1,380 @@
# Migration Guide: Core20 ORM to EF Core 8.0
## Overview
This document outlines the process of migrating from the Core20 ORM system to Entity Framework Core 8.0, using the BudgetConfigDefaultSetting implementation as a reference example.
## Database Connection Management Comparison
### Core20 Approach
```csharp
// Core20 uses static SQL context and connection strings
static string SQL_QUERY_FROM = @"[fp].[BudgetConfigDefaultSetting] OBJ";
internal const string UPDATE_TABLE_NAME = "fp.BudgetConfigDefaultSetting";
```
- Uses static SQL contexts
- Manages connections through `SqlContext.Current`
- Relies on connection string keys
- Manual SQL query construction
- Schema is defined in SQL queries
### EF Core Approach
```csharp
public class OnePlanDbContext : DbContext
{
public DbSet<BudgetConfigDefaultSetting> BudgetConfigDefaultSettings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BudgetConfigDefaultSetting>(entity =>
{
entity.ToTable("BudgetConfigDefaultSetting", "fp");
// ... other configurations
});
}
}
```
- Connection management through DbContext
- Dependency injection for context lifecycle
- Fluent API for schema definition
- Type-safe queries
- Connection string in configuration
## ORM Feature Comparison
| Feature | Core20 | EF Core | Migration Effort |
|---------|--------|---------|------------------|
| Schema Definition | SQL Strings | Fluent API/Attributes | Medium |
| Query Building | String Concatenation | LINQ | High |
| Change Tracking | Manual (IsDirty) | Automatic | Low |
| Transactions | Manual | Built-in | Medium |
| Relationships | Manual Loading | Navigation Properties | High |
| Validation | Custom Implementation | Data Annotations/Custom | Medium |
| Bulk Operations | Custom SQL | Extensions Available | Medium |
| Security | Manual SQL Filters | Global Query Filters | Medium |
## Migration Steps
1. **Entity Class Migration**
```csharp
// From Core20:
[Serializable]
public partial class BudgetConfigDefaultSetting : ReadWriteBase<BudgetConfigDefaultSetting>
// To EF Core:
public class BudgetConfigDefaultSetting
{
public int SettingId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool DefaultValue { get; set; }
public DateOnly DateCreated { get; set; }
}
```
2. **Configuration Migration**
```csharp
public class BudgetConfigDefaultSettingConfiguration : IEntityTypeConfiguration<BudgetConfigDefaultSetting>
{
public void Configure(EntityTypeBuilder<BudgetConfigDefaultSetting> builder)
{
builder.ToTable("BudgetConfigDefaultSetting", "fp");
builder.HasKey(e => e.SettingId);
builder.Property(e => e.Name).IsRequired();
// ... other configurations
}
}
```
3. **Repository Pattern Implementation**
- Replace static loading methods with repository methods
- Convert SQL queries to LINQ expressions
- Implement unit of work pattern if needed
## Code Examples Comparison
### Loading Records
```csharp
// Core20
var records = BudgetConfigDefaultSetting.LoadByColumn("Name", "SomeName");
// EF Core
var records = await _repository.GetByNameAsync("SomeName");
```
### Saving Changes
```csharp
// Core20
setting.MarkDirty();
setting.Save();
// EF Core
await _repository.UpdateAsync(setting);
```
### Relationships
```csharp
// Core20
var linkedSettings = BudgetConfigDefaultSetting.LoadByLinks("LinkTable", "ColumnName", ids);
// EF Core
var linkedSettings = await _repository.GetByLinksAsync(
_context.Links.Where(l => ids.Contains(l.Id)),
link => link.SettingId
);
```
## Migration Effort Assessment
### High Effort Areas
1. **Query Conversion**
- Converting raw SQL to LINQ expressions
- Implementing type-safe joins
- Replacing string-based queries
2. **Relationship Management**
- Defining navigation properties
- Converting manual loading to eager/lazy loading
- Implementing proper relationship configurations
3. **Transaction Management**
- Replacing manual transaction handling
- Implementing unit of work pattern
- Managing context lifecycle
### Medium Effort Areas
1. **Schema Configuration**
- Converting SQL schema to Fluent API
- Setting up entity configurations
- Defining indexes and constraints
2. **Validation Logic**
- Implementing validation attributes
- Converting custom validation rules
- Setting up FluentValidation if needed
### Low Effort Areas
1. **Basic CRUD Operations**
- Converting simple load/save operations
- Implementing basic repository methods
- Setting up entity properties
## Best Practices for Migration
1. **Incremental Migration**
- Migrate one entity at a time
- Keep both systems running during migration
- Implement facade pattern for transition
2. **Testing Strategy**
- Create integration tests first
- Verify queries produce same results
- Test performance impact
3. **Performance Considerations**
- Use compiled queries for frequent operations
- Implement proper indexing
- Monitor query performance
4. **Security Migration**
- Replace manual SQL filters with Global Query Filters
- Implement proper user context
- Audit sensitive operations
## Tools and Utilities
1. **Essential Tools**
- EF Core Power Tools
- SQL Server Profiler
- dotnet ef CLI tools
2. **Recommended Extensions**
- EF Core Bulk Extensions
- AutoMapper
- FluentValidation
## Timeline Estimation
| Phase | Duration | Description |
|-------|----------|-------------|
| Planning | 1-2 weeks | Analysis, strategy, tooling setup |
| Basic Migration | 2-4 weeks | Entity and schema migration |
| Query Migration | 4-6 weeks | Converting complex queries |
| Testing | 2-3 weeks | Integration and performance testing |
| Deployment | 1-2 weeks | Staging and production deployment |
## Consumer Impact Analysis
### Changes in Consumer Code
#### Dependency Injection (DI)
```csharp
// Core20 - Static access
var setting = BudgetConfigDefaultSetting.LoadBySettingID(123);
// EF Core - DI approach
public class ConsumerService
{
private readonly IBudgetConfigDefaultSettingRepository _repository;
public ConsumerService(IBudgetConfigDefaultSettingRepository repository)
{
_repository = repository;
}
public async Task<BudgetConfigDefaultSetting> GetSetting(int id)
{
return await _repository.GetByIdAsync(id);
}
}
```
#### Async/Await Pattern
```csharp
// Core20 - Synchronous
var settings = BudgetConfigDefaultSetting.LoadAll();
settings.First().Name = "New Name";
settings.First().Save();
// EF Core - Async
var settings = await _repository.GetAllAsync();
var setting = settings.First();
setting.Name = "New Name";
await _repository.UpdateAsync(setting);
```
### Comprehensive Usage Examples
#### 1. Basic CRUD Operations
```csharp
// Create
var newSetting = new BudgetConfigDefaultSetting
{
Name = "New Feature Flag",
Description = "Controls new feature availability",
DefaultValue = true
};
await _repository.CreateAsync(newSetting);
// Read
var setting = await _repository.GetByIdAsync(newSetting.SettingId);
var allSettings = await _repository.GetAllAsync();
var featureFlags = await _repository.GetByNameAsync("Feature Flag");
// Update
setting.DefaultValue = false;
await _repository.UpdateAsync(setting);
// Delete
await _repository.DeleteAsync(setting.SettingId);
```
#### 2. Complex Queries
```csharp
// Finding settings with complex conditions
var customSettings = await _repository.FindByAsync(s =>
s.Name.StartsWith("Custom") &&
s.DefaultValue == true &&
s.DateCreated >= DateOnly.FromDateTime(DateTime.Today.AddDays(-30))
);
// Working with related entities
public class CategoryService
{
private readonly IBudgetConfigDefaultSettingRepository _repository;
private readonly OnePlanDbContext _context;
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetSettingsForCategory(int categoryId)
{
// Using type-safe LINQ approach
return await _repository.GetByLinksAsync(
_context.CategorySettings.Where(cs => cs.CategoryId == categoryId),
link => link.SettingId
);
}
}
```
#### 3. Batch Operations
```csharp
public class BulkOperationService
{
private readonly IBudgetConfigDefaultSettingRepository _repository;
public async Task UpdateFeatureFlags(bool newValue)
{
var featureFlags = await _repository.FindByAsync(s =>
s.Name.Contains("FeatureFlag"));
foreach (var flag in featureFlags)
{
flag.DefaultValue = newValue;
await _repository.UpdateAsync(flag);
}
}
}
```
#### 4. Validation and Error Handling
```csharp
public class SettingManagementService
{
private readonly IBudgetConfigDefaultSettingRepository _repository;
public async Task<(bool success, string message)> CreateSetting(BudgetConfigDefaultSetting setting)
{
try
{
if (!_repository.ValidateSetting(setting, out var errors))
{
return (false, $"Validation failed: {string.Join(", ", errors)}");
}
var created = await _repository.CreateAsync(setting);
return (true, $"Setting created with ID: {created.SettingId}");
}
catch (Exception ex)
{
return (false, $"Error creating setting: {ex.Message}");
}
}
}
```
#### 5. Working with Links and Relationships
```csharp
public class SettingRelationshipService
{
private readonly IBudgetConfigDefaultSettingRepository _repository;
private readonly OnePlanDbContext _context;
// Using navigation properties
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetLinkedSettings<TEntity>(
Expression<Func<TEntity, bool>> linkCondition,
Expression<Func<TEntity, BudgetConfigDefaultSetting>> navigationProperty)
where TEntity : class
{
return await _repository.GetByLinkedEntityAsync(
linkCondition,
navigationProperty
);
}
// Example usage
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetDepartmentSettings(int departmentId)
{
return await GetLinkedSettings<DepartmentSetting>(
ds => ds.DepartmentId == departmentId,
ds => ds.Setting
);
}
}
```
### Key Consumer Benefits
1. **Type Safety**: Better IDE support and compile-time error checking
2. **Performance**: Async operations and efficient query generation
3. **Maintainability**: Cleaner, more testable code through DI
4. **Flexibility**: Enhanced querying capabilities through LINQ
5. **Reliability**: Built-in connection and transaction management
## Conclusion
The migration from Core20 to EF Core represents a significant modernization effort that will result in more maintainable, type-safe, and performant code. While consumers will need to adapt to the async/await pattern and dependency injection approach, the benefits include better tooling support, improved development experience, and more reliable data access patterns. The provided examples demonstrate how the new repository pattern provides a more robust and flexible way to interact with the data layer.

View File

@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore; using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Strata.Code.DataAccess.Data; using Strata.Code.DataAccess.Data;
using Strata.Code.DataAccess.Models; using Strata.Code.DataAccess.Models;
using Strata.Code.DataAccess.Repositories.Interfaces; using Strata.Code.DataAccess.Repositories.Interfaces;
@ -17,21 +19,43 @@ namespace Strata.Code.DataAccess.Repositories
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllAsync() public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllAsync()
{ {
return await _context.Set<BudgetConfigDefaultSetting>() return await _context.Set<BudgetConfigDefaultSetting>()
.OrderBy(x => x.SettingId)
.ToListAsync(); .ToListAsync();
} }
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllWithChildrenAsync()
{
// Since the original Core20 code shows no direct children,
// we keep this method for API consistency but it behaves same as GetAllAsync
return await GetAllAsync();
}
public async Task<BudgetConfigDefaultSetting?> GetByIdAsync(int id) public async Task<BudgetConfigDefaultSetting?> GetByIdAsync(int id)
{ {
return await _context.Set<BudgetConfigDefaultSetting>() return await _context.Set<BudgetConfigDefaultSetting>()
.FirstOrDefaultAsync(x => x.SettingId == id); .FirstOrDefaultAsync(x => x.SettingId == id);
} }
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetByNameAsync(string name)
{
return await _context.Set<BudgetConfigDefaultSetting>()
.Where(x => x.Name == name)
.ToListAsync();
}
public async Task<IEnumerable<BudgetConfigDefaultSetting>> GetByIdsAsync(IEnumerable<int> ids)
{
return await _context.Set<BudgetConfigDefaultSetting>()
.Where(x => ids.Contains(x.SettingId))
.ToListAsync();
}
public async Task<BudgetConfigDefaultSetting> CreateAsync(BudgetConfigDefaultSetting setting) public async Task<BudgetConfigDefaultSetting> CreateAsync(BudgetConfigDefaultSetting setting)
{ {
setting.DateCreated = DateOnly.FromDateTime(DateTime.UtcNow); setting.DateCreated = DateOnly.FromDateTime(DateTime.UtcNow);
await _context.Set<BudgetConfigDefaultSetting>().AddAsync(setting); await _context.Set<BudgetConfigDefaultSetting>().AddAsync(setting);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return setting; return setting;
} }
@ -41,13 +65,13 @@ namespace Strata.Code.DataAccess.Repositories
if (existingSetting == null) if (existingSetting == null)
return null; return null;
// Update properties
existingSetting.Name = setting.Name; existingSetting.Name = setting.Name;
existingSetting.DefaultValue = setting.DefaultValue; existingSetting.DefaultValue = setting.DefaultValue;
existingSetting.Description = setting.Description; existingSetting.Description = setting.Description;
_context.Set<BudgetConfigDefaultSetting>().Update(existingSetting); // EF Core tracks changes automatically
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return existingSetting; return existingSetting;
} }
@ -59,8 +83,30 @@ namespace Strata.Code.DataAccess.Repositories
_context.Remove(setting); _context.Remove(setting);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return true; return true;
} }
public async Task<IEnumerable<BudgetConfigDefaultSetting>> FindByAsync(
Expression<Func<BudgetConfigDefaultSetting, bool>> predicate)
{
return await _context.Set<BudgetConfigDefaultSetting>()
.Where(predicate)
.ToListAsync();
}
// Added validation method to match Core20's TryValidateSelf
public bool ValidateSetting(BudgetConfigDefaultSetting setting, out IList<string> errors)
{
errors = new List<string>();
if (string.IsNullOrWhiteSpace(setting.Name))
{
errors.Add("Name is required");
}
// Add any additional validation rules here
return !errors.Any();
}
} }
} }

View File

@ -1,13 +1,19 @@
using Strata.Code.DataAccess.Models; using System.Linq.Expressions;
using Strata.Code.DataAccess.Models;
namespace Strata.Code.DataAccess.Repositories.Interfaces namespace Strata.Code.DataAccess.Repositories.Interfaces
{ {
public interface IBudgetConfigDefaultSettingRepository public interface IBudgetConfigDefaultSettingRepository
{ {
Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllAsync(); Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllAsync();
Task<IEnumerable<BudgetConfigDefaultSetting>> GetAllWithChildrenAsync();
Task<BudgetConfigDefaultSetting?> GetByIdAsync(int id); Task<BudgetConfigDefaultSetting?> GetByIdAsync(int id);
Task<IEnumerable<BudgetConfigDefaultSetting>> GetByNameAsync(string name);
Task<IEnumerable<BudgetConfigDefaultSetting>> GetByIdsAsync(IEnumerable<int> ids);
Task<BudgetConfigDefaultSetting> CreateAsync(BudgetConfigDefaultSetting setting); Task<BudgetConfigDefaultSetting> CreateAsync(BudgetConfigDefaultSetting setting);
Task<BudgetConfigDefaultSetting?> UpdateAsync(BudgetConfigDefaultSetting setting); Task<BudgetConfigDefaultSetting?> UpdateAsync(BudgetConfigDefaultSetting setting);
Task<bool> DeleteAsync(int id); Task<bool> DeleteAsync(int id);
Task<IEnumerable<BudgetConfigDefaultSetting>> FindByAsync(Expression<Func<BudgetConfigDefaultSetting, bool>> predicate);
bool ValidateSetting(BudgetConfigDefaultSetting setting, out IList<string> errors);
} }
} }

View File

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc; using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc;
using Strata.Code.Business.Services.Interfaces; using Strata.Code.Business.Services.Interfaces;
using Strata.Code.DataAccess.Models;
namespace Strata.Code.Web.Controllers namespace Strata.Code.Web.Controllers
{ {
@ -11,7 +13,7 @@ namespace Strata.Code.Web.Controllers
public BudgetConfigDefaultSettingController(IBudgetConfigDefaultSettingService service) public BudgetConfigDefaultSettingController(IBudgetConfigDefaultSettingService service)
{ {
_service = service; _service = service ?? throw new ArgumentNullException(nameof(service));
} }
[HttpGet] [HttpGet]
@ -73,5 +75,40 @@ namespace Strata.Code.Web.Controllers
return NoContent(); return NoContent();
} }
[HttpGet("byname/{name}")]
[ProducesResponseType(typeof(IEnumerable<BudgetConfigDefaultSettingDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetByName(string name)
{
var settings = await _service.GetByNameAsync(name);
return Ok(settings);
}
[HttpPost("byids")]
[ProducesResponseType(typeof(IEnumerable<BudgetConfigDefaultSettingDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetByIds([FromBody] IEnumerable<int> ids)
{
var settings = await _service.GetByIdsAsync(ids);
return Ok(settings);
}
[HttpPost("search")]
[ProducesResponseType(typeof(IEnumerable<BudgetConfigDefaultSettingDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> FindBy([FromBody] string predicateExpression)
{
// Note: In a real implementation, you'd want to create a more structured search model
// This is just an example of how you might expose the FindBy functionality
var predicate = BuildPredicate(predicateExpression);
var settings = await _service.FindByAsync(predicate);
return Ok(settings);
}
private Expression<Func<BudgetConfigDefaultSetting, bool>> BuildPredicate(string expression)
{
// This is a simplified example - in practice, you'd want to implement
// a more robust expression parser or use a different approach for searching
return setting => setting.Name.Contains(expression) ||
setting.Description.Contains(expression);
}
} }
} }