Files
LightlessClient/CONTRIBUTING.md

1087 lines
27 KiB
Markdown

# Lightless Sync Development Guidelines
This document outlines the coding standards, architectural patterns, and best practices for contributing to the Lightless Sync project.
## Table of Contents
1. [Project Structure](#project-structure)
2. [Coding Standards](#coding-standards)
3. [Architecture Patterns](#architecture-patterns)
4. [Dependency Injection](#dependency-injection)
5. [Mediator Pattern](#mediator-pattern)
6. [Service Development](#service-development)
7. [UI Development](#ui-development)
8. [Dalamud Integration](#dalamud-integration)
9. [Performance Considerations](#performance-considerations)
10. [Testing](#testing)
11. [Common Patterns](#common-patterns)
---
## Project Structure
### Directory Organization
```
LightlessSync/
├── Changelog/ # Version history and update notes
├── FileCache/ # File caching and management
├── Interop/ # Game interop and IPC with other plugins
│ └── Ipc/ # IPC callers for external plugins
├── LightlessConfiguration/ # Configuration management
├── PlayerData/ # Player data handling and pairs
│ ├── Factories/
│ ├── Handlers/
│ ├── Pairs/
│ └── Services/
├── Services/ # Core business logic services
│ ├── CharaData/ # Character data management
│ ├── Events/ # Event aggregation
│ ├── Mediator/ # Mediator pattern implementation
│ └── ServerConfiguration/
├── UI/ # ImGui-based user interfaces
│ ├── Components/ # Reusable UI components
│ ├── Handlers/ # UI event handlers
│ └── Models/ # UI data models
├── Utils/ # Utility classes
└── WebAPI/ # SignalR and HTTP client logic
├── Files/ # File transfer operations
└── SignalR/ # SignalR hub connections
```
### File Organization Rules
- **One primary class per file**: File name must match the primary type name
- **Partial classes**: Use for large classes (e.g., `CharaDataManager.cs`, `CharaDataManager.Upload.cs`)
- **Nested types**: Keep in the same file unless they grow large enough to warrant separation
- **Interfaces**: Prefix with `I` (e.g., `IIpcCaller`, `IMediatorSubscriber`)
---
## Coding Standards
### General C# Conventions
Follow the `.editorconfig` settings:
```ini
# Indentation
indent_size = 4
tab_width = 4
end_of_line = crlf
# Naming
- Interfaces: IPascalCase (begins with I)
- Types: PascalCase
- Methods: PascalCase
- Properties: PascalCase
- Private fields: _camelCase (underscore prefix)
- Local variables: camelCase
- Parameters: camelCase
- Constants: PascalCase or UPPER_CASE
```
### Code Style Preferences
#### Namespaces
```csharp
// Use block-scoped namespaces (not file-scoped)
namespace LightlessSync.Services;
public class MyService
{
// ...
}
```
#### Braces
```csharp
// Always use braces for control statements
if (condition)
{
DoSomething();
}
// Even for single-line statements
if (condition)
{
return;
}
```
#### Expression-Bodied Members
```csharp
// Prefer expression bodies for properties and accessors
public string Name { get; init; } = string.Empty;
public bool IsEnabled => _config.Current.Enabled;
// Avoid for methods, constructors, and operators
public void DoWork()
{
// Full method body
}
```
#### Object Initialization
```csharp
// Prefer object initializers
var notification = new LightlessNotification
{
Id = "example",
Title = "Example",
Message = "This is an example",
Type = NotificationType.Info
};
// Prefer collection initializers
var list = new List<string> { "item1", "item2", "item3" };
```
#### Null Handling
```csharp
// Prefer null coalescing
var value = possiblyNull ?? defaultValue;
// Prefer null propagation
var length = text?.Length ?? 0;
// Prefer pattern matching for null checks
if (value is null)
{
return;
}
// Use nullable reference types
public string? OptionalValue { get; set; }
public string RequiredValue { get; set; } = string.Empty;
```
#### Modern C# Features
```csharp
// Use target-typed new expressions when type is apparent
List<string> items = new();
Dictionary<string, int> map = new();
// Use primary constructors sparingly (prefer traditional constructors for clarity)
// AVOID: public class MyService(ILogger logger) : IService
// Use record types for DTOs and immutable data
public record UserData(string Name, string Id);
public record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
// Use pattern matching
if (obj is MyType { Property: "value" } typedObj)
{
// Use typedObj
}
```
---
## Architecture Patterns
### Mediator Pattern (Core Communication)
The Lightless Sync uses a **custom mediator pattern** for decoupled communication between components.
#### Key Components
1. **LightlessMediator**: Central message bus
2. **MessageBase**: Base class for all messages
3. **IMediatorSubscriber**: Interface for subscribers
4. **MediatorSubscriberBase**: Base class with auto-cleanup
#### Creating Messages
```csharp
// In Services/Mediator/Messages.cs
public record MyCustomMessage(string Data, int Value) : MessageBase;
// For messages that need to stay on the same thread
public record SynchronousMessage : MessageBase
{
public override bool KeepThreadContext => true;
}
```
#### Subscribing to Messages
```csharp
public class MyService : DisposableMediatorSubscriberBase, IHostedService
{
public MyService(ILogger<MyService> logger, LightlessMediator mediator)
: base(logger, mediator)
{
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Subscribe to messages
Mediator.Subscribe<MyCustomMessage>(this, HandleMyMessage);
Mediator.Subscribe<AnotherMessage>(this, HandleAnotherMessage);
return Task.CompletedTask;
}
private void HandleMyMessage(MyCustomMessage message)
{
Logger.LogDebug("Received: {Data}, {Value}", message.Data, message.Value);
// Handle the message
}
public Task StopAsync(CancellationToken cancellationToken)
{
// Cleanup is automatic with DisposableMediatorSubscriberBase
return Task.CompletedTask;
}
}
```
#### Publishing Messages
```csharp
// Publish to all subscribers
Mediator.Publish(new MyCustomMessage("test", 42));
// Publish from non-mediator classes
_mediator.Publish(new NotificationMessage(
"Title",
"Message",
NotificationType.Info));
```
#### Message Guidelines
- **Use records** for messages (immutable and concise)
- **Keep messages simple**: Only carry data, no logic
- **Name clearly**: `<Action><Subject>Message` (e.g., `PairRequestReceivedMessage`)
- **Thread-safe data**: Messages are processed asynchronously unless `KeepThreadContext = true`
- **Avoid circular dependencies**: Messages should flow in one direction
---
## Dependency Injection
### Service Registration (Plugin.cs)
```csharp
// In Plugin.cs constructor:
.ConfigureServices(collection =>
{
// Singleton services (shared state, long-lived)
collection.AddSingleton<LightlessMediator>();
collection.AddSingleton<ApiController>();
collection.AddSingleton<PairManager>();
// Scoped services (per-resolution scope)
collection.AddScoped<UiService>();
collection.AddScoped<CacheMonitor>();
// Hosted services (background services with lifecycle)
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
// Lazy dependencies (avoid circular dependencies)
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
// Factory pattern
collection.AddSingleton<GameObjectHandlerFactory>();
})
```
### Service Patterns
#### Standard Service
```csharp
public class MyService
{
private readonly ILogger<MyService> _logger;
private readonly SomeDependency _dependency;
public MyService(
ILogger<MyService> logger,
SomeDependency dependency)
{
_logger = logger;
_dependency = dependency;
}
public void DoWork()
{
_logger.LogInformation("Working...");
_dependency.PerformAction();
}
}
```
#### Hosted Service (Background Service)
```csharp
public class MyHostedService : IHostedService, IMediatorSubscriber
{
private readonly ILogger<MyHostedService> _logger;
private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator;
public MyHostedService(
ILogger<MyHostedService> logger,
LightlessMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting {Service}", nameof(MyHostedService));
// Subscribe to mediator messages
_mediator.Subscribe<SomeMessage>(this, HandleSomeMessage);
// Initialize resources
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping {Service}", nameof(MyHostedService));
// Cleanup
_mediator.UnsubscribeAll(this);
return Task.CompletedTask;
}
private void HandleSomeMessage(SomeMessage msg)
{
// Handle message
}
}
```
#### Mediator Subscriber Service
```csharp
public sealed class MyService : DisposableMediatorSubscriberBase
{
private readonly SomeDependency _dependency;
public MyService(
ILogger<MyService> logger,
LightlessMediator mediator,
SomeDependency dependency)
: base(logger, mediator)
{
_dependency = dependency;
// Subscribe in constructor or in separate Init method
Mediator.Subscribe<SomeMessage>(this, HandleMessage);
}
private void HandleMessage(SomeMessage msg)
{
Logger.LogDebug("Handling message: {Msg}", msg);
// Process message
}
// Dispose is handled by base class
}
```
---
## Service Development
### Service Responsibilities
Services should follow **Single Responsibility Principle**:
- **NotificationService**: Handles all in-game notifications
- **BroadcastService**: Manages Lightfinder broadcast state
- **PairRequestService**: Manages incoming pair requests
- **DalamudUtilService**: Utility methods for Dalamud framework operations
### Service Guidelines
1. **Logging**: Always use `ILogger<T>` with appropriate log levels
2. **Error Handling**: Wrap risky operations in try-catch, log exceptions
3. **Async/Await**: Use `ConfigureAwait(false)` for non-UI operations
4. **Thread Safety**: Use locks, `ConcurrentDictionary`, or `SemaphoreSlim` for shared state
5. **Disposal**: Implement `IDisposable` or inherit from `DisposableMediatorSubscriberBase`
### Example Service Template
```csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class TemplateService : DisposableMediatorSubscriberBase, IHostedService
{
private readonly ILogger<TemplateService> _logger;
private readonly SomeDependency _dependency;
private readonly SemaphoreSlim _lock = new(1);
public TemplateService(
ILogger<TemplateService> logger,
LightlessMediator mediator,
SomeDependency dependency)
: base(logger, mediator)
{
_logger = logger;
_dependency = dependency;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting {Service}", nameof(TemplateService));
Mediator.Subscribe<SomeMessage>(this, HandleSomeMessage);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping {Service}", nameof(TemplateService));
_lock.Dispose();
return Task.CompletedTask;
}
private async void HandleSomeMessage(SomeMessage msg)
{
await _lock.WaitAsync().ConfigureAwait(false);
try
{
_logger.LogDebug("Processing message: {Msg}", msg);
// Do work
await _dependency.ProcessAsync(msg).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process message");
}
finally
{
_lock.Release();
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_lock?.Dispose();
}
base.Dispose(disposing);
}
}
```
---
## UI Development
### UI Base Classes
All UI windows inherit from `WindowMediatorSubscriberBase`:
```csharp
public class MyWindow : WindowMediatorSubscriberBase
{
private readonly UiSharedService _uiShared;
public MyWindow(
ILogger<MyWindow> logger,
LightlessMediator mediator,
UiSharedService uiShared,
PerformanceCollectorService performanceCollector)
: base(logger, mediator, "My Window Title", performanceCollector)
{
_uiShared = uiShared;
// Window configuration
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600, 400),
MaximumSize = new Vector2(800, 600)
};
}
protected override void DrawInternal()
{
// ImGui drawing code here
ImGui.Text("Hello, World!");
}
}
```
### UI Guidelines
1. **Separation of Concerns**: UI should only handle rendering and user input
2. **Use UiSharedService**: Centralized UI utilities (icons, buttons, styling)
3. **Performance**: Use `PerformanceCollectorService` to track rendering performance
4. **Disposal**: Windows are scoped; don't store state that needs to persist
5. **Mediator**: Use mediator to communicate with services
6. **ImGui Best Practices**:
- Use `ImRaii` for push/pop operations
- Always pair `Begin`/`End` calls
- Use `ImGuiHelpers.ScaledDummy()` for responsive spacing
- Check `IsOpen` property for visibility
### ImGui Patterns
```csharp
// Using ImRaii for automatic cleanup
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")))
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))
{
if (ImGui.Button("Styled Button"))
{
// Handle click
}
}
// Tab bars
using (var tabBar = ImRaii.TabBar("###my_tabs"))
{
if (!tabBar) return;
using (var tab1 = ImRaii.TabItem("Tab 1"))
{
if (tab1)
{
ImGui.Text("Tab 1 content");
}
}
using (var tab2 = ImRaii.TabItem("Tab 2"))
{
if (tab2)
{
ImGui.Text("Tab 2 content");
}
}
}
// Child windows for scrolling regions
using (var child = ImRaii.Child("###content", new Vector2(0, -40), false,
ImGuiWindowFlags.AlwaysVerticalScrollbar))
{
if (!child) return;
// Scrollable content here
}
```
---
## Dalamud Integration
### Dalamud Services
Dalamud provides various services accessible through constructor injection:
```csharp
// Common Dalamud services:
IClientState clientState // Player and world state
IFramework framework // Game framework events
IObjectTable objectTable // Game objects (players, NPCs, etc.)
IChatGui chatGui // Chat window access
ICommandManager commandManager // Command registration
IDataManager gameData // Game data sheets
IPluginLog pluginLog // Logging
INotificationManager notificationManager // Toast notifications
IGameGui gameGui // Game UI access
ITextureProvider textureProvider // Icon/texture loading
```
### Framework Threading
**CRITICAL**: Dalamud operates on the **game's main thread**.
```csharp
// Safe: Already on framework thread
private void OnFrameworkUpdate(IFramework framework)
{
// Can safely access game state
var player = _clientState.LocalPlayer;
}
// Unsafe: Called from background thread
private async Task BackgroundWork()
{
// DO NOT access game state here!
// Use DalamudUtilService to run on framework thread
await _dalamudUtil.RunOnFrameworkThread(() =>
{
// Now safe to access game state
var player = _clientState.LocalPlayer;
});
}
```
### Game Object Access
```csharp
// Always check for null
var player = _clientState.LocalPlayer;
if (player == null)
{
return;
}
// Iterating game objects
foreach (var obj in _objectTable)
{
if (obj is IPlayerCharacter pc)
{
// Handle player character
}
}
// Finding specific objects
var target = _targetManager.Target;
if (target is IPlayerCharacter targetPlayer)
{
// Handle targeted player
}
```
### IPC with Other Plugins
Use the IPC pattern for communicating with other Dalamud plugins:
```csharp
public sealed class IpcCallerExample : DisposableMediatorSubscriberBase, IIpcCaller
{
private readonly IDalamudPluginInterface _pi;
private readonly ICallGateSubscriber<int> _apiVersion;
public string Name => "ExamplePlugin";
public bool Available { get; private set; }
public IpcCallerExample(
ILogger<IpcCallerExample> logger,
IDalamudPluginInterface pluginInterface,
LightlessMediator mediator)
: base(logger, mediator)
{
_pi = pluginInterface;
try
{
_apiVersion = _pi.GetIpcSubscriber<int>("ExamplePlugin.GetApiVersion");
CheckAvailability();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to initialize IPC for ExamplePlugin");
}
}
private void CheckAvailability()
{
try
{
var version = _apiVersion.InvokeFunc();
Available = version >= 1;
Logger.LogInformation("ExamplePlugin available, version: {Version}", version);
}
catch
{
Available = false;
}
}
public void DoSomething()
{
if (!Available)
{
Logger.LogWarning("ExamplePlugin not available");
return;
}
// Call IPC methods
}
}
```
---
## Performance Considerations
### Framework Updates
The game runs at ~60 FPS. Avoid heavy operations in framework updates:
```csharp
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
private void OnTick(PriorityFrameworkUpdateMessage _)
{
// Throttle expensive operations
if ((DateTime.UtcNow - _lastCheck).TotalSeconds < 1)
{
return;
}
_lastCheck = DateTime.UtcNow;
// Do lightweight work only
}
```
### Async Operations
```csharp
// Good: Non-blocking async
public async Task<Data> FetchDataAsync()
{
var result = await _httpClient.GetAsync(url).ConfigureAwait(false);
return await result.Content.ReadFromJsonAsync<Data>().ConfigureAwait(false);
}
// Bad: Blocking async
public Data FetchDataBlocking()
{
return FetchDataAsync().GetAwaiter().GetResult(); // Blocks thread!
}
```
### Collection Performance
```csharp
// Good: Thread-safe concurrent collections
private readonly ConcurrentDictionary<string, Data> _cache = new();
// Good: Lock-protected regular collections
private readonly List<Item> _items = new();
private readonly object _itemsLock = new();
public void AddItem(Item item)
{
lock (_itemsLock)
{
_items.Add(item);
}
}
// Good: SemaphoreSlim for async locks
private readonly SemaphoreSlim _asyncLock = new(1);
public async Task ProcessAsync()
{
await _asyncLock.WaitAsync().ConfigureAwait(false);
try
{
// Critical section
}
finally
{
_asyncLock.Release();
}
}
```
### Memory Management
```csharp
// Dispose pattern for unmanaged resources
public class ResourceManager : IDisposable
{
private bool _disposed;
private readonly SemaphoreSlim _semaphore = new(1);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose managed resources
_semaphore?.Dispose();
}
// Free unmanaged resources
_disposed = true;
}
}
```
---
## Testing
### Unit Testing
```csharp
[TestClass]
public class MyServiceTests
{
private Mock<ILogger<MyService>> _mockLogger;
private Mock<LightlessMediator> _mockMediator;
private MyService _service;
[TestInitialize]
public void Setup()
{
_mockLogger = new Mock<ILogger<MyService>>();
_mockMediator = new Mock<LightlessMediator>();
_service = new MyService(_mockLogger.Object, _mockMediator.Object);
}
[TestMethod]
public void DoWork_WithValidInput_ReturnsExpectedResult()
{
// Arrange
var input = "test";
// Act
var result = _service.DoWork(input);
// Assert
Assert.AreEqual("expected", result);
}
}
```
### Integration Testing
Test with real Dalamud services in a controlled environment:
```csharp
// Create test harness that mimics Dalamud environment
public class DalamudTestHarness
{
public IClientState ClientState { get; }
public IFramework Framework { get; }
// ... other services
public void SimulateFrameworkUpdate()
{
// Trigger framework update event
}
}
```
---
## Common Patterns
### Notification Pattern
```csharp
// Simple notification
Mediator.Publish(new NotificationMessage(
"Title",
"Message body",
NotificationType.Info));
// Rich notification with actions
var notification = new LightlessNotification
{
Id = "unique_id",
Title = "Action Required",
Message = "Do you want to proceed?",
Type = NotificationType.Warning,
Duration = TimeSpan.FromSeconds(10),
Actions = new List<LightlessNotificationAction>
{
new()
{
Id = "confirm",
Label = "Confirm",
Icon = FontAwesomeIcon.Check,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("User confirmed");
DoAction();
n.IsDismissed = true;
}
},
new()
{
Id = "cancel",
Label = "Cancel",
Icon = FontAwesomeIcon.Times,
OnClick = (n) => n.IsDismissed = true
}
}
};
Mediator.Publish(new LightlessNotificationMessage(notification));
```
### Configuration Pattern
```csharp
public class MyConfigService : ConfigurationServiceBase<MyConfig>
{
public MyConfigService(string configDirectory)
: base(Path.Combine(configDirectory, "myconfig.json"))
{
}
}
// Usage
_configService.Current.SomeSetting = newValue;
_configService.Save();
```
### Factory Pattern
```csharp
public class GameObjectHandlerFactory
{
private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessMediator _mediator;
public GameObjectHandlerFactory(
DalamudUtilService dalamudUtil,
LightlessMediator mediator)
{
_dalamudUtil = dalamudUtil;
_mediator = mediator;
}
public GameObjectHandler Create(IntPtr address, string identifier)
{
return new GameObjectHandler(
_dalamudUtil,
_mediator,
() => address,
identifier);
}
}
```
---
## Error Handling
### Logging Guidelines
```csharp
// Debug: Verbose information for debugging
_logger.LogDebug("Processing request {RequestId}", requestId);
// Information: General informational messages
_logger.LogInformation("User {UserId} connected", userId);
// Warning: Recoverable issues
_logger.LogWarning("Cache miss for {Key}, will fetch from server", key);
// Error: Errors that need attention
_logger.LogError(ex, "Failed to process message {MessageId}", messageId);
// Critical: Application-breaking errors
_logger.LogCritical(ex, "Database connection failed");
```
### Exception Handling
```csharp
public async Task<Result> DoWorkAsync()
{
try
{
// Risky operation
return await PerformOperationAsync().ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Network error during operation");
// Show user-friendly notification
Mediator.Publish(new NotificationMessage(
"Network Error",
"Failed to connect to server. Please check your connection.",
NotificationType.Error));
return Result.Failure;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during operation");
throw; // Re-throw unexpected exceptions
}
}
```
---
## Best Practices Summary
### DO ✅
- Use `DisposableMediatorSubscriberBase` for services that subscribe to mediator
- Use `IHostedService` for background services
- Always inject `ILogger<T>` for logging
- Use `ConfigureAwait(false)` for async operations not requiring UI thread
- Dispose of resources properly (SemaphoreSlim, HttpClient, etc.)
- Check for null when accessing Dalamud game objects
- Use thread-safe collections for shared state
- Keep UI logic separate from business logic
- Use mediator for cross-component communication
- Write unit tests for business logic
- Document public APIs with XML comments
### DON'T ❌
- Don't access game objects from background threads without `RunOnFrameworkThread`
- Don't block async methods with `.Result` or `.Wait()`
- Don't store UI state in services (services are often singletons)
- Don't use `async void` except for event handlers
- Don't catch `Exception` without re-throwing or logging
- Don't create circular dependencies between services
- Don't perform heavy operations in framework updates
- Don't forget to unsubscribe from mediator messages
- Don't hardcode file paths (use `IDalamudPluginInterface.ConfigDirectory`)
- Don't use primary constructors (prefer traditional for clarity)
---
## Code Review Checklist
Before submitting a pull request, ensure:
- [ ] Code follows naming conventions
- [ ] All new services are registered in `Plugin.cs`
- [ ] Mediator messages are defined in `Messages.cs`
- [ ] Logging is appropriate and informative
- [ ] Thread safety is considered for shared state
- [ ] Disposable resources are properly disposed
- [ ] Async operations use `ConfigureAwait(false)`
- [ ] UI code is separated from business logic
- [ ] No direct game object access from background threads
- [ ] Error handling is comprehensive
- [ ] Performance impact is considered
- [ ] Comments explain "why", not "what"
- [ ] No compiler warnings
- [ ] Tests pass (if applicable)
---
## Resources
- [Dalamud Documentation](https://dalamud.dev)
- [ImGui.NET Documentation](https://github.com/mellinoe/ImGui.NET)
- [C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
- [.NET API Guidelines](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/api-guidelines)
---
**Happy Coding!** 🚀