diff --git a/.dockerignore b/.dockerignore index fe1152b..38a960a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,6 +21,7 @@ **/obj **/secrets.dev.yaml **/values.dev.yaml +**/changelogs LICENSE README.md !**/.gitignore diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e327422 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,31 @@ +# GitHub Copilot Instructions + +Welcome to the `csla-mcp` repository! To ensure Copilot provides relevant and high-quality suggestions, please follow these guidelines: + +## Coding Style +- Use consistent indentation (4 spaces). +- Prefer explicit variable names. +- Follow C# conventions for naming and structure. + +## Documentation +- Add XML comments to public classes and methods. +- Use summary tags for method descriptions. + +## Best Practices +- Write clean, maintainable code. +- Avoid hardcoding values; use configuration where possible. +- Implement error handling and logging. + +## Pull Requests +- Ensure code is tested before submitting. +- Include a clear description of changes. + +## Copilot Usage +- Use Copilot to generate boilerplate, tests, and documentation. +- Review and edit Copilot suggestions for accuracy and security. + +## Change logs and change documents +- Create all change logs and change documents in the `changelogs` folder. +- When documenting fixes or other changes, put the documents in the `changelogs` folder. + +Thank you for contributing! \ No newline at end of file diff --git a/.gitignore b/.gitignore index bc78471..d5dde41 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from `dotnet new gitignore` +changelogs/ + # dotenv files .env diff --git a/SEMANTIC_SEARCH_README.md b/SEMANTIC_SEARCH_README.md deleted file mode 100644 index b5f2e0c..0000000 --- a/SEMANTIC_SEARCH_README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Semantic Search Implementation - -This document describes the semantic search feature that has been added to the CSLA MCP Server. - -## Overview - -The server now supports both **word-based matching** (original functionality) and **semantic search** using vector embeddings. Both search methods run in parallel and return results in separate sections. - -## Key Components - -### 1. VectorStoreService (`Services/VectorStoreService.cs`) - -An in-memory vector store that: -- Communicates with Ollama's API to generate embeddings using the `nomic-embed-text:latest` model -- Stores document embeddings in memory -- Performs cosine similarity calculations for semantic search -- Manages the document index - -**Key Methods:** -- `GenerateEmbeddingAsync(string text)` - Generates a vector embedding for text using Ollama -- `IndexDocumentAsync(string fileName, string content)` - Indexes a document into the vector store -- `SearchAsync(string query, int topK)` - Performs semantic search and returns top K results -- `CosineSimilarity(float[] vector1, float[] vector2)` - Calculates cosine similarity between vectors - -### 2. Updated CslaCodeTool (`Tools/CslaCodeTool.cs`) - -The search tool now: -- Maintains a reference to the VectorStoreService -- Performs both word-based and semantic searches -- Returns results in a new `CombinedSearchResult` format - -**New Classes:** -- `SemanticMatch` - Represents a semantic search result with similarity score -- `CombinedSearchResult` - Container for both semantic and word matches - -**Updated Search Response Format:** -```json -{ - "SemanticMatches": [ - { - "FileName": "EditableRoot.md", - "SimilarityScore": 0.87 - } - ], - "WordMatches": [ - { - "Score": 15, - "FileName": "ReadWriteProperty.md", - "MatchingWords": [ - { - "Word": "property", - "Count": 10 - } - ] - } - ] -} -``` - -### 3. Startup Initialization (`Program.cs`) - -On startup, the application: -1. Creates a VectorStoreService instance -2. Asynchronously indexes all .cs and .md files from the code samples directory -3. Starts the web server without waiting for indexing to complete -4. Semantic search becomes available as files are indexed - -The indexing happens in the background and logs progress: -``` -[Startup] Initializing vector store with Ollama... -[Startup] Starting to index 15 files... -[Startup] Indexed 5/15 files... -[Startup] Indexed 10/15 files... -[Startup] Completed indexing 15 files -``` - -## Configuration - -### Ollama Setup - -The implementation uses Ollama running locally with the `nomic-embed-text:latest` model. - -**Prerequisites:** -1. Install Ollama: https://ollama.ai -2. Pull the embedding model: - ```bash - ollama pull nomic-embed-text:latest - ``` -3. Ensure Ollama is running (default endpoint: `http://localhost:11434`) - -### Environment Variables - -You can customize the Ollama endpoint by modifying the VectorStoreService initialization in `Program.cs`: - -```csharp -var vectorStore = new VectorStoreService( - ollamaEndpoint: "http://localhost:11434", // Custom endpoint - modelName: "nomic-embed-text:latest" // Custom model -); -``` - -## Usage - -### Search Behavior - -When a search is performed: -1. **Word Matching** (always runs): Searches for keyword matches in document content -2. **Semantic Matching** (if vector store ready): Searches for semantically similar documents using embeddings - -Both results are returned simultaneously in the combined response. - -### Semantic Match Filtering - -Semantic matches with similarity scores below 0.1 are filtered out to reduce noise. This threshold can be adjusted in `VectorStoreService.SearchAsync()`: - -```csharp -.Where(r => r.SimilarityScore > 0.1f) // Adjust threshold here -``` - -### Top K Results - -By default, semantic search returns up to 10 results. This can be adjusted in `CslaCodeTool.Search()`: - -```csharp -var semanticResults = VectorStore.SearchAsync(message, topK: 10).GetAwaiter().GetResult(); -``` - -## Dependencies - -New package added: -- `System.Numerics.Tensors` - Provides efficient tensor operations for cosine similarity calculations - -## Performance Considerations - -1. **Startup Time**: Indexing happens asynchronously, so the server starts immediately but semantic search may not be available for the first few seconds. - -2. **Memory Usage**: All embeddings are stored in memory. For large document collections, consider: - - Implementing pagination - - Using a persistent vector database - - Adding memory limits - -3. **Ollama Performance**: Embedding generation depends on Ollama's performance. Ensure Ollama has adequate resources. - -## Error Handling - -The implementation gracefully handles: -- Ollama connection failures (semantic search disabled, word search continues) -- Individual file indexing failures (logs error, continues with other files) -- Missing or invalid embeddings (skips semantic results, returns word matches) - -## Future Enhancements - -Potential improvements: -1. Persistent vector store (e.g., using SQLite with vector extensions) -2. Incremental indexing for new/modified files -3. Configurable similarity threshold via environment variable -4. Hybrid scoring that combines word and semantic scores -5. Batch embedding generation for faster indexing -6. Support for alternative embedding models diff --git a/csla-examples/v10/Command.md b/csla-examples/v10/Command.md new file mode 100644 index 0000000..3206c54 --- /dev/null +++ b/csla-examples/v10/Command.md @@ -0,0 +1,39 @@ +# Command Stereotype + +This example demonstrates a complete CSLA business class named `CustomerExists` that includes various property types and data access methods. The class derives from `CommandBase` and includes properties for input parameters and output results. + +This class demonstrates the command business class stereotype. + +It also includes a data portal operation method for executing the command. Note that the data access method contains a placeholder comment where actual data access logic should be invoked. + +```csharp +using System; +using Csla; + +[CslaImplementProperties] +public partial class CustomerExists : CommandBase +{ + public partial bool Exists { get; set; } + + [Execute] + private async Task Execute(string email, [Inject] ICustomerDal dal) + { + // Placeholder for actual data access logic + var customer = await dal.GetByEmailAsync(email); + Exists = customer != null; + } +} +``` + +> **Note:** The `ICustomerDal` interface is assumed to be defined elsewhere in your codebase and is responsible for data access operations related to customers. The `GetByEmailAsync` method is a placeholder for the actual implementation that retrieves a customer by their email address. + +This class can be used to check if a customer with a specific email address exists in the data store by setting the `Email` property and then calling the data portal to execute the command. The result will be available in the `Exists` property. + +To execute this command, you would typically do something like the following: + +```csharp +var command = await customerExistsPortal.ExecuteAsync("customer@example.com"); +bool customerExists = command.Exists; +``` + +This assumes `customerExistsPortal` is a service of type `IDataPortal`. diff --git a/csla-examples/v10/CslaClassLibrary.md b/csla-examples/v10/CslaClassLibrary.md new file mode 100644 index 0000000..cc5ae71 --- /dev/null +++ b/csla-examples/v10/CslaClassLibrary.md @@ -0,0 +1,32 @@ +# CSLA .NET Class Library + +Most CSLA .NET applications use a class library project to hold the business classes. This project typically references the CSLA .NET framework and any other necessary libraries. + +```xml + + + + net10.0 + enable + enable + 14 + + + + + + + +``` + +CSLA 10 supports `TargetFramework` of .NET Framework 4.8, net8.0, net9.0, and net10.0. + +CSLA 10 supports nullable reference types in its API, so `Nullable` is enabled. + +CSLA 10 uses features from C# version 14, so the `LangVersion` is set to 14 (or higher). + +The `Csla` package is referenced to enable the use of CSLA .NET features such as the rules engine, data portal, and other capabilities. + +The `AutoImplementProperties` code generator package is referenced to enable standard code generation for CSLA properties. diff --git a/csla-examples/v10/EditableChild.md b/csla-examples/v10/EditableChild.md new file mode 100644 index 0000000..ca86650 --- /dev/null +++ b/csla-examples/v10/EditableChild.md @@ -0,0 +1,91 @@ +# Editable Child Stereotype + +This example demonstrates a complete CSLA business class named `OrderItemEdit` that includes various property types, business rules, authorization rules, and data access methods. The class derives from `BusinessBase` and includes both read-only and read-write properties. + +This class demonstrates the editable child business class stereotype. + +It also shows how to implement business rules for validation, including required fields and range constraints. Additionally, it includes object-level authorization rules to control access based on user roles. + +It also includes data portal operation methods for creating, fetching, inserting, updating, and deleting order item records. Note that the data access methods contain placeholder comments where actual data access logic should be invoked. + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using Csla; + +[CslaImplementProperties] +public partial class OrderItemEdit : BusinessBase +{ + public partial int Id { get; private set; } + [Required] + [StringLength(100)] + public partial string ProductName { get; set; } + [Range(1, 1000)] + public partial int Quantity { get; set; } + [Range(0.01, 10000.00)] + public partial decimal Price { get; set; } + + protected override void AddBusinessRules() + { + base.AddBusinessRules(); + // Add any custom business rules here if needed + } + + protected override void AddAuthorizationRules() + { + base.AddAuthorizationRules(); + // Example: Only users in the "Manager" role can edit the Price property + BusinessRules.AddRule(new Csla.Rules.CommonRules.IsInRole(PriceProperty, "Manager")); + } + + [CreateChild] + private void CreateChild() + { + // Initialize default values here if needed + LoadProperty(QuantityProperty, 1); + LoadProperty(PriceProperty, 0.01); + } + + [FetchChild] + private void FetchChild(OrderDetailData data) + { + // Load properties from data object + LoadProperty(IdProperty, data.Id); + LoadProperty(ProductNameProperty, data.ProductName); + LoadProperty(QuantityProperty, data.Quantity); + LoadProperty(PriceProperty, data.Price); + } + + [InsertChild] + private void InsertChild([Inject] IOrderDetailDal dal) + { + var data = new OrderDetailData + { + ProductName = ReadProperty(ProductNameProperty), + Quantity = ReadProperty(QuantityProperty), + Price = ReadProperty(PriceProperty) + }; + var newId = dal.Insert(data); + LoadProperty(IdProperty, newId); + } + + [UpdateChild] + private void UpdateChild([Inject] IOrderDetailDal dal) + { + var data = new OrderDetailData + { + Id = ReadProperty(IdProperty), + ProductName = ReadProperty(ProductNameProperty), + Quantity = ReadProperty(QuantityProperty), + Price = ReadProperty(PriceProperty) + }; + dal.Update(data); + } + + [DeleteSelfChild] + private void DeleteSelfChild([Inject] IOrderDetailDal dal) + { + dal.Delete(ReadProperty(IdProperty)); + } +} +``` diff --git a/csla-examples/v10/EditableRoot.md b/csla-examples/v10/EditableRoot.md new file mode 100644 index 0000000..6300868 --- /dev/null +++ b/csla-examples/v10/EditableRoot.md @@ -0,0 +1,176 @@ +# Editable Root Stereotype + +This example demonstrates a complete CSLA business class named `CustomerEdit` that includes various property types, business rules, authorization rules, and data access methods. The class derives from `BusinessBase` and includes both read-only and read-write properties. + +This class demonstrates the editable root business class stereotype. + +> **Note:** This implementation uses the `CslaImplementProperties` attribute to generate most of the code you had to write by hand in previous versions of CSLA. You can still use the CSLA v9 coding approach if desired, but the code generation in CSLA 10 makes things much simpler. + +The example also shows how to implement business rules for validation, including required fields, string length constraints, and a custom rule to ensure email uniqueness. Additionally, it includes object-level authorization rules to control access based on user roles. + +It also includes data portal operation methods for creating, fetching, inserting, updating, and deleting customer records. Note that the data access methods contain placeholder comments where actual data access logic should be invoked. + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using Csla; + +namespace CslaExamples +{ + [CslaImplementProperties] + public partial class CustomerEdit : BusinessBase + { + public partial int Id { get; private set; } + [Required] + [StringLength(50, MinimumLength = 2)] + public partial string Name { get; set; } + [Required] + [EmailAddress] + public partial string Email { get; set; } + public partial DateTime CreatedDate { get; private set; } + public partial bool IsActive { get; private set; } + + protected override void AddBusinessRules() + { + // Call base first + base.AddBusinessRules(); + + // Add custom business rules + BusinessRules.AddRule(new Rules.CommonRules.Required(NameProperty)); + BusinessRules.AddRule(new Rules.CommonRules.MaxLength(NameProperty, 50)); + BusinessRules.AddRule(new Rules.CommonRules.MinLength(NameProperty, 2)); + + BusinessRules.AddRule(new Rules.CommonRules.Required(EmailProperty)); + BusinessRules.AddRule(new Rules.CommonRules.RegEx(EmailProperty, @"^[^@\s]+@[^@\s]+\.[^@\s]+$")); + + // Custom rule example + BusinessRules.AddRule(new EmailUniqueRule(EmailProperty)); + + // Dependency rules + BusinessRules.AddRule(new Rules.CommonRules.Dependency(NameProperty, EmailProperty)); + } + + [ObjectAuthorizationRules] + public static void AddObjectAuthorizationRules() + { + // Example authorization rules + BusinessRules.AddRule(typeof(Customer), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.CreateObject, "Admin", "Manager")); + BusinessRules.AddRule(typeof(Customer), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.EditObject, "Admin", "Manager", "User")); + BusinessRules.AddRule(typeof(Customer), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.DeleteObject, "Admin")); + } + + [Create] + private async Task Create([Inject] ICustomerDal customerDal) + { + // Call DAL Create method to get default values + var customerData = await customerDal.Create(); + + // Load default values from DAL + LoadProperty(CreatedDateProperty, customerData.CreatedDate); + LoadProperty(IsActiveProperty, customerData.IsActive); + + BusinessRules.CheckRules(); + } + + [Fetch] + private async Task Fetch(int id, [Inject] ICustomerDal customerDal) + { + // Get data from DAL + var customerData = await customerDal.Get(id); + + // Load properties from DAL data + if (customerData != null) + { + LoadProperty(IdProperty, customerData.Id); + LoadProperty(NameProperty, customerData.Name); + LoadProperty(EmailProperty, customerData.Email); + LoadProperty(CreatedDateProperty, customerData.CreatedDate); + LoadProperty(IsActiveProperty, customerData.IsActive); + } + else + { + throw new ArgumentException($"Customer {id} not found"); + } + + BusinessRules.CheckRules(); + } + + private static CustomerData CreateCustomerData(Customer customer) + { + return new CustomerData + { + Id = customer.ReadProperty(IdProperty), + Name = customer.ReadProperty(NameProperty), + Email = customer.ReadProperty(EmailProperty), + CreatedDate = customer.ReadProperty(CreatedDateProperty), + IsActive = customer.ReadProperty(IsActiveProperty) + }; + } + + [Insert] + private async Task Insert([Inject] ICustomerDal customerDal) + { + // Prepare customerData with current property values + var customerData = CreateCustomerData(this); + + // Call DAL Upsert method for insert and get result with new ID + var result = await customerDal.Upsert(customerData); + + // Load the new ID from the result + LoadProperty(IdProperty, result.Id); + LoadProperty(CreatedDateProperty, result.CreatedDate); + } + + [Update] + private async Task Update([Inject] ICustomerDal customerDal) + { + // Prepare customerData with current property values + var customerData = CreateCustomerData(this); + + // Call DAL Upsert method for update + await customerDal.Upsert(customerData); + } + + [DeleteSelf] + private async Task DeleteSelf([Inject] ICustomerDal customerDal) + { + // Call DAL Delete method + await customerDal.Delete(ReadProperty(IdProperty)); + + // Mark as new + MarkNew(); + } + + [Delete] + private async Task Delete(int id, [Inject] ICustomerDal customerDal) + { + // Call DAL Delete method + await customerDal.Delete(id); + } + + private class EmailUniqueRule : Rules.BusinessRule + { + public EmailUniqueRule(Core.IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties = new List { primaryProperty }; + } + + protected override void Execute(Rules.IRuleContext context) + { + var email = (string)context.InputPropertyValues[PrimaryProperty]; + + if (!string.IsNullOrEmpty(email)) + { + // Simulate checking for unique email + // In real implementation, this would check against database + if (email.ToLower() == "duplicate@example.com") + { + context.AddErrorResult("Email address is already in use."); + } + } + } + } + } +} +``` diff --git a/csla-examples/EditableRootList.md b/csla-examples/v10/EditableRootList.md similarity index 100% rename from csla-examples/EditableRootList.md rename to csla-examples/v10/EditableRootList.md diff --git a/csla-examples/v10/Properties.md b/csla-examples/v10/Properties.md new file mode 100644 index 0000000..f42d63e --- /dev/null +++ b/csla-examples/v10/Properties.md @@ -0,0 +1,60 @@ +# Implementing CSLA Properties + +In CSLA 10 properties are usually defined using the `partial` keyword, in combination with the containing partial class having the `CslaImplementProperties` attribute. + +> **Note:** You can still declare CSLA properties using the older style where you do all the coding explicitly, like in CSLA 9 for example. However, the CSLA 10 code generation does that work for you, and so it is recommended to use the new approach. + +## Types of Property + +Properties may be read-write, read-only, private, or ignored by CSLA entirely. + +### Read-write property + +A read-write property has public get and set code, and is typically used in an editable root business domain class that inherits from `BusinessBase`. + +```csharp +public partial string Name { get; set; } +``` + +### Read-only property + +A read-only property has public get and private set code This type of property may be used in editable root or readonly root business domain types. + +```csharp +public partial string Name { get; private set; } +``` + +### Private property + +A private property has private get and set code. This type of property is used to manage state that is internal to a business domain class, where the property value should still be available to the rules engine and should be automatically serialized as the domain object flows through the data portal. + +```csharp +private partial string Name { get; set; } +``` + +### CSLA Ignored property + +Sometimes you may want to define a property that is ignored by CSLA. It will not automatically participate in the rules engine, n-level undo, or be serialized by the data portal. To implement a property like this, use the `CslaIgnoreProperty` attribute. + +```csharp +[CslaIgnoreProperty] +public string Name { get; set; } +``` + +An ignored property can be public or private. It is ignored by CSLA code generation. Also notice that this is not a partial property. You _can_ define your own partial properties, as long as you also define the property implementation, because the implementation isn't automatically generated by CSLA. + +## Data Annotations + +Properties can use the annotations attributes from `System.ComponentModel.DataAnnotations` and those annotations will be automatically incorporated into the CSLA rules engine for the type. + +For example: + +```csharp +[Display(Name = "Full name")] +[Required] +public partial string Name { get; set; } +``` + +The `Display` attribute (which supports localization) is used to create the friendly name for the property in CSLA. + +The `Required` attribute indicates that the property value is required. This rule will be enforced by the CSLA rules engine in addition to any enforcement that might occur at the UI framework level. diff --git a/csla-examples/v10/ReadOnlyRoot.md b/csla-examples/v10/ReadOnlyRoot.md new file mode 100644 index 0000000..dd4fe7d --- /dev/null +++ b/csla-examples/v10/ReadOnlyRoot.md @@ -0,0 +1,57 @@ +# ReadOnly Root Stereotype + +This example demonstrates a complete CSLA business class named `CustomerInfo` that includes various property types, authorization rules, and data access methods. The class derives from `ReadOnlyBase` and includes only read-only properties. + +This class demonstrates the read-only root business class stereotype. + +It also shows how to implement property and object-level authorization rules to control access based on user roles. + +It also includes a data portal operation method for fetching customer records. Note that the data access method contains a placeholder comment where actual data access logic should be invoked. + +```csharp +using System; +using Csla; +using Csla.Rules; +using Csla.Rules.CommonRules; + +[CslaImplementProperties] +public partial class CustomerInfo : ReadOnlyBase +{ + public partial int Id { get; private set; } + public partial string Name { get; private set; } + public partial string Email { get; private set; } + public partial DateTime CreatedDate { get; private set; } + public partial bool IsActive { get; private set; } + + protected override void AddBusinessRules() + { + base.AddBusinessRules(); + // Example: Only users in the "Admin" role can view the Name property + BusinessRules.AddRule(new IsInRole(NameProperty, "Admin")); + } + + protected override void AddAuthorizationRules() + { + base.AddAuthorizationRules(); + // Example: Only users in the "Admin" role can read this object + AuthorizationRules.AllowRead(typeof(CustomerInfo), "Admin"); + } + + private async Task DataPortal_Fetch(int id, [Inject] ICustomerDal customerDal) + { + var customerData = await customerDal.Get(id); + if (customerData != null) + { + LoadProperty(IdProperty, customerData.Id); + LoadProperty(NameProperty, customerData.Name); + LoadProperty(EmailProperty, customerData.Email); + LoadProperty(CreatedDateProperty, customerData.CreatedDate); + LoadProperty(IsActiveProperty, customerData.IsActive); + } + else + { + throw new ArgumentException($"Customer {id} not found"); + } + } +} +``` diff --git a/csla-examples/ReadOnlyRootList.md b/csla-examples/v10/ReadOnlyRootList.md similarity index 100% rename from csla-examples/ReadOnlyRootList.md rename to csla-examples/v10/ReadOnlyRootList.md diff --git a/csla-examples/Command.md b/csla-examples/v9/Command.md similarity index 100% rename from csla-examples/Command.md rename to csla-examples/v9/Command.md diff --git a/csla-examples/CslaClassLibrary.md b/csla-examples/v9/CslaClassLibrary.md similarity index 100% rename from csla-examples/CslaClassLibrary.md rename to csla-examples/v9/CslaClassLibrary.md diff --git a/csla-examples/EditableChild.md b/csla-examples/v9/EditableChild.md similarity index 100% rename from csla-examples/EditableChild.md rename to csla-examples/v9/EditableChild.md diff --git a/csla-examples/EditableRoot.md b/csla-examples/v9/EditableRoot.md similarity index 100% rename from csla-examples/EditableRoot.md rename to csla-examples/v9/EditableRoot.md diff --git a/csla-examples/v9/EditableRootList.md b/csla-examples/v9/EditableRootList.md new file mode 100644 index 0000000..78c4e04 --- /dev/null +++ b/csla-examples/v9/EditableRootList.md @@ -0,0 +1,51 @@ +# Editable Root List Stereotype + +This example demonstrates a complete CSLA business list class named `OrderItemList` that includes a list of editable child items, authorization rules, and data access methods. The class derives from `BusinessListBase` and includes a collection of editable child objects of type `OrderItemEdit`. + +This class demonstrates the editable root list business class stereotype. + +It also shows how to implement object-level authorization rules to control access based on user roles. + +It also includes data portal operation methods for creating, fetching, inserting, updating, and deleting order item records. Note that the data access methods contain placeholder comments where actual data access logic should be invoked. + +```csharp +using System; +using Csla; + +public class OrderItemList : BusinessListBase +{ + [ObjectAuthorizationRules] + public static void AddObjectAuthorizationRules() + { + // Example authorization rules + BusinessRules.AddRule(typeof(Customer), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.GetObject, "Admin", "Manager")); + BusinessRules.AddRule(typeof(Customer), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.EditObject, "Admin", "Manager", "User")); + } + + [Create] + private void Create() + { + // Initialization logic if needed + } + + [Fetch] + private async Task Fetch([Inject] IOrderDetailDal dal, [Inject] IChildDataPortal childDataPortal) + { + var dataList = await dal.FetchAllAsync(); + using (LoadListMode) + { + foreach (var data in dataList) + { + var item = childDataPortal.FetchChild(data); + Add(item); + } + } + } + + [Update] + private async Task Update() + { + await Child_UpdateAsync(); + } +} +``` diff --git a/csla-examples/PrivateProperty.md b/csla-examples/v9/PrivateProperty.md similarity index 100% rename from csla-examples/PrivateProperty.md rename to csla-examples/v9/PrivateProperty.md diff --git a/csla-examples/ReadOnlyProperty.md b/csla-examples/v9/ReadOnlyProperty.md similarity index 100% rename from csla-examples/ReadOnlyProperty.md rename to csla-examples/v9/ReadOnlyProperty.md diff --git a/csla-examples/ReadOnlyRoot.md b/csla-examples/v9/ReadOnlyRoot.md similarity index 100% rename from csla-examples/ReadOnlyRoot.md rename to csla-examples/v9/ReadOnlyRoot.md diff --git a/csla-examples/v9/ReadOnlyRootList.md b/csla-examples/v9/ReadOnlyRootList.md new file mode 100644 index 0000000..7ac9546 --- /dev/null +++ b/csla-examples/v9/ReadOnlyRootList.md @@ -0,0 +1,38 @@ +# Read-Only Root List Stereotype + +This example demonstrates a complete CSLA business list class named `CustomerList` that includes a list of read-only child items, authorization rules, and data access methods. The class derives from `ReadOnlyListBase` and includes a collection of read-only child objects of type `CustomerInfo`. + +This class demonstrates the read-only root list business class stereotype. + +It also shows how to implement object-level authorization rules to control access based on user roles. + +It also includes a data portal operation method for fetching customer records. Note that the data access method contains a placeholder comment where actual data access logic should be invoked. + +```csharp +using System; +using Csla; + +public class CustomerList : ReadOnlyListBase +{ + [ObjectAuthorizationRules] + public static void AddObjectAuthorizationRules() + { + // Example authorization rules + BusinessRules.AddRule(typeof(CustomerInfo), new Rules.CommonRules.IsInRole(Rules.AuthorizationActions.GetObject, "Admin", "Manager")); + } + + [Fetch] + private async Task Fetch([Inject] ICustomerDal dal, [Inject] IChildDataPortal childDataPortal) + { + var dataList = await dal.FetchAllAsync(); + using (LoadListMode) + { + foreach (var data in dataList) + { + var item = childDataPortal.FetchChild(data); + Add(item); + } + } + } +} +``` diff --git a/csla-examples/ReadWriteProperty.md b/csla-examples/v9/ReadWriteProperty.md similarity index 100% rename from csla-examples/ReadWriteProperty.md rename to csla-examples/v9/ReadWriteProperty.md diff --git a/csla-mcp-server/Program.cs b/csla-mcp-server/Program.cs index 711c2c0..3804e30 100644 --- a/csla-mcp-server/Program.cs +++ b/csla-mcp-server/Program.cs @@ -173,8 +173,22 @@ public override int Execute([NotNull] CommandContext context, [NotNull] AppSetti try { var content = File.ReadAllText(file); - var fileName = Path.GetFileName(file); - await vectorStore.IndexDocumentAsync(fileName, content); + + // Get relative path from CodeSamplesPath + var relativePath = Path.GetRelativePath(CslaCodeTool.CodeSamplesPath, file); + + // Detect version from path + int? version = null; + var pathParts = relativePath.Split(Path.DirectorySeparatorChar); + if (pathParts.Length > 1 && pathParts[0].StartsWith("v") && int.TryParse(pathParts[0].Substring(1), out var versionNumber)) + { + version = versionNumber; + } + + // Normalize path separators to forward slash for consistency + var normalizedPath = relativePath.Replace("\\", "/"); + + await vectorStore.IndexDocumentAsync(normalizedPath, content, version); indexedCount++; if (indexedCount % 5 == 0) diff --git a/csla-mcp-server/Services/VectorStoreService.cs b/csla-mcp-server/Services/VectorStoreService.cs index 576f0f4..1be1d16 100644 --- a/csla-mcp-server/Services/VectorStoreService.cs +++ b/csla-mcp-server/Services/VectorStoreService.cs @@ -19,6 +19,7 @@ public class DocumentEmbedding public string FileName { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public float[] Embedding { get; set; } = Array.Empty(); + public int? Version { get; set; } = null; // null means common to all versions } public class SemanticSearchResult @@ -139,11 +140,12 @@ public async Task TestConnectivityAsync() } } - public async Task IndexDocumentAsync(string fileName, string content) + public async Task IndexDocumentAsync(string fileName, string content, int? version = null) { try { - Console.WriteLine($"[VectorStore] Indexing document: {fileName}"); + var versionInfo = version.HasValue ? $" (v{version})" : " (common)"; + Console.WriteLine($"[VectorStore] Indexing document: {fileName}{versionInfo}"); var embedding = await GenerateEmbeddingAsync(content); @@ -153,10 +155,11 @@ public async Task IndexDocumentAsync(string fileName, string content) { FileName = fileName, Content = content, - Embedding = embedding + Embedding = embedding, + Version = version }; - Console.WriteLine($"[VectorStore] Successfully indexed {fileName} with {embedding.Length} dimensions"); + Console.WriteLine($"[VectorStore] Successfully indexed {fileName}{versionInfo} with {embedding.Length} dimensions"); } else { @@ -169,11 +172,17 @@ public async Task IndexDocumentAsync(string fileName, string content) } } - public async Task> SearchAsync(string query, int topK = 10) + public async Task> SearchAsync(string query, int? version = null, int topK = 10) { try { - Console.WriteLine($"[VectorStore] Performing semantic search for: {query}"); + // If no version specified, default to highest version + if (!version.HasValue) + { + version = GetHighestVersion(); + } + + Console.WriteLine($"[VectorStore] Performing semantic search for: {query} (version: {version})"); var queryEmbedding = await GenerateEmbeddingAsync(query); @@ -185,14 +194,18 @@ public async Task> SearchAsync(string query, int topK var results = new List(); + // Filter documents: include common (Version == null) and version-specific (Version == version) foreach (var doc in _vectorStore.Values) { - var similarity = CosineSimilarity(queryEmbedding, doc.Embedding); - results.Add(new SemanticSearchResult + if (doc.Version == null || doc.Version == version) { - FileName = doc.FileName, - SimilarityScore = similarity - }); + var similarity = CosineSimilarity(queryEmbedding, doc.Embedding); + results.Add(new SemanticSearchResult + { + FileName = doc.FileName, + SimilarityScore = similarity + }); + } } // Sort by similarity score descending and take top K @@ -202,7 +215,7 @@ public async Task> SearchAsync(string query, int topK .Where(r => r.SimilarityScore > 0.5f) // Filter out low similarity scores .ToList(); - Console.WriteLine($"[VectorStore] Found {topResults.Count} semantic matches"); + Console.WriteLine($"[VectorStore] Found {topResults.Count} semantic matches for version {version}"); return topResults; } @@ -213,6 +226,27 @@ public async Task> SearchAsync(string query, int topK } } + private int GetHighestVersion() + { + var versions = _vectorStore.Values + .Where(doc => doc.Version.HasValue) + .Select(doc => doc.Version!.Value) + .Distinct() + .ToList(); + + if (versions.Any()) + { + var highest = versions.Max(); + Console.WriteLine($"[VectorStore] Highest version detected: {highest}"); + return highest; + } + + // No version-specific content indexed - return a reasonable default + // This will be used when all content is common (version = null) + Console.WriteLine("[VectorStore] No version-specific content found, defaulting to latest known CSLA version"); + return 10; // Default fallback when no version-specific documents exist + } + private float CosineSimilarity(float[] vector1, float[] vector2) { if (vector1.Length != vector2.Length) diff --git a/csla-mcp-server/Tools/CslaCodeTool.cs b/csla-mcp-server/Tools/CslaCodeTool.cs index 67229e8..dc0ecf5 100644 --- a/csla-mcp-server/Tools/CslaCodeTool.cs +++ b/csla-mcp-server/Tools/CslaCodeTool.cs @@ -39,9 +39,11 @@ public class ErrorResult } [McpServerTool, Description("Searches CSLA .NET code samples and snippets for examples of how to implement code that makes use of #cslanet. Returns a JSON array of consolidated search results that merge semantic and word search scores.")] - public static string Search([Description("Keywords used to match against CSLA code samples and snippets. For example, read-write property, editable root, read-only list.")]string message) + public static string Search( + [Description("Keywords used to match against CSLA code samples and snippets. For example, read-write property, editable root, read-only list.")]string message, + [Description("Optional CSLA version number (e.g., 9 or 10). If not provided, defaults to the highest version available.")]int? version = null) { - Console.WriteLine($"[CslaCodeTool.Search] Called with message: '{message}'"); + Console.WriteLine($"[CslaCodeTool.Search] Called with message: '{message}', version: {version?.ToString() ?? "not specified (will use highest)"}"); try { @@ -59,6 +61,13 @@ public static string Search([Description("Keywords used to match against CSLA co }, new JsonSerializerOptions { WriteIndented = true }); } + // If version not specified, detect highest version from subdirectories + if (!version.HasValue) + { + version = GetHighestVersionFromFileSystem(); + Console.WriteLine($"[CslaCodeTool.Search] Version not specified, defaulting to highest: v{version}"); + } + var csFiles = Directory.GetFiles(CodeSamplesPath, "*.cs", SearchOption.AllDirectories); var mdFiles = Directory.GetFiles(CodeSamplesPath, "*.md", SearchOption.AllDirectories); var allFiles = csFiles.Concat(mdFiles); @@ -103,8 +112,8 @@ public static string Search([Description("Keywords used to match against CSLA co } // Create tasks for parallel execution - var wordSearchTask = Task.Run(() => PerformWordSearch(allFiles, searchTerms)); - var semanticSearchTask = Task.Run(() => PerformSemanticSearch(message)); + var wordSearchTask = Task.Run(() => PerformWordSearch(allFiles, searchTerms, version.Value)); + var semanticSearchTask = Task.Run(() => PerformSemanticSearch(message, version)); // Wait for both tasks to complete Task.WaitAll(wordSearchTask, semanticSearchTask); @@ -190,15 +199,27 @@ private static List ConsolidateSearchResults(List PerformWordSearch(IEnumerable allFiles, List searchTerms) + private static List PerformWordSearch(IEnumerable allFiles, List searchTerms, int version) { - Console.WriteLine("[CslaCodeTool.PerformWordSearch] Starting word search"); + Console.WriteLine($"[CslaCodeTool.PerformWordSearch] Starting word search for version {version}"); var results = new List(); foreach (var file in allFiles) { try { + // Get relative path from CodeSamplesPath + var relativePath = Path.GetRelativePath(CodeSamplesPath, file); + + // Filter by version: include if in top directory (common) or in matching version subdirectory + var isCommon = !relativePath.Contains(Path.DirectorySeparatorChar); + var isMatchingVersion = relativePath.StartsWith($"v{version}{Path.DirectorySeparatorChar}"); + + if (!isCommon && !isMatchingVersion) + { + continue; // Skip files from other version directories + } + var content = File.ReadAllText(file); var totalScore = 0; @@ -210,17 +231,17 @@ private static List PerformWordSearch(IEnumerable allFiles // Give higher weight to multi-word phrases var weight = term.Contains(' ') ? 2 : 1; totalScore += count * weight; - Console.WriteLine($"[CslaCodeTool.PerformWordSearch] Found {count} matches for '{term}' in '{Path.GetFileName(file)}' (weight: {weight})"); + Console.WriteLine($"[CslaCodeTool.PerformWordSearch] Found {count} matches for '{term}' in '{relativePath}' (weight: {weight})"); } } if (totalScore > 0) { - Console.WriteLine($"[CslaCodeTool.PerformWordSearch] Found matches in '{Path.GetFileName(file)}' with total score {totalScore}"); + Console.WriteLine($"[CslaCodeTool.PerformWordSearch] Found matches in '{relativePath}' with total score {totalScore}"); results.Add(new SearchResult { Score = totalScore, - FileName = Path.GetFileName(file) + FileName = relativePath.Replace("\\", "/") // Normalize path separators }); } } @@ -269,15 +290,15 @@ private static List NormalizeWordSearchResults(List return normalizedResults; } - private static List PerformSemanticSearch(string message) + private static List PerformSemanticSearch(string message, int? version) { - Console.WriteLine("[CslaCodeTool.PerformSemanticSearch] Starting semantic search"); + Console.WriteLine($"[CslaCodeTool.PerformSemanticSearch] Starting semantic search for version {version}"); var semanticMatches = new List(); if (VectorStore != null && VectorStore.IsReady()) { Console.WriteLine("[CslaCodeTool.PerformSemanticSearch] Performing semantic search"); - var semanticResults = VectorStore.SearchAsync(message, topK: 10).GetAwaiter().GetResult(); + var semanticResults = VectorStore.SearchAsync(message, version, topK: 10).GetAwaiter().GetResult(); semanticMatches = semanticResults.Select(r => new SemanticMatch { FileName = r.FileName, @@ -297,6 +318,34 @@ private static List PerformSemanticSearch(string message) return semanticMatches; } + private static int GetHighestVersionFromFileSystem() + { + try + { + var versionDirs = Directory.GetDirectories(CodeSamplesPath, "v*") + .Select(dir => Path.GetFileName(dir)) + .Where(name => name.StartsWith("v") && int.TryParse(name.Substring(1), out _)) + .Select(name => int.Parse(name.Substring(1))) + .ToList(); + + if (versionDirs.Any()) + { + var highest = versionDirs.Max(); + Console.WriteLine($"[CslaCodeTool.GetHighestVersionFromFileSystem] Found versions: [{string.Join(", ", versionDirs.OrderBy(v => v))}], highest: {highest}"); + return highest; + } + } + catch (Exception ex) + { + Console.WriteLine($"[CslaCodeTool.GetHighestVersionFromFileSystem] Error detecting versions: {ex.Message}"); + } + + // No version directories found - return a reasonable default + // This will be used when all content is in the root directory (common to all versions) + Console.WriteLine("[CslaCodeTool.GetHighestVersionFromFileSystem] No version directories found, defaulting to latest known CSLA version"); + return 10; // Default fallback when no version subdirectories exist + } + private static int CountWordOccurrences(string content, string searchTerm) { // Handle multi-word phrases @@ -362,7 +411,9 @@ public static string Fetch([Description("FileName from the search tool.")]string }, new JsonSerializerOptions { WriteIndented = true }); } - var filePath = Path.Combine(CodeSamplesPath, fileName); + // Normalize path separator to system default + var normalizedFileName = fileName.Replace("/", Path.DirectorySeparatorChar.ToString()); + var filePath = Path.Combine(CodeSamplesPath, normalizedFileName); Console.WriteLine($"[CslaCodeTool.Fetch] Attempting to read file: '{filePath}'"); if (File.Exists(filePath))