604 lines
19 KiB
C#
Executable File
604 lines
19 KiB
C#
Executable File
using MarketAlly.AIPlugin;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace MarketAlly.AIPlugin.Refactoring.Plugins
|
|
{
|
|
[AIPlugin("CodeAnalysis", "Analyzes code structure, complexity metrics, and suggests refactoring improvements")]
|
|
public class CodeAnalysisPlugin : IAIPlugin
|
|
{
|
|
[AIParameter("Full path to the file or directory to analyze", required: true)]
|
|
public string Path { get; set; }
|
|
|
|
[AIParameter("Analysis depth: basic, detailed, comprehensive", required: false)]
|
|
public string AnalysisDepth { get; set; } = "detailed";
|
|
|
|
[AIParameter("Include complexity metrics in analysis", required: false)]
|
|
public bool IncludeComplexity { get; set; } = true;
|
|
|
|
[AIParameter("Include code smell detection", required: false)]
|
|
public bool IncludeCodeSmells { get; set; } = true;
|
|
|
|
[AIParameter("Include refactoring suggestions", required: false)]
|
|
public bool IncludeSuggestions { get; set; } = true;
|
|
|
|
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["path"] = typeof(string),
|
|
["analysisDepth"] = typeof(string),
|
|
["includeComplexity"] = typeof(bool),
|
|
["includeCodeSmells"] = typeof(bool),
|
|
["includeSuggestions"] = typeof(bool)
|
|
};
|
|
|
|
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
|
|
{
|
|
try
|
|
{
|
|
// Extract parameters
|
|
string path = parameters["path"].ToString();
|
|
string analysisDepth = parameters.TryGetValue("analysisDepth", out var depthObj)
|
|
? depthObj.ToString().ToLower()
|
|
: "detailed";
|
|
bool includeComplexity = parameters.TryGetValue("includeComplexity", out var complexityObj)
|
|
? Convert.ToBoolean(complexityObj)
|
|
: true;
|
|
bool includeCodeSmells = parameters.TryGetValue("includeCodeSmells", out var smellsObj)
|
|
? Convert.ToBoolean(smellsObj)
|
|
: true;
|
|
bool includeSuggestions = parameters.TryGetValue("includeSuggestions", out var suggestionsObj)
|
|
? Convert.ToBoolean(suggestionsObj)
|
|
: true;
|
|
|
|
// Validate path
|
|
if (!File.Exists(path) && !Directory.Exists(path))
|
|
{
|
|
return new AIPluginResult(
|
|
new FileNotFoundException($"Path not found: {path}"),
|
|
"Invalid path"
|
|
);
|
|
}
|
|
|
|
var analysisResults = new List<CodeAnalysisResult>();
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
// Analyze single file
|
|
var result = await AnalyzeFileAsync(path, analysisDepth, includeComplexity, includeCodeSmells, includeSuggestions);
|
|
if (result != null)
|
|
analysisResults.Add(result);
|
|
}
|
|
else
|
|
{
|
|
// Analyze directory
|
|
var csharpFiles = Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories)
|
|
.Where(f => !ShouldExcludeFile(f))
|
|
.ToList();
|
|
|
|
foreach (var file in csharpFiles)
|
|
{
|
|
var result = await AnalyzeFileAsync(file, analysisDepth, includeComplexity, includeCodeSmells, includeSuggestions);
|
|
if (result != null)
|
|
analysisResults.Add(result);
|
|
}
|
|
}
|
|
|
|
// Generate summary
|
|
var summary = GenerateAnalysisSummary(analysisResults, analysisDepth);
|
|
|
|
return new AIPluginResult(new
|
|
{
|
|
Message = $"Code analysis completed for {analysisResults.Count} file(s)",
|
|
Path = path,
|
|
AnalysisDepth = analysisDepth,
|
|
Summary = summary,
|
|
DetailedResults = analysisResults,
|
|
Timestamp = DateTime.UtcNow
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, $"Code analysis failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task<CodeAnalysisResult> AnalyzeFileAsync(string filePath, string analysisDepth,
|
|
bool includeComplexity, bool includeCodeSmells, bool includeSuggestions)
|
|
{
|
|
try
|
|
{
|
|
var sourceCode = await File.ReadAllTextAsync(filePath);
|
|
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
|
|
var root = syntaxTree.GetRoot();
|
|
|
|
var result = new CodeAnalysisResult
|
|
{
|
|
FilePath = filePath,
|
|
FileName = System.IO.Path.GetFileName(filePath),
|
|
LinesOfCode = sourceCode.Split('\n').Length,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
// Basic structure analysis
|
|
await AnalyzeStructure(result, root);
|
|
|
|
// Complexity analysis
|
|
if (includeComplexity)
|
|
{
|
|
await AnalyzeComplexity(result, root);
|
|
}
|
|
|
|
// Code smell detection
|
|
if (includeCodeSmells)
|
|
{
|
|
await DetectCodeSmells(result, root, sourceCode);
|
|
}
|
|
|
|
// Generate suggestions
|
|
if (includeSuggestions)
|
|
{
|
|
await GenerateRefactoringSuggestions(result, root, analysisDepth);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new CodeAnalysisResult
|
|
{
|
|
FilePath = filePath,
|
|
FileName = System.IO.Path.GetFileName(filePath),
|
|
Error = ex.Message,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
}
|
|
|
|
private async Task AnalyzeStructure(CodeAnalysisResult result, SyntaxNode root)
|
|
{
|
|
var structure = new CodeStructure();
|
|
|
|
// Count different types of declarations
|
|
structure.Classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>().Count();
|
|
structure.Interfaces = root.DescendantNodes().OfType<InterfaceDeclarationSyntax>().Count();
|
|
structure.Methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
|
|
structure.Properties = root.DescendantNodes().OfType<PropertyDeclarationSyntax>().Count();
|
|
structure.Fields = root.DescendantNodes().OfType<FieldDeclarationSyntax>().Count();
|
|
|
|
// Analyze using statements
|
|
structure.UsingStatements = root.DescendantNodes().OfType<UsingDirectiveSyntax>().Count();
|
|
|
|
// Analyze namespaces
|
|
var namespaces = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
|
|
.Select(n => n.Name.ToString())
|
|
.Distinct()
|
|
.ToList();
|
|
structure.Namespaces = namespaces;
|
|
|
|
result.Structure = structure;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private async Task AnalyzeComplexity(CodeAnalysisResult result, SyntaxNode root)
|
|
{
|
|
var complexity = new ComplexityMetrics();
|
|
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
|
|
|
|
foreach (var method in methods)
|
|
{
|
|
var methodComplexity = CalculateCyclomaticComplexity(method);
|
|
var cognitiveComplexity = CalculateCognitiveComplexity(method);
|
|
|
|
complexity.Methods.Add(new MethodComplexity
|
|
{
|
|
MethodName = method.Identifier.ValueText,
|
|
CyclomaticComplexity = methodComplexity,
|
|
CognitiveComplexity = cognitiveComplexity,
|
|
LineCount = method.GetText().Lines.Count,
|
|
ParameterCount = method.ParameterList.Parameters.Count
|
|
});
|
|
}
|
|
|
|
complexity.AverageCyclomaticComplexity = complexity.Methods.Any()
|
|
? complexity.Methods.Average(m => m.CyclomaticComplexity)
|
|
: 0;
|
|
complexity.AverageCognitiveComplexity = complexity.Methods.Any()
|
|
? complexity.Methods.Average(m => m.CognitiveComplexity)
|
|
: 0;
|
|
complexity.MaxComplexity = complexity.Methods.Any()
|
|
? complexity.Methods.Max(m => m.CyclomaticComplexity)
|
|
: 0;
|
|
|
|
result.Complexity = complexity;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private async Task DetectCodeSmells(CodeAnalysisResult result, SyntaxNode root, string sourceCode)
|
|
{
|
|
var codeSmells = new List<CodeSmell>();
|
|
|
|
// God Class detection
|
|
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
|
|
foreach (var cls in classes)
|
|
{
|
|
var methodCount = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
|
|
var lineCount = cls.GetText().Lines.Count;
|
|
|
|
if (methodCount > 20 || lineCount > 500)
|
|
{
|
|
codeSmells.Add(new CodeSmell
|
|
{
|
|
Type = "God Class",
|
|
Severity = "High",
|
|
Description = $"Class '{cls.Identifier.ValueText}' is too large ({methodCount} methods, {lineCount} lines)",
|
|
Location = $"Line {cls.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Suggestion = "Consider splitting this class into smaller, more focused classes"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Long Method detection
|
|
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
|
|
foreach (var method in methods)
|
|
{
|
|
var lineCount = method.GetText().Lines.Count;
|
|
if (lineCount > 50)
|
|
{
|
|
codeSmells.Add(new CodeSmell
|
|
{
|
|
Type = "Long Method",
|
|
Severity = "Medium",
|
|
Description = $"Method '{method.Identifier.ValueText}' is too long ({lineCount} lines)",
|
|
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Suggestion = "Consider extracting parts of this method into smaller methods"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Long Parameter List detection
|
|
foreach (var method in methods)
|
|
{
|
|
var paramCount = method.ParameterList.Parameters.Count;
|
|
if (paramCount > 5)
|
|
{
|
|
codeSmells.Add(new CodeSmell
|
|
{
|
|
Type = "Long Parameter List",
|
|
Severity = "Medium",
|
|
Description = $"Method '{method.Identifier.ValueText}' has too many parameters ({paramCount})",
|
|
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Suggestion = "Consider grouping parameters into a class or using method overloads"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Duplicate Code detection (simplified)
|
|
var stringLiterals = root.DescendantNodes().OfType<LiteralExpressionSyntax>()
|
|
.Where(l => l.Token.IsKind(SyntaxKind.StringLiteralToken))
|
|
.GroupBy(l => l.Token.ValueText)
|
|
.Where(g => g.Count() > 3 && g.Key.Length > 10)
|
|
.ToList();
|
|
|
|
foreach (var group in stringLiterals)
|
|
{
|
|
codeSmells.Add(new CodeSmell
|
|
{
|
|
Type = "Duplicate String Literals",
|
|
Severity = "Low",
|
|
Description = $"String literal '{group.Key}' appears {group.Count()} times",
|
|
Location = "Multiple locations",
|
|
Suggestion = "Consider extracting this string to a constant"
|
|
});
|
|
}
|
|
|
|
result.CodeSmells = codeSmells;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private async Task GenerateRefactoringSuggestions(CodeAnalysisResult result, SyntaxNode root, string analysisDepth)
|
|
{
|
|
var suggestions = new List<RefactoringSuggestion>();
|
|
|
|
// Extract Method suggestions
|
|
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
|
|
foreach (var method in methods)
|
|
{
|
|
var complexity = CalculateCyclomaticComplexity(method);
|
|
if (complexity > 10)
|
|
{
|
|
suggestions.Add(new RefactoringSuggestion
|
|
{
|
|
Type = "Extract Method",
|
|
Priority = "High",
|
|
Description = $"Method '{method.Identifier.ValueText}' has high complexity ({complexity})",
|
|
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Recommendation = "Consider extracting complex logic into separate methods",
|
|
EstimatedEffort = "Medium"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Extract Class suggestions
|
|
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
|
|
foreach (var cls in classes)
|
|
{
|
|
var methodCount = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
|
|
if (methodCount > 15)
|
|
{
|
|
suggestions.Add(new RefactoringSuggestion
|
|
{
|
|
Type = "Extract Class",
|
|
Priority = "High",
|
|
Description = $"Class '{cls.Identifier.ValueText}' has too many responsibilities ({methodCount} methods)",
|
|
Location = $"Line {cls.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Recommendation = "Consider splitting into multiple classes with single responsibilities",
|
|
EstimatedEffort = "High"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Introduce Parameter Object suggestions
|
|
foreach (var method in methods)
|
|
{
|
|
var paramCount = method.ParameterList.Parameters.Count;
|
|
if (paramCount > 4)
|
|
{
|
|
suggestions.Add(new RefactoringSuggestion
|
|
{
|
|
Type = "Introduce Parameter Object",
|
|
Priority = "Medium",
|
|
Description = $"Method '{method.Identifier.ValueText}' has many parameters ({paramCount})",
|
|
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
|
|
Recommendation = "Consider grouping related parameters into a class",
|
|
EstimatedEffort = "Low"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Documentation suggestions
|
|
var undocumentedPublicMembers = root.DescendantNodes()
|
|
.Where(n => IsPublicMember(n) && !HasDocumentation(n))
|
|
.Take(10) // Limit suggestions
|
|
.ToList();
|
|
|
|
if (undocumentedPublicMembers.Any())
|
|
{
|
|
suggestions.Add(new RefactoringSuggestion
|
|
{
|
|
Type = "Add Documentation",
|
|
Priority = "Low",
|
|
Description = $"{undocumentedPublicMembers.Count} public members lack XML documentation",
|
|
Location = "Multiple locations",
|
|
Recommendation = "Add XML documentation to improve code maintainability",
|
|
EstimatedEffort = "Low"
|
|
});
|
|
}
|
|
|
|
result.Suggestions = suggestions;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private int CalculateCyclomaticComplexity(MethodDeclarationSyntax method)
|
|
{
|
|
int complexity = 1; // Base complexity
|
|
|
|
// Count decision points
|
|
var decisionNodes = method.DescendantNodes().Where(node =>
|
|
node.IsKind(SyntaxKind.IfStatement) ||
|
|
node.IsKind(SyntaxKind.WhileStatement) ||
|
|
node.IsKind(SyntaxKind.ForStatement) ||
|
|
node.IsKind(SyntaxKind.ForEachStatement) ||
|
|
node.IsKind(SyntaxKind.DoStatement) ||
|
|
node.IsKind(SyntaxKind.SwitchStatement) ||
|
|
node.IsKind(SyntaxKind.CaseSwitchLabel) ||
|
|
node.IsKind(SyntaxKind.CatchClause) ||
|
|
node.IsKind(SyntaxKind.ConditionalExpression)
|
|
);
|
|
|
|
complexity += decisionNodes.Count();
|
|
|
|
// Count logical operators
|
|
var logicalOperators = method.DescendantTokens().Where(token =>
|
|
token.IsKind(SyntaxKind.AmpersandAmpersandToken) ||
|
|
token.IsKind(SyntaxKind.BarBarToken)
|
|
);
|
|
|
|
complexity += logicalOperators.Count();
|
|
|
|
return complexity;
|
|
}
|
|
|
|
private int CalculateCognitiveComplexity(MethodDeclarationSyntax method)
|
|
{
|
|
// Simplified cognitive complexity calculation
|
|
// In practice, this would need more sophisticated nesting depth tracking
|
|
int complexity = 0;
|
|
int nestingLevel = 0;
|
|
|
|
foreach (var node in method.DescendantNodes())
|
|
{
|
|
switch (node.Kind())
|
|
{
|
|
case SyntaxKind.IfStatement:
|
|
case SyntaxKind.WhileStatement:
|
|
case SyntaxKind.ForStatement:
|
|
case SyntaxKind.ForEachStatement:
|
|
case SyntaxKind.DoStatement:
|
|
complexity += 1 + nestingLevel;
|
|
break;
|
|
case SyntaxKind.SwitchStatement:
|
|
complexity += 1 + nestingLevel;
|
|
break;
|
|
case SyntaxKind.CatchClause:
|
|
complexity += 1 + nestingLevel;
|
|
break;
|
|
}
|
|
|
|
// Track nesting (simplified)
|
|
if (node.IsKind(SyntaxKind.Block))
|
|
{
|
|
nestingLevel++;
|
|
}
|
|
}
|
|
|
|
return complexity;
|
|
}
|
|
|
|
private bool IsPublicMember(SyntaxNode node)
|
|
{
|
|
if (node is MemberDeclarationSyntax member)
|
|
{
|
|
return member.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool HasDocumentation(SyntaxNode node)
|
|
{
|
|
return node.GetLeadingTrivia()
|
|
.Any(trivia => trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
|
|
trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia));
|
|
}
|
|
|
|
private bool ShouldExcludeFile(string filePath)
|
|
{
|
|
var fileName = System.IO.Path.GetFileName(filePath);
|
|
var excludePatterns = new[] { ".Designer.cs", ".generated.cs", "AssemblyInfo.cs", "GlobalAssemblyInfo.cs" };
|
|
|
|
return excludePatterns.Any(pattern => fileName.EndsWith(pattern, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private object GenerateAnalysisSummary(List<CodeAnalysisResult> results, string analysisDepth)
|
|
{
|
|
if (!results.Any())
|
|
{
|
|
return new { Message = "No files analyzed" };
|
|
}
|
|
|
|
var totalFiles = results.Count;
|
|
var totalLinesOfCode = results.Sum(r => r.LinesOfCode);
|
|
var totalClasses = results.Sum(r => r.Structure?.Classes ?? 0);
|
|
var totalMethods = results.Sum(r => r.Structure?.Methods ?? 0);
|
|
var averageComplexity = results
|
|
.Where(r => r.Complexity?.AverageCyclomaticComplexity > 0)
|
|
.Average(r => r.Complexity?.AverageCyclomaticComplexity ?? 0);
|
|
|
|
var topIssues = results
|
|
.SelectMany(r => r.CodeSmells ?? new List<CodeSmell>())
|
|
.GroupBy(cs => cs.Type)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(5)
|
|
.Select(g => new { Type = g.Key, Count = g.Count() })
|
|
.ToList();
|
|
|
|
var topSuggestions = results
|
|
.SelectMany(r => r.Suggestions ?? new List<RefactoringSuggestion>())
|
|
.GroupBy(s => s.Type)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(5)
|
|
.Select(g => new { Type = g.Key, Count = g.Count() })
|
|
.ToList();
|
|
|
|
return new
|
|
{
|
|
FilesAnalyzed = totalFiles,
|
|
TotalLinesOfCode = totalLinesOfCode,
|
|
TotalClasses = totalClasses,
|
|
TotalMethods = totalMethods,
|
|
AverageComplexity = Math.Round(averageComplexity, 2),
|
|
TopCodeSmells = topIssues,
|
|
TopRefactoringSuggestions = topSuggestions,
|
|
QualityScore = CalculateQualityScore(results),
|
|
AnalysisDepth = analysisDepth
|
|
};
|
|
}
|
|
|
|
private double CalculateQualityScore(List<CodeAnalysisResult> results)
|
|
{
|
|
if (!results.Any()) return 0;
|
|
|
|
double score = 100.0;
|
|
|
|
// Penalize high complexity
|
|
var avgComplexity = results
|
|
.Where(r => r.Complexity?.AverageCyclomaticComplexity > 0)
|
|
.Average(r => r.Complexity?.AverageCyclomaticComplexity ?? 0);
|
|
if (avgComplexity > 10) score -= (avgComplexity - 10) * 2;
|
|
|
|
// Penalize code smells
|
|
var totalSmells = results.Sum(r => r.CodeSmells?.Count ?? 0);
|
|
var totalMethods = results.Sum(r => r.Structure?.Methods ?? 1);
|
|
var smellRatio = (double)totalSmells / totalMethods;
|
|
score -= smellRatio * 20;
|
|
|
|
return Math.Max(0, Math.Min(100, Math.Round(score, 1)));
|
|
}
|
|
}
|
|
|
|
// Supporting classes for code analysis
|
|
public class CodeAnalysisResult
|
|
{
|
|
public string FilePath { get; set; }
|
|
public string FileName { get; set; }
|
|
public int LinesOfCode { get; set; }
|
|
public CodeStructure Structure { get; set; }
|
|
public ComplexityMetrics Complexity { get; set; }
|
|
public List<CodeSmell> CodeSmells { get; set; } = new List<CodeSmell>();
|
|
public List<RefactoringSuggestion> Suggestions { get; set; } = new List<RefactoringSuggestion>();
|
|
public string Error { get; set; }
|
|
public DateTime Timestamp { get; set; }
|
|
}
|
|
|
|
public class CodeStructure
|
|
{
|
|
public int Classes { get; set; }
|
|
public int Interfaces { get; set; }
|
|
public int Methods { get; set; }
|
|
public int Properties { get; set; }
|
|
public int Fields { get; set; }
|
|
public int UsingStatements { get; set; }
|
|
public List<string> Namespaces { get; set; } = new List<string>();
|
|
}
|
|
|
|
public class ComplexityMetrics
|
|
{
|
|
public List<MethodComplexity> Methods { get; set; } = new List<MethodComplexity>();
|
|
public double AverageCyclomaticComplexity { get; set; }
|
|
public double AverageCognitiveComplexity { get; set; }
|
|
public int MaxComplexity { get; set; }
|
|
}
|
|
|
|
public class MethodComplexity
|
|
{
|
|
public string MethodName { get; set; }
|
|
public int CyclomaticComplexity { get; set; }
|
|
public int CognitiveComplexity { get; set; }
|
|
public int LineCount { get; set; }
|
|
public int ParameterCount { get; set; }
|
|
}
|
|
|
|
public class CodeSmell
|
|
{
|
|
public string Type { get; set; }
|
|
public string Severity { get; set; }
|
|
public string Description { get; set; }
|
|
public string Location { get; set; }
|
|
public string Suggestion { get; set; }
|
|
}
|
|
|
|
public class RefactoringSuggestion
|
|
{
|
|
public string Type { get; set; }
|
|
public string Priority { get; set; }
|
|
public string Description { get; set; }
|
|
public string Location { get; set; }
|
|
public string Recommendation { get; set; }
|
|
public string EstimatedEffort { get; set; }
|
|
}
|
|
} |