From 2015284949f53dd8d9a1d89b0751b1cbcc55fc28 Mon Sep 17 00:00:00 2001 From: Jorge Burgos Date: Thu, 6 Feb 2025 14:00:32 -0500 Subject: [PATCH] Initial migration from Core 20 Core 30 --- .../BudgetConfigDefaultSettingService.cs | 22 +- .../IBudgetConfigDefaultSettingService.cs | 12 + .../src/Strata.Code.DataAccess/ReadMe.MD | 380 ++++++++++++++++++ .../BudgetConfigDefaultSettingRepository.cs | 56 ++- .../IBudgetConfigDefaultSettingRepository.cs | 8 +- .../BudgetConfigDefaultSettingController .cs | 41 +- 6 files changed, 510 insertions(+), 9 deletions(-) create mode 100644 ef-migration/src/Strata.Code.DataAccess/ReadMe.MD diff --git a/ef-migration/src/Strata.Code.Business/Services/BudgetConfigDefaultSettingService.cs b/ef-migration/src/Strata.Code.Business/Services/BudgetConfigDefaultSettingService.cs index 04078b3..40804d0 100644 --- a/ef-migration/src/Strata.Code.Business/Services/BudgetConfigDefaultSettingService.cs +++ b/ef-migration/src/Strata.Code.Business/Services/BudgetConfigDefaultSettingService.cs @@ -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.Repositories.Interfaces; @@ -44,6 +45,25 @@ namespace Strata.Code.Business.Services return await _repository.DeleteAsync(id); } + public async Task> GetByNameAsync(string name) + { + var settings = await _repository.GetByNameAsync(name); + return settings.Select(MapToDto); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + var settings = await _repository.GetByIdsAsync(ids); + return settings.Select(MapToDto); + } + + public async Task> FindByAsync( + Expression> predicate) + { + var settings = await _repository.FindByAsync(predicate); + return settings.Select(MapToDto); + } + private BudgetConfigDefaultSettingDto MapToDto(BudgetConfigDefaultSetting entity) { return new BudgetConfigDefaultSettingDto diff --git a/ef-migration/src/Strata.Code.Business/Services/Interfaces/IBudgetConfigDefaultSettingService.cs b/ef-migration/src/Strata.Code.Business/Services/Interfaces/IBudgetConfigDefaultSettingService.cs index d4fc601..330ad06 100644 --- a/ef-migration/src/Strata.Code.Business/Services/Interfaces/IBudgetConfigDefaultSettingService.cs +++ b/ef-migration/src/Strata.Code.Business/Services/Interfaces/IBudgetConfigDefaultSettingService.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; +using Strata.Code.DataAccess.Models; namespace Strata.Code.Business.Services.Interfaces { @@ -19,12 +21,22 @@ namespace Strata.Code.Business.Services.Interfaces public DateOnly DateCreated { get; set; } } + public class LinkedSettingsRequest + { + public string LinkTable { get; set; } = null!; + public string LinkColumn { get; set; } = null!; + public IEnumerable LinkIds { get; set; } = null!; + } + public interface IBudgetConfigDefaultSettingService { Task> GetAllAsync(); Task GetByIdAsync(int id); + Task> GetByNameAsync(string name); + Task> GetByIdsAsync(IEnumerable ids); Task CreateAsync(BudgetConfigDefaultSettingDto setting); Task UpdateAsync(BudgetConfigDefaultSettingDto setting); Task DeleteAsync(int id); + Task> FindByAsync(Expression> predicate); } } diff --git a/ef-migration/src/Strata.Code.DataAccess/ReadMe.MD b/ef-migration/src/Strata.Code.DataAccess/ReadMe.MD new file mode 100644 index 0000000..0ab8c26 --- /dev/null +++ b/ef-migration/src/Strata.Code.DataAccess/ReadMe.MD @@ -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 BudgetConfigDefaultSettings { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(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 + + // 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 + { + public void Configure(EntityTypeBuilder 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 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> 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> GetLinkedSettings( + Expression> linkCondition, + Expression> navigationProperty) + where TEntity : class + { + return await _repository.GetByLinkedEntityAsync( + linkCondition, + navigationProperty + ); + } + + // Example usage + public async Task> GetDepartmentSettings(int departmentId) + { + return await GetLinkedSettings( + 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. \ No newline at end of file diff --git a/ef-migration/src/Strata.Code.DataAccess/Repositories/BudgetConfigDefaultSettingRepository.cs b/ef-migration/src/Strata.Code.DataAccess/Repositories/BudgetConfigDefaultSettingRepository.cs index 4301fe4..2f252d0 100644 --- a/ef-migration/src/Strata.Code.DataAccess/Repositories/BudgetConfigDefaultSettingRepository.cs +++ b/ef-migration/src/Strata.Code.DataAccess/Repositories/BudgetConfigDefaultSettingRepository.cs @@ -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.Models; using Strata.Code.DataAccess.Repositories.Interfaces; @@ -17,21 +19,43 @@ namespace Strata.Code.DataAccess.Repositories public async Task> GetAllAsync() { return await _context.Set() + .OrderBy(x => x.SettingId) .ToListAsync(); } + public async Task> 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 GetByIdAsync(int id) { return await _context.Set() .FirstOrDefaultAsync(x => x.SettingId == id); } + public async Task> GetByNameAsync(string name) + { + return await _context.Set() + .Where(x => x.Name == name) + .ToListAsync(); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + return await _context.Set() + .Where(x => ids.Contains(x.SettingId)) + .ToListAsync(); + } + public async Task CreateAsync(BudgetConfigDefaultSetting setting) { setting.DateCreated = DateOnly.FromDateTime(DateTime.UtcNow); + await _context.Set().AddAsync(setting); await _context.SaveChangesAsync(); - return setting; } @@ -41,13 +65,13 @@ namespace Strata.Code.DataAccess.Repositories if (existingSetting == null) return null; + // Update properties existingSetting.Name = setting.Name; existingSetting.DefaultValue = setting.DefaultValue; existingSetting.Description = setting.Description; - _context.Set().Update(existingSetting); + // EF Core tracks changes automatically await _context.SaveChangesAsync(); - return existingSetting; } @@ -59,8 +83,30 @@ namespace Strata.Code.DataAccess.Repositories _context.Remove(setting); await _context.SaveChangesAsync(); - return true; } + + public async Task> FindByAsync( + Expression> predicate) + { + return await _context.Set() + .Where(predicate) + .ToListAsync(); + } + + // Added validation method to match Core20's TryValidateSelf + public bool ValidateSetting(BudgetConfigDefaultSetting setting, out IList errors) + { + errors = new List(); + + if (string.IsNullOrWhiteSpace(setting.Name)) + { + errors.Add("Name is required"); + } + + // Add any additional validation rules here + + return !errors.Any(); + } } } diff --git a/ef-migration/src/Strata.Code.DataAccess/Repositories/Interfaces/IBudgetConfigDefaultSettingRepository.cs b/ef-migration/src/Strata.Code.DataAccess/Repositories/Interfaces/IBudgetConfigDefaultSettingRepository.cs index d5b4b00..2d1c5c0 100644 --- a/ef-migration/src/Strata.Code.DataAccess/Repositories/Interfaces/IBudgetConfigDefaultSettingRepository.cs +++ b/ef-migration/src/Strata.Code.DataAccess/Repositories/Interfaces/IBudgetConfigDefaultSettingRepository.cs @@ -1,13 +1,19 @@ -using Strata.Code.DataAccess.Models; +using System.Linq.Expressions; +using Strata.Code.DataAccess.Models; namespace Strata.Code.DataAccess.Repositories.Interfaces { public interface IBudgetConfigDefaultSettingRepository { Task> GetAllAsync(); + Task> GetAllWithChildrenAsync(); Task GetByIdAsync(int id); + Task> GetByNameAsync(string name); + Task> GetByIdsAsync(IEnumerable ids); Task CreateAsync(BudgetConfigDefaultSetting setting); Task UpdateAsync(BudgetConfigDefaultSetting setting); Task DeleteAsync(int id); + Task> FindByAsync(Expression> predicate); + bool ValidateSetting(BudgetConfigDefaultSetting setting, out IList errors); } } diff --git a/ef-migration/src/Strata.Code.Web/Controllers/BudgetConfigDefaultSettingController .cs b/ef-migration/src/Strata.Code.Web/Controllers/BudgetConfigDefaultSettingController .cs index 4b1e891..4f96599 100644 --- a/ef-migration/src/Strata.Code.Web/Controllers/BudgetConfigDefaultSettingController .cs +++ b/ef-migration/src/Strata.Code.Web/Controllers/BudgetConfigDefaultSettingController .cs @@ -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.DataAccess.Models; namespace Strata.Code.Web.Controllers { @@ -11,7 +13,7 @@ namespace Strata.Code.Web.Controllers public BudgetConfigDefaultSettingController(IBudgetConfigDefaultSettingService service) { - _service = service; + _service = service ?? throw new ArgumentNullException(nameof(service)); } [HttpGet] @@ -73,5 +75,40 @@ namespace Strata.Code.Web.Controllers return NoContent(); } + + [HttpGet("byname/{name}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetByName(string name) + { + var settings = await _service.GetByNameAsync(name); + return Ok(settings); + } + + [HttpPost("byids")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetByIds([FromBody] IEnumerable ids) + { + var settings = await _service.GetByIdsAsync(ids); + return Ok(settings); + } + + [HttpPost("search")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task 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> 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); + } } }