diff --git a/.editorconfig b/.editorconfig
index 65488a6..1e15504 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,50 +1,127 @@
-# Top-most EditorConfig file
root = true
-# use unix newline characters
-end_of_line = lf
-
-[*.cs]
-dotnet_naming_rule.methods_must_be_camel_case.severity = warning
-dotnet_naming_rule.methods_must_be_camel_case.symbols = private_methods
-dotnet_naming_rule.methods_must_be_camel_case.style = camel_case_style
-
-dotnet_naming_symbols.private_methods.applicable_kinds = method
-dotnet_naming_symbols.private_methods.applicable_accessibilities = private
-
-dotnet_naming_style.camel_case_style.capitalization = camel_case
-dotnet_diagnostic.CA2254.severity = none
-
-[*.razor]
-indent_style = ignore
-indent_size = ignore
-
-
-# Default settings:
-# A newline ending every file
-# Use 4 spaces as indentation
+# All files
[*]
-insert_final_newline = true
indent_style = space
indent_size = 2
-trim_trailing_whitespace = true
-
-[project.json]
-indent_size = 2
-
-# Generated code
-[*{_AssemblyInfo.cs,.notsupported.cs}]
-generated_code = true
+end_of_line = lf
# C# files
[*.cs]
+tab_width = 2
+insert_final_newline = false
+
+[*.{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_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_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_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]
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# 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_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_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+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_open_brace = all
-csharp_new_line_before_else = true
csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
csharp_new_line_before_finally = true
-csharp_new_line_before_members_in_object_initializers = 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
@@ -52,100 +129,8 @@ csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
-csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
-
-# Modifier preferences
-csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
-
-# avoid this. unless absolutely necessary
-dotnet_style_qualification_for_field = false:suggestion
-dotnet_style_qualification_for_property = false:suggestion
-dotnet_style_qualification_for_method = false:suggestion
-dotnet_style_qualification_for_event = false:suggestion
-
-# Types: use keywords instead of BCL types, and permit var only when the type is clear
-# csharp_style_var_for_built_in_types = false:suggestion
-csharp_style_var_when_type_is_apparent = false:none
-# csharp_style_var_elsewhere = false:suggestion
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
-
-# name all constant fields using PascalCase
-dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
-dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
-dotnet_naming_symbols.constant_fields.applicable_kinds = field
-dotnet_naming_symbols.constant_fields.required_modifiers = const
-dotnet_naming_style.pascal_case_style.capitalization = pascal_case
-
-# static fields should have s_ prefix
-# dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
-# dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
-# dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
-# dotnet_naming_symbols.static_fields.applicable_kinds = field
-# dotnet_naming_symbols.static_fields.required_modifiers = static
-# dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
-# dotnet_naming_style.static_prefix_style.required_prefix = s_
-# dotnet_naming_style.static_prefix_style.capitalization = camel_case
-
-# internal and private fields should be _camelCase
-dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
-dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
-dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
-dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
-dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
-dotnet_naming_style.camel_case_underscore_style.required_prefix = _
-dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
-
-# Code style defaults
-csharp_using_directive_placement = outside_namespace:suggestion
-dotnet_sort_system_directives_first = true
-csharp_prefer_braces = true:silent
-csharp_preserve_single_line_blocks = true:none
-csharp_preserve_single_line_statements = false:none
-csharp_prefer_static_local_function = true:suggestion
-csharp_prefer_simple_using_statement = false:none
-csharp_style_prefer_switch_expression = true:suggestion
-dotnet_style_readonly_field = true:suggestion
-
-# Expression-level preferences
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_auto_properties = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:silent
-dotnet_style_prefer_conditional_expression_over_return = true:silent
-csharp_prefer_simple_default_expression = true:suggestion
-
-# Expression-bodied members
-csharp_style_expression_bodied_methods = true:silent
-csharp_style_expression_bodied_constructors = true:silent
-csharp_style_expression_bodied_operators = true:silent
-csharp_style_expression_bodied_properties = true:silent
-csharp_style_expression_bodied_indexers = true:silent
-csharp_style_expression_bodied_accessors = true:silent
-csharp_style_expression_bodied_lambdas = true:silent
-csharp_style_expression_bodied_local_functions = true:silent
-
-# Pattern matching
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-
-# Null checking preferences
-csharp_style_throw_expression = true:suggestion
-csharp_style_conditional_delegate_call = true:suggestion
-
-# Other features
-csharp_style_prefer_index_operator = false:none
-csharp_style_prefer_range_operator = false:none
-csharp_style_pattern_local_over_anonymous_function = false:none
+csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
@@ -155,7 +140,7 @@ 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 = do_not_ignore
+csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
@@ -171,34 +156,193 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
-# C++ Files
-[*.{cpp,h,in}]
-curly_bracket_next_line = true
-indent_brace_style = Allman
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
-# Xml project files
-[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
-indent_size = 2
+#### Naming styles ####
+[*.{cs,vb}]
-[*.{csproj,vbproj,proj,nativeproj,locproj}]
-charset = utf-8
+# Naming rules
-# Xml build files
-[*.builds]
-indent_size = 2
+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
-# Xml files
-[*.{xml,stylecop,resx,ruleset}]
-indent_size = 2
+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
-# Xml config files
-[*.{props,targets,config,nuspec}]
-indent_size = 2
+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
-# YAML config files
-[*.{yml,yaml}]
-indent_size = 2
+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_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be_s_camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be_s_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.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
-# Shell scripts
-[*.sh]
-end_of_line = lf
diff --git a/Management.Test/CanvasModels/TermTests.cs b/Management.Test/CanvasModels/TermTests.cs
index f8834ea..a348d11 100644
--- a/Management.Test/CanvasModels/TermTests.cs
+++ b/Management.Test/CanvasModels/TermTests.cs
@@ -3,7 +3,7 @@ using CanvasModel.EnrollmentTerms;
public class DeserializationTests
{
- [Test]
+ [Fact]
public void TestTerm()
{
diff --git a/Management.Test/Features/CalendarMonthTests.cs b/Management.Test/Features/CalendarMonthTests.cs
index dfd1373..3d4e4c5 100644
--- a/Management.Test/Features/CalendarMonthTests.cs
+++ b/Management.Test/Features/CalendarMonthTests.cs
@@ -1,6 +1,6 @@
public class CalendarMonthTests
{
- [Test]
+ [Fact]
public void TestCalendarMonthCanGetFirstWeek()
{
var month = new CalendarMonth(2023, 2);
@@ -12,7 +12,7 @@ public class CalendarMonthTests
month.Weeks.First().Should().BeEquivalentTo(expectedFirstWeek);
}
- [Test]
+ [Fact]
public void TestCanGetAnotherMonthsFirstWeek()
{
var month = new CalendarMonth(2023, 4);
@@ -24,7 +24,7 @@ public class CalendarMonthTests
month.Weeks.First().Should().BeEquivalentTo(expectedFirstWeek);
}
- [Test]
+ [Fact]
public void TestCorrectNumberOfWeeks()
{
var month = new CalendarMonth(2023, 4);
@@ -32,7 +32,7 @@ public class CalendarMonthTests
month.Weeks.Count().Should().Be(6);
}
- [Test]
+ [Fact]
public void TestLastWeekIsCorrect()
{
var month = new CalendarMonth(2023, 4);
diff --git a/Management.Test/Features/ConfigurationTests.cs b/Management.Test/Features/ConfigurationTests.cs
index d331aed..2276028 100644
--- a/Management.Test/Features/ConfigurationTests.cs
+++ b/Management.Test/Features/ConfigurationTests.cs
@@ -2,7 +2,7 @@
// public class ConfigurationTests
// {
-// [Test]
+// [Fact]
// public void TestCanCreateConfigFromTermAndDays()
// {
// DateTime startAt = new DateTime(2022, 1, 1);
diff --git a/Management.Test/Features/ModuleTests.cs b/Management.Test/Features/ModuleTests.cs
index 3c8d570..732ac41 100644
--- a/Management.Test/Features/ModuleTests.cs
+++ b/Management.Test/Features/ModuleTests.cs
@@ -1,6 +1,6 @@
// public class ModuleTests
// {
-// [Test]
+// [Fact]
// public void CanAddModule()
// {
// var manager = new ModuleManager();
@@ -11,7 +11,7 @@
// manager.Modules.First().Should().Be(module);
// }
-// [Test]
+// [Fact]
// public void CanAddAssignmentToCorrectModule()
// {
// var manager = new ModuleManager();
diff --git a/Management.Test/Features/SemesterPlannerTests.cs b/Management.Test/Features/SemesterPlannerTests.cs
index 1dedad0..6409eb4 100644
--- a/Management.Test/Features/SemesterPlannerTests.cs
+++ b/Management.Test/Features/SemesterPlannerTests.cs
@@ -4,7 +4,7 @@
// public class SemesterPlannerTests
// {
-// [Test]
+// [Fact]
// public void TestCanCreatePlanner()
// {
@@ -19,7 +19,7 @@
// semester.Months.Count().Should().Be(1);
// }
-// [Test]
+// [Fact]
// public void TestNewPlannerHasCorrectNumberOfMonths()
// {
// var config = new SemesterCalendarConfig(
@@ -33,7 +33,7 @@
// semester.Months.Count().Should().Be(2);
// }
-// [Test]
+// [Fact]
// public void TestNewPlannerHandlesTermsThatWrapYears()
// {
// var config = new SemesterCalendarConfig(
@@ -47,7 +47,7 @@
// semester.Months.Count().Should().Be(2);
// }
-// [Test]
+// [Fact]
// public void TestSemesterGetsCorrectMonths()
// {
// var config = new SemesterCalendarConfig(
@@ -63,7 +63,7 @@
// }
-// [Test]
+// [Fact]
// public void TestMonthsCanWrapYears()
// {
// var config = new SemesterCalendarConfig(
@@ -81,7 +81,7 @@
// semester.Months.Last().Year.Should().Be(2023);
// }
-// [Test]
+// [Fact]
// public void TestSemesterTracksDaysOfWeek()
// {
// DayOfWeek[] days = new DayOfWeek[] { DayOfWeek.Monday };
diff --git a/Management.Test/Management.Test.csproj b/Management.Test/Management.Test.csproj
index abf1b49..f9c8e43 100644
--- a/Management.Test/Management.Test.csproj
+++ b/Management.Test/Management.Test.csproj
@@ -9,15 +9,25 @@
+
+
-
-
-
-
-
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
diff --git a/Management.Test/Markdown/AssignmentMarkdownTests.cs b/Management.Test/Markdown/AssignmentMarkdownTests.cs
index 2ce5c60..941f7eb 100644
--- a/Management.Test/Markdown/AssignmentMarkdownTests.cs
+++ b/Management.Test/Markdown/AssignmentMarkdownTests.cs
@@ -2,7 +2,7 @@ using LocalModels;
public class AssignmentMarkdownTests
{
- [Test]
+ [Fact]
public void TestCanParseAssignmentSettings()
{
var assignment = new LocalAssignment()
@@ -24,7 +24,7 @@ public class AssignmentMarkdownTests
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
- [Test]
+ [Fact]
public void AssignmentWithEmptyRubric_CanBeParsed()
{
var assignment = new LocalAssignment()
@@ -43,7 +43,7 @@ public class AssignmentMarkdownTests
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
- [Test]
+ [Fact]
public void AssignmentWithEmptySubmissionTypes_CanBeParsed()
{
var assignment = new LocalAssignment()
@@ -66,7 +66,7 @@ public class AssignmentMarkdownTests
parsedAssignment.Should().BeEquivalentTo(assignment);
}
- [Test]
+ [Fact]
public void AssignmentWithoutLockAtDate_CanBeParsed()
{
var assignment = new LocalAssignment()
@@ -89,7 +89,7 @@ public class AssignmentMarkdownTests
parsedAssignment.Should().BeEquivalentTo(assignment);
}
- [Test]
+ [Fact]
public void AssignmentWithoutDescription_CanBeParsed()
{
var assignment = new LocalAssignment()
@@ -111,7 +111,7 @@ public class AssignmentMarkdownTests
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
- [Test]
+ [Fact]
public void Assignments_CanHaveThreeDashes()
{
var assignment = new LocalAssignment()
diff --git a/Management.Test/Markdown/FileStorageTests.cs b/Management.Test/Markdown/FileStorageTests.cs
index e72056e..038d608 100644
--- a/Management.Test/Markdown/FileStorageTests.cs
+++ b/Management.Test/Markdown/FileStorageTests.cs
@@ -5,14 +5,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
-using NUnit.Framework.Internal;
public class FileStorageTests
{
- private IFileStorageManager fileManager { get; set; }
+ private FileStorageManager fileManager { get; set; }
- private static string setupTempDirectory()
+ public FileStorageTests()
{
var tempDirectory = Path.GetTempPath();
var storageDirectory = tempDirectory + "fileStorageTests";
@@ -29,14 +28,6 @@ public class FileStorageTests
dir.Delete(true);
}
- return storageDirectory;
- }
-
- [SetUp]
- public void SetUp()
- {
- var storageDirectory = setupTempDirectory();
-
var fileManagerLogger = new MyLogger(NullLogger.Instance);
var markdownLoaderLogger = new MyLogger(NullLogger.Instance);
var markdownSaverLogger = new MyLogger(NullLogger.Instance);
@@ -52,7 +43,7 @@ public class FileStorageTests
fileManager = new FileStorageManager(fileManagerLogger, markdownLoader, markdownSaver, otherLogger, fileConfiguration);
}
- [Test]
+ [Fact]
public async Task EmptyCourse_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new LocalCourse
@@ -69,7 +60,7 @@ public class FileStorageTests
loadedCourse.Should().BeEquivalentTo(testCourse);
}
- [Test]
+ [Fact]
public async Task CourseSettings_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
@@ -95,7 +86,7 @@ public class FileStorageTests
}
- [Test]
+ [Fact]
public async Task EmptyCourseModules_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
@@ -119,7 +110,7 @@ public class FileStorageTests
loadedCourse.Modules.Should().BeEquivalentTo(testCourse.Modules);
}
- [Test]
+ [Fact]
public async Task CourseModules_WithAssignments_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
@@ -160,7 +151,7 @@ public class FileStorageTests
}
- [Test]
+ [Fact]
public async Task CourseModules_WithQuizzes_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
@@ -204,7 +195,7 @@ public class FileStorageTests
}
- [Test]
+ [Fact]
public async Task MarkdownStorage_FullyPopulated_DoesNotLoseData()
{
LocalCourse testCourse = new()
@@ -271,7 +262,7 @@ public class FileStorageTests
}
- [Test]
+ [Fact]
public async Task MarkdownStorage_CanPersistPages()
{
LocalCourse testCourse = new()
diff --git a/Management.Test/Markdown/PageMarkdownTests.cs b/Management.Test/Markdown/PageMarkdownTests.cs
index cd5a802..523509e 100644
--- a/Management.Test/Markdown/PageMarkdownTests.cs
+++ b/Management.Test/Markdown/PageMarkdownTests.cs
@@ -2,7 +2,7 @@ using LocalModels;
public class PageMarkdownTests
{
- [Test]
+ [Fact]
public void TestCanParsePage()
{
var page = new LocalCoursePage
diff --git a/Management.Test/Markdown/Quiz/MatchingTests.cs b/Management.Test/Markdown/Quiz/MatchingTests.cs
index fa78559..1a66e5f 100644
--- a/Management.Test/Markdown/Quiz/MatchingTests.cs
+++ b/Management.Test/Markdown/Quiz/MatchingTests.cs
@@ -2,7 +2,7 @@ using LocalModels;
public class MatchingTests
{
- [Test]
+ [Fact]
public void CanParseMatchingQuestion()
{
var rawMarkdownQuiz = @"
@@ -29,7 +29,7 @@ Match the following terms & definitions
firstQuestion.Answers.First().MatchedText.Should().Be("a single command to be executed");
}
- [Test]
+ [Fact]
public void CanCreateMarkdownForMatchingQuesiton()
{
var rawMarkdownQuiz = @"
@@ -59,7 +59,7 @@ Match the following terms & definitions
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)";
questionMarkdown.Should().Contain(expectedMarkdown);
}
- [Test]
+ [Fact]
public void WhitespaceIsOptional()
{
var rawMarkdownQuiz = @"
diff --git a/Management.Test/Markdown/Quiz/MultipleAnswersTests.cs b/Management.Test/Markdown/Quiz/MultipleAnswersTests.cs
index e9a123d..e297ade 100644
--- a/Management.Test/Markdown/Quiz/MultipleAnswersTests.cs
+++ b/Management.Test/Markdown/Quiz/MultipleAnswersTests.cs
@@ -3,7 +3,7 @@ using LocalModels;
public class MultipleAnswersTests
{
- [Test]
+ [Fact]
public void QuzMarkdownIncludesMultipleAnswerQuestion()
{
var quiz = new LocalQuiz()
@@ -43,7 +43,7 @@ oneline question
markdown.Should().Contain(expectedQuestionString);
}
- [Test]
+ [Fact]
public void CanParseQuestionWithMultipleAnswers()
{
var rawMarkdownQuiz = @"
@@ -81,7 +81,7 @@ Which events are triggered when the user clicks on an input field?
}
- [Test]
+ [Fact]
public void CanUseBracesInAnswerFormultipleAnswer()
{
var rawMarkdownQuestion = @"
@@ -95,7 +95,7 @@ Which events are triggered when the user clicks on an input field?
question.Answers.Count().Should().Be(2);
}
- [Test]
+ [Fact]
public void CanUseBracesInAnswerFormultipleAnswer_MultiLine()
{
var rawMarkdownQuestion = @"
diff --git a/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs b/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs
index 715076d..39d366f 100644
--- a/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs
+++ b/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs
@@ -2,7 +2,7 @@ using LocalModels;
public class MultipleChoiceTests
{
- [Test]
+ [Fact]
public void QuzMarkdownIncludesMultipleChoiceQuestion()
{
var quiz = new LocalQuiz()
@@ -58,7 +58,7 @@ endline
}
- [Test]
+ [Fact]
public void LetterOptionalForMultipleChoice()
{
diff --git a/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs
index 825bbdf..a675b6c 100644
--- a/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs
+++ b/Management.Test/Markdown/Quiz/QuizDeterministicChecks.cs
@@ -4,7 +4,7 @@ using LocalModels;
public class QuizDeterministicChecks
{
- [Test]
+ [Fact]
public void SerializationIsDeterministic_EmptyQuiz()
{
var quiz = new LocalQuiz()
@@ -23,7 +23,7 @@ public class QuizDeterministicChecks
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_ShowCorrectAnswers()
{
var quiz = new LocalQuiz()
@@ -43,7 +43,7 @@ public class QuizDeterministicChecks
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_ShortAnswer()
{
var quiz = new LocalQuiz()
@@ -71,7 +71,7 @@ public class QuizDeterministicChecks
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_Essay()
{
var quiz = new LocalQuiz()
@@ -99,7 +99,7 @@ public class QuizDeterministicChecks
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_MultipleAnswer()
{
var quiz = new LocalQuiz()
@@ -138,7 +138,7 @@ public class QuizDeterministicChecks
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_MultipleChoice()
{
var quiz = new LocalQuiz()
@@ -176,7 +176,7 @@ public class QuizDeterministicChecks
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
- [Test]
+ [Fact]
public void SerializationIsDeterministic_Matching()
{
var quiz = new LocalQuiz()
diff --git a/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs b/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs
index 7ec7ce4..9f1116e 100644
--- a/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs
+++ b/Management.Test/Markdown/Quiz/QuizMarkdownTests.cs
@@ -4,7 +4,7 @@ using LocalModels;
// try to follow syntax from https://github.com/gpoore/text2qti
public class QuizMarkdownTests
{
- [Test]
+ [Fact]
public void CanSerializeQuizToMarkdown()
{
var quiz = new LocalQuiz()
@@ -37,7 +37,7 @@ this is my description in markdown
}
- [Test]
+ [Fact]
public void TestCanParseMarkdownQuizWithNoQuestions()
{
var rawMarkdownQuiz = new StringBuilder();
@@ -68,7 +68,7 @@ this is my description in markdown
quiz.AllowedAttempts.Should().Be(-1);
quiz.Description.Should().Be(expectedDescription.ToString());
}
- [Test]
+ [Fact]
public void TestCanParseMarkdownQuizPassword()
{
@@ -94,7 +94,7 @@ this is my description in markdown
quiz.Password.Should().Be(password);
}
- [Test]
+ [Fact]
public void TestCanParseMarkdownQuiz_CanConfigureToShowCorrectAnswers()
{
var rawMarkdownQuiz = new StringBuilder();
@@ -118,7 +118,7 @@ this is my description in markdown
quiz.showCorrectAnswers.Should().BeFalse();
}
- [Test]
+ [Fact]
public void TestCanParseQuizWithQuestions()
{
var rawMarkdownQuiz = @"
@@ -159,7 +159,7 @@ b) false
firstQuestion.Answers.ElementAt(1).Text.Should().Contain("endline");
}
- [Test]
+ [Fact]
public void CanParseMultipleQuestions()
{
var rawMarkdownQuiz = @"
@@ -192,7 +192,7 @@ b) false
secondQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_CHOICE);
}
- [Test]
+ [Fact]
public void ShortAnswerToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
@@ -220,7 +220,7 @@ Which events are triggered when the user clicks on an input field?
short_answer";
questionMarkdown.Should().Contain(expectedMarkdown);
}
- [Test]
+ [Fact]
public void NegativePoints_IsAllowed()
{
var rawMarkdownQuiz = @"
@@ -244,7 +244,7 @@ short answer
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(-4);
}
- [Test]
+ [Fact]
public void FloatingPointPoints_IsAllowed()
{
var rawMarkdownQuiz = @"
diff --git a/Management.Test/Markdown/Quiz/TextAnswerTests.cs b/Management.Test/Markdown/Quiz/TextAnswerTests.cs
index af49a21..f4dbe69 100644
--- a/Management.Test/Markdown/Quiz/TextAnswerTests.cs
+++ b/Management.Test/Markdown/Quiz/TextAnswerTests.cs
@@ -2,7 +2,7 @@ using LocalModels;
public class TextAnswerTests
{
- [Test]
+ [Fact]
public void CanParseEssay()
{
var rawMarkdownQuiz = @"
@@ -28,7 +28,7 @@ essay
firstQuestion.Text.Should().NotContain("essay");
}
- [Test]
+ [Fact]
public void CanParseShortAnswer()
{
var rawMarkdownQuiz = @"
@@ -54,7 +54,7 @@ short answer
firstQuestion.Text.Should().NotContain("short answer");
}
- [Test]
+ [Fact]
public void ShortAnswerToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
@@ -83,7 +83,7 @@ short_answer";
questionMarkdown.Should().Contain(expectedMarkdown);
}
- [Test]
+ [Fact]
public void EssayQuestionToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
diff --git a/Management.Test/Markdown/RubricMarkdownTests.cs b/Management.Test/Markdown/RubricMarkdownTests.cs
index 542806c..23b0aa4 100644
--- a/Management.Test/Markdown/RubricMarkdownTests.cs
+++ b/Management.Test/Markdown/RubricMarkdownTests.cs
@@ -3,7 +3,7 @@ using LocalModels;
public class RubricMarkdownTests
{
- [Test]
+ [Fact]
public void TestCanParseOneItem()
{
var rawRubric = @"
@@ -17,7 +17,7 @@ public class RubricMarkdownTests
rubric.First().Points.Should().Be(2);
}
- [Test]
+ [Fact]
public void TestCanParseMultipleItems()
{
var rawRubric = @"
@@ -32,7 +32,7 @@ public class RubricMarkdownTests
rubric.ElementAt(1).Points.Should().Be(3);
}
- [Test]
+ [Fact]
public void TestCanParseSinglePoint()
{
var rawRubric = @"
@@ -45,7 +45,7 @@ public class RubricMarkdownTests
rubric.First().Points.Should().Be(1);
}
- [Test]
+ [Fact]
public void TestCanParseSingleExtraCredit_LowerCase()
{
var rawRubric = @"
@@ -57,7 +57,7 @@ public class RubricMarkdownTests
rubric.First().Label.Should().Be("(extra credit) this is the task");
}
- [Test]
+ [Fact]
public void TestCanParseSingleExtraCredit_UpperCase()
{
var rawRubric = @"
@@ -69,7 +69,7 @@ public class RubricMarkdownTests
rubric.First().Label.Should().Be("(Extra Credit) this is the task");
}
- [Test]
+ [Fact]
public void TestCanParseFloatingPointNubmers()
{
var rawRubric = @"
@@ -79,7 +79,7 @@ public class RubricMarkdownTests
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().Points.Should().Be(1.5);
}
- [Test]
+ [Fact]
public void TestCanParseNegativeNubmers()
{
var rawRubric = @"
@@ -89,7 +89,7 @@ public class RubricMarkdownTests
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().Points.Should().Be(-2.0);
}
- [Test]
+ [Fact]
public void TestCanParseNegativeFloatingPointNubmers()
{
var rawRubric = @"
diff --git a/Management.Test/Services/CanvasServiceTests.cs b/Management.Test/Services/CanvasServiceTests.cs
index 1504306..d0dc1fe 100644
--- a/Management.Test/Services/CanvasServiceTests.cs
+++ b/Management.Test/Services/CanvasServiceTests.cs
@@ -9,7 +9,7 @@
// public class ICanvasServiceTests
// {
-// [Test]
+// [Fact]
// public async Task CanReadCanvasSemesters()
// {
// var expectedTerms = new EnrollmentTermModel[] {
@@ -28,7 +28,7 @@
// canvasTerms.Should().BeEquivalentTo(expectedTerms);
// }
-// [Test]
+// [Fact]
// public async Task CanGetActiveTerms()
// {
// var expectedTerms = new EnrollmentTermModel[] {
diff --git a/Management.Test/Usings.cs b/Management.Test/Usings.cs
index 41fe2b8..600fb6d 100644
--- a/Management.Test/Usings.cs
+++ b/Management.Test/Usings.cs
@@ -1,3 +1,3 @@
global using System.Text.Json;
global using FluentAssertions;
-global using NUnit.Framework;
+global using Xunit;
diff --git a/Management.Test/ViewModels/MonthDetailTests.cs b/Management.Test/ViewModels/MonthDetailTests.cs
index 6969a2b..4da2ff8 100644
--- a/Management.Test/ViewModels/MonthDetailTests.cs
+++ b/Management.Test/ViewModels/MonthDetailTests.cs
@@ -2,7 +2,7 @@ using Management.Web.Pages.Course.CourseCalendar;
public class MonthDetailTests
{
- [Test]
+ [Fact]
public void TestCanGetMonthName()
{
var calendarMonth = new CalendarMonth(2022, 2);
diff --git a/Management.Web/Management.Web.csproj b/Management.Web/Management.Web.csproj
index 510e607..ca8c602 100644
--- a/Management.Web/Management.Web.csproj
+++ b/Management.Web/Management.Web.csproj
@@ -5,6 +5,8 @@
+
+
diff --git a/Management.Web/Program.cs b/Management.Web/Program.cs
index 1d6e128..1dec766 100644
--- a/Management.Web/Program.cs
+++ b/Management.Web/Program.cs
@@ -1,19 +1,25 @@
global using System.ComponentModel.DataAnnotations;
global using System.Text.Json;
global using System.Text.Json.Serialization;
+
global using CanvasModel;
global using CanvasModel.Courses;
global using CanvasModel.EnrollmentTerms;
+
global using LocalModels;
+
global using Management.Planner;
global using Management.Services;
global using Management.Services.Canvas;
global using Management.Web.Shared;
global using Management.Web.Shared.Components;
+
using dotenv.net;
+
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.ResponseCompression;
+
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
@@ -69,25 +75,40 @@ builder.Services.AddServerSideBlazor();
builder.Services.AddLogging();
-builder.Services.AddScoped(typeof(MyLogger<>));
+builder.Services.AddSingleton(typeof(MyLogger<>));
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
+// stateless services
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped(sp =>
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddSingleton();
+
+// one actor system, maybe different actor for different pages?
+builder.Services.AddSingleton();
+builder.Services.AddHostedService(sp => sp.GetRequiredService());
+
+
+// TODO: need to handle scoped requirements
+builder.Services.AddSingleton(sp =>
{
- var manager = ActivatorUtilities.CreateInstance(sp);
- var logger = sp.GetRequiredService>();
- return new FileStorageManagerCached(manager, logger);
+ var akka = sp.GetRequiredService();
+ return new CanvasQueue(akka.CanvasQueueActor ?? throw new Exception("Canvas queue actor not properly created"));
});
+builder.Services.AddSingleton(sp =>
+{
+ var akka = sp.GetRequiredService();
+ return new LocalStorageCache(akka.StorageActor ?? throw new Exception("Canvas queue actor not properly created"));
+});
+
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/Management.Web/Shared/InitializeNewCourse.razor b/Management.Web/Shared/InitializeNewCourse.razor
index 144b227..43001ae 100644
--- a/Management.Web/Shared/InitializeNewCourse.razor
+++ b/Management.Web/Shared/InitializeNewCourse.razor
@@ -56,7 +56,7 @@
loadingTerms = true;
terms = await canvas.GetCurrentTermsFor();
loadingTerms = false;
- directoriesNotUsed = fileStorageManager.GetEmptyDirectories();
+ directoriesNotUsed = await fileStorageManager.GetEmptyDirectories();
}
private async Task SaveNewCourse()
diff --git a/Management/Management.csproj b/Management/Management.csproj
index 23d6e0b..6ae3274 100644
--- a/Management/Management.csproj
+++ b/Management/Management.csproj
@@ -7,6 +7,8 @@
+
+
@@ -16,4 +18,7 @@
+
+
+
diff --git a/Management/Services/Actors/CanvasQueue.cs b/Management/Services/Actors/CanvasQueue.cs
new file mode 100644
index 0000000..f7ca500
--- /dev/null
+++ b/Management/Services/Actors/CanvasQueue.cs
@@ -0,0 +1,9 @@
+using Akka.Actor;
+
+using Management.Services.Canvas;
+
+public class CanvasQueue(IActorRef canvasQueueActor)
+{
+ private readonly IActorRef canvasQueueActor = canvasQueueActor;
+
+}
\ No newline at end of file
diff --git a/Management/Services/Actors/CanvasQueueActor.cs b/Management/Services/Actors/CanvasQueueActor.cs
new file mode 100644
index 0000000..7c841c0
--- /dev/null
+++ b/Management/Services/Actors/CanvasQueueActor.cs
@@ -0,0 +1,18 @@
+using Akka.Actor;
+
+using Microsoft.Extensions.DependencyInjection;
+
+public class CanvasQueueActor : ReceiveActor
+{
+ private readonly IServiceProvider serviceProvider;
+ private readonly IServiceScope scope;
+ private readonly ILogger logger;
+
+ public CanvasQueueActor(IServiceProvider serviceProviderArg)
+ {
+ serviceProvider = serviceProviderArg;
+ scope = serviceProvider.CreateScope();
+ logger = scope.ServiceProvider.GetRequiredService>();
+
+ }
+}
\ No newline at end of file
diff --git a/Management/Services/Actors/LocalStorageActor.cs b/Management/Services/Actors/LocalStorageActor.cs
new file mode 100644
index 0000000..44d840e
--- /dev/null
+++ b/Management/Services/Actors/LocalStorageActor.cs
@@ -0,0 +1,61 @@
+using Akka.Actor;
+
+using LocalModels;
+
+using Microsoft.Extensions.DependencyInjection;
+
+public class LocalStorageActor : ReceiveActor
+{
+ private readonly IServiceProvider serviceProvider;
+ private readonly IServiceScope scope;
+ private readonly ILogger logger;
+ private readonly FileStorageManager storage;
+
+ private DateTime? cacheTime { get; set; } = null;
+ private IEnumerable? cachedCourses { get; set; } = null;
+ private readonly int cacheSeconds = 2;
+
+ public LocalStorageActor(IServiceProvider serviceProviderArg)
+ {
+ serviceProvider = serviceProviderArg;
+ scope = serviceProvider.CreateScope();
+ logger = scope.ServiceProvider.GetRequiredService>();
+ storage = scope.ServiceProvider.GetRequiredService();
+
+ Receive(m =>
+ {
+ storage
+ .GetEmptyDirectories()
+ .PipeTo(Sender);
+ });
+
+ ReceiveAsync(async m =>
+ {
+ var secondsFromLastLoad = (DateTime.Now - cacheTime)?.Seconds;
+
+ if (cachedCourses != null && secondsFromLastLoad < cacheSeconds)
+ {
+ logger.LogInformation("returning cached courses from file");
+ Sender.Tell(cachedCourses);
+ return;
+ }
+
+ cachedCourses = await storage.LoadSavedCourses();
+ cacheTime = DateTime.Now;
+ Sender.Tell(cachedCourses);
+ });
+
+ ReceiveAsync(async m =>
+ {
+ cacheTime = null;
+ cachedCourses = null;
+ await storage.SaveCourseAsync(m.Course, m.PreviouslyStoredCourse);
+ });
+ }
+}
+
+public record EmptyDirectoryAsk();
+public record SavedCoursesAsk();
+
+public record SaveCoursesRequest(LocalCourse Course, LocalCourse? PreviouslyStoredCourse);
+public record SaveCoursesResponseSuccess();
\ No newline at end of file
diff --git a/Management/Services/Actors/LocalStorageCache.cs b/Management/Services/Actors/LocalStorageCache.cs
new file mode 100644
index 0000000..b00c64a
--- /dev/null
+++ b/Management/Services/Actors/LocalStorageCache.cs
@@ -0,0 +1,23 @@
+using Akka.Actor;
+
+using LocalModels;
+
+public class LocalStorageCache(IActorRef storageActor) : IFileStorageManager
+{
+ private readonly IActorRef storageActor = storageActor;
+
+ public async Task> GetEmptyDirectories()
+ {
+ return await storageActor.Ask>(new EmptyDirectoryAsk());
+ }
+
+ public async Task> LoadSavedCourses()
+ {
+ return await storageActor.Ask>(new SavedCoursesAsk());
+ }
+
+ public async Task SaveCourseAsync(LocalCourse course, LocalCourse? previouslyStoredCourse)
+ {
+ await storageActor.Ask(new SaveCoursesRequest(course, previouslyStoredCourse));
+ }
+}
\ No newline at end of file
diff --git a/Management/Services/AkkaService.cs b/Management/Services/AkkaService.cs
new file mode 100644
index 0000000..5186f85
--- /dev/null
+++ b/Management/Services/AkkaService.cs
@@ -0,0 +1,60 @@
+
+using Akka.Actor;
+using Akka.DependencyInjection;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+
+namespace Management.Services;
+
+
+public class AkkaService(
+ IServiceProvider serviceProvider,
+ IHostApplicationLifetime appLifetime,
+ IConfiguration configuration
+) : IHostedService
+{
+ private ActorSystem? actorSystem;
+ private readonly IConfiguration configuration = configuration;
+ private readonly IServiceProvider serviceProvider = serviceProvider;
+ private readonly IHostApplicationLifetime applicationLifetime = appLifetime;
+ public IActorRef? CanvasQueueActor { get; private set; }
+ public IActorRef? StorageActor { get; private set; }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ var bootstrap = BootstrapSetup.Create();
+ var dependencyInjectionSetup = DependencyResolverSetup.Create(serviceProvider);
+
+ var mergedSystemSetup = bootstrap.And(dependencyInjectionSetup);
+
+ actorSystem = ActorSystem.Create("canavas-management-actor-system", mergedSystemSetup);
+
+ var canvasQueueProps = DependencyResolver.For(actorSystem).Props();
+ CanvasQueueActor = actorSystem.ActorOf(canvasQueueProps, "canvasQueue");
+ var localStorageProps = DependencyResolver.For(actorSystem).Props();
+ StorageActor = actorSystem.ActorOf(localStorageProps, "localStorage");
+
+ // crash if the actor system crashes, awaiting never returns...
+ actorSystem.WhenTerminated.ContinueWith(tr =>
+ {
+ applicationLifetime.StopApplication();
+ });
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+
+ // public void Tell(object message)
+ // {
+ // userSessionSupervisor?.Tell(message);
+ // }
+
+ // public Task Ask(object message)
+ // {
+ // return userSessionSupervisor.Ask(message);
+ // }
+
+}
diff --git a/Management/Services/Canvas/CanvasService.cs b/Management/Services/Canvas/CanvasService.cs
index 3f961db..c0378c5 100644
--- a/Management/Services/Canvas/CanvasService.cs
+++ b/Management/Services/Canvas/CanvasService.cs
@@ -4,7 +4,6 @@ using CanvasModel.Courses;
using CanvasModel.EnrollmentTerms;
using CanvasModel.Modules;
using CanvasModel.Pages;
-using Microsoft.Extensions.Logging;
using RestSharp;
namespace Management.Services.Canvas;
@@ -35,7 +34,7 @@ public class CanvasService(
ICanvasQuizService Quizzes,
ICanvasCoursePageService Pages,
MyLogger logger
-):ICanvasService
+) : ICanvasService
{
private readonly IWebRequestor webRequestor = webRequestor;
private readonly CanvasServiceUtils utils = utils;
diff --git a/Management/Services/Files/FileStorageManager.cs b/Management/Services/Files/FileStorageManager.cs
index bd48c0e..5a24958 100644
--- a/Management/Services/Files/FileStorageManager.cs
+++ b/Management/Services/Files/FileStorageManager.cs
@@ -1,7 +1,7 @@
using LocalModels;
using Management.Services;
-public class FileStorageManager : IFileStorageManager
+public class FileStorageManager
{
private readonly MyLogger logger;
private readonly CourseMarkdownLoader _courseMarkdownLoader;
@@ -39,7 +39,7 @@ public class FileStorageManager : IFileStorageManager
return await _courseMarkdownLoader.LoadSavedCourses();
}
- public IEnumerable GetEmptyDirectories()
+ public async Task> GetEmptyDirectories()
{
if (!Directory.Exists(_basePath))
throw new DirectoryNotFoundException($"Cannot get empty directories, {_basePath} does not exist");
diff --git a/Management/Services/Files/FileStorageManagerCached.cs b/Management/Services/Files/FileStorageManagerCached.cs
index be4ff02..f0d2d80 100644
--- a/Management/Services/Files/FileStorageManagerCached.cs
+++ b/Management/Services/Files/FileStorageManagerCached.cs
@@ -1,50 +1,50 @@
-using System.Diagnostics.CodeAnalysis;
-using LocalModels;
+// using System.Diagnostics.CodeAnalysis;
+// using LocalModels;
-public class FileStorageManagerCached : IFileStorageManager
-{
- private readonly FileStorageManager manager;
+// public class FileStorageManagerCached : IFileStorageManager
+// {
+// private readonly FileStorageManager manager;
- private readonly object cacheLock = new object(); // Lock object for synchronization
+// private readonly object cacheLock = new object(); // Lock object for synchronization
- private DateTime? cacheTime { get; set; } = null;
- private IEnumerable? cachedCourses { get; set; } = null;
- private ILogger logger { get; }
+// private DateTime? cacheTime { get; set; } = null;
+// private IEnumerable? cachedCourses { get; set; } = null;
+// private ILogger logger { get; }
- private readonly int cacheSeconds = 2;
- public FileStorageManagerCached(FileStorageManager manager, ILogger logger)
- {
- this.manager = manager;
- this.logger = logger;
- }
- public IEnumerable GetEmptyDirectories()
- {
- return manager.GetEmptyDirectories();
- }
+// private readonly int cacheSeconds = 2;
+// public FileStorageManagerCached(FileStorageManager manager, ILogger logger)
+// {
+// this.manager = manager;
+// this.logger = logger;
+// }
+// public Task> GetEmptyDirectories()
+// {
+// return manager.GetEmptyDirectories();
+// }
- public async Task> LoadSavedCourses()
- {
+// public async Task> LoadSavedCourses()
+// {
- var secondsFromLastLoad = (DateTime.Now - cacheTime)?.Seconds;
+// var secondsFromLastLoad = (DateTime.Now - cacheTime)?.Seconds;
- if (cachedCourses != null && secondsFromLastLoad < cacheSeconds)
- {
- logger.LogInformation("returning cached courses from file");
- return cachedCourses;
- }
+// if (cachedCourses != null && secondsFromLastLoad < cacheSeconds)
+// {
+// logger.LogInformation("returning cached courses from file");
+// return cachedCourses;
+// }
- cachedCourses = await manager.LoadSavedCourses();
- cacheTime = DateTime.Now;
- return cachedCourses;
- }
+// cachedCourses = await manager.LoadSavedCourses();
+// cacheTime = DateTime.Now;
+// return cachedCourses;
+// }
- public async Task SaveCourseAsync(LocalCourse course, LocalCourse? previouslyStoredCourse)
- {
- // race condition...
- cacheTime = null;
- cachedCourses = null;
- await manager.SaveCourseAsync(course, previouslyStoredCourse);
- }
-}
+// public async Task SaveCourseAsync(LocalCourse course, LocalCourse? previouslyStoredCourse)
+// {
+// // race condition...
+// cacheTime = null;
+// cachedCourses = null;
+// await manager.SaveCourseAsync(course, previouslyStoredCourse);
+// }
+// }
diff --git a/Management/Services/Files/IFileStorageManager.cs b/Management/Services/Files/IFileStorageManager.cs
index dfc5c11..b45ffc5 100644
--- a/Management/Services/Files/IFileStorageManager.cs
+++ b/Management/Services/Files/IFileStorageManager.cs
@@ -4,5 +4,5 @@ public interface IFileStorageManager
{
Task SaveCourseAsync(LocalCourse course, LocalCourse? previouslyStoredCourse);
Task> LoadSavedCourses();
- IEnumerable GetEmptyDirectories();
+ Task> GetEmptyDirectories();
}
diff --git a/README.md b/README.md
index 6022bf0..571ea82 100644
--- a/README.md
+++ b/README.md
@@ -48,3 +48,5 @@ schedule planning view? just outline concepts? (maybe some non-canvas scheduled
holiday schedule
multi-seciton support for due dates/times
+
+better error handling when files are unparseable
\ No newline at end of file