This document provides comprehensive coding standards for the Neatoo.RemoteFactory solution, derived from analysis of configuration files, project structures, and actual code patterns.
- Solution-Wide Build Properties
- Code Style Rules (.editorconfig)
- Analyzer Packages and Rules
- Suppressed Warnings and Exceptions
- Project-Specific Configurations
- Project Structure Patterns
- Naming Conventions
- Code Patterns and Best Practices
- Areas for Improvement
- Applying These Standards to New Projects
Located in src/Directory.Build.props, these settings apply to ALL projects in the solution:
| Property | Value | Description |
|---|---|---|
AnalysisMode |
all |
Enables all .NET code analysis rules by default |
LangVersion |
preview |
Uses the latest C# preview features |
Nullable |
enable |
Nullable reference types are enabled solution-wide |
ImplicitUsings |
enable |
Enables implicit global usings |
TargetFrameworks |
net9.0 |
Targets .NET 9.0 |
TreatWarningsAsErrors |
True |
All warnings are treated as errors (strict quality) |
ManagePackageVersionsCentrally |
true |
Uses Central Package Management |
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>critical</NuGetAuditLevel>The solution enforces critical-level security auditing for all NuGet packages.
<Authors>Keith Voels</Authors>
<Copyright>Copyright 2025</Copyright>
<FileVersion>9.19.1</FileVersion>
<PackageVersion>9.19.1</PackageVersion>Located in src/.editorconfig, defining formatting and style enforcement:
[*.cs]
indent_style = tab
indent_size = 3
end_of_line = crlfKey Style Decision: Uses tabs (not spaces) with a tab width of 3 spaces.
csharp_style_expression_bodied_accessors = true:error
csharp_style_expression_bodied_constructors = true:error
csharp_style_expression_bodied_indexers = true:error
csharp_style_expression_bodied_local_functions = true:error
csharp_style_expression_bodied_methods = true:error
csharp_style_expression_bodied_operators = true:error
csharp_style_expression_bodied_properties = true:errorStandard: Expression-bodied members are REQUIRED where applicable.
csharp_style_var_elsewhere = true:error
csharp_style_var_for_built_in_types = true:error
csharp_style_var_when_type_is_apparent = true:errorStandard: Always use var - explicit type declarations are prohibited.
csharp_style_namespace_declarations = file_scoped:errorStandard: File-scoped namespaces are REQUIRED.
dotnet_style_qualification_for_event = true:error
dotnet_style_qualification_for_field = true:error
dotnet_style_qualification_for_method = true:error
dotnet_style_qualification_for_property = true:errorStandard: this. prefix is REQUIRED for all member access.
csharp_style_pattern_matching_over_as_with_null_check = true:error
csharp_style_pattern_matching_over_is_with_cast_check = true:error
csharp_style_throw_expression = true:error
csharp_style_inlined_variable_declaration = true:error
csharp_style_conditional_delegate_call = true:errordotnet_style_collection_initializer = true:error
dotnet_style_object_initializer = true:error
dotnet_style_explicit_tuple_names = true:error
dotnet_style_null_propagation = true:error
dotnet_style_predefined_type_for_locals_parameters_members = true:errorThe Microsoft/.NET standard recommends async methods end with "Async" suffix. This solution intentionally does NOT enforce this convention.
The .editorconfig explicitly sets this rule to none:
# Async methods - "Async" suffix is NOT enforced (exception to Microsoft standard)
# Rationale: Async suffix adds noise in modern codebases where async is pervasive
dotnet_naming_rule.async_methods_end_in_async.severity = noneRationale: The async suffix adds noise without significant value in modern codebases where async is pervasive.
Additionally, IDE1006 in NoWarn (Directory.Build.props) helps suppress any remaining naming rule violations.
From Directory.Packages.props:
| Package | Version | Purpose |
|---|---|---|
Microsoft.CodeAnalysis.Analyzers |
4.14.0 | Roslyn analyzer development |
Microsoft.CodeAnalysis.NetAnalyzers |
9.0.0 | .NET code quality analyzers |
The solution elevates all NUnit analyzer warnings to errors:
dotnet_diagnostic.NUnit2001.severity=error # through NUnit2043This ensures test code quality is enforced strictly.
<NoWarn>CA1861, CA1865, CA1510, IDE0021, IDE0022, IDE0023, IDE1006, CA1050, CA1822</NoWarn>| Rule | Description | Reason |
|---|---|---|
| CA1861 | Avoid constant arrays as arguments | Performance optimization conflicts |
| CA1865 | Use char overload | String/char overload preference |
| CA1510 | Use ArgumentNullException.ThrowIfNull | Legacy compatibility |
| IDE0021 | Use expression body for constructors | Conflicts with expression-bodied rule |
| IDE0022 | Use expression body for methods | Conflicts with expression-bodied rule |
| IDE0023 | Use expression body for operators | Conflicts with expression-bodied rule |
| IDE1006 | Naming rule violation | Custom naming conventions (includes async suffix) |
| CA1050 | Declare types in namespaces | Source generator output |
| CA1822 | Mark members as static | Flexibility for future changes |
Special Note on Async Suffix (IDE1006): The IDE1006 suppression covers general naming rule violations. The .editorconfig also explicitly sets the async suffix naming rule to severity = none (lines 107-112).
# CA1002: Do not expose generic lists
# TODO: I really should NOT suppress this, but it's too much of a PITA
dotnet_diagnostic.CA1002.severity = none
# CA1014: Mark assemblies with CLSCompliant
dotnet_diagnostic.CA1014.severity = none
# CA1030: Use events where appropriate
dotnet_diagnostic.CA1030.severity = none
# CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
# CA1515: Consider making public types internal
dotnet_diagnostic.CA1515.severity = none
# CA1812: Internal class never instantiated (Program class issue)
dotnet_diagnostic.CA1812.severity = none
# CA2007: Consider calling ConfigureAwait
dotnet_diagnostic.CA2007.severity = none
# RS1024: Compare symbols correctly (Source generator specific)
dotnet_diagnostic.RS1024.severity = none
# IDE0055: Formatting rule (VS 2022 Preview issue)
dotnet_diagnostic.IDE0055.severity = none
# IDE0130: Namespace doesn't match folder structure
dotnet_diagnostic.IDE0130.severity = noneProjects: RemoteFactory.csproj, RemoteFactory.AspNetCore.csproj
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<DebugType>embedded</DebugType>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<EnforceExtendedAnalyzerRules>false</EnforceExtendedAnalyzerRules>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>Project: RemoteFactory.FactoryGenerator.csproj
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>Note: Source generators target netstandard2.0 for maximum compatibility and have stricter analyzer rules.
Projects: FactoryGeneratorTests.csproj, RemoteFactory.AspNet.Tests.csproj
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AnalysisMode>default</AnalysisMode> <!-- Relaxed from "all" -->
<NoWarn>..., IDE0044, CS4014</NoWarn> <!-- Additional suppressions -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>Test projects have relaxed analysis (default instead of all) and additional warning suppressions:
| Rule | Description | Reason |
|---|---|---|
| IDE0044 | Add readonly modifier | Test flexibility |
| CS4014 | Unawaited async call | Test patterns |
| CS0051 | Inconsistent accessibility | Test infrastructure |
Projects: Person.Server.csproj, Person.Client.csproj, Person.Ef.csproj
<TargetFramework>net9.0</TargetFramework> <!-- May use older TFM -->
<NoWarn>CA1062, IDE0022</NoWarn> <!-- EF-specific suppressions -->Example projects may target older frameworks and have domain-specific suppressions.
RemoteFactory/
├── .github/ # GitHub workflows and templates
├── src/
│ ├── Directory.Build.props # Solution-wide build settings
│ ├── Directory.Packages.props # Central Package Management
│ ├── .editorconfig # Code style rules
│ ├── Neatoo.RemoteFactory.sln
│ │
│ ├── RemoteFactory/ # Core library
│ ├── RemoteFactory.AspNetCore/ # ASP.NET Core integration
│ ├── RemoteFactory.FactoryGenerator/ # Roslyn source generator
│ │
│ ├── Tests/
│ │ ├── FactoryGeneratorTests/ # Unit tests
│ │ ├── FactoryGeneratorSandbox/ # Development sandbox
│ │ ├── RemoteFactory.AspNet.Tests/ # Integration tests
│ │ └── RemoteFactory.AspNetCore.TestServer/ # Test server
│ │
│ └── Examples/
│ ├── Person/ # Full example application
│ │ ├── Person.DomainModel/
│ │ ├── Person.Ef/
│ │ ├── Person.Server/
│ │ └── Person.Client/ # Blazor client
│ └── HorseFarm/ # Additional example
│
├── LICENSE
├── README.md
└── neatoo_icon.png
| Type | Pattern | Example |
|---|---|---|
| Core Library | {ProductName} |
RemoteFactory |
| Integration Library | {ProductName}.{Platform} |
RemoteFactory.AspNetCore |
| Source Generator | {ProductName}.{Generator}Generator |
RemoteFactory.FactoryGenerator |
| Test Projects | {LibraryName}Tests or {LibraryName}.Tests |
FactoryGeneratorTests |
| Domain Model | {Domain}.DomainModel |
Person.DomainModel |
| EF/Data Layer | {Domain}.Ef |
Person.Ef |
| Server/API | {Domain}.Server |
Person.Server |
| Client App | {Domain}.Client |
Person.Client |
// Interface with I prefix
public interface IPersonModel { }
// Implementation without I
internal partial class PersonModel : IPersonModel { }
// Factory naming: I{Type}Factory
public interface IShowcaseSaveFactory { }
internal class ShowcaseSaveFactory { }// Async suffix NOT required (exception to Microsoft standard)
public async Task<bool> Fetch([Service] IPersonContext personContext)
// Factory operations as verbs
public void Insert() { }
public void Update() { }
public void Delete() { }
public void Create() { }
public void Fetch() { }// PascalCase with explicit this. qualification
public string? FirstName { get; set { field = value; this.OnPropertyChanged(); } }
public bool IsDeleted { get; set; }
public bool IsNew { get; set; } = true;// camelCase
public void Insert([Service] IService service) { }
public Task Fetch([Service] IPersonContext personContext)// Factory attributes on classes
[Factory]
[AuthorizeFactory<IPersonModelAuth>]
internal partial class PersonModel { }
// Operation attributes on methods
[Remote]
[Fetch]
public async Task<bool> Fetch() { }
// Service injection attribute
public void Insert([Service] IService service) { }namespace Neatoo.RemoteFactory;
public class Authorized { }public class Authorized
{
public bool HasAccess { get; init; }
public Authorized(bool hasAccess)
{
this.HasAccess = hasAccess; // Always use this.
}
}public string? Message { get; init; } // Nullable
public bool HasAccess { get; init; } // Non-nullable
// Null checking
ArgumentNullException.ThrowIfNull(result, nameof(result));// Properties
public bool IsSave => this.CallMethod.IsSave;
// Methods (when simple)
public static implicit operator bool(Authorized result) =>
result?.HasAccess ?? false;// Async suffix is NOT required (exception to Microsoft standard)
public async Task<bool> Fetch([Service] IPersonContext personContext)
{
var personEntity = await personContext.Persons.FirstOrDefaultAsync(x => x.Id == 1);
if (personEntity == null)
{
return false;
}
this.MapFrom(personEntity);
return true;
}Note: The "Async" suffix convention is intentionally NOT enforced in this solution.
// Modern collection initialization
List<FactoryOperation> defaultFactoryOperations = [];
this.UsingStatements = new EquatableArray<string>([.. usingStatements.Distinct()]);if (methodSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not BaseMethodDeclarationSyntax methodSyntax)
{
continue;
}internal record TypeInfo
{
public string Name { get; }
public bool IsPartial { get; }
// ...
}
internal sealed record MethodParameterInfo
{
public string Name { get; set; } = null!;
public string Type { get; set; } = null!;
}internal abstract class FactoryMethod(string serviceType, string implementationType)
{
public string ServiceType { get; protected set; } = serviceType;
public string ImplementationType { get; set; } = implementationType;
}The codebase explicitly acknowledges this as technical debt:
# CA1002: Do not expose generic lists
# TODO: I really should NOT suppress this, but
# it's too much of a PITA at the present moment.
dotnet_diagnostic.CA1002.severity = noneRecommendation: Gradually replace List<T> in public APIs with IReadOnlyList<T> or ICollection<T>.
- Core libraries target
net9.0 - Example projects target
net9.0 - Source generator targets
netstandard2.0
Consideration: While the source generator must remain netstandard2.0, example projects could be upgraded to match core libraries.
<GenerateDocumentationFile>false</GenerateDocumentationFile>Recommendation: Enable XML documentation for public APIs to improve IntelliSense and API documentation.
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>Consideration: Review if unsafe code is actually used; if not, this could be removed for additional safety.
# CA2007: Consider calling ConfigureAwait
dotnet_diagnostic.CA2007.severity = noneNote: This is intentional for application code but should be reconsidered for library code targeting broader consumption.
Some source generator output and code samples show mixed indentation. The source generator's output formatting should be reviewed.
<Project>
<PropertyGroup>
<AnalysisMode>all</AnalysisMode>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<NoWarn>CA1861, CA1865, CA1510, IDE0021, IDE0022, IDE0023, IDE1006, CA1050, CA1822</NoWarn>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>critical</NuGetAuditLevel>
<Nullable>enable</Nullable>
<TargetFrameworks>net9.0</TargetFrameworks>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
</Project>Copy the full .editorconfig from src/.editorconfig, which includes:
- Tab indentation (size 3)
- Expression-bodied members required
varrequired everywhere- File-scoped namespaces required
this.qualification required- All NUnit rules elevated to errors
- Async suffix naming rule explicitly disabled (set to
none)
For Library Projects:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DebugType>embedded</DebugType>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
</Project>For Test Projects:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AnalysisMode>default</AnalysisMode>
<NoWarn>$(NoWarn);IDE0044;CS4014</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>For Source Generators:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
</ItemGroup>
</Project>The Neatoo.RemoteFactory solution employs a strict, modern C# coding standard characterized by:
- Strict Quality Enforcement - All warnings as errors, comprehensive analysis
- Modern C# Features - Preview language features, .NET 9.0, nullable reference types
- Consistent Style - File-scoped namespaces, expression-bodied members,
vareverywhere - Explicit Self-Reference -
this.prefix required for all member access - Async Suffix NOT Enforced - Exception to Microsoft standard (see IDE1006 in NoWarn)
- Central Package Management - Coordinated dependency versions
- Security Focused - NuGet package auditing enabled
- Test Quality - NUnit rules elevated to errors
The standards balance strictness with pragmatism, acknowledging certain suppressions as intentional trade-offs while maintaining high overall code quality.