moai-adk 0.4.5__py3-none-any.whl → 0.20.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +1 -1
- moai_adk/__main__.py +74 -1
- moai_adk/cli/commands/__init__.py +1 -1
- moai_adk/cli/commands/analyze.py +119 -0
- moai_adk/cli/commands/backup.py +25 -1
- moai_adk/cli/commands/doctor.py +31 -5
- moai_adk/cli/commands/improve_user_experience.py +307 -0
- moai_adk/cli/commands/init.py +111 -10
- moai_adk/cli/commands/status.py +33 -3
- moai_adk/cli/commands/update.py +921 -130
- moai_adk/cli/commands/validate_links.py +120 -0
- moai_adk/cli/prompts/init_prompts.py +22 -87
- moai_adk/core/analysis/__init__.py +9 -0
- moai_adk/core/analysis/session_analyzer.py +388 -0
- moai_adk/core/analysis/tag_chain_analyzer.py +344 -0
- moai_adk/core/analysis/tag_chain_repair.py +879 -0
- moai_adk/core/config/__init__.py +19 -0
- moai_adk/core/config/migration.py +235 -0
- moai_adk/core/git/__init__.py +1 -1
- moai_adk/core/git/branch.py +1 -1
- moai_adk/core/git/commit.py +1 -1
- moai_adk/core/git/manager.py +1 -1
- moai_adk/core/issue_creator.py +313 -0
- moai_adk/core/mcp/setup.py +56 -0
- moai_adk/core/mcp/setup_old.py +296 -0
- moai_adk/core/project/backup_utils.py +1 -1
- moai_adk/core/project/checker.py +2 -2
- moai_adk/core/project/detector.py +211 -12
- moai_adk/core/project/initializer.py +85 -15
- moai_adk/core/project/phase_executor.py +76 -13
- moai_adk/core/project/validator.py +13 -13
- moai_adk/core/quality/__init__.py +1 -1
- moai_adk/core/quality/trust_checker.py +1 -1
- moai_adk/core/quality/validators/__init__.py +1 -1
- moai_adk/core/quality/validators/base_validator.py +1 -1
- moai_adk/core/tags/__init__.py +86 -0
- moai_adk/core/tags/auto_corrector.py +693 -0
- moai_adk/core/tags/ci_validator.py +463 -0
- moai_adk/core/tags/cli.py +283 -0
- moai_adk/core/tags/generator.py +109 -0
- moai_adk/core/tags/inserter.py +99 -0
- moai_adk/core/tags/mapper.py +126 -0
- moai_adk/core/tags/parser.py +76 -0
- moai_adk/core/tags/policy_validator.py +580 -0
- moai_adk/core/tags/pre_commit_validator.py +421 -0
- moai_adk/core/tags/reporter.py +956 -0
- moai_adk/core/tags/rollback_manager.py +525 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/core/template/__init__.py +1 -1
- moai_adk/core/template/backup.py +1 -1
- moai_adk/core/template/merger.py +50 -1
- moai_adk/core/template/processor.py +119 -13
- moai_adk/core/template_engine.py +268 -0
- moai_adk/templates/.claude/agents/alfred/backend-expert.md +348 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +209 -944
- moai_adk/templates/.claude/agents/alfred/database-expert.md +352 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +34 -5
- moai_adk/templates/.claude/agents/alfred/devops-expert.md +464 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +38 -8
- moai_adk/templates/.claude/agents/alfred/format-expert.md +469 -0
- moai_adk/templates/.claude/agents/alfred/frontend-expert.md +357 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +128 -9
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +104 -6
- moai_adk/templates/.claude/agents/alfred/project-manager.md +88 -16
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +36 -9
- moai_adk/templates/.claude/agents/alfred/security-expert.md +270 -0
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +865 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +214 -43
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +111 -9
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +309 -160
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +36 -7
- moai_adk/templates/.claude/agents/alfred/ui-ux-expert.md +605 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +393 -966
- moai_adk/templates/.claude/commands/alfred/1-plan.md +651 -367
- moai_adk/templates/.claude/commands/alfred/2-run.md +388 -241
- moai_adk/templates/.claude/commands/alfred/3-sync.md +1921 -410
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +153 -0
- moai_adk/templates/.claude/commands/alfred/release-new.md +3604 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +484 -20
- moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
- moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +108 -0
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +14 -6
- moai_adk/templates/.claude/hooks/alfred/post_tool__enable_streaming_ui.py +50 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__tag_auto_corrector.py +407 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +99 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__realtime_tag_monitor.py +335 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__tag_policy_validator.py +325 -0
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__auto_cleanup.py +580 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +298 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +170 -0
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +3 -3
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +5 -5
- moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +749 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +230 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/__init__.py +21 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/daily_analysis.py +351 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +154 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +174 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +87 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/user.py +61 -0
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +111 -0
- moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
- moai_adk/templates/.claude/hooks/alfred/utils/hook_config.py +94 -0
- moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +161 -0
- moai_adk/templates/.claude/output-styles/alfred/alfred-moai-adk-beginner.md +267 -0
- moai_adk/templates/.claude/output-styles/alfred/keating-personal-tutor.md +440 -0
- moai_adk/templates/.claude/output-styles/alfred/r2d2-agentic-coding.md +583 -0
- moai_adk/templates/.claude/settings.json +96 -14
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/SKILL.md +70 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/examples.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/reference.md +242 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/SKILL.md +237 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/examples.md +871 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/reference.md +653 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/README.md +162 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/SKILL.md +227 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/examples.md +354 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/reference.md +158 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/SKILL.md +179 -79
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/examples.md +117 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/scripts/pre-review-check.sh +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/SKILL.md +132 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/reference.md +444 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/SKILL.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/SKILL.md +51 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/examples.md +355 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/reference.md +239 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/SKILL.md +323 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/examples.md +286 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/reference.md +126 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/SKILL.md +229 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/reference.md +150 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/SKILL.md +87 -73
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/README.md +42 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/SKILL.md +429 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/examples.md +520 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/SKILL.md +89 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/examples.md +122 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/reference.md +369 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/SKILL.md +508 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/examples.md +481 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/reference.md +100 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/SKILL.md +77 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/examples.md +265 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/reference.md +539 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/reference.md +84 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/README.md +137 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/SKILL.md +219 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples/validate-spec.sh +161 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples.md +541 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/reference.md +622 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/SKILL.md +19 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/reference.md +211 -0
- moai_adk/templates/.claude/skills/moai-alfred-workflow/SKILL.md +288 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/SKILL.md +269 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/templates/agent-template.md +32 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/SKILL.md +298 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/templates/CLAUDE-template.md +26 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/SKILL.md +307 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/templates/command-template.md +21 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/SKILL.md +252 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/pre-bash-check.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/preserve-permissions.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/validate-bash-command.py +24 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/SKILL.md +199 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/templates/settings-mcp-template.json +39 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/SKILL.md +316 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/templates/session-summary-template.md +18 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/SKILL.md +263 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/templates/settings-complete-template.json +30 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/CHECKLIST.md +482 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/EXAMPLES.md +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/INTERACTIVE-DISCOVERY.md +524 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/METADATA.md +477 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PARALLEL-ANALYSIS-REPORT.md +429 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PYTHON-VERSION-MATRIX.md +391 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-FACTORY-WORKFLOW.md +431 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-UPDATE-ADVISOR.md +577 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL.md +273 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STEP-BY-STEP-GUIDE.md +466 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STRUCTURE.md +583 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/WEB-RESEARCH.md +526 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/reference.md +608 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/generate-structure.sh +328 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/validate-skill.sh +312 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/SKILL_TEMPLATE.md +245 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/examples-template.md +285 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/reference-template.md +278 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/scripts-template.sh +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/SKILL.md +291 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/templates/SKILL-template.md +15 -0
- moai_adk/templates/.claude/skills/moai-change-logger/SKILL.md +563 -0
- moai_adk/templates/.claude/skills/moai-design-systems/SKILL.md +802 -0
- moai_adk/templates/.claude/skills/moai-design-systems/examples.md +1238 -0
- moai_adk/templates/.claude/skills/moai-design-systems/reference.md +673 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +234 -43
- moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +1633 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +660 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/SKILL.md +97 -69
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/SKILL.md +97 -72
- moai_adk/templates/.claude/skills/moai-domain-data-science/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-domain-database/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-database/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-domain-devops/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +102 -73
- moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-domain-ml/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/SKILL.md +97 -67
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-security/SKILL.md +97 -79
- moai_adk/templates/.claude/skills/moai-domain-security/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-security/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/SKILL.md +97 -71
- moai_adk/templates/.claude/skills/moai-domain-web-api/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/SKILL.md +265 -64
- moai_adk/templates/.claude/skills/moai-essentials-debug/examples.md +1064 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/reference.md +1047 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/SKILL.md +87 -78
- moai_adk/templates/.claude/skills/moai-essentials-perf/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/SKILL.md +87 -70
- moai_adk/templates/.claude/skills/moai-essentials-refactor/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/SKILL.md +87 -86
- moai_adk/templates/.claude/skills/moai-essentials-review/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +80 -62
- moai_adk/templates/.claude/skills/moai-foundation-ears/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/SKILL.md +207 -50
- moai_adk/templates/.claude/skills/moai-foundation-git/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/reference.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/SKILL.md +90 -71
- moai_adk/templates/.claude/skills/moai-foundation-langs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/SKILL.md +78 -58
- moai_adk/templates/.claude/skills/moai-foundation-specs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/SKILL.md +78 -51
- moai_adk/templates/.claude/skills/moai-foundation-tags/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/.!11330!examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/SKILL.md +253 -32
- moai_adk/templates/.claude/skills/moai-foundation-trust/examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/reference.md +1099 -0
- moai_adk/templates/.claude/skills/moai-jit-docs-enhanced/SKILL.md +460 -0
- moai_adk/templates/.claude/skills/moai-lang-c/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-c/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-c/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +98 -76
- moai_adk/templates/.claude/skills/moai-lang-cpp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +2358 -70
- moai_adk/templates/.claude/skills/moai-lang-csharp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/SKILL.md +2962 -68
- moai_adk/templates/.claude/skills/moai-lang-dart/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +1898 -70
- moai_adk/templates/.claude/skills/moai-lang-go/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-go/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +1465 -68
- moai_adk/templates/.claude/skills/moai-lang-java/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-java/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/SKILL.md +2364 -66
- moai_adk/templates/.claude/skills/moai-lang-javascript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/reference.md +32 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +1630 -69
- moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +89 -61
- moai_adk/templates/.claude/skills/moai-lang-php/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-php/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +735 -66
- moai_adk/templates/.claude/skills/moai-lang-python/examples.md +624 -0
- moai_adk/templates/.claude/skills/moai-lang-python/reference.md +316 -0
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-lang-r/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-r/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +98 -73
- moai_adk/templates/.claude/skills/moai-lang-ruby/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +1834 -70
- moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +99 -74
- moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-lang-shell/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-sql/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +1959 -69
- moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-template/SKILL.md +348 -0
- moai_adk/templates/.claude/skills/moai-lang-template/VARIABLES.md +98 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +1230 -66
- moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +34 -0
- moai_adk/templates/.claude/skills/moai-learning-optimizer/SKILL.md +575 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/README.md +50 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/SKILL.md +304 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/examples.md +417 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/reference.md +704 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/README.md +87 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/examples.md +1109 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/reference.md +514 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/validate.py +106 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/README.md +11 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/SKILL.md +622 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/examples.md +20 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/reference.md +12 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/README.md +152 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/SKILL.md +285 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/examples.md +333 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/reference.md +386 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/README.md +49 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/examples.md +58 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/reference.md +123 -0
- moai_adk/templates/.claude/skills/moai-session-info/SKILL.md +314 -0
- moai_adk/templates/.claude/skills/moai-streaming-ui/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-tag-policy-validator/SKILL.md +570 -0
- moai_adk/templates/.git-hooks/pre-commit +66 -0
- moai_adk/templates/.git-hooks/pre-push +255 -0
- moai_adk/templates/.github/workflows/c-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/cpp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/csharp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/dart-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/go-tag-validation.yml +130 -0
- moai_adk/templates/.github/workflows/java-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/javascript-tag-validation.yml +135 -0
- moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +166 -3
- moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +188 -0
- moai_adk/templates/.github/workflows/php-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/python-tag-validation.yml +118 -0
- moai_adk/templates/.github/workflows/release.yml +118 -0
- moai_adk/templates/.github/workflows/ruby-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/rust-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/shell-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +338 -0
- moai_adk/templates/.github/workflows/swift-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/tag-report.yml +269 -0
- moai_adk/templates/.github/workflows/tag-validation.yml +186 -0
- moai_adk/templates/.github/workflows/typescript-tag-validation.yml +154 -0
- moai_adk/templates/.mcp.json +31 -0
- moai_adk/templates/.moai/config.json +80 -7
- moai_adk/templates/CLAUDE.md +562 -546
- moai_adk/utils/banner.py +5 -5
- moai_adk/utils/common.py +294 -0
- moai_adk/utils/link_validator.py +235 -0
- moai_adk/utils/logger.py +8 -8
- moai_adk/utils/user_experience.py +451 -0
- moai_adk-0.20.1.dist-info/METADATA +233 -0
- moai_adk-0.20.1.dist-info/RECORD +404 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +0 -230
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -156
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +0 -85
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +0 -25
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +0 -92
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +0 -70
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +0 -41
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -636
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -692
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -470
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-ears-authoring/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-git-workflow/SKILL.md +0 -95
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/SKILL.md +0 -105
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-validation/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-tag-scanning/SKILL.md +0 -90
- moai_adk/templates/.claude/skills/moai-alfred-trust-validation/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/SKILL.md +0 -87
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/examples.md +0 -62
- moai_adk/templates/.claude/skills/moai-claude-code/SKILL.md +0 -94
- moai_adk/templates/.claude/skills/moai-claude-code/examples.md +0 -513
- moai_adk/templates/.claude/skills/moai-claude-code/reference.md +0 -433
- moai_adk/templates/.claude/skills/moai-claude-code/templates/agent-full.md +0 -332
- moai_adk/templates/.claude/skills/moai-claude-code/templates/command-full.md +0 -384
- moai_adk/templates/.claude/skills/moai-claude-code/templates/plugin-full.json +0 -363
- moai_adk/templates/.claude/skills/moai-claude-code/templates/settings-full.json +0 -595
- moai_adk/templates/.claude/skills/moai-claude-code/templates/skill-full.md +0 -496
- moai_adk/templates/.claude/skills/moai-lang-clojure/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-lang-haskell/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-julia/SKILL.md +0 -98
- moai_adk/templates/.claude/skills/moai-lang-lua/SKILL.md +0 -98
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +0 -69
- moai_adk/templates/.moai/memory/development-guide.md +0 -344
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
- moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
- moai_adk/templates/.moai/project/product.md +0 -161
- moai_adk/templates/.moai/project/structure.md +0 -156
- moai_adk/templates/.moai/project/tech.md +0 -227
- moai_adk/templates/__init__.py +0 -2
- moai_adk-0.4.5.dist-info/METADATA +0 -369
- moai_adk-0.4.5.dist-info/RECORD +0 -152
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,100 +1,2388 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
name: moai-lang-csharp
|
|
4
|
-
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
created: 2025-11-06
|
|
5
|
+
updated: 2025-11-06
|
|
6
|
+
status: active
|
|
7
|
+
description: "C# best practices with .NET 8, ASP.NET Core, Entity Framework, and modern async programming for 2025"
|
|
8
|
+
keywords: [csharp, programming, dotnet, aspnetcore, entityframework, backend, async]
|
|
5
9
|
allowed-tools:
|
|
6
10
|
- Read
|
|
11
|
+
- Write
|
|
12
|
+
- Edit
|
|
7
13
|
- Bash
|
|
14
|
+
- WebFetch
|
|
15
|
+
- WebSearch
|
|
8
16
|
---
|
|
9
17
|
|
|
10
|
-
# C#
|
|
18
|
+
# C# Development Mastery
|
|
19
|
+
|
|
20
|
+
**Modern C# Development with 2025 Best Practices**
|
|
21
|
+
|
|
22
|
+
> Comprehensive C# development guidance covering .NET 8 applications, ASP.NET Core APIs, Entity Framework Core with modern async/await patterns, and cross-platform development using the latest tools and frameworks.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
### Backend Development
|
|
27
|
+
- **Web API Development**: ASP.NET Core with minimal APIs, controllers, and modern routing
|
|
28
|
+
- **Database Integration**: Entity Framework Core with LINQ, migrations, and performance optimization
|
|
29
|
+
- **Microservices**: gRPC, message queuing, distributed systems patterns
|
|
30
|
+
- **Real-time Communication**: SignalR, WebSockets with async/await
|
|
31
|
+
- **Testing**: xUnit, Moq, FluentAssertions with integration testing
|
|
11
32
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| Trigger cues | C# code discussions, framework guidance, or file extensions such as .cs. |
|
|
18
|
-
| Tier | 3 |
|
|
33
|
+
### Cross-Platform Development
|
|
34
|
+
- **Desktop Applications**: WPF, MAUI, WinUI 3 for Windows and cross-platform
|
|
35
|
+
- **Mobile Applications**: .NET MAUI for iOS, Android, Windows
|
|
36
|
+
- **Console Applications**: Modern CLI apps with DI and configuration
|
|
37
|
+
- **Background Services**: .NET Core Workers, Hosted Services
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
### Cloud Integration
|
|
40
|
+
- **Azure Integration**: Azure Functions, App Service, Blob Storage
|
|
41
|
+
- **Docker & Kubernetes**: Containerization and orchestration
|
|
42
|
+
- **DevOps**: CI/CD pipelines, health checks, monitoring
|
|
43
|
+
- **Performance**: Profiling, optimization, memory management
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
## When to Use
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
### Perfect Scenarios
|
|
48
|
+
- **Building REST APIs and microservices with ASP.NET Core**
|
|
49
|
+
- **Developing enterprise applications with Entity Framework Core**
|
|
50
|
+
- **Creating cross-platform mobile apps with .NET MAUI**
|
|
51
|
+
- **Implementing real-time applications with SignalR**
|
|
52
|
+
- **Building cloud-native applications with Azure integration**
|
|
53
|
+
- **Developing high-performance backend services**
|
|
54
|
+
- **Creating modern desktop applications with WPF/MAUI**
|
|
25
55
|
|
|
26
|
-
|
|
27
|
-
- "
|
|
28
|
-
-
|
|
29
|
-
-
|
|
56
|
+
### Common Triggers
|
|
57
|
+
- "Create C# web API"
|
|
58
|
+
- "Build ASP.NET Core application"
|
|
59
|
+
- "Set up Entity Framework Core"
|
|
60
|
+
- "Implement async/await patterns"
|
|
61
|
+
- "Optimize C# performance"
|
|
62
|
+
- "Test C# application"
|
|
63
|
+
- "C# best practices"
|
|
30
64
|
|
|
31
|
-
##
|
|
65
|
+
## Tool Version Matrix (2025-11-06)
|
|
32
66
|
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
-
|
|
67
|
+
### Core .NET
|
|
68
|
+
- **.NET**: 8.0 (current LTS) / 9.0 Preview
|
|
69
|
+
- **C#**: 12.0 (current) / 13.0 Preview
|
|
70
|
+
- **Package Managers**: NuGet, .NET CLI
|
|
71
|
+
- **Runtime**: .NET 8.0 LTS
|
|
38
72
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
73
|
+
### Web Frameworks
|
|
74
|
+
- **ASP.NET Core**: 8.0 - Web framework
|
|
75
|
+
- **Entity Framework Core**: 8.0 - ORM framework
|
|
76
|
+
- **Blazor**: 8.0 - Web UI framework
|
|
77
|
+
- **SignalR**: 8.0 - Real-time communication
|
|
78
|
+
- **gRPC**: 2.57.x - High-performance RPC
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
47
|
-
- **
|
|
80
|
+
### Testing Tools
|
|
81
|
+
- **xUnit**: 2.6.x - Testing framework
|
|
82
|
+
- **Moq**: 4.20.x - Mocking framework
|
|
83
|
+
- **FluentAssertions**: 6.12.x - Assertion library
|
|
84
|
+
- **Bogus**: 35.5.x - Test data generation
|
|
85
|
+
- **Microsoft.AspNetCore.Mvc.Testing**: 8.0 - Integration testing
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
-
|
|
54
|
-
- **Nullable reference types**: Null safety (C# 8+)
|
|
87
|
+
### Development Tools
|
|
88
|
+
- **Visual Studio 2022**: 17.10+
|
|
89
|
+
- **Visual Studio Code**: C# Dev Kit extension
|
|
90
|
+
- **Rider**: 2024.2+
|
|
91
|
+
- **.NET CLI**: 8.0.400+
|
|
55
92
|
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
93
|
+
### Database Tools
|
|
94
|
+
- **SQL Server**: 2022 / Azure SQL
|
|
95
|
+
- **PostgreSQL**: 16.x
|
|
96
|
+
- **SQLite**: 3.45.x
|
|
97
|
+
- **MongoDB**: 7.0.x (with MongoDB.Driver)
|
|
98
|
+
|
|
99
|
+
## Ecosystem Overview
|
|
100
|
+
|
|
101
|
+
### Package Management
|
|
62
102
|
|
|
63
|
-
## Examples
|
|
64
103
|
```bash
|
|
65
|
-
|
|
104
|
+
# Create new projects
|
|
105
|
+
dotnet new webapi -n MyApi
|
|
106
|
+
dotnet new mvc -n MyMvcApp
|
|
107
|
+
dotnet new maui -n MyMauiApp
|
|
108
|
+
dotnet new worker -n MyWorkerService
|
|
109
|
+
dotnet new classlib -n MyLibrary
|
|
110
|
+
|
|
111
|
+
# Add packages
|
|
112
|
+
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
|
|
113
|
+
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
|
|
114
|
+
dotnet add package Swashbuckle.AspNetCore
|
|
115
|
+
dotnet add package xunit
|
|
116
|
+
|
|
117
|
+
# Build and run
|
|
118
|
+
dotnet build
|
|
119
|
+
dotnet run --project MyApi.csproj
|
|
120
|
+
dotnet test
|
|
121
|
+
|
|
122
|
+
# Global tools
|
|
123
|
+
dotnet tool install --global dotnet-ef
|
|
124
|
+
dotnet tool install --global dotnet-aspnet-codegenerator
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Project Structure (2025 Best Practice)
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
MyDotNetSolution/
|
|
131
|
+
├── src/
|
|
132
|
+
│ ├── MyApi/ # Web API project
|
|
133
|
+
│ │ ├── Controllers/ # API controllers
|
|
134
|
+
│ │ ├── Endpoints/ # Minimal API endpoints
|
|
135
|
+
│ │ ├── Services/ # Business logic services
|
|
136
|
+
│ │ ├── Models/ # Data models and DTOs
|
|
137
|
+
│ │ ├── Data/ # Data access layer
|
|
138
|
+
│ │ ├── Configuration/ # Configuration classes
|
|
139
|
+
│ │ ├── Filters/ # Action filters and middleware
|
|
140
|
+
│ │ └── Program.cs # Application entry point
|
|
141
|
+
│ ├── MyCore/ # Core business logic
|
|
142
|
+
│ │ ├── Entities/ # Domain entities
|
|
143
|
+
│ │ ├── Interfaces/ # Service interfaces
|
|
144
|
+
│ │ ├── ValueObjects/ # Value objects
|
|
145
|
+
│ │ └── Enums/ # Enumerations
|
|
146
|
+
│ ├── MyInfrastructure/ # Infrastructure concerns
|
|
147
|
+
│ │ ├── Persistence/ # Database implementations
|
|
148
|
+
│ │ ├── ExternalServices/ # External API clients
|
|
149
|
+
│ │ ├── Messaging/ # Message queue implementations
|
|
150
|
+
│ │ └── Caching/ # Cache implementations
|
|
151
|
+
│ └── MyTests/ # Test projects
|
|
152
|
+
│ ├── Unit/ # Unit tests
|
|
153
|
+
│ ├── Integration/ # Integration tests
|
|
154
|
+
│ └── Functional/ # Functional tests
|
|
155
|
+
├── tests/ # Additional test projects
|
|
156
|
+
├── docs/ # Documentation
|
|
157
|
+
├── docker/ # Docker configurations
|
|
158
|
+
├── .github/workflows/ # GitHub Actions
|
|
159
|
+
├── Directory.Build.props # Solution-wide MSBuild properties
|
|
160
|
+
└── MySolution.sln # Solution file
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Modern Development Patterns
|
|
164
|
+
|
|
165
|
+
### C# 12.0 Language Features
|
|
166
|
+
|
|
167
|
+
```csharp
|
|
168
|
+
// Primary constructors for classes
|
|
169
|
+
public class UserService(
|
|
170
|
+
IUserRepository userRepository,
|
|
171
|
+
IEmailService emailService,
|
|
172
|
+
ILogger<UserService> logger) : IUserService
|
|
173
|
+
{
|
|
174
|
+
// Fields are automatically created from constructor parameters
|
|
175
|
+
public async Task<User> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default)
|
|
176
|
+
{
|
|
177
|
+
logger.LogInformation("Creating user with username: {Username}", request.Username);
|
|
178
|
+
|
|
179
|
+
var user = new User(request.Username, request.Email);
|
|
180
|
+
|
|
181
|
+
await userRepository.AddAsync(user, cancellationToken);
|
|
182
|
+
await emailService.SendWelcomeEmailAsync(user.Email, cancellationToken);
|
|
183
|
+
|
|
184
|
+
logger.LogInformation("User created successfully with ID: {UserId}", user.Id);
|
|
185
|
+
return user;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Collection expressions
|
|
190
|
+
public class DataProcessor
|
|
191
|
+
{
|
|
192
|
+
public int[] ProcessNumbers(IEnumerable<int> numbers)
|
|
193
|
+
{
|
|
194
|
+
return numbers
|
|
195
|
+
.Where(n => n > 0)
|
|
196
|
+
.OrderByDescending(n => n)
|
|
197
|
+
.Take(10)
|
|
198
|
+
.ToArray();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public List<string> GetDefaultPermissions()
|
|
202
|
+
{
|
|
203
|
+
return ["read", "write", "delete"]; // Collection expression
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Required members
|
|
208
|
+
public class CreateUserRequest
|
|
209
|
+
{
|
|
210
|
+
public required string Username { get; init; }
|
|
211
|
+
public required string Email { get; init; }
|
|
212
|
+
public string? FirstName { get; init; }
|
|
213
|
+
public string? LastName { get; init; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Raw string literals
|
|
217
|
+
public class EmailService
|
|
218
|
+
{
|
|
219
|
+
private readonly string _welcomeEmailTemplate = """
|
|
220
|
+
Welcome to our platform!
|
|
221
|
+
|
|
222
|
+
Hello {FirstName} {LastName},
|
|
223
|
+
|
|
224
|
+
Thank you for registering with us. Your account has been created successfully.
|
|
225
|
+
|
|
226
|
+
Best regards,
|
|
227
|
+
The Team
|
|
228
|
+
""";
|
|
229
|
+
|
|
230
|
+
public async Task SendWelcomeEmailAsync(string email, string? firstName = null, string? lastName = null)
|
|
231
|
+
{
|
|
232
|
+
var emailBody = _welcomeEmailTemplate
|
|
233
|
+
.Replace("{FirstName}", firstName ?? "User")
|
|
234
|
+
.Replace("{LastName}", lastName ?? "");
|
|
235
|
+
|
|
236
|
+
// Send email logic
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Using aliases for numeric types
|
|
241
|
+
using Age = int;
|
|
242
|
+
using UserId = System.Guid;
|
|
243
|
+
using Price = decimal;
|
|
244
|
+
|
|
245
|
+
public class User
|
|
246
|
+
{
|
|
247
|
+
public UserId Id { get; set; }
|
|
248
|
+
public required string Username { get; set; }
|
|
249
|
+
public Age Age { get; set; }
|
|
250
|
+
public Price AccountBalance { get; set; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// List patterns
|
|
254
|
+
public class NotificationService
|
|
255
|
+
{
|
|
256
|
+
public string ProcessMessage(string[] messageParts)
|
|
257
|
+
{
|
|
258
|
+
return messageParts switch
|
|
259
|
+
{
|
|
260
|
+
[] => "Empty message",
|
|
261
|
+
[var single] => $"Single part: {single}",
|
|
262
|
+
[var first, var second] => $"Two parts: {first}, {second}",
|
|
263
|
+
[var first, .. var middle, var last] => $"Multiple parts: {first} ... {last}",
|
|
264
|
+
_ => "Complex message"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### ASP.NET Core 8.0 Minimal APIs
|
|
271
|
+
|
|
272
|
+
```csharp
|
|
273
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
274
|
+
|
|
275
|
+
// Add services
|
|
276
|
+
builder.Services.AddEndpointsApiExplorer();
|
|
277
|
+
builder.Services.AddSwaggerGen();
|
|
278
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
279
|
+
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
|
280
|
+
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
|
281
|
+
builder.Services.AddScoped<IUserService, UserService>();
|
|
282
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
283
|
+
.AddJwtBearer(options =>
|
|
284
|
+
{
|
|
285
|
+
options.TokenValidationParameters = new TokenValidationParameters
|
|
286
|
+
{
|
|
287
|
+
ValidateIssuer = true,
|
|
288
|
+
ValidateAudience = true,
|
|
289
|
+
ValidateLifetime = true,
|
|
290
|
+
ValidateIssuerSigningKey = true,
|
|
291
|
+
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
|
292
|
+
ValidAudience = builder.Configuration["Jwt:Audience"],
|
|
293
|
+
IssuerSigningKey = new SymmetricSecurityKey(
|
|
294
|
+
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
builder.Services.AddRateLimiter(options =>
|
|
299
|
+
{
|
|
300
|
+
options.AddPolicy("Default", context =>
|
|
301
|
+
RateLimitPartition.GetSlidingWindowLimiter(
|
|
302
|
+
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
|
|
303
|
+
factory: _ => new SlidingWindowRateLimiterOptions
|
|
304
|
+
{
|
|
305
|
+
PermitLimit = 100,
|
|
306
|
+
Window = TimeSpan.FromMinutes(1),
|
|
307
|
+
SegmentsPerWindow = 2
|
|
308
|
+
}));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
var app = builder.Build();
|
|
312
|
+
|
|
313
|
+
// Middleware pipeline
|
|
314
|
+
if (app.Environment.IsDevelopment())
|
|
315
|
+
{
|
|
316
|
+
app.UseSwagger();
|
|
317
|
+
app.UseSwaggerUI();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
app.UseHttpsRedirection();
|
|
321
|
+
app.UseAuthentication();
|
|
322
|
+
app.UseAuthorization();
|
|
323
|
+
app.UseRateLimiter();
|
|
324
|
+
|
|
325
|
+
// API Endpoint Groups
|
|
326
|
+
var userGroup = app.MapGroup("/api/users")
|
|
327
|
+
.RequireAuthorization()
|
|
328
|
+
.AddEndpointFilter<ValidationFilter>();
|
|
329
|
+
|
|
330
|
+
// Minimal API endpoints
|
|
331
|
+
userGroup.MapGet("/", async (IUserService userService, CancellationToken cancellationToken) =>
|
|
332
|
+
{
|
|
333
|
+
var users = await userService.GetAllUsersAsync(cancellationToken);
|
|
334
|
+
return Results.Ok(users);
|
|
335
|
+
})
|
|
336
|
+
.WithName("GetAllUsers")
|
|
337
|
+
.WithOpenApi();
|
|
338
|
+
|
|
339
|
+
userGroup.MapGet("/{id:guid}", async (Guid id, IUserService userService, CancellationToken cancellationToken) =>
|
|
340
|
+
{
|
|
341
|
+
var user = await userService.GetUserByIdAsync(id, cancellationToken);
|
|
342
|
+
return user is not null ? Results.Ok(user) : Results.NotFound();
|
|
343
|
+
})
|
|
344
|
+
.WithName("GetUserById")
|
|
345
|
+
.WithOpenApi();
|
|
346
|
+
|
|
347
|
+
userGroup.MapPost("/", async (CreateUserRequest request, IUserService userService,
|
|
348
|
+
IValidator<CreateUserRequest> validator, CancellationToken cancellationToken) =>
|
|
349
|
+
{
|
|
350
|
+
var validationResult = await validator.ValidateAsync(request, cancellationToken);
|
|
351
|
+
if (!validationResult.IsValid)
|
|
352
|
+
{
|
|
353
|
+
return Results.ValidationProblem(validationResult.ToDictionary());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var user = await userService.CreateUserAsync(request, cancellationToken);
|
|
357
|
+
return Results.Created($"/api/users/{user.Id}", user);
|
|
358
|
+
})
|
|
359
|
+
.WithName("CreateUser")
|
|
360
|
+
.WithOpenApi()
|
|
361
|
+
.RequireRateLimiting("Default");
|
|
362
|
+
|
|
363
|
+
userGroup.MapPut("/{id:guid}", async (Guid id, UpdateUserRequest request,
|
|
364
|
+
IUserService userService, CancellationToken cancellationToken) =>
|
|
365
|
+
{
|
|
366
|
+
var user = await userService.UpdateUserAsync(id, request, cancellationToken);
|
|
367
|
+
return user is not null ? Results.Ok(user) : Results.NotFound();
|
|
368
|
+
})
|
|
369
|
+
.WithName("UpdateUser")
|
|
370
|
+
.WithOpenApi();
|
|
371
|
+
|
|
372
|
+
userGroup.MapDelete("/{id:guid}", async (Guid id, IUserService userService,
|
|
373
|
+
CancellationToken cancellationToken) =>
|
|
374
|
+
{
|
|
375
|
+
var success = await userService.DeleteUserAsync(id, cancellationToken);
|
|
376
|
+
return success ? Results.NoContent() : Results.NotFound();
|
|
377
|
+
})
|
|
378
|
+
.WithName("DeleteUser")
|
|
379
|
+
.WithOpenApi();
|
|
380
|
+
|
|
381
|
+
// Real-time endpoints
|
|
382
|
+
var chatGroup = app.MapGroup("/api/chat")
|
|
383
|
+
.RequireAuthorization();
|
|
384
|
+
|
|
385
|
+
app.MapHub<ChatHub>("/chatHub");
|
|
386
|
+
|
|
387
|
+
app.Run();
|
|
388
|
+
|
|
389
|
+
// Custom endpoint filters
|
|
390
|
+
public class ValidationFilter : IEndpointFilter
|
|
391
|
+
{
|
|
392
|
+
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
|
393
|
+
{
|
|
394
|
+
// Add global validation logic here
|
|
395
|
+
return await next(context);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Entity Framework Core 8.0 Patterns
|
|
401
|
+
|
|
402
|
+
```csharp
|
|
403
|
+
// Domain entities with value objects
|
|
404
|
+
public class User
|
|
405
|
+
{
|
|
406
|
+
public Guid Id { get; private set; }
|
|
407
|
+
public string Username { get; private set; } = string.Empty;
|
|
408
|
+
public Email Email { get; private set; } = null!;
|
|
409
|
+
public UserProfile Profile { get; private set; } = null!;
|
|
410
|
+
public IReadOnlyCollection<UserRole> Roles => _roles.AsReadOnly();
|
|
411
|
+
|
|
412
|
+
private readonly List<UserRole> _roles = new();
|
|
413
|
+
|
|
414
|
+
// Private constructor for EF Core
|
|
415
|
+
private User() { }
|
|
416
|
+
|
|
417
|
+
public User(string username, Email email, UserProfile profile)
|
|
418
|
+
{
|
|
419
|
+
Id = Guid.NewGuid();
|
|
420
|
+
Username = username;
|
|
421
|
+
Email = email;
|
|
422
|
+
Profile = profile;
|
|
423
|
+
CreatedAt = DateTime.UtcNow;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
public void UpdateProfile(UserProfile newProfile)
|
|
427
|
+
{
|
|
428
|
+
Profile = newProfile;
|
|
429
|
+
UpdatedAt = DateTime.UtcNow;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public void AddRole(UserRole role)
|
|
433
|
+
{
|
|
434
|
+
if (!_roles.Contains(role))
|
|
435
|
+
{
|
|
436
|
+
_roles.Add(role);
|
|
437
|
+
UpdatedAt = DateTime.UtcNow;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
public DateTime CreatedAt { get; private set; }
|
|
442
|
+
public DateTime? UpdatedAt { get; private set; }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Value object
|
|
446
|
+
public readonly record struct Email(string Value)
|
|
447
|
+
{
|
|
448
|
+
public static Email Create(string email)
|
|
449
|
+
{
|
|
450
|
+
if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"))
|
|
451
|
+
throw new ArgumentException("Invalid email format", nameof(email));
|
|
452
|
+
|
|
453
|
+
return new Email(email.ToLowerInvariant());
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// EF Core configuration
|
|
458
|
+
public class AppDbContext : DbContext
|
|
459
|
+
{
|
|
460
|
+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
|
461
|
+
|
|
462
|
+
public DbSet<User> Users => Set<User>();
|
|
463
|
+
public DbSet<UserRole> UserRoles => Set<UserRole>();
|
|
464
|
+
public DbSet<Role> Roles => Set<Role>();
|
|
465
|
+
|
|
466
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
467
|
+
{
|
|
468
|
+
modelBuilder.Entity<User>(builder =>
|
|
469
|
+
{
|
|
470
|
+
builder.HasKey(u => u.Id);
|
|
471
|
+
builder.Property(u => u.Username).IsRequired().HasMaxLength(50);
|
|
472
|
+
builder.Property(u => u.Email)
|
|
473
|
+
.HasConversion(
|
|
474
|
+
email => email.Value,
|
|
475
|
+
value => Email.Create(value))
|
|
476
|
+
.IsRequired()
|
|
477
|
+
.HasMaxLength(100);
|
|
478
|
+
|
|
479
|
+
builder.OwnsOne(u => u.Profile, profile =>
|
|
480
|
+
{
|
|
481
|
+
profile.Property(p => p.FirstName).HasMaxLength(50);
|
|
482
|
+
profile.Property(p => p.LastName).HasMaxLength(50);
|
|
483
|
+
profile.Property(p => p.Bio).HasMaxLength(500);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
builder.Property(u => u.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
|
|
487
|
+
|
|
488
|
+
builder.HasMany(u => u.Roles)
|
|
489
|
+
.WithMany(r => r.Users)
|
|
490
|
+
.UsingEntity<UserRole>(
|
|
491
|
+
join => join.HasOne<Role>().WithMany(),
|
|
492
|
+
join => join.HasOne<User>().WithMany());
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
modelBuilder.Entity<Role>(builder =>
|
|
496
|
+
{
|
|
497
|
+
builder.HasKey(r => r.Id);
|
|
498
|
+
builder.Property(r => r.Name).IsRequired().HasMaxLength(50);
|
|
499
|
+
|
|
500
|
+
builder.HasData(
|
|
501
|
+
new Role { Id = 1, Name = "Admin" },
|
|
502
|
+
new Role { Id = 2, Name = "User" },
|
|
503
|
+
new Role { Id = 3, Name = "Moderator" });
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
508
|
+
{
|
|
509
|
+
UpdateTimestamps();
|
|
510
|
+
return await base.SaveChangesAsync(cancellationToken);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private void UpdateTimestamps()
|
|
514
|
+
{
|
|
515
|
+
var entries = ChangeTracker
|
|
516
|
+
.Entries()
|
|
517
|
+
.Where(e => e.Entity is User && (e.State == EntityState.Added || e.State == EntityState.Modified));
|
|
518
|
+
|
|
519
|
+
foreach (var entry in entries)
|
|
520
|
+
{
|
|
521
|
+
var user = (User)entry.Entity;
|
|
522
|
+
|
|
523
|
+
if (entry.State == EntityState.Added)
|
|
524
|
+
{
|
|
525
|
+
user.CreatedAt = DateTime.UtcNow;
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
{
|
|
529
|
+
user.UpdatedAt = DateTime.UtcNow;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Repository pattern with async/await
|
|
536
|
+
public interface IUserRepository
|
|
537
|
+
{
|
|
538
|
+
Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
|
539
|
+
Task<User?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
|
540
|
+
Task<User?> GetByEmailAsync(Email email, CancellationToken cancellationToken = default);
|
|
541
|
+
Task<IReadOnlyList<User>> GetAllAsync(int page = 1, int pageSize = 20,
|
|
542
|
+
CancellationToken cancellationToken = default);
|
|
543
|
+
Task<User> AddAsync(User user, CancellationToken cancellationToken = default);
|
|
544
|
+
Task<User> UpdateAsync(User user, CancellationToken cancellationToken = default);
|
|
545
|
+
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
|
546
|
+
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public class UserRepository : IUserRepository
|
|
550
|
+
{
|
|
551
|
+
private readonly AppDbContext _context;
|
|
552
|
+
|
|
553
|
+
public UserRepository(AppDbContext context)
|
|
554
|
+
{
|
|
555
|
+
_context = context;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
public async Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
|
559
|
+
{
|
|
560
|
+
return await _context.Users
|
|
561
|
+
.Include(u => u.Roles)
|
|
562
|
+
.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
public async Task<User?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default)
|
|
566
|
+
{
|
|
567
|
+
return await _context.Users
|
|
568
|
+
.Include(u => u.Roles)
|
|
569
|
+
.FirstOrDefaultAsync(u => u.Username == username, cancellationToken);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
public async Task<User?> GetByEmailAsync(Email email, CancellationToken cancellationToken = default)
|
|
573
|
+
{
|
|
574
|
+
return await _context.Users
|
|
575
|
+
.Include(u => u.Roles)
|
|
576
|
+
.FirstOrDefaultAsync(u => u.Email == email, cancellationToken);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
public async Task<IReadOnlyList<User>> GetAllAsync(int page = 1, int pageSize = 20,
|
|
580
|
+
CancellationToken cancellationToken = default)
|
|
581
|
+
{
|
|
582
|
+
return await _context.Users
|
|
583
|
+
.Include(u => u.Roles)
|
|
584
|
+
.OrderBy(u => u.Username)
|
|
585
|
+
.Skip((page - 1) * pageSize)
|
|
586
|
+
.Take(pageSize)
|
|
587
|
+
.AsNoTracking()
|
|
588
|
+
.ToListAsync(cancellationToken);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
public async Task<User> AddAsync(User user, CancellationToken cancellationToken = default)
|
|
592
|
+
{
|
|
593
|
+
await _context.Users.AddAsync(user, cancellationToken);
|
|
594
|
+
await _context.SaveChangesAsync(cancellationToken);
|
|
595
|
+
return user;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
public async Task<User> UpdateAsync(User user, CancellationToken cancellationToken = default)
|
|
599
|
+
{
|
|
600
|
+
_context.Users.Update(user);
|
|
601
|
+
await _context.SaveChangesAsync(cancellationToken);
|
|
602
|
+
return user;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
|
606
|
+
{
|
|
607
|
+
var user = await GetByIdAsync(id, cancellationToken);
|
|
608
|
+
if (user is null) return false;
|
|
609
|
+
|
|
610
|
+
_context.Users.Remove(user);
|
|
611
|
+
await _context.SaveChangesAsync(cancellationToken);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
|
|
616
|
+
{
|
|
617
|
+
return await _context.Users.AnyAsync(u => u.Id == id, cancellationToken);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Modern Async Patterns
|
|
623
|
+
|
|
624
|
+
```csharp
|
|
625
|
+
// Service layer with proper async handling
|
|
626
|
+
public class UserService : IUserService
|
|
627
|
+
{
|
|
628
|
+
private readonly IUserRepository _userRepository;
|
|
629
|
+
private readonly IEmailService _emailService;
|
|
630
|
+
private readonly ILogger<UserService> _logger;
|
|
631
|
+
private readonly IMemoryCache _cache;
|
|
632
|
+
|
|
633
|
+
public UserService(
|
|
634
|
+
IUserRepository userRepository,
|
|
635
|
+
IEmailService emailService,
|
|
636
|
+
ILogger<UserService> logger,
|
|
637
|
+
IMemoryCache cache)
|
|
638
|
+
{
|
|
639
|
+
_userRepository = userRepository;
|
|
640
|
+
_emailService = emailService;
|
|
641
|
+
_logger = logger;
|
|
642
|
+
_cache = cache;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
public async Task<User> CreateUserAsync(CreateUserRequest request,
|
|
646
|
+
CancellationToken cancellationToken = default)
|
|
647
|
+
{
|
|
648
|
+
// Validate input
|
|
649
|
+
var email = Email.Create(request.Email);
|
|
650
|
+
|
|
651
|
+
// Check for existing user
|
|
652
|
+
var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken);
|
|
653
|
+
if (existingUser is not null)
|
|
654
|
+
{
|
|
655
|
+
throw new UserAlreadyExistsException($"User with email {email.Value} already exists");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Create new user
|
|
659
|
+
var profile = new UserProfile(request.FirstName, request.LastName, request.Bio);
|
|
660
|
+
var user = new User(request.Username, email, profile);
|
|
661
|
+
|
|
662
|
+
// Save to database
|
|
663
|
+
var createdUser = await _userRepository.AddAsync(user, cancellationToken);
|
|
664
|
+
|
|
665
|
+
// Send welcome email (fire and forget)
|
|
666
|
+
_ = Task.Run(async () =>
|
|
667
|
+
{
|
|
668
|
+
try
|
|
669
|
+
{
|
|
670
|
+
await _emailService.SendWelcomeEmailAsync(email, profile.FirstName, profile.LastName);
|
|
671
|
+
}
|
|
672
|
+
catch (Exception ex)
|
|
673
|
+
{
|
|
674
|
+
_logger.LogError(ex, "Failed to send welcome email to {Email}", email.Value);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Cache the user
|
|
679
|
+
_cache.Set($"user_{createdUser.Id}", createdUser, TimeSpan.FromMinutes(30));
|
|
680
|
+
|
|
681
|
+
return createdUser;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
public async Task<User?> GetUserByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
|
685
|
+
{
|
|
686
|
+
// Try cache first
|
|
687
|
+
if (_cache.TryGetValue($"user_{id}", out User? cachedUser))
|
|
688
|
+
{
|
|
689
|
+
return cachedUser;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Fetch from database
|
|
693
|
+
var user = await _userRepository.GetByIdAsync(id, cancellationToken);
|
|
694
|
+
|
|
695
|
+
// Cache if found
|
|
696
|
+
if (user is not null)
|
|
697
|
+
{
|
|
698
|
+
_cache.Set($"user_{id}", user, TimeSpan.FromMinutes(30));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return user;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
public async Task<IReadOnlyList<User>> GetAllUsersAsync(int page = 1, int pageSize = 20,
|
|
705
|
+
CancellationToken cancellationToken = default)
|
|
706
|
+
{
|
|
707
|
+
return await _userRepository.GetAllAsync(page, pageSize, cancellationToken);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
public async Task<User?> UpdateUserAsync(Guid id, UpdateUserRequest request,
|
|
711
|
+
CancellationToken cancellationToken = default)
|
|
712
|
+
{
|
|
713
|
+
var user = await _userRepository.GetByIdAsync(id, cancellationToken);
|
|
714
|
+
if (user is null) return null;
|
|
715
|
+
|
|
716
|
+
var updatedProfile = new UserProfile(
|
|
717
|
+
request.FirstName ?? user.Profile.FirstName,
|
|
718
|
+
request.LastName ?? user.Profile.LastName,
|
|
719
|
+
request.Bio ?? user.Profile.Bio);
|
|
720
|
+
|
|
721
|
+
user.UpdateProfile(updatedProfile);
|
|
722
|
+
|
|
723
|
+
var updatedUser = await _userRepository.UpdateAsync(user, cancellationToken);
|
|
724
|
+
|
|
725
|
+
// Update cache
|
|
726
|
+
_cache.Set($"user_{id}", updatedUser, TimeSpan.FromMinutes(30));
|
|
727
|
+
|
|
728
|
+
return updatedUser;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
public async Task<bool> DeleteUserAsync(Guid id, CancellationToken cancellationToken = default)
|
|
732
|
+
{
|
|
733
|
+
var success = await _userRepository.DeleteAsync(id, cancellationToken);
|
|
734
|
+
|
|
735
|
+
if (success)
|
|
736
|
+
{
|
|
737
|
+
// Remove from cache
|
|
738
|
+
_cache.Remove($"user_{id}");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return success;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Parallel operations example
|
|
745
|
+
public async Task<UserProfileSummary> GetUserProfileSummaryAsync(Guid userId,
|
|
746
|
+
CancellationToken cancellationToken = default)
|
|
747
|
+
{
|
|
748
|
+
var userTask = GetUserByIdAsync(userId, cancellationToken);
|
|
749
|
+
var postsTask = GetUserPostsCountAsync(userId, cancellationToken);
|
|
750
|
+
var followersTask = GetUserFollowersCountAsync(userId, cancellationToken);
|
|
751
|
+
|
|
752
|
+
await Task.WhenAll(userTask, postsTask, followersTask);
|
|
753
|
+
|
|
754
|
+
var user = await userTask;
|
|
755
|
+
if (user is null) throw new UserNotFoundException(userId);
|
|
756
|
+
|
|
757
|
+
var postsCount = await postsTask;
|
|
758
|
+
var followersCount = await followersTask;
|
|
759
|
+
|
|
760
|
+
return new UserProfileSummary
|
|
761
|
+
{
|
|
762
|
+
User = user,
|
|
763
|
+
PostsCount = postsCount,
|
|
764
|
+
FollowersCount = followersCount
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private async Task<int> GetUserPostsCountAsync(Guid userId, CancellationToken cancellationToken = default)
|
|
769
|
+
{
|
|
770
|
+
// Simulate external API call
|
|
771
|
+
await Task.Delay(100, cancellationToken);
|
|
772
|
+
return Random.Shared.Next(0, 100);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private async Task<int> GetUserFollowersCountAsync(Guid userId, CancellationToken cancellationToken = default)
|
|
776
|
+
{
|
|
777
|
+
// Simulate external API call
|
|
778
|
+
await Task.Delay(150, cancellationToken);
|
|
779
|
+
return Random.Shared.Next(0, 1000);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Background service with async patterns
|
|
784
|
+
public class UserCleanupService : BackgroundService
|
|
785
|
+
{
|
|
786
|
+
private readonly ILogger<UserCleanupService> _logger;
|
|
787
|
+
private readonly IUserRepository _userRepository;
|
|
788
|
+
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(1);
|
|
789
|
+
|
|
790
|
+
public UserCleanupService(
|
|
791
|
+
ILogger<UserCleanupService> logger,
|
|
792
|
+
IUserRepository userRepository)
|
|
793
|
+
{
|
|
794
|
+
_logger = logger;
|
|
795
|
+
_userRepository = userRepository;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
799
|
+
{
|
|
800
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
801
|
+
{
|
|
802
|
+
try
|
|
803
|
+
{
|
|
804
|
+
_logger.LogInformation("Starting user cleanup process");
|
|
805
|
+
|
|
806
|
+
await CleanupInactiveUsersAsync(stoppingToken);
|
|
807
|
+
|
|
808
|
+
_logger.LogInformation("User cleanup process completed");
|
|
809
|
+
}
|
|
810
|
+
catch (OperationCanceledException)
|
|
811
|
+
{
|
|
812
|
+
_logger.LogInformation("User cleanup service was cancelled");
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
catch (Exception ex)
|
|
816
|
+
{
|
|
817
|
+
_logger.LogError(ex, "Error during user cleanup process");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await Task.Delay(_cleanupInterval, stoppingToken);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private async Task CleanupInactiveUsersAsync(CancellationToken cancellationToken)
|
|
825
|
+
{
|
|
826
|
+
const int batchSize = 100;
|
|
827
|
+
var cutoffDate = DateTime.UtcNow.AddDays(-365);
|
|
828
|
+
|
|
829
|
+
var inactiveUsers = await _userRepository.GetInactiveUsersBeforeAsync(cutoffDate, cancellationToken);
|
|
830
|
+
|
|
831
|
+
foreach (var batch in inactiveUsers.Chunk(batchSize))
|
|
832
|
+
{
|
|
833
|
+
await Parallel.ForEachAsync(batch, cancellationToken, async (user, ct) =>
|
|
834
|
+
{
|
|
835
|
+
try
|
|
836
|
+
{
|
|
837
|
+
await _userRepository.DeleteAsync(user.Id, ct);
|
|
838
|
+
_logger.LogInformation("Deleted inactive user {UserId} ({Username})", user.Id, user.Username);
|
|
839
|
+
}
|
|
840
|
+
catch (Exception ex)
|
|
841
|
+
{
|
|
842
|
+
_logger.LogError(ex, "Failed to delete user {UserId}", user.Id);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
## Performance Considerations
|
|
851
|
+
|
|
852
|
+
### Memory Management
|
|
853
|
+
|
|
854
|
+
```csharp
|
|
855
|
+
// Efficient data structures and memory usage
|
|
856
|
+
public class DataProcessor
|
|
857
|
+
{
|
|
858
|
+
// Use Span<T> for zero-allocation string processing
|
|
859
|
+
public static bool IsValidEmail(ReadOnlySpan<char> email)
|
|
860
|
+
{
|
|
861
|
+
var atIdx = email.IndexOf('@');
|
|
862
|
+
if (atIdx <= 0 || atIdx == email.Length - 1) return false;
|
|
863
|
+
|
|
864
|
+
var dotIdx = email.LastIndexOf('.');
|
|
865
|
+
if (dotIdx <= atIdx + 1 || dotIdx == email.Length - 1) return false;
|
|
866
|
+
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Use ArrayPool for temporary arrays
|
|
871
|
+
public static byte[] ProcessLargeData(ReadOnlySpan<byte> data)
|
|
872
|
+
{
|
|
873
|
+
var buffer = ArrayPool<byte>.Shared.Rent(data.Length * 2);
|
|
874
|
+
|
|
875
|
+
try
|
|
876
|
+
{
|
|
877
|
+
// Process data into buffer
|
|
878
|
+
var processedLength = ProcessDataInternal(data, buffer);
|
|
879
|
+
|
|
880
|
+
var result = new byte[processedLength];
|
|
881
|
+
buffer.AsSpan(0, processedLength).CopyTo(result);
|
|
882
|
+
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
finally
|
|
886
|
+
{
|
|
887
|
+
ArrayPool<byte>.Shared.Return(buffer);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private static int ProcessDataInternal(ReadOnlySpan<byte> source, Span<byte> destination)
|
|
892
|
+
{
|
|
893
|
+
// Processing logic
|
|
894
|
+
return Math.Min(source.Length * 2, destination.Length);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Efficient LINQ usage with immediate execution when needed
|
|
899
|
+
public class UserRepository
|
|
900
|
+
{
|
|
901
|
+
private readonly AppDbContext _context;
|
|
902
|
+
|
|
903
|
+
public UserRepository(AppDbContext context)
|
|
904
|
+
{
|
|
905
|
+
_context = context;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Use AsNoTracking for read-only queries
|
|
909
|
+
public async Task<IReadOnlyList<User>> GetActiveUsersAsync(int page = 1, int pageSize = 20,
|
|
910
|
+
CancellationToken cancellationToken = default)
|
|
911
|
+
{
|
|
912
|
+
return await _context.Users
|
|
913
|
+
.AsNoTracking() // No change tracking for read-only
|
|
914
|
+
.Where(u => u.IsActive)
|
|
915
|
+
.OrderBy(u => u.Username)
|
|
916
|
+
.Select(u => new UserDto // Project only needed fields
|
|
917
|
+
{
|
|
918
|
+
Id = u.Id,
|
|
919
|
+
Username = u.Username,
|
|
920
|
+
Email = u.Email.Value,
|
|
921
|
+
CreatedAt = u.CreatedAt
|
|
922
|
+
})
|
|
923
|
+
.Skip((page - 1) * pageSize)
|
|
924
|
+
.Take(pageSize)
|
|
925
|
+
.ToListAsync(cancellationToken);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Use compiled queries for frequently executed queries
|
|
929
|
+
private static readonly Func<AppDbContext, Guid, Task<User?>> GetUserByIdCompiled =
|
|
930
|
+
EF.CompileAsyncQuery((AppDbContext context, Guid id) =>
|
|
931
|
+
context.Users
|
|
932
|
+
.Include(u => u.Roles)
|
|
933
|
+
.FirstOrDefaultAsync(u => u.Id == id));
|
|
934
|
+
|
|
935
|
+
public async Task<User?> GetUserByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
|
936
|
+
{
|
|
937
|
+
return await GetUserByIdCompiled(_context, id);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Batch operations for better performance
|
|
941
|
+
public async Task UpdateUsersLastLoginAsync(IEnumerable<Guid> userIds,
|
|
942
|
+
CancellationToken cancellationToken = default)
|
|
943
|
+
{
|
|
944
|
+
const int batchSize = 1000;
|
|
945
|
+
|
|
946
|
+
foreach (var batch in userIds.Chunk(batchSize))
|
|
947
|
+
{
|
|
948
|
+
await _context.Users
|
|
949
|
+
.Where(u => batch.Contains(u.Id))
|
|
950
|
+
.ExecuteUpdateAsync(setters => setters
|
|
951
|
+
.SetProperty(u => u.LastLoginAt, DateTime.UtcNow), cancellationToken);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Memory-efficient caching
|
|
957
|
+
public class CacheService
|
|
958
|
+
{
|
|
959
|
+
private readonly IMemoryCache _cache;
|
|
960
|
+
private readonly Timer _cleanupTimer;
|
|
961
|
+
|
|
962
|
+
public CacheService(IMemoryCache cache)
|
|
963
|
+
{
|
|
964
|
+
_cache = cache;
|
|
965
|
+
|
|
966
|
+
// Set up periodic cleanup
|
|
967
|
+
_cleanupTimer = new Timer(CleanupExpiredEntries, null,
|
|
968
|
+
TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
public async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> factory,
|
|
972
|
+
TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null)
|
|
973
|
+
{
|
|
974
|
+
if (_cache.TryGetValue(key, out T? cachedValue))
|
|
975
|
+
{
|
|
976
|
+
return cachedValue;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
var value = await factory();
|
|
980
|
+
|
|
981
|
+
var options = new MemoryCacheEntryOptions
|
|
982
|
+
{
|
|
983
|
+
AbsoluteExpirationRelativeToNow = absoluteExpiration,
|
|
984
|
+
SlidingExpiration = slidingExpiration,
|
|
985
|
+
Size = 1 // Enable size-based eviction
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
_cache.Set(key, value, options);
|
|
989
|
+
return value;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private void CleanupExpiredEntries(object? state)
|
|
993
|
+
{
|
|
994
|
+
// MemoryCache automatically removes expired entries, but we can
|
|
995
|
+
// implement additional cleanup logic here if needed
|
|
996
|
+
_cache.Compact(0.95); // Remove 5% of least recently used items
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
public void Remove(string key)
|
|
1000
|
+
{
|
|
1001
|
+
_cache.Remove(key);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
public void Clear()
|
|
1005
|
+
{
|
|
1006
|
+
_cache.Compact(1.0); // Remove all entries
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Database Performance
|
|
1012
|
+
|
|
1013
|
+
```csharp
|
|
1014
|
+
// Optimized database operations
|
|
1015
|
+
public class OptimizedDbContext : DbContext
|
|
1016
|
+
{
|
|
1017
|
+
private readonly ILogger<OptimizedDbContext> _logger;
|
|
1018
|
+
|
|
1019
|
+
public OptimizedDbContext(DbContextOptions<OptimizedDbContext> options,
|
|
1020
|
+
ILogger<OptimizedDbContext> logger) : base(options)
|
|
1021
|
+
{
|
|
1022
|
+
_logger = logger;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Use connection resiliency
|
|
1026
|
+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
1027
|
+
{
|
|
1028
|
+
optionsBuilder.EnableRetryOnFailure(
|
|
1029
|
+
maxRetryCount: 3,
|
|
1030
|
+
maxRetryDelay: TimeSpan.FromSeconds(30),
|
|
1031
|
+
errorNumbersToAdd: null);
|
|
1032
|
+
|
|
1033
|
+
// Configure sensitive data logging for development
|
|
1034
|
+
optionsBuilder.EnableSensitiveDataLogging(false);
|
|
1035
|
+
optionsBuilder.EnableDetailedErrors(false);
|
|
1036
|
+
|
|
1037
|
+
// Set command timeout
|
|
1038
|
+
optionsBuilder.UseSqlServer(sqlOptions =>
|
|
1039
|
+
{
|
|
1040
|
+
sqlOptions.CommandTimeout(30);
|
|
1041
|
+
sqlOptions.EnableRetryOnFailure();
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Batch operations
|
|
1046
|
+
public async Task BulkInsertAsync<T>(IEnumerable<T> entities,
|
|
1047
|
+
CancellationToken cancellationToken = default) where T : class
|
|
1048
|
+
{
|
|
1049
|
+
const int batchSize = 1000;
|
|
1050
|
+
|
|
1051
|
+
foreach (var batch in entities.Chunk(batchSize))
|
|
1052
|
+
{
|
|
1053
|
+
await Set<T>().AddRangeAsync(batch, cancellationToken);
|
|
1054
|
+
await SaveChangesAsync(cancellationToken);
|
|
1055
|
+
|
|
1056
|
+
// Clear change tracker for memory efficiency
|
|
1057
|
+
ChangeTracker.Clear();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Optimized queries with proper indexing hints
|
|
1062
|
+
public async Task<IReadOnlyList<User>> SearchUsersAsync(string searchTerm,
|
|
1063
|
+
int page = 1, int pageSize = 20, CancellationToken cancellationToken = default)
|
|
1064
|
+
{
|
|
1065
|
+
var normalizedSearch = searchTerm.Trim().ToUpperInvariant();
|
|
1066
|
+
|
|
1067
|
+
return await Users
|
|
1068
|
+
.AsNoTracking()
|
|
1069
|
+
.Where(u => EF.Functions.ILike(u.Username, $"%{normalizedSearch}%") ||
|
|
1070
|
+
EF.Functions.ILike(u.Profile.FirstName, $"%{normalizedSearch}%") ||
|
|
1071
|
+
EF.Functions.ILike(u.Profile.LastName, $"%{normalizedSearch}%"))
|
|
1072
|
+
.OrderBy(u => u.Username)
|
|
1073
|
+
.Select(u => new UserDto // Project to DTO for less data transfer
|
|
1074
|
+
{
|
|
1075
|
+
Id = u.Id,
|
|
1076
|
+
Username = u.Username,
|
|
1077
|
+
Email = u.Email.Value,
|
|
1078
|
+
FullName = $"{u.Profile.FirstName} {u.Profile.LastName}",
|
|
1079
|
+
CreatedAt = u.CreatedAt
|
|
1080
|
+
})
|
|
1081
|
+
.Skip((page - 1) * pageSize)
|
|
1082
|
+
.Take(pageSize)
|
|
1083
|
+
.ToListAsync(cancellationToken);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Use raw SQL for performance-critical operations
|
|
1087
|
+
public async Task<int> DeleteOldRecordsAsync(DateTime cutoffDate,
|
|
1088
|
+
CancellationToken cancellationToken = default)
|
|
1089
|
+
{
|
|
1090
|
+
var sql = """
|
|
1091
|
+
DELETE FROM UserLogs
|
|
1092
|
+
WHERE CreatedAt < @cutoffDate
|
|
1093
|
+
""";
|
|
1094
|
+
|
|
1095
|
+
return await Database.ExecuteSqlRawAsync(sql,
|
|
1096
|
+
new SqlParameter("@cutoffDate", cutoffDate));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
1100
|
+
{
|
|
1101
|
+
try
|
|
1102
|
+
{
|
|
1103
|
+
return await base.SaveChangesAsync(cancellationToken);
|
|
1104
|
+
}
|
|
1105
|
+
catch (DbUpdateConcurrencyException ex)
|
|
1106
|
+
{
|
|
1107
|
+
_logger.LogWarning(ex, "Concurrency conflict detected");
|
|
1108
|
+
throw new ConcurrencyException("Data was modified by another user", ex);
|
|
1109
|
+
}
|
|
1110
|
+
catch (DbUpdateException ex)
|
|
1111
|
+
{
|
|
1112
|
+
_logger.LogError(ex, "Database update failed");
|
|
1113
|
+
throw new DataUpdateException("Failed to save changes to database", ex);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Connection pooling and transaction management
|
|
1119
|
+
public class UnitOfWork : IUnitOfWork, IDisposable
|
|
1120
|
+
{
|
|
1121
|
+
private readonly OptimizedDbContext _context;
|
|
1122
|
+
private IDbContextTransaction? _transaction;
|
|
1123
|
+
|
|
1124
|
+
public UnitOfWork(OptimizedDbContext context)
|
|
1125
|
+
{
|
|
1126
|
+
_context = context;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted,
|
|
1130
|
+
CancellationToken cancellationToken = default)
|
|
1131
|
+
{
|
|
1132
|
+
_transaction = await _context.Database.BeginTransactionAsync(isolationLevel, cancellationToken);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
public async Task CommitAsync(CancellationToken cancellationToken = default)
|
|
1136
|
+
{
|
|
1137
|
+
try
|
|
1138
|
+
{
|
|
1139
|
+
await _context.SaveChangesAsync(cancellationToken);
|
|
1140
|
+
await _transaction?.CommitAsync(cancellationToken)!;
|
|
1141
|
+
}
|
|
1142
|
+
catch
|
|
1143
|
+
{
|
|
1144
|
+
await RollbackAsync(cancellationToken);
|
|
1145
|
+
throw;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
public async Task RollbackAsync(CancellationToken cancellationToken = default)
|
|
1150
|
+
{
|
|
1151
|
+
if (_transaction != null)
|
|
1152
|
+
{
|
|
1153
|
+
await _transaction.RollbackAsync(cancellationToken);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
public void Dispose()
|
|
1158
|
+
{
|
|
1159
|
+
_transaction?.Dispose();
|
|
1160
|
+
_context.Dispose();
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
66
1163
|
```
|
|
67
1164
|
|
|
68
|
-
|
|
69
|
-
- Language-specific source directories (e.g. `src/`, `app/`).
|
|
70
|
-
- Language-specific build/test configuration files (e.g. `package.json`, `pyproject.toml`, `go.mod`).
|
|
71
|
-
- Relevant test suites and sample data.
|
|
1165
|
+
### HTTP Client Performance
|
|
72
1166
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1167
|
+
```csharp
|
|
1168
|
+
// Optimized HTTP client with connection pooling and circuit breaker
|
|
1169
|
+
public class OptimizedHttpClient
|
|
1170
|
+
{
|
|
1171
|
+
private readonly HttpClient _httpClient;
|
|
1172
|
+
private readonly ILogger<OptimizedHttpClient> _logger;
|
|
1173
|
+
|
|
1174
|
+
public OptimizedHttpClient(IHttpClientFactory httpClientFactory,
|
|
1175
|
+
ILogger<OptimizedHttpClient> logger)
|
|
1176
|
+
{
|
|
1177
|
+
_httpClient = httpClientFactory.CreateClient("OptimizedClient");
|
|
1178
|
+
_logger = logger;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
public async Task<T?> GetAsync<T>(string uri, CancellationToken cancellationToken = default)
|
|
1182
|
+
{
|
|
1183
|
+
try
|
|
1184
|
+
{
|
|
1185
|
+
var response = await _httpClient.GetAsync(uri, cancellationToken);
|
|
1186
|
+
response.EnsureSuccessStatusCode();
|
|
1187
|
+
|
|
1188
|
+
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellationToken);
|
|
1189
|
+
}
|
|
1190
|
+
catch (HttpRequestException ex)
|
|
1191
|
+
{
|
|
1192
|
+
_logger.LogError(ex, "HTTP request failed for URI: {Uri}", uri);
|
|
1193
|
+
throw;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
public async Task<T?> PostAsync<T>(string uri, T data, CancellationToken cancellationToken = default)
|
|
1198
|
+
{
|
|
1199
|
+
try
|
|
1200
|
+
{
|
|
1201
|
+
var response = await _httpClient.PostAsJsonAsync(uri, data, cancellationToken);
|
|
1202
|
+
response.EnsureSuccessStatusCode();
|
|
1203
|
+
|
|
1204
|
+
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellationToken);
|
|
1205
|
+
}
|
|
1206
|
+
catch (HttpRequestException ex)
|
|
1207
|
+
{
|
|
1208
|
+
_logger.LogError(ex, "HTTP POST request failed for URI: {Uri}", uri);
|
|
1209
|
+
throw;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
76
1213
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1214
|
+
// HTTP client configuration in Program.cs
|
|
1215
|
+
builder.Services.AddHttpClient("OptimizedClient", client =>
|
|
1216
|
+
{
|
|
1217
|
+
client.BaseAddress = new Uri("https://api.example.com/");
|
|
1218
|
+
client.Timeout = TimeSpan.FromSeconds(30);
|
|
1219
|
+
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
|
|
1220
|
+
})
|
|
1221
|
+
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
|
|
1222
|
+
{
|
|
1223
|
+
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
|
1224
|
+
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
|
|
1225
|
+
MaxConnectionsPerServer = 100
|
|
1226
|
+
})
|
|
1227
|
+
.AddPolicyHandler(GetRetryPolicy())
|
|
1228
|
+
.AddPolicyHandler(GetCircuitBreakerPolicy());
|
|
80
1229
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1230
|
+
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
|
1231
|
+
{
|
|
1232
|
+
return HttpPolicyExtensions
|
|
1233
|
+
.HandleTransientHttpError()
|
|
1234
|
+
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
|
1235
|
+
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
|
1236
|
+
}
|
|
84
1237
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
1238
|
+
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
|
|
1239
|
+
{
|
|
1240
|
+
return HttpPolicyExtensions
|
|
1241
|
+
.HandleTransientHttpError()
|
|
1242
|
+
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
|
|
1243
|
+
}
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
## Testing Strategy
|
|
1247
|
+
|
|
1248
|
+
### xUnit Configuration
|
|
1249
|
+
|
|
1250
|
+
```csharp
|
|
1251
|
+
// Test project setup with dependency injection
|
|
1252
|
+
public class TestFixture : IDisposable
|
|
1253
|
+
{
|
|
1254
|
+
public ServiceProvider ServiceProvider { get; }
|
|
1255
|
+
public AppDbContext DbContext { get; }
|
|
1256
|
+
|
|
1257
|
+
public TestFixture()
|
|
1258
|
+
{
|
|
1259
|
+
var services = new ServiceCollection();
|
|
1260
|
+
|
|
1261
|
+
// Configure in-memory database
|
|
1262
|
+
services.AddDbContext<AppDbContext>(options =>
|
|
1263
|
+
options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
|
|
1264
|
+
|
|
1265
|
+
// Register services
|
|
1266
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
1267
|
+
services.AddScoped<IUserService, UserService>();
|
|
1268
|
+
services.AddScoped<IEmailService, MockEmailService>();
|
|
1269
|
+
|
|
1270
|
+
// Configure logging
|
|
1271
|
+
services.AddLogging(builder =>
|
|
1272
|
+
builder.SetMinimumLevel(LogLevel.Warning));
|
|
1273
|
+
|
|
1274
|
+
ServiceProvider = services.BuildServiceProvider();
|
|
1275
|
+
DbContext = ServiceProvider.GetRequiredService<AppDbContext>();
|
|
1276
|
+
|
|
1277
|
+
// Seed test data
|
|
1278
|
+
SeedTestData();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
private void SeedTestData()
|
|
1282
|
+
{
|
|
1283
|
+
var users = new List<User>
|
|
1284
|
+
{
|
|
1285
|
+
new User("admin", Email.Create("admin@example.com"),
|
|
1286
|
+
new UserProfile("Admin", "User", "System administrator")),
|
|
1287
|
+
new User("user1", Email.Create("user1@example.com"),
|
|
1288
|
+
new UserProfile("Regular", "User", "Regular user"))
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
DbContext.Users.AddRange(users);
|
|
1292
|
+
DbContext.SaveChanges();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
public void Dispose()
|
|
1296
|
+
{
|
|
1297
|
+
DbContext.Dispose();
|
|
1298
|
+
ServiceProvider.Dispose();
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Integration tests with WebApplicationFactory
|
|
1303
|
+
public class UserIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
|
1304
|
+
{
|
|
1305
|
+
private readonly WebApplicationFactory<Program> _factory;
|
|
1306
|
+
private readonly HttpClient _client;
|
|
1307
|
+
|
|
1308
|
+
public UserIntegrationTests(WebApplicationFactory<Program> factory)
|
|
1309
|
+
{
|
|
1310
|
+
_factory = factory.WithWebHostBuilder(builder =>
|
|
1311
|
+
{
|
|
1312
|
+
builder.ConfigureServices(services =>
|
|
1313
|
+
{
|
|
1314
|
+
// Configure test services
|
|
1315
|
+
services.Remove(services.SingleOrDefault(
|
|
1316
|
+
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>)));
|
|
1317
|
+
|
|
1318
|
+
services.AddDbContext<AppDbContext>(options =>
|
|
1319
|
+
options.UseInMemoryDatabase("TestDb"));
|
|
1320
|
+
|
|
1321
|
+
// Replace email service with mock
|
|
1322
|
+
services.AddScoped<IEmailService, MockEmailService>();
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
_client = _factory.CreateClient();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
[Fact]
|
|
1330
|
+
public async Task CreateUser_ValidInput_ReturnsCreated()
|
|
1331
|
+
{
|
|
1332
|
+
// Arrange
|
|
1333
|
+
var request = new CreateUserRequest
|
|
1334
|
+
{
|
|
1335
|
+
Username = "newuser",
|
|
1336
|
+
Email = "newuser@example.com",
|
|
1337
|
+
FirstName = "New",
|
|
1338
|
+
LastName = "User"
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// Act
|
|
1342
|
+
var response = await _client.PostAsJsonAsync("/api/users", request);
|
|
1343
|
+
|
|
1344
|
+
// Assert
|
|
1345
|
+
response.EnsureSuccessStatusCode();
|
|
1346
|
+
|
|
1347
|
+
var user = await response.Content.ReadFromJsonAsync<UserDto>();
|
|
1348
|
+
Assert.NotNull(user);
|
|
1349
|
+
Assert.Equal("newuser", user!.Username);
|
|
1350
|
+
Assert.Equal("newuser@example.com", user.Email);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
[Fact]
|
|
1354
|
+
public async Task GetUsers_ReturnsUserList()
|
|
1355
|
+
{
|
|
1356
|
+
// Act
|
|
1357
|
+
var response = await _client.GetAsync("/api/users");
|
|
1358
|
+
|
|
1359
|
+
// Assert
|
|
1360
|
+
response.EnsureSuccessStatusCode();
|
|
1361
|
+
|
|
1362
|
+
var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
|
|
1363
|
+
Assert.NotNull(users);
|
|
1364
|
+
Assert.True(users!.Count >= 2); // At least the seeded users
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
[Fact]
|
|
1368
|
+
public async Task GetUser_ExistingId_ReturnsUser()
|
|
1369
|
+
{
|
|
1370
|
+
// Arrange
|
|
1371
|
+
var createResponse = await _client.PostAsJsonAsync("/api/users", new CreateUserRequest
|
|
1372
|
+
{
|
|
1373
|
+
Username = "testuser",
|
|
1374
|
+
Email = "test@example.com",
|
|
1375
|
+
FirstName = "Test",
|
|
1376
|
+
LastName = "User"
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
var createdUser = await createResponse.Content.ReadFromJsonAsync<UserDto>();
|
|
1380
|
+
Assert.NotNull(createdUser);
|
|
1381
|
+
|
|
1382
|
+
// Act
|
|
1383
|
+
var response = await _client.GetAsync($"/api/users/{createdUser!.Id}");
|
|
1384
|
+
|
|
1385
|
+
// Assert
|
|
1386
|
+
response.EnsureSuccessStatusCode();
|
|
1387
|
+
|
|
1388
|
+
var user = await response.Content.ReadFromJsonAsync<UserDto>();
|
|
1389
|
+
Assert.NotNull(user);
|
|
1390
|
+
Assert.Equal(createdUser!.Id, user!.Id);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
### Unit Testing Patterns
|
|
1396
|
+
|
|
1397
|
+
```csharp
|
|
1398
|
+
public class UserServiceTests : IClassFixture<TestFixture>
|
|
1399
|
+
{
|
|
1400
|
+
private readonly TestFixture _fixture;
|
|
1401
|
+
private readonly IUserService _userService;
|
|
1402
|
+
private readonly Mock<IEmailService> _mockEmailService;
|
|
1403
|
+
|
|
1404
|
+
public UserServiceTests(TestFixture fixture)
|
|
1405
|
+
{
|
|
1406
|
+
_fixture = fixture;
|
|
1407
|
+
_userService = fixture.ServiceProvider.GetRequiredService<IUserService>();
|
|
1408
|
+
_mockEmailService = new Mock<IEmailService>();
|
|
1409
|
+
_userService = new UserService(
|
|
1410
|
+
_fixture.ServiceProvider.GetRequiredService<IUserRepository>(),
|
|
1411
|
+
_mockEmailService.Object,
|
|
1412
|
+
Mock.Of<ILogger<UserService>>(),
|
|
1413
|
+
Mock.Of<IMemoryCache>());
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
[Fact]
|
|
1417
|
+
public async Task CreateUser_ValidRequest_ReturnsUser()
|
|
1418
|
+
{
|
|
1419
|
+
// Arrange
|
|
1420
|
+
var request = new CreateUserRequest
|
|
1421
|
+
{
|
|
1422
|
+
Username = "newuser",
|
|
1423
|
+
Email = "newuser@example.com",
|
|
1424
|
+
FirstName = "New",
|
|
1425
|
+
LastName = "User"
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
_mockEmailService
|
|
1429
|
+
.Setup(s => s.SendWelcomeEmailAsync(It.IsAny<Email>(), It.IsAny<string>(), It.IsAny<string>()))
|
|
1430
|
+
.Returns(Task.CompletedTask);
|
|
1431
|
+
|
|
1432
|
+
// Act
|
|
1433
|
+
var result = await _userService.CreateUserAsync(request);
|
|
1434
|
+
|
|
1435
|
+
// Assert
|
|
1436
|
+
Assert.NotNull(result);
|
|
1437
|
+
Assert.Equal("newuser", result.Username);
|
|
1438
|
+
Assert.Equal("newuser@example.com", result.Email.Value);
|
|
1439
|
+
|
|
1440
|
+
_mockEmailService.Verify(
|
|
1441
|
+
s => s.SendWelcomeEmailAsync(It.Is<Email>(e => e.Value == "newuser@example.com"), "New", "User"),
|
|
1442
|
+
Times.Once);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
[Fact]
|
|
1446
|
+
public async Task CreateUser_DuplicateEmail_ThrowsException()
|
|
1447
|
+
{
|
|
1448
|
+
// Arrange
|
|
1449
|
+
var request = new CreateUserRequest
|
|
1450
|
+
{
|
|
1451
|
+
Username = "duplicate",
|
|
1452
|
+
Email = "admin@example.com", // Already exists in seed data
|
|
1453
|
+
FirstName = "Duplicate",
|
|
1454
|
+
LastName = "User"
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
// Act & Assert
|
|
1458
|
+
var exception = await Assert.ThrowsAsync<UserAlreadyExistsException>(
|
|
1459
|
+
() => _userService.CreateUserAsync(request));
|
|
1460
|
+
|
|
1461
|
+
Assert.Contains("already exists", exception.Message);
|
|
1462
|
+
|
|
1463
|
+
_mockEmailService.Verify(
|
|
1464
|
+
s => s.SendWelcomeEmailAsync(It.IsAny<Email>(), It.IsAny<string>(), It.IsAny<string>()),
|
|
1465
|
+
Times.Never);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
[Theory]
|
|
1469
|
+
[InlineData("test@example.com", true)]
|
|
1470
|
+
[InlineData("invalid-email", false)]
|
|
1471
|
+
[InlineData("test@.com", false)]
|
|
1472
|
+
[InlineData("@example.com", false)]
|
|
1473
|
+
public void EmailValidation_ValidatesEmailCorrectly(string email, bool expectedValid)
|
|
1474
|
+
{
|
|
1475
|
+
// Act
|
|
1476
|
+
var isValid = EmailValidator.IsValid(email);
|
|
1477
|
+
|
|
1478
|
+
// Assert
|
|
1479
|
+
Assert.Equal(expectedValid, isValid);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
[Fact]
|
|
1483
|
+
public async Task UpdateUser_ValidRequest_UpdatesUser()
|
|
1484
|
+
{
|
|
1485
|
+
// Arrange
|
|
1486
|
+
var existingUser = await _userService.GetUserByIdAsync(Guid.Parse("00000000-0000-0000-0000-000000000001"));
|
|
1487
|
+
Assert.NotNull(existingUser);
|
|
1488
|
+
|
|
1489
|
+
var updateRequest = new UpdateUserRequest
|
|
1490
|
+
{
|
|
1491
|
+
FirstName = "Updated",
|
|
1492
|
+
LastName = "Name",
|
|
1493
|
+
Bio = "Updated bio"
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// Act
|
|
1497
|
+
var result = await _userService.UpdateUserAsync(existingUser!.Id, updateRequest);
|
|
1498
|
+
|
|
1499
|
+
// Assert
|
|
1500
|
+
Assert.NotNull(result);
|
|
1501
|
+
Assert.Equal("Updated", result!.Profile.FirstName);
|
|
1502
|
+
Assert.Equal("Name", result.Profile.LastName);
|
|
1503
|
+
Assert.Equal("Updated bio", result.Profile.Bio);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Performance testing
|
|
1508
|
+
public class PerformanceTests : IClassFixture<TestFixture>
|
|
1509
|
+
{
|
|
1510
|
+
private readonly TestFixture _fixture;
|
|
1511
|
+
|
|
1512
|
+
public PerformanceTests(TestFixture fixture)
|
|
1513
|
+
{
|
|
1514
|
+
_fixture = fixture;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
[Fact]
|
|
1518
|
+
public async Task BulkInsert_PerformanceTest()
|
|
1519
|
+
{
|
|
1520
|
+
// Arrange
|
|
1521
|
+
const int userCount = 1000;
|
|
1522
|
+
var users = Enumerable.Range(1, userCount)
|
|
1523
|
+
.Select(i => new User($"user{i}", Email.Create($"user{i}@example.com"),
|
|
1524
|
+
new UserProfile($"User{i}", $"Test{i}", $"Bio for user {i}")))
|
|
1525
|
+
.ToList();
|
|
1526
|
+
|
|
1527
|
+
var stopwatch = Stopwatch.StartNew();
|
|
1528
|
+
|
|
1529
|
+
// Act
|
|
1530
|
+
await _fixture.DbContext.BulkInsertAsync(users);
|
|
1531
|
+
|
|
1532
|
+
stopwatch.Stop();
|
|
1533
|
+
|
|
1534
|
+
// Assert
|
|
1535
|
+
Assert.True(stopwatch.ElapsedMilliseconds < 5000, // Should complete within 5 seconds
|
|
1536
|
+
$"Bulk insert took {stopwatch.ElapsedMilliseconds}ms, expected less than 5000ms");
|
|
1537
|
+
|
|
1538
|
+
var dbUsers = await _fixture.DbContext.Users.CountAsync();
|
|
1539
|
+
Assert.True(dbUsers >= userCount, $"Expected at least {userCount} users, got {dbUsers}");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
### API Testing
|
|
1545
|
+
|
|
1546
|
+
```csharp
|
|
1547
|
+
// API contract testing
|
|
1548
|
+
public class ApiContractTests : IClassFixture<WebApplicationFactory<Program>>
|
|
1549
|
+
{
|
|
1550
|
+
private readonly WebApplicationFactory<Program> _factory;
|
|
1551
|
+
private readonly HttpClient _client;
|
|
1552
|
+
|
|
1553
|
+
public ApiContractTests(WebApplicationFactory<Program> factory)
|
|
1554
|
+
{
|
|
1555
|
+
_factory = factory;
|
|
1556
|
+
_client = _factory.CreateClient();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
[Fact]
|
|
1560
|
+
public async Task GetUsers_ReturnsCorrectContract()
|
|
1561
|
+
{
|
|
1562
|
+
// Act
|
|
1563
|
+
var response = await _client.GetAsync("/api/users");
|
|
1564
|
+
|
|
1565
|
+
// Assert
|
|
1566
|
+
response.EnsureSuccessStatusCode();
|
|
1567
|
+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
|
|
1568
|
+
|
|
1569
|
+
var content = await response.Content.ReadAsStringAsync();
|
|
1570
|
+
Assert.NotEmpty(content);
|
|
1571
|
+
|
|
1572
|
+
// Validate JSON schema
|
|
1573
|
+
var users = JsonSerializer.Deserialize<List<UserDto>>(content);
|
|
1574
|
+
Assert.NotNull(users);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
[Fact]
|
|
1578
|
+
public async Task CreateUser_ValidInput_ReturnsCorrectContract()
|
|
1579
|
+
{
|
|
1580
|
+
// Arrange
|
|
1581
|
+
var request = new
|
|
1582
|
+
{
|
|
1583
|
+
Username = "newuser",
|
|
1584
|
+
Email = "newuser@example.com",
|
|
1585
|
+
FirstName = "New",
|
|
1586
|
+
LastName = "User"
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// Act
|
|
1590
|
+
var response = await _client.PostAsJsonAsync("/api/users", request);
|
|
1591
|
+
|
|
1592
|
+
// Assert
|
|
1593
|
+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
|
1594
|
+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
|
|
1595
|
+
|
|
1596
|
+
var content = await response.Content.ReadAsStringAsync();
|
|
1597
|
+
var user = JsonSerializer.Deserialize<UserDto>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
1598
|
+
|
|
1599
|
+
Assert.NotNull(user);
|
|
1600
|
+
Assert.Equal("newuser", user!.Username);
|
|
1601
|
+
Assert.Equal("newuser@example.com", user.Email);
|
|
1602
|
+
Assert.NotNull(user.Id);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
[Fact]
|
|
1606
|
+
public async Task GetUsers_WithoutAuthentication_ReturnsUnauthorized()
|
|
1607
|
+
{
|
|
1608
|
+
// Act
|
|
1609
|
+
var response = await _client.GetAsync("/api/users/protected-endpoint");
|
|
1610
|
+
|
|
1611
|
+
// Assert
|
|
1612
|
+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
## Security Best Practices
|
|
1618
|
+
|
|
1619
|
+
### Input Validation and Sanitization
|
|
1620
|
+
|
|
1621
|
+
```csharp
|
|
1622
|
+
// Data annotations for validation
|
|
1623
|
+
public class CreateUserRequest
|
|
1624
|
+
{
|
|
1625
|
+
[Required(ErrorMessage = "Username is required")]
|
|
1626
|
+
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
|
|
1627
|
+
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers, and underscores")]
|
|
1628
|
+
public string Username { get; init; } = string.Empty;
|
|
1629
|
+
|
|
1630
|
+
[Required(ErrorMessage = "Email is required")]
|
|
1631
|
+
[EmailAddress(ErrorMessage = "Invalid email format")]
|
|
1632
|
+
[StringLength(100, ErrorMessage = "Email must be less than 100 characters")]
|
|
1633
|
+
public string Email { get; init; } = string.Empty;
|
|
1634
|
+
|
|
1635
|
+
[StringLength(50, ErrorMessage = "First name must be less than 50 characters")]
|
|
1636
|
+
[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "First name can only contain letters and spaces")]
|
|
1637
|
+
public string? FirstName { get; init; }
|
|
1638
|
+
|
|
1639
|
+
[StringLength(50, ErrorMessage = "Last name must be less than 50 characters")]
|
|
1640
|
+
[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Last name can only contain letters and spaces")]
|
|
1641
|
+
public string? LastName { get; init; }
|
|
1642
|
+
|
|
1643
|
+
[StringLength(500, ErrorMessage = "Bio must be less than 500 characters")]
|
|
1644
|
+
[RegularExpression(@"^[^<>]*$", ErrorMessage = "Bio cannot contain HTML tags")]
|
|
1645
|
+
public string? Bio { get; init; }
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Custom validation attributes
|
|
1649
|
+
public class StrongPasswordAttribute : ValidationAttribute
|
|
1650
|
+
{
|
|
1651
|
+
public StrongPasswordAttribute() : base("Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character")
|
|
1652
|
+
{
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
|
1656
|
+
{
|
|
1657
|
+
if (value is string password)
|
|
1658
|
+
{
|
|
1659
|
+
if (password.Length < 8)
|
|
1660
|
+
{
|
|
1661
|
+
return new ValidationResult("Password must be at least 8 characters long");
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (!Regex.IsMatch(password, @"[A-Z]"))
|
|
1665
|
+
{
|
|
1666
|
+
return new ValidationResult("Password must contain at least one uppercase letter");
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (!Regex.IsMatch(password, @"[a-z]"))
|
|
1670
|
+
{
|
|
1671
|
+
return new ValidationResult("Password must contain at least one lowercase letter");
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (!Regex.IsMatch(password, @"\d"))
|
|
1675
|
+
{
|
|
1676
|
+
return new ValidationResult("Password must contain at least one digit");
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (!Regex.IsMatch(password, @"[!@#$%^&*(),.?""{}|<>]"))
|
|
1680
|
+
{
|
|
1681
|
+
return new ValidationResult("Password must contain at least one special character");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
return ValidationResult.Success;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return new ValidationResult("Password is required");
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
88
1690
|
|
|
89
|
-
|
|
90
|
-
|
|
1691
|
+
// HTML sanitization
|
|
1692
|
+
public static class HtmlSanitizer
|
|
1693
|
+
{
|
|
1694
|
+
private static readonly HtmlSanitizer _sanitizer = new HtmlSanitizer()
|
|
1695
|
+
.AllowTags("p", "br", "strong", "em", "u", "ol", "ul", "li")
|
|
1696
|
+
.AllowAttributes("class").OnAllTags()
|
|
1697
|
+
.RemoveAttributes("style", "onclick", "onload");
|
|
1698
|
+
|
|
1699
|
+
public static string Sanitize(string? html)
|
|
1700
|
+
{
|
|
1701
|
+
if (string.IsNullOrEmpty(html))
|
|
1702
|
+
{
|
|
1703
|
+
return string.Empty;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return _sanitizer.Sanitize(html);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
### Authentication and Authorization
|
|
1712
|
+
|
|
1713
|
+
```csharp
|
|
1714
|
+
// JWT authentication configuration
|
|
1715
|
+
public static class AuthenticationConfiguration
|
|
1716
|
+
{
|
|
1717
|
+
public static IServiceCollection AddJwtAuthentication(this IServiceCollection services,
|
|
1718
|
+
IConfiguration configuration)
|
|
1719
|
+
{
|
|
1720
|
+
var jwtSettings = configuration.GetSection("JwtSettings");
|
|
1721
|
+
var key = Encoding.UTF8.GetBytes(jwtSettings["Secret"]!);
|
|
1722
|
+
|
|
1723
|
+
services.AddAuthentication(options =>
|
|
1724
|
+
{
|
|
1725
|
+
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
1726
|
+
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
1727
|
+
})
|
|
1728
|
+
.AddJwtBearer(options =>
|
|
1729
|
+
{
|
|
1730
|
+
options.TokenValidationParameters = new TokenValidationParameters
|
|
1731
|
+
{
|
|
1732
|
+
ValidateIssuer = true,
|
|
1733
|
+
ValidateAudience = true,
|
|
1734
|
+
ValidateLifetime = true,
|
|
1735
|
+
ValidateIssuerSigningKey = true,
|
|
1736
|
+
ValidIssuer = jwtSettings["Issuer"],
|
|
1737
|
+
ValidAudience = jwtSettings["Audience"],
|
|
1738
|
+
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
1739
|
+
ClockSkew = TimeSpan.Zero
|
|
1740
|
+
};
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
return services;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// JWT service
|
|
1748
|
+
public interface IJwtService
|
|
1749
|
+
{
|
|
1750
|
+
string GenerateToken(User user);
|
|
1751
|
+
ClaimsPrincipal? GetPrincipalFromToken(string token);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
public class JwtService : IJwtService
|
|
1755
|
+
{
|
|
1756
|
+
private readonly IConfiguration _configuration;
|
|
1757
|
+
private readonly string _secret;
|
|
1758
|
+
private readonly string _issuer;
|
|
1759
|
+
private readonly string _audience;
|
|
1760
|
+
|
|
1761
|
+
public JwtService(IConfiguration configuration)
|
|
1762
|
+
{
|
|
1763
|
+
_configuration = configuration;
|
|
1764
|
+
_secret = _configuration["JwtSettings:Secret"]!;
|
|
1765
|
+
_issuer = _configuration["JwtSettings:Issuer"]!;
|
|
1766
|
+
_audience = _configuration["JwtSettings:Audience"]!;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
public string GenerateToken(User user)
|
|
1770
|
+
{
|
|
1771
|
+
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret));
|
|
1772
|
+
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
1773
|
+
|
|
1774
|
+
var claims = new[]
|
|
1775
|
+
{
|
|
1776
|
+
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
|
1777
|
+
new Claim(JwtRegisteredClaimNames.Email, user.Email.Value),
|
|
1778
|
+
new Claim(JwtRegisteredClaimNames.Name, user.Username),
|
|
1779
|
+
new Claim(ClaimTypes.Name, user.Username),
|
|
1780
|
+
new Claim(ClaimTypes.Email, user.Email.Value),
|
|
1781
|
+
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
1782
|
+
new Claim("role", string.Join(",", user.Roles.Select(r => r.Name)))
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
var token = new JwtSecurityToken(
|
|
1786
|
+
issuer: _issuer,
|
|
1787
|
+
audience: _audience,
|
|
1788
|
+
claims: claims,
|
|
1789
|
+
expires: DateTime.UtcNow.AddHours(24),
|
|
1790
|
+
signingCredentials: credentials);
|
|
1791
|
+
|
|
1792
|
+
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
public ClaimsPrincipal? GetPrincipalFromToken(string token)
|
|
1796
|
+
{
|
|
1797
|
+
try
|
|
1798
|
+
{
|
|
1799
|
+
var tokenHandler = new JwtSecurityTokenHandler();
|
|
1800
|
+
var key = Encoding.UTF8.GetBytes(_secret);
|
|
1801
|
+
|
|
1802
|
+
var validationParameters = new TokenValidationParameters
|
|
1803
|
+
{
|
|
1804
|
+
ValidateIssuer = true,
|
|
1805
|
+
ValidateAudience = true,
|
|
1806
|
+
ValidateLifetime = true,
|
|
1807
|
+
ValidateIssuerSigningKey = true,
|
|
1808
|
+
ValidIssuer = _issuer,
|
|
1809
|
+
ValidAudience = _audience,
|
|
1810
|
+
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
1811
|
+
ClockSkew = TimeSpan.Zero
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken);
|
|
1815
|
+
|
|
1816
|
+
return principal;
|
|
1817
|
+
}
|
|
1818
|
+
catch
|
|
1819
|
+
{
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Authorization policies
|
|
1826
|
+
public static class AuthorizationConfiguration
|
|
1827
|
+
{
|
|
1828
|
+
public static IServiceCollection AddAuthorizationPolicies(this IServiceCollection services)
|
|
1829
|
+
{
|
|
1830
|
+
services.AddAuthorization(options =>
|
|
1831
|
+
{
|
|
1832
|
+
options.AddPolicy("AdminOnly", policy =>
|
|
1833
|
+
policy.RequireRole("Admin"));
|
|
1834
|
+
|
|
1835
|
+
options.AddPolicy("UserOrAdmin", policy =>
|
|
1836
|
+
policy.RequireRole("User", "Admin"));
|
|
1837
|
+
|
|
1838
|
+
options.AddPolicy("CanManageUsers", policy =>
|
|
1839
|
+
policy.Requirements.Add(new CanManageUsersRequirement()));
|
|
1840
|
+
|
|
1841
|
+
options.AddPolicy("MinimumAge", policy =>
|
|
1842
|
+
policy.Requirements.Add(new MinimumAgeRequirement(18)));
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
services.AddScoped<IAuthorizationHandler, CanManageUsersHandler>();
|
|
1846
|
+
services.AddScoped<IAuthorizationHandler, MinimumAgeHandler>();
|
|
1847
|
+
|
|
1848
|
+
return services;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Custom authorization requirements
|
|
1853
|
+
public class CanManageUsersRequirement : IAuthorizationRequirement { }
|
|
1854
|
+
|
|
1855
|
+
public class MinimumAgeRequirement : IAuthorizationRequirement
|
|
1856
|
+
{
|
|
1857
|
+
public MinimumAgeRequirement(int minimumAge)
|
|
1858
|
+
{
|
|
1859
|
+
MinimumAge = minimumAge;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
public int MinimumAge { get; }
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// Authorization handlers
|
|
1866
|
+
public class CanManageUsersHandler : AuthorizationHandler<CanManageUsersRequirement>
|
|
1867
|
+
{
|
|
1868
|
+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
|
1869
|
+
CanManageUsersRequirement requirement)
|
|
1870
|
+
{
|
|
1871
|
+
if (context.User.IsInRole("Admin"))
|
|
1872
|
+
{
|
|
1873
|
+
context.Succeed(requirement);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
return Task.CompletedTask;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Using authorization in controllers
|
|
1881
|
+
[ApiController]
|
|
1882
|
+
[Route("api/[controller]")]
|
|
1883
|
+
[Authorize]
|
|
1884
|
+
public class UsersController : ControllerBase
|
|
1885
|
+
{
|
|
1886
|
+
[HttpGet]
|
|
1887
|
+
[AllowAnonymous]
|
|
1888
|
+
public async Task<IActionResult> GetUsers()
|
|
1889
|
+
{
|
|
1890
|
+
// Public endpoint
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
[HttpPost]
|
|
1894
|
+
[Authorize(Roles = "Admin")]
|
|
1895
|
+
public async Task<IActionResult> CreateUser()
|
|
1896
|
+
{
|
|
1897
|
+
// Admin only endpoint
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
[HttpDelete("{id}")]
|
|
1901
|
+
[Authorize(Policy = "CanManageUsers")]
|
|
1902
|
+
public async Task<IActionResult> DeleteUser(Guid id)
|
|
1903
|
+
{
|
|
1904
|
+
// Users who can manage users
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
### Security Headers and Middleware
|
|
1910
|
+
|
|
1911
|
+
```csharp
|
|
1912
|
+
// Security headers middleware
|
|
1913
|
+
public class SecurityHeadersMiddleware
|
|
1914
|
+
{
|
|
1915
|
+
private readonly RequestDelegate _next;
|
|
1916
|
+
|
|
1917
|
+
public SecurityHeadersMiddleware(RequestDelegate next)
|
|
1918
|
+
{
|
|
1919
|
+
_next = next;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
public async Task InvokeAsync(HttpContext context)
|
|
1923
|
+
{
|
|
1924
|
+
// Add security headers
|
|
1925
|
+
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
|
1926
|
+
context.Response.Headers.Add("X-Frame-Options", "DENY");
|
|
1927
|
+
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
|
|
1928
|
+
context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
1929
|
+
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
|
1930
|
+
context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1931
|
+
|
|
1932
|
+
await _next(context);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// Rate limiting middleware
|
|
1937
|
+
public class RateLimitingMiddleware
|
|
1938
|
+
{
|
|
1939
|
+
private readonly RequestDelegate _next;
|
|
1940
|
+
private readonly IMemoryCache _cache;
|
|
1941
|
+
private readonly RateLimitOptions _options;
|
|
1942
|
+
|
|
1943
|
+
public RateLimitingMiddleware(RequestDelegate next, IMemoryCache cache, IOptions<RateLimitOptions> options)
|
|
1944
|
+
{
|
|
1945
|
+
_next = next;
|
|
1946
|
+
_cache = cache;
|
|
1947
|
+
_options = options.Value;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
public async Task InvokeAsync(HttpContext context)
|
|
1951
|
+
{
|
|
1952
|
+
var clientId = GetClientId(context);
|
|
1953
|
+
var cacheKey = $"rate_limit_{clientId}";
|
|
1954
|
+
|
|
1955
|
+
var requestCount = await _cache.GetOrCreateAsync(cacheKey, async () =>
|
|
1956
|
+
{
|
|
1957
|
+
await Task.CompletedTask;
|
|
1958
|
+
return 1;
|
|
1959
|
+
}, new MemoryCacheEntryOptions
|
|
1960
|
+
{
|
|
1961
|
+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1),
|
|
1962
|
+
SlidingExpiration = TimeSpan.FromSeconds(30)
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
if (requestCount > _options.MaxRequestsPerMinute)
|
|
1966
|
+
{
|
|
1967
|
+
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
1968
|
+
await context.Response.WriteAsync("Rate limit exceeded");
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
await _next(context);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
private string GetClientId(HttpContext context)
|
|
1976
|
+
{
|
|
1977
|
+
// Try to get user ID from claims, otherwise use IP address
|
|
1978
|
+
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
1979
|
+
return userId ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
public class RateLimitOptions
|
|
1984
|
+
{
|
|
1985
|
+
public int MaxRequestsPerMinute { get; set; } = 100;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Configure middleware in Program.cs
|
|
1989
|
+
app.UseMiddleware<SecurityHeadersMiddleware>();
|
|
1990
|
+
app.UseMiddleware<RateLimitingMiddleware>();
|
|
1991
|
+
```
|
|
91
1992
|
|
|
92
|
-
##
|
|
1993
|
+
## Integration Patterns
|
|
1994
|
+
|
|
1995
|
+
### Entity Framework Core Integration
|
|
1996
|
+
|
|
1997
|
+
```csharp
|
|
1998
|
+
// Repository pattern with Unit of Work
|
|
1999
|
+
public interface IUnitOfWork : IDisposable
|
|
2000
|
+
{
|
|
2001
|
+
IUserRepository Users { get; }
|
|
2002
|
+
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
|
2003
|
+
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
|
|
2004
|
+
Task CommitAsync(CancellationToken cancellationToken = default);
|
|
2005
|
+
Task RollbackAsync(CancellationToken cancellationToken = default);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
public class UnitOfWork : IUnitOfWork
|
|
2009
|
+
{
|
|
2010
|
+
private readonly AppDbContext _context;
|
|
2011
|
+
private IDbContextTransaction? _transaction;
|
|
2012
|
+
|
|
2013
|
+
public UnitOfWork(AppDbContext context)
|
|
2014
|
+
{
|
|
2015
|
+
_context = context;
|
|
2016
|
+
Users = new UserRepository(_context);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
public IUserRepository Users { get; }
|
|
2020
|
+
|
|
2021
|
+
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
2022
|
+
{
|
|
2023
|
+
return await _context.SaveChangesAsync(cancellationToken);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
|
|
2027
|
+
{
|
|
2028
|
+
_transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
public async Task CommitAsync(CancellationToken cancellationToken = default)
|
|
2032
|
+
{
|
|
2033
|
+
try
|
|
2034
|
+
{
|
|
2035
|
+
await _context.SaveChangesAsync(cancellationToken);
|
|
2036
|
+
await _transaction?.CommitAsync(cancellationToken)!;
|
|
2037
|
+
}
|
|
2038
|
+
catch
|
|
2039
|
+
{
|
|
2040
|
+
await RollbackAsync(cancellationToken);
|
|
2041
|
+
throw;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
public async Task RollbackAsync(CancellationToken cancellationToken = default)
|
|
2046
|
+
{
|
|
2047
|
+
if (_transaction != null)
|
|
2048
|
+
{
|
|
2049
|
+
await _transaction.RollbackAsync(cancellationToken);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
public void Dispose()
|
|
2054
|
+
{
|
|
2055
|
+
_transaction?.Dispose();
|
|
2056
|
+
_context.Dispose();
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Service with Unit of Work
|
|
2061
|
+
public class UserService
|
|
2062
|
+
{
|
|
2063
|
+
private readonly IUnitOfWork _unitOfWork;
|
|
2064
|
+
private readonly ILogger<UserService> _logger;
|
|
2065
|
+
|
|
2066
|
+
public UserService(IUnitOfWork unitOfWork, ILogger<UserService> logger)
|
|
2067
|
+
{
|
|
2068
|
+
_unitOfWork = unitOfWork;
|
|
2069
|
+
_logger = logger;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
public async Task<User> CreateUserAsync(CreateUserRequest request,
|
|
2073
|
+
CancellationToken cancellationToken = default)
|
|
2074
|
+
{
|
|
2075
|
+
await _unitOfWork.BeginTransactionAsync(cancellationToken);
|
|
2076
|
+
|
|
2077
|
+
try
|
|
2078
|
+
{
|
|
2079
|
+
var user = new User(request.Username, Email.Create(request.Email),
|
|
2080
|
+
new UserProfile(request.FirstName, request.LastName, request.Bio));
|
|
2081
|
+
|
|
2082
|
+
await _unitOfWork.Users.AddAsync(user, cancellationToken);
|
|
2083
|
+
await _unitOfWork.CommitAsync(cancellationToken);
|
|
2084
|
+
|
|
2085
|
+
_logger.LogInformation("User created successfully with ID: {UserId}", user.Id);
|
|
2086
|
+
return user;
|
|
2087
|
+
}
|
|
2088
|
+
catch
|
|
2089
|
+
{
|
|
2090
|
+
await _unitOfWork.RollbackAsync(cancellationToken);
|
|
2091
|
+
throw;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
```
|
|
2096
|
+
|
|
2097
|
+
### Message Queue Integration
|
|
2098
|
+
|
|
2099
|
+
```csharp
|
|
2100
|
+
// RabbitMQ integration
|
|
2101
|
+
public interface IMessagePublisher
|
|
2102
|
+
{
|
|
2103
|
+
Task PublishAsync<T>(T message, string routingKey, CancellationToken cancellationToken = default);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
public class RabbitMqPublisher : IMessagePublisher
|
|
2107
|
+
{
|
|
2108
|
+
private readonly IConnection _connection;
|
|
2109
|
+
private readonly ILogger<RabbitMqPublisher> _logger;
|
|
2110
|
+
|
|
2111
|
+
public RabbitMqPublisher(IConnection connection, ILogger<RabbitMqPublisher> logger)
|
|
2112
|
+
{
|
|
2113
|
+
_connection = connection;
|
|
2114
|
+
_logger = logger;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
public async Task PublishAsync<T>(T message, string routingKey,
|
|
2118
|
+
CancellationToken cancellationToken = default)
|
|
2119
|
+
{
|
|
2120
|
+
using var channel = await _connection.CreateChannelAsync(cancellationToken);
|
|
2121
|
+
|
|
2122
|
+
await channel.ExchangeDeclareAsync("app_exchange", ExchangeType.Direct, durable: true,
|
|
2123
|
+
cancellationToken: cancellationToken);
|
|
2124
|
+
|
|
2125
|
+
var messageBody = JsonSerializer.SerializeToUtf8Bytes(message);
|
|
2126
|
+
|
|
2127
|
+
await channel.BasicPublishAsync("app_exchange", routingKey, true, new BasicProperties
|
|
2128
|
+
{
|
|
2129
|
+
ContentType = "application/json",
|
|
2130
|
+
DeliveryMode = DeliveryModes.Persistent
|
|
2131
|
+
}, messageBody, cancellationToken);
|
|
2132
|
+
|
|
2133
|
+
_logger.LogInformation("Message published to routing key: {RoutingKey}", routingKey);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Message consumer
|
|
2138
|
+
public interface IMessageConsumer<T>
|
|
2139
|
+
{
|
|
2140
|
+
Task ConsumeAsync(T message, CancellationToken cancellationToken = default);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
public class UserCreatedEventConsumer : BackgroundService
|
|
2144
|
+
{
|
|
2145
|
+
private readonly IConnection _connection;
|
|
2146
|
+
private readonly IServiceScopeFactory _scopeFactory;
|
|
2147
|
+
private readonly ILogger<UserCreatedEventConsumer> _logger;
|
|
2148
|
+
|
|
2149
|
+
public UserCreatedEventConsumer(IConnection connection, IServiceScopeFactory scopeFactory,
|
|
2150
|
+
ILogger<UserCreatedEventConsumer> logger)
|
|
2151
|
+
{
|
|
2152
|
+
_connection = connection;
|
|
2153
|
+
_scopeFactory = scopeFactory;
|
|
2154
|
+
_logger = logger;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
2158
|
+
{
|
|
2159
|
+
using var channel = await _connection.CreateChannelAsync(stoppingToken);
|
|
2160
|
+
|
|
2161
|
+
await channel.QueueDeclareAsync("user_created_queue", durable: true, exclusive: false,
|
|
2162
|
+
autoDelete: false, cancellationToken: stoppingToken);
|
|
2163
|
+
|
|
2164
|
+
await channel.QueueBindAsync("user_created_queue", "app_exchange", "user.created",
|
|
2165
|
+
cancellationToken: stoppingToken);
|
|
2166
|
+
|
|
2167
|
+
var consumer = new AsyncEventingBasicConsumer(channel);
|
|
2168
|
+
consumer.ReceivedAsync += async (sender, args) =>
|
|
2169
|
+
{
|
|
2170
|
+
try
|
|
2171
|
+
{
|
|
2172
|
+
var message = JsonSerializer.Deserialize<UserCreatedEvent>(args.Body.ToArray());
|
|
2173
|
+
|
|
2174
|
+
if (message != null)
|
|
2175
|
+
{
|
|
2176
|
+
await HandleUserCreatedEvent(message, stoppingToken);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
await channel.BasicAckAsync(args.DeliveryTag, false, stoppingToken);
|
|
2180
|
+
}
|
|
2181
|
+
catch (Exception ex)
|
|
2182
|
+
{
|
|
2183
|
+
_logger.LogError(ex, "Error processing user created event");
|
|
2184
|
+
await channel.BasicNackAsync(args.DeliveryTag, false, true, stoppingToken);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
|
|
2188
|
+
await channel.BasicConsumeAsync("user_created_queue", false, consumer, stoppingToken);
|
|
2189
|
+
|
|
2190
|
+
_logger.LogInformation("User created event consumer started");
|
|
2191
|
+
|
|
2192
|
+
// Keep the consumer running
|
|
2193
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
2194
|
+
{
|
|
2195
|
+
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
private async Task HandleUserCreatedEvent(UserCreatedEvent @event,
|
|
2200
|
+
CancellationToken cancellationToken = default)
|
|
2201
|
+
{
|
|
2202
|
+
using var scope = _scopeFactory.CreateScope();
|
|
2203
|
+
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
|
|
2204
|
+
|
|
2205
|
+
await emailService.SendWelcomeEmailAsync(@event.Email, @event.FirstName, @event.LastName, cancellationToken);
|
|
2206
|
+
|
|
2207
|
+
_logger.LogInformation("Processed user created event for user: {UserId}", @event.UserId);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
public record UserCreatedEvent(
|
|
2212
|
+
Guid UserId,
|
|
2213
|
+
string Email,
|
|
2214
|
+
string FirstName,
|
|
2215
|
+
string LastName,
|
|
2216
|
+
DateTime CreatedAt);
|
|
2217
|
+
```
|
|
2218
|
+
|
|
2219
|
+
## Modern Development Workflow
|
|
2220
|
+
|
|
2221
|
+
### Project Configuration
|
|
2222
|
+
|
|
2223
|
+
```xml
|
|
2224
|
+
<!-- Directory.Build.props -->
|
|
2225
|
+
<Project>
|
|
2226
|
+
<PropertyGroup>
|
|
2227
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
2228
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
2229
|
+
<Nullable>enable</Nullable>
|
|
2230
|
+
<LangVersion>12.0</LangVersion>
|
|
2231
|
+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
2232
|
+
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
2233
|
+
</PropertyGroup>
|
|
2234
|
+
|
|
2235
|
+
<ItemGroup>
|
|
2236
|
+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
|
2237
|
+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
|
2238
|
+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
|
2239
|
+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
|
2240
|
+
</ItemGroup>
|
|
2241
|
+
</Project>
|
|
2242
|
+
|
|
2243
|
+
<!-- MyApi.csproj -->
|
|
2244
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
2245
|
+
<PropertyGroup>
|
|
2246
|
+
<AssemblyName>MyApi</AssemblyName>
|
|
2247
|
+
<RootNamespace>MyApi</RootNamespace>
|
|
2248
|
+
</PropertyGroup>
|
|
2249
|
+
|
|
2250
|
+
<ItemGroup>
|
|
2251
|
+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
|
2252
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
|
2253
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
|
2254
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
|
|
2255
|
+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
|
2256
|
+
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
|
2257
|
+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
|
2258
|
+
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
|
2259
|
+
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
|
2260
|
+
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
|
2261
|
+
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
|
2262
|
+
</ItemGroup>
|
|
2263
|
+
</Project>
|
|
2264
|
+
```
|
|
2265
|
+
|
|
2266
|
+
### Docker Configuration
|
|
2267
|
+
|
|
2268
|
+
```dockerfile
|
|
2269
|
+
# Multi-stage Dockerfile for .NET 8.0
|
|
2270
|
+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
|
2271
|
+
WORKDIR /app
|
|
2272
|
+
|
|
2273
|
+
# Copy csproj and restore dependencies
|
|
2274
|
+
COPY *.csproj ./
|
|
2275
|
+
RUN dotnet restore
|
|
2276
|
+
|
|
2277
|
+
# Copy everything else and build
|
|
2278
|
+
COPY . ./
|
|
2279
|
+
RUN dotnet publish -c Release -o out
|
|
2280
|
+
|
|
2281
|
+
# Build runtime image
|
|
2282
|
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
|
2283
|
+
WORKDIR /app
|
|
2284
|
+
COPY --from=build-env /app/out .
|
|
2285
|
+
|
|
2286
|
+
# Create non-root user
|
|
2287
|
+
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app
|
|
2288
|
+
USER appuser
|
|
2289
|
+
|
|
2290
|
+
EXPOSE 8080
|
|
2291
|
+
ENV ASPNETCORE_URLS=http://+:8080
|
|
2292
|
+
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
2293
|
+
|
|
2294
|
+
ENTRYPOINT ["dotnet", "MyApi.dll"]
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
### CI/CD Configuration
|
|
2298
|
+
|
|
2299
|
+
```yaml
|
|
2300
|
+
# .github/workflows/dotnet.yml
|
|
2301
|
+
name: .NET CI/CD
|
|
2302
|
+
|
|
2303
|
+
on:
|
|
2304
|
+
push:
|
|
2305
|
+
branches: [ main, develop ]
|
|
2306
|
+
pull_request:
|
|
2307
|
+
branches: [ main ]
|
|
2308
|
+
|
|
2309
|
+
env:
|
|
2310
|
+
DOTNET_VERSION: '8.0.x'
|
|
2311
|
+
SOLUTION_FILE: 'MySolution.sln'
|
|
2312
|
+
|
|
2313
|
+
jobs:
|
|
2314
|
+
test:
|
|
2315
|
+
runs-on: ubuntu-latest
|
|
2316
|
+
|
|
2317
|
+
steps:
|
|
2318
|
+
- uses: actions/checkout@v4
|
|
2319
|
+
|
|
2320
|
+
- name: Setup .NET
|
|
2321
|
+
uses: actions/setup-dotnet@v4
|
|
2322
|
+
with:
|
|
2323
|
+
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
2324
|
+
|
|
2325
|
+
- name: Cache dependencies
|
|
2326
|
+
uses: actions/cache@v3
|
|
2327
|
+
with:
|
|
2328
|
+
path: ~/.nuget/packages
|
|
2329
|
+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
|
2330
|
+
restore-keys: |
|
|
2331
|
+
${{ runner.os }}-nuget-
|
|
2332
|
+
|
|
2333
|
+
- name: Restore dependencies
|
|
2334
|
+
run: dotnet restore ${{ env.SOLUTION_FILE }}
|
|
2335
|
+
|
|
2336
|
+
- name: Build
|
|
2337
|
+
run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release
|
|
2338
|
+
|
|
2339
|
+
- name: Run tests
|
|
2340
|
+
run: dotnet test ${{ env.SOLUTION_FILE }} --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
|
|
2341
|
+
|
|
2342
|
+
- name: Upload coverage to Codecov
|
|
2343
|
+
uses: codecov/codecov-action@v3
|
|
2344
|
+
with:
|
|
2345
|
+
file: '**/coverage.cobertura.xml'
|
|
2346
|
+
|
|
2347
|
+
- name: Run security scan
|
|
2348
|
+
run: |
|
|
2349
|
+
dotnet tool install --global dotnet-ssh --version 0.2.0
|
|
2350
|
+
dotnet-ssh scan MyApi/bin/Release/net8.0/MyApi.dll
|
|
2351
|
+
|
|
2352
|
+
build-and-deploy:
|
|
2353
|
+
needs: test
|
|
2354
|
+
runs-on: ubuntu-latest
|
|
2355
|
+
if: github.ref == 'refs/heads/main'
|
|
2356
|
+
|
|
2357
|
+
steps:
|
|
2358
|
+
- uses: actions/checkout@v4
|
|
2359
|
+
|
|
2360
|
+
- name: Setup .NET
|
|
2361
|
+
uses: actions/setup-dotnet@v4
|
|
2362
|
+
with:
|
|
2363
|
+
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
2364
|
+
|
|
2365
|
+
- name: Build and publish
|
|
2366
|
+
run: dotnet publish ${{ env.SOLUTION_FILE }} --configuration Release --output ./publish
|
|
2367
|
+
|
|
2368
|
+
- name: Build Docker image
|
|
2369
|
+
run: |
|
|
2370
|
+
docker build -t myregistry.com/myapi:${{ github.sha }} .
|
|
2371
|
+
docker tag myregistry.com/myapi:${{ github.sha }} myregistry.com/myapi:latest
|
|
2372
|
+
|
|
2373
|
+
- name: Push Docker image
|
|
2374
|
+
if: github.ref == 'refs/heads/main'
|
|
2375
|
+
run: |
|
|
2376
|
+
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
|
2377
|
+
docker push myregistry.com/myapi:${{ github.sha }}
|
|
2378
|
+
docker push myregistry.com/myapi:latest
|
|
2379
|
+
```
|
|
2380
|
+
|
|
2381
|
+
---
|
|
93
2382
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
2383
|
+
**Created by**: MoAI Language Skill Factory
|
|
2384
|
+
**Last Updated**: 2025-11-06
|
|
2385
|
+
**Version**: 2.0.0
|
|
2386
|
+
**C# Target**: .NET 8.0 LTS with C# 12.0 and modern ASP.NET Core patterns
|
|
97
2387
|
|
|
98
|
-
|
|
99
|
-
- Enable automatic validation by matching your linter with the language's official style guide.
|
|
100
|
-
- Fix test/build pipelines with reproducible commands in CI.
|
|
2388
|
+
This skill provides comprehensive C# development guidance with 2025 best practices, covering everything from modern ASP.NET Core APIs to Entity Framework Core optimization and cross-platform development with .NET MAUI.
|