feat: working on adding abstractions

This commit is contained in:
Stevan Freeborn
2025-02-09 23:22:21 -06:00
parent 651869c27f
commit baf4035616
34 changed files with 1122 additions and 37 deletions
+385
View File
@@ -0,0 +1,385 @@
root = true
# All files
[*]
indent_style = space
# Xml files
[*.xml]
indent_size = 2
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 2
tab_width = 2
# New line preferences
insert_final_newline = false
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]
# diagnostics
dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1515.severity = none
# var preferences
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_anonymous_function = true:suggestion
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:silent
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
[*.{cs,vb}]
# Naming rules
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
# Symbol specifications
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
-21
View File
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="modnet.onnx" />
<EmbeddedResource Include="rmbg.onnx" />
<EmbeddedResource Include="u2net.onnx" />
</ItemGroup>
</Project>
View File
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[BGR.Console]*</Include>
<ExcludeByFile>**/Program.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec Command="reportgenerator -reports:./TestResults/coverage/*.xml -targetdir:./TestResults/coverage/report/ -reporttypes:Html_Dark" />
</Target>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="Shouldly" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BGR.Console\BGR.Console.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,26 @@
namespace BGR.Console.Tests.Unit;
public class ResourceManagerTests
{
private readonly ResourceManager _resourceManager = new();
[Fact]
public void GetResource_WhenResourceExists_ItShouldReturnStream()
{
var resourceName = "u2net.onnx";
using var stream = _resourceManager.GetResource(resourceName);
stream.ShouldNotBeNull();
}
[Fact]
public void GetResource_WhenResourceDoesNotExist_ItShouldThrowException()
{
var resourceName = "nonexistent.onnx";
var act = () => _resourceManager.GetResource(resourceName);
act.ShouldThrow<FileNotFoundException>();
}
}
@@ -0,0 +1,84 @@
namespace BGR.Console.Tests.Unit;
public class TypeRegistrarTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldNotThrowShould()
{
var mockBuilder = new Mock<IHostBuilder>();
Should.NotThrow(() => new TypeRegistrar(mockBuilder.Object));
}
[Fact]
public void Build_WhenCalled_ItShouldReturnResolverAndBuildHost()
{
var mockHost = new Mock<IHost>();
var mockBuilder = new Mock<IHostBuilder>();
mockBuilder
.Setup(static b => b.Build())
.Returns(mockHost.Object);
var registrar = new TypeRegistrar(mockBuilder.Object);
var resolver = registrar.Build();
resolver.ShouldBeOfType<TypeResolver>();
mockBuilder.Verify(static b => b.Build(), Times.Once);
}
[Fact]
public void Register_WhenCalledWithType_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
registrar.Register(typeof(IService), typeof(ServiceImplementation));
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldNotBeNull();
service.ShouldBeOfType<ServiceImplementation>();
}
[Fact]
public void RegisterInstance_WhenCalledWithInstance_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
var instance = new ServiceImplementation();
registrar.RegisterInstance(typeof(IService), instance);
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldBeSameAs(instance);
}
[Fact]
public void RegisterLazy_WhenCalledWithFunc_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
registrar.RegisterLazy(typeof(IService), static () => new ServiceImplementation());
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldNotBeNull();
service.ShouldBeOfType<ServiceImplementation>();
}
[Fact]
public void RegisterLazy_WhenFuncIsNull_ItShouldThrow()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
Should.Throw<ArgumentNullException>(() => registrar.RegisterLazy(typeof(IService), null!));
}
private interface IService { }
private sealed class ServiceImplementation : IService { }
}
@@ -0,0 +1,71 @@
namespace BGR.Console.Tests.Unit;
public class TypeResolverTests
{
[Fact]
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
{
Should.Throw<ArgumentNullException>(static () => new TypeResolver(null!));
}
[Fact]
public void Resolve_WhenTypeIsNull_ItShouldReturnNull()
{
var mockHost = new Mock<IHost>();
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(null);
result.ShouldBeNull();
}
[Fact]
public void Resolve_WhenCalledWithRegisteredType_ItShouldReturnAnInstance()
{
var services = new ServiceCollection();
services.AddSingleton(new TestService());
var mockHost = new Mock<IHost>();
mockHost
.Setup(static h => h.Services)
.Returns(services.BuildServiceProvider());
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(typeof(TestService));
result.ShouldNotBeNull();
result.ShouldBeOfType<TestService>();
}
[Fact]
public void Resolve_WhenCalledWithUnregisteredType_ItShouldReturnNull()
{
var services = new ServiceCollection();
var mockHost = new Mock<IHost>();
mockHost
.Setup(static h => h.Services)
.Returns(services.BuildServiceProvider());
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(typeof(TestService));
result.ShouldBeNull();
}
[Fact]
public void Dispose_WhenCalled_ItShouldAlsoDisposeHost()
{
var mockHost = new Mock<IHost>();
var resolver = new TypeResolver(mockHost.Object);
resolver.Dispose();
mockHost.Verify(static h => h.Dispose(), Times.Once);
}
private sealed class TestService { }
}
+7
View File
@@ -0,0 +1,7 @@
global using BGR.Console.Common;
global using BGR.Console.Resources;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Moq;
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Files\**\*" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
namespace BGR.Console.Common;
internal static class HostBuilderExtensions
{
public static CommandApp BuildApp(this IHostBuilder builder)
{
var registrar = new TypeRegistrar(builder);
var app = new CommandApp(registrar);
return app;
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace BGR.Console.Common;
internal class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
{
private readonly IHostBuilder _builder = builder;
public ITypeResolver Build()
{
return new TypeResolver(_builder.Build());
}
public void Register(Type service, Type implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterInstance(Type service, object implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterLazy(Type service, Func<object> func)
{
ArgumentNullException.ThrowIfNull(func);
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace BGR.Console.Common;
internal class TypeResolver(IHost provider) : ITypeResolver, IDisposable
{
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
public object? Resolve(Type? type)
{
return type is not null ? _host.Services.GetService(type) : null;
}
public void Dispose()
{
_host.Dispose();
}
}
@@ -1,12 +1,40 @@
using System.Reflection;
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
formatter: new CompactJsonFormatter(),
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
rollingInterval: RollingInterval.Day
)
.Enrich.FromLogContext()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Fatal)
.CreateLogger();
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
try
{
var appName = Assembly.GetExecutingAssembly().GetName().Name;
Log.Information("Starting {AppName}", appName);
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
await Host.CreateDefaultBuilder()
.ConfigureLogging(static logging => logging.ClearProviders())
.ConfigureServices(static (_, services) =>
{
services.AddSerilog();
services.AddSingleton<IResourceManager, ResourceManager>();
})
.BuildApp()
.RunAsync(args);
Log.Information("Stopping {AppName}", appName);
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
throw;
}
finally
{
await Log.CloseAndFlushAsync();
}
if (args.Length < 1)
{
@@ -21,13 +49,13 @@ var outputImagePath = Path.ChangeExtension(inputImagePath, null) + "_no_bg.png";
try
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "BGR.Console.u2net.onnx";
var resourceName = "BGR.Console.Resources.Files.rmbg.onnx";
using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new FileNotFoundException("Model not found in embedded resources.");
var modelBytes = new byte[stream.Length];
stream.ReadExactly(modelBytes);
using var image = Image.Load<Rgba32>(inputImagePath);
using var image = await Image.LoadAsync<Rgba32>(inputImagePath);
var inputTensor = CreateTensorInput(image);
using var options = new SessionOptions() { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR };
@@ -46,8 +74,8 @@ try
var encoder = new PngEncoder { CompressionLevel = PngCompressionLevel.BestCompression };
mask.Save(maskImagePath, encoder);
bgRemoved.Save(outputImagePath, encoder);
await mask.SaveAsync(maskImagePath, encoder);
await bgRemoved.SaveAsync(outputImagePath, encoder);
Console.WriteLine($"Background removed and saved to {outputImagePath}");
}
@@ -57,11 +85,11 @@ catch (Exception ex)
throw;
}
static DenseTensor<float> CreateTensorInput(Image<Rgba32> image)
static Tensor<float> CreateTensorInput(Image<Rgba32> image)
{
// U2Net expects input images to be 320x320. This is dependent on the model.
const int targetWidth = 320;
const int targetHeight = 320;
const int targetWidth = 1024;
const int targetHeight = 1024;
// ImageNet normalization parameters
// source:
@@ -203,5 +231,4 @@ static void WalkImage(int height, int width, Action<int, int> action)
action(x, y);
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace BGR.Console.Removal;
internal interface IImage
{
int Width { get; }
int Height { get; }
void Resize(int width, int height);
IPixel GetPixel(int x, int y);
}
+8
View File
@@ -0,0 +1,8 @@
namespace BGR.Console.Removal;
internal interface IPixel
{
float R { get; }
float G { get; }
float B { get; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace BGR.Console.Removal;
internal interface ITensor<T>
{
int Height { get; }
int Width { get; }
void SetValue(int batch, int channel, int y, int x, T value);
float GetValue(int batch, int channel, int y, int x);
}
+22
View File
@@ -0,0 +1,22 @@
namespace BGR.Console.Removal;
internal abstract class ImageProcessor
{
public abstract Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model);
public abstract Task<Stream> GenerateMaskAsync(OnnxTensor maskTensor, int width, int height);
public abstract Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask);
protected static void WalkImage(int height, int width, Action<int, int> action)
{
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
action(x, y);
}
}
}
}
@@ -0,0 +1,102 @@
namespace BGR.Console.Removal.ImageSharp;
internal class ImageSharpProcessor : ImageProcessor
{
public override async Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model)
{
using var resized = await Image.LoadAsync<Rgba32>(image);
resized.Mutate(x => x.Resize(model.InputWidth, model.InputHeight));
const int batchSize = 1;
const int channels = 3;
var tensor = new OnnxTensor(batchSize, channels, model.InputHeight, model.InputWidth);
WalkImage(resized.Height, resized.Width, (x, y) =>
{
var pixel = resized[x, y];
tensor.SetValue(0, 0, y, x, model.NormalizeRed(pixel.R));
tensor.SetValue(0, 1, y, x, model.NormalizeGreen(pixel.G));
tensor.SetValue(0, 2, y, x, model.NormalizeBlue(pixel.B));
});
return tensor;
}
public override async Task<Stream> GenerateMaskAsync(OnnxTensor maskTensor, int width, int height)
{
using var mask = new Image<Rgba32>(width, height);
using Image<Rgba32> tempMask = new(maskTensor.Width, maskTensor.Height);
const byte opaqueAlpha = 255;
WalkImage(maskTensor.Height, maskTensor.Width, (x, y) =>
{
var sigmoidValue = CalculateSigmoid(maskTensor.GetValue(0, 0, y, x));
var normalizedValue = Normalize(sigmoidValue);
var intensity = ConvertToGreyscale(normalizedValue);
tempMask[x, y] = new Rgba32(intensity, intensity, intensity, opaqueAlpha);
});
tempMask.Mutate(x => x.Resize(width, height));
WalkImage(height, width, (x, y) => mask[x, y] = tempMask[x, y]);
var stream = new MemoryStream();
await mask.SaveAsync(stream, new PngEncoder());
return stream;
}
public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask)
{
var imageWithBg = await Image.LoadAsync<Rgba32>(image);
var maskImage = await Image.LoadAsync<Rgba32>(mask);
using var imageWithBgRemoved = new Image<Rgba32>(imageWithBg.Width, imageWithBg.Height);
const byte alphaThreshold = 20;
var transparentPixel = new Rgba32(0, 0, 0, 0);
WalkImage(imageWithBg.Height, imageWithBg.Width, (x, y) =>
{
var sourcePixel = imageWithBg[x, y];
var maskPixel = maskImage[x, y];
var alpha = maskPixel.R;
imageWithBgRemoved[x, y] = alpha > alphaThreshold
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
: transparentPixel;
});
var result = new MemoryStream();
await imageWithBgRemoved.SaveAsync(result, new PngEncoder());
return result;
}
private static float Normalize(float value)
{
const float binarizationThreshold = 0.5f;
const float normalizationFactor = 2f;
return value > binarizationThreshold
? (value - binarizationThreshold) * normalizationFactor
: 0f;
}
private static byte ConvertToGreyscale(float value)
{
const float maxIntensity = 255f;
return (byte)(value * maxIntensity);
}
private static float CalculateSigmoid(float x)
{
const float sigmoidScale = 1f;
const float sigmoidShift = 1f;
const float sigmoidDivisor = -1f;
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
}
}
@@ -0,0 +1,19 @@
namespace BGR.Console.Removal.ImageSharp;
internal class SharpImage(Image<Rgba32> image) : IImage
{
private readonly Image<Rgba32> _image = image;
public int Width => _image.Width;
public int Height => _image.Height;
public void Resize(int width, int height)
{
_image.Mutate(x => x.Resize(width, height));
}
public IPixel GetPixel(int x, int y)
{
return new SharpPixel(_image[x, y]);
}
}
@@ -0,0 +1,10 @@
namespace BGR.Console.Removal.ImageSharp;
internal class SharpPixel(Rgba32 pixel) : IPixel
{
private readonly Rgba32 _pixel = pixel;
public float R => _pixel.R;
public float G => _pixel.G;
public float B => _pixel.B;
}
@@ -0,0 +1,13 @@
namespace BGR.Console.Removal.Models;
internal class ModNetModel : Model
{
public override int InputWidth => 512;
public override int InputHeight => 512;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
+34
View File
@@ -0,0 +1,34 @@
namespace BGR.Console.Removal.Models;
internal abstract class Model
{
private const float PixelMax = 255f;
public abstract int InputWidth { get; }
public abstract int InputHeight { get; }
public abstract float RedNormalizationMean { get; }
public abstract float GreenNormalizationMean { get; }
public abstract float BlueNormalizationMean { get; }
public abstract float RedNormalizationStd { get; }
public abstract float GreenNormalizationStd { get; }
public abstract float BlueNormalizationStd { get; }
public float NormalizeRed(float value)
{
return Normalize(value, RedNormalizationMean, RedNormalizationStd);
}
public float NormalizeGreen(float value)
{
return Normalize(value, GreenNormalizationMean, GreenNormalizationStd);
}
public float NormalizeBlue(float value)
{
return Normalize(value, BlueNormalizationMean, BlueNormalizationStd);
}
private static float Normalize(float value, float mean, float std)
{
return ((value / PixelMax) - mean) / std;
}
}
@@ -0,0 +1,14 @@
namespace BGR.Console.Removal.Models;
internal class RmbgModel : Model
{
public override int InputWidth => 1024;
public override int InputHeight => 1024;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
@@ -0,0 +1,13 @@
namespace BGR.Console.Removal.Models;
internal class U2NetModel : Model
{
public override int InputWidth => 320;
public override int InputHeight => 320;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
@@ -0,0 +1,37 @@
namespace BGR.Console.Removal.Onnx;
public class OnnxTensor(
int batchSize,
int channels,
int height,
int width
) : ITensor<float>
{
private readonly DenseTensor<float> _tensor =
new([batchSize, channels, height, width]);
public int Height => _tensor.Dimensions[2];
public int Width => _tensor.Dimensions[3];
public void SetValue(
int batch,
int channel,
int y,
int x,
float value
)
{
_tensor[batch, channel, y, x] = value;
}
public float GetValue(
int batch,
int channel,
int y,
int x
)
{
return _tensor[batch, channel, y, x];
}
}
@@ -0,0 +1,6 @@
namespace BGR.Console.Resources;
internal interface IResourceManager
{
Stream GetResource(string resourceName);
}
@@ -0,0 +1,14 @@
namespace BGR.Console.Resources;
internal sealed class ResourceManager : IResourceManager
{
public Stream GetResource(string resourceName)
{
var name = $"{nameof(BGR)}.{nameof(Console)}.{nameof(Resources)}.Files.{resourceName}";
var assembly = Assembly.GetExecutingAssembly();
var names = assembly.GetManifestResourceNames();
var stream = assembly.GetManifestResourceStream(name);
return stream ?? throw new FileNotFoundException("Model not found in embedded resources.");
}
}
+23
View File
@@ -0,0 +1,23 @@
global using System.Reflection;
global using BGR.Console.Common;
global using BGR.Console.Removal.Models;
global using BGR.Console.Removal.Onnx;
global using BGR.Console.Resources;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.ML.OnnxRuntime;
global using Microsoft.ML.OnnxRuntime.Tensors;
global using Serilog;
global using Serilog.Events;
global using Serilog.Formatting.Compact;
global using SixLabors.ImageSharp;
global using SixLabors.ImageSharp.Formats.Png;
global using SixLabors.ImageSharp.PixelFormats;
global using SixLabors.ImageSharp.Processing;
global using Spectre.Console.Cli;
+28
View File
@@ -0,0 +1,28 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console", "BGR.Console\BGR.Console.csproj", "{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console.Tests", "BGR.Console.Tests\BGR.Console.Tests.csproj", "{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.Build.0 = Release|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>