ndsdk-cli 1.0.0__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.
Files changed (51) hide show
  1. ndsdk/__init__.py +3 -0
  2. ndsdk/config_normalizer.py +306 -0
  3. ndsdk/generators/__init__.py +8 -0
  4. ndsdk/generators/base.py +93 -0
  5. ndsdk/generators/engine.py +715 -0
  6. ndsdk/main.py +536 -0
  7. ndsdk/naming.py +112 -0
  8. ndsdk/packages/client_provider/Program.fragment.cs.j2 +1 -0
  9. ndsdk/packages/client_provider/appsettings.fragment.json.j2 +14 -0
  10. ndsdk/packages/client_provider/package.yaml +13 -0
  11. ndsdk/packages/db_provider/Program.fragment.cs.j2 +3 -0
  12. ndsdk/packages/db_provider/appsettings.fragment.json.j2 +30 -0
  13. ndsdk/packages/db_provider/package.yaml +123 -0
  14. ndsdk/packages/exception_handling/Program.app.fragment.cs.j2 +1 -0
  15. ndsdk/packages/exception_handling/Program.fragment.cs.j2 +1 -0
  16. ndsdk/packages/exception_handling/package.yaml +7 -0
  17. ndsdk/packages/file_storage_azure/Program.fragment.cs.j2 +1 -0
  18. ndsdk/packages/file_storage_azure/appsettings.fragment.json.j2 +6 -0
  19. ndsdk/packages/file_storage_azure/package.yaml +9 -0
  20. ndsdk/packages/observability/Program.fragment.cs.j2 +2 -0
  21. ndsdk/packages/observability/appsettings.fragment.json.j2 +39 -0
  22. ndsdk/packages/observability/package.yaml +16 -0
  23. ndsdk/registry.py +192 -0
  24. ndsdk/templates/_common/BO.cs.j2 +10 -0
  25. ndsdk/templates/_common/Controller.cs.j2 +131 -0
  26. ndsdk/templates/_common/DbProviderBase.cs.j2 +26 -0
  27. ndsdk/templates/_common/Dockerfile.j2 +15 -0
  28. ndsdk/templates/_common/Dto.cs.j2 +16 -0
  29. ndsdk/templates/_common/Entity.cs.j2 +16 -0
  30. ndsdk/templates/_common/IService.cs.j2 +25 -0
  31. ndsdk/templates/_common/Model.cs.j2 +16 -0
  32. ndsdk/templates/_common/Provider.cs.j2 +151 -0
  33. ndsdk/templates/_common/ProviderBase.cs.j2 +10 -0
  34. ndsdk/templates/_common/Service.cs.j2 +49 -0
  35. ndsdk/templates/_common/ServiceManager.cs.j2 +28 -0
  36. ndsdk/templates/_common/Solution.sln.j2 +22 -0
  37. ndsdk/templates/_common/_csproj_refs.inc.j2 +14 -0
  38. ndsdk/templates/_common/launchSettings.json.j2 +17 -0
  39. ndsdk/templates/microservice/clean/Api.csproj.j2 +12 -0
  40. ndsdk/templates/microservice/clean/ClassLib.csproj.j2 +11 -0
  41. ndsdk/templates/microservice/clean/Program.cs.j2 +35 -0
  42. ndsdk/templates/microservice/clean/layout.yaml +64 -0
  43. ndsdk/templates/microservice/layered/Program.cs.j2 +61 -0
  44. ndsdk/templates/microservice/layered/Project.csproj.j2 +12 -0
  45. ndsdk/templates/microservice/layered/layout.yaml +41 -0
  46. ndsdk/validators.py +213 -0
  47. ndsdk_cli-1.0.0.dist-info/METADATA +11 -0
  48. ndsdk_cli-1.0.0.dist-info/RECORD +51 -0
  49. ndsdk_cli-1.0.0.dist-info/WHEEL +5 -0
  50. ndsdk_cli-1.0.0.dist-info/entry_points.txt +2 -0
  51. ndsdk_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ {
2
+ "DB_Provider": {
3
+ "RelationalDatabase": {
4
+ "Provider": "{{ settings.relational.provider }}",
5
+ "SqlServer": {
6
+ "SchemaName": "{{ settings.relational.schema_name }}",
7
+ "ConnectionString": "{{ settings.relational.connection_string }}"
8
+ }
9
+ },
10
+ "NonRelationalDatabase": {
11
+ "Provider": "{{ settings.non_relational.provider }}",
12
+ "Cassandra": {
13
+ "Keyspace": "{{ settings.non_relational.keyspace }}",
14
+ "ContactPoint": "{{ settings.non_relational.contact_point }}",
15
+ "Port": "{{ settings.non_relational.port }}",
16
+ "Username": "{{ settings.non_relational.username }}",
17
+ "Password": "{{ settings.non_relational.password }}",
18
+ "CoreConnectionsPerHost": "{{ settings.non_relational.core_connections_per_host }}",
19
+ "MaxConnectionsPerHost": "{{ settings.non_relational.max_connections_per_host }}",
20
+ "MaxRequestsPerConnection": "{{ settings.non_relational.max_requests_per_connection }}"
21
+ },
22
+ "RetryPolicy": {
23
+ "Postgres": {
24
+ "RetryCount": {{ settings.relational.retry_count }},
25
+ "RetryDelayMs": {{ settings.relational.retry_delay_ms }}
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,123 @@
1
+ key: db_provider
2
+ display_name: Data Access Manager
3
+ description: ND.FW.DbAccessManager data-access manager with relational (rdb) and non-relational (ndb) backing stores.
4
+ nuget:
5
+ - { name: ND.FW.DbAccessManager, version: 1.0.2 }
6
+ defaults:
7
+ enabled: false
8
+ db_type: non_relational # relational | non_relational
9
+ provider: cassandra # concrete store (free text: sqlserver, postgres, cassandra, ...)
10
+ connection_string_name: DefaultConnection
11
+ facade_namespace: DataAccessLayer.Facade
12
+ types_namespace: DataAccessLayer.Models
13
+ facade_type: DbExternalizationFacade
14
+ request_type: ExternalizationRequest
15
+ response_type: ExternalizationResponse
16
+ dbtype_enum: DatabaseType
17
+ dbprovider_enum: DatabaseProvider
18
+
19
+ # --------------------------------------------------------------------------
20
+ # Backing-store connection settings.
21
+ # NOTE: no secrets are shipped here. Leave these blank and supply real values
22
+ # from the USER config (services[].packages.db_provider.relational.*) or, at
23
+ # runtime, from environment variables. .NET config layering means an env var
24
+ # named with the `__` hierarchy separator overrides the appsettings value, e.g.
25
+ # DB_Provider__RelationalDatabase__SqlServer__ConnectionString=...
26
+ # DB_Provider__RelationalDatabase__SqlServer__SchemaName=idp_proc
27
+ # so the schema name / connection string are never baked into code.
28
+ # --------------------------------------------------------------------------
29
+ relational:
30
+ provider: MSSQL
31
+ schema_name: "" # supply via user config or env
32
+ # Config key the generated provider reads at runtime to resolve the schema.
33
+ # Fed by appsettings AND overridable by the env var of the same path.
34
+ schema_name_config_key: "DB_Provider:RelationalDatabase:SqlServer:SchemaName"
35
+ connection_string: "" # supply via user config or env
36
+ retry_count: 3
37
+ retry_delay_ms: 1000
38
+ non_relational:
39
+ provider: cassandra
40
+ keyspace: ""
41
+ contact_point: ""
42
+ port: ""
43
+ username: ""
44
+ password: ""
45
+ core_connections_per_host: "1"
46
+ max_connections_per_host: "1"
47
+ max_requests_per_connection: "2048"
48
+
49
+ # --------------------------------------------------------------------------
50
+ # Database adapters. Adding a new engine is a config edit here — no Python
51
+ # change. The generator picks the adapter by store name (normalised), falling
52
+ # back to relational_default / non_relational_default by db_type.
53
+ # di_register -> Program.cs DI lines for this store
54
+ # extended_context_type -> the *ExtendedContext class the SP request uses
55
+ # param_type_enum -> C# type used for typed output params
56
+ # extended_context_using -> namespace to import for that enum
57
+ # default_param_type -> param type when a parameter omits `type`
58
+ # supports_cursors -> emit CursorNames from cursor-direction params
59
+ # --------------------------------------------------------------------------
60
+ adapters:
61
+ mssql:
62
+ dbtype_enum_value: Relational
63
+ db_provider_enum: MSSQL
64
+ extended_context_type: MssqlExtendedContext
65
+ extended_context_using: System.Data
66
+ param_type_enum: SqlDbType
67
+ default_param_type: VarChar
68
+ output_params_field: OutputParameters # name -> SqlDbType map on the ExtendedContext
69
+ output_sizes_field: OutputParameterSizes # name -> int map (VARCHAR/NVARCHAR sizes)
70
+ supports_cursors: false
71
+ di_register:
72
+ - builder.Services.AddDBAccessLayer();
73
+ - builder.Services.AddSqlProvider(builder.Configuration);
74
+ - builder.Services.AddDbProviderServices();
75
+ postgres:
76
+ dbtype_enum_value: Relational
77
+ db_provider_enum: PostgreSQL
78
+ extended_context_type: PostgresExtendedContext
79
+ extended_context_using: NpgsqlTypes
80
+ param_type_enum: NpgsqlDbType
81
+ default_param_type: Varchar
82
+ output_params_field: UserTypes # Postgres model names this UserTypes
83
+ cursor_field: CursorNames
84
+ supports_cursors: true
85
+ di_register:
86
+ - builder.Services.AddDBAccessLayer();
87
+ - builder.Services.AddSqlProvider(builder.Configuration);
88
+ - builder.Services.AddDbProviderServices();
89
+ cassandra:
90
+ dbtype_enum_value: NonRelational
91
+ db_provider_enum: Cassandra
92
+ di_register:
93
+ - builder.Services.AddDBAccessLayer();
94
+ - builder.Services.AddNoSqlProvider(builder.Configuration);
95
+ - builder.Services.AddDbProviderServices();
96
+ relational_default:
97
+ dbtype_enum_value: Relational
98
+ db_provider_enum: MSSQL
99
+ extended_context_type: MssqlExtendedContext
100
+ extended_context_using: System.Data
101
+ param_type_enum: SqlDbType
102
+ default_param_type: VarChar
103
+ output_params_field: OutputParameters
104
+ output_sizes_field: OutputParameterSizes
105
+ supports_cursors: false
106
+ di_register:
107
+ - builder.Services.AddDBAccessLayer();
108
+ - builder.Services.AddSqlProvider(builder.Configuration);
109
+ - builder.Services.AddDbProviderServices();
110
+ non_relational_default:
111
+ dbtype_enum_value: NonRelational
112
+ di_register:
113
+ - builder.Services.AddDBAccessLayer();
114
+ - builder.Services.AddNoSqlProvider(builder.Configuration);
115
+ - builder.Services.AddDbProviderServices();
116
+
117
+ dependencies:
118
+ - { type: DbExternalizationFacade, field: _dbFacade, role: facade }
119
+ - { type: IClientProvider, field: _clientProvider }
120
+ interactive:
121
+ - { key: db_type, prompt: "Database type", choices: [non_relational, relational], default: non_relational }
122
+ - { key: provider, prompt: "Store / engine (e.g. cassandra, dynamodb, postgres, sqlserver)", default: cassandra }
123
+ - { key: connection_string_name, prompt: "Connection string name", default: DefaultConnection }
@@ -0,0 +1 @@
1
+ app.UseMiddleware<NDExceptionHandlingMiddleware>();
@@ -0,0 +1 @@
1
+ builder.Services.AddExceptionHandling();
@@ -0,0 +1,7 @@
1
+ key: exception_handling
2
+ display_name: Exception Handling
3
+ description: ND.FW.ExceptionHandling global exception middleware and problem-details responses.
4
+ nuget:
5
+ - { name: ND.FW.ExceptionHandling, version: 0.0.5 }
6
+ defaults:
7
+ enabled: true
@@ -0,0 +1 @@
1
+ builder.Services.AddAzureBlobServices();
@@ -0,0 +1,6 @@
1
+ {
2
+ "AzureBlobStorage": {
3
+ "ConnectionString": "{{ settings.connection_string }}",
4
+ "Container": "{{ settings.container }}"
5
+ }
6
+ }
@@ -0,0 +1,9 @@
1
+ key: file_storage_azure
2
+ display_name: Azure File Storage Provider
3
+ description: ND.FW.FileStorageProvider.Azure blob storage provider.
4
+ nuget:
5
+ - { name: ND.FW.FileStorageProvider.Azure, version: 0.0.1 }
6
+ defaults:
7
+ enabled: false
8
+ connection_string: "DefaultEndpointsProtocol=https;AccountName=ndwtchpidpdev;AccountKey=6Fi+iskcWsYeiemkLG6MSBhdeb5fDt/RKYnCYNK1i7BrcV+uSiYSIZU8F16Njwdwg7+3S1QZ2Vjl+ASt9xEMLw==;EndpointSuffix=core.windows.net"
9
+ container: dev
@@ -0,0 +1,2 @@
1
+ builder.Services.AddNDLogging(builder.Configuration);
2
+ builder.Services.AddNDTracing(builder.Configuration);
@@ -0,0 +1,39 @@
1
+ {
2
+ "Observability": {
3
+ "Logging": {
4
+ "Enabled": true,
5
+ "EnableExporter": true,
6
+ "ServiceName": "{{ settings.service_name | default(service_name) }}",
7
+ "MinimumLevel": "{{ settings.log_level }}",
8
+ "EnableConsole": {{ (settings.enable_console | default(true)) | tojson }},
9
+ "ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/logs",
10
+ "Protocol": "{{ settings.protocol }}"
11
+ },
12
+ "Tracing": {
13
+ "Enabled": true,
14
+ "EnableExporter": true,
15
+ "ServiceName": "{{ settings.service_name | default(service_name) }}",
16
+ "ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/traces",
17
+ "Protocol": "{{ settings.protocol }}",
18
+ "Instrumentation": {
19
+ "AspNet": true,
20
+ "HttpClient": true,
21
+ "SqlClient": true
22
+ }
23
+ },
24
+ "Metrics": {
25
+ "Enabled": true,
26
+ "EnableExporter": true,
27
+ "ServiceName": "{{ settings.service_name | default(service_name) }}",
28
+ "ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/metrics",
29
+ "Protocol": "{{ settings.protocol }}",
30
+ "Instrumentation": {
31
+ "Runtime": true,
32
+ "AspNet": true,
33
+ "HttpClient": true,
34
+ "Database": false
35
+ },
36
+ "CollectionIntervalSeconds": 10
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,16 @@
1
+ key: observability
2
+ display_name: Observability
3
+ description: ND structured logging, tracing and metrics (OpenTelemetry exporters).
4
+ nuget:
5
+ - { name: ND.FW.Observability, version: 0.0.7 }
6
+ defaults:
7
+ enabled: true
8
+ logger_type: INDLoggerService
9
+ logger_namespace: ND.Observability.Framework.Core.Application.Handlers.Interface.Logging
10
+ service_name: IDP_WEB_API
11
+ log_level: Debug
12
+ enable_console: true
13
+ exporter_endpoint: http://nd-app-fw.ecarevantage.com:4318
14
+ protocol: http
15
+ interactive:
16
+ - { key: log_level, prompt: "Log level", choices: [Trace, Debug, Information, Warning, Error], default: Debug }
ndsdk/registry.py ADDED
@@ -0,0 +1,192 @@
1
+ """Package registry.
2
+
3
+ A *package* is an ND-provided capability (observability, db provider, storage,
4
+ queue, …) that, when enabled in a service's config, contributes three things to
5
+ the generated project:
6
+
7
+ 1. NuGet ``<PackageReference>`` entries -> the .csproj
8
+ 2. A Program.cs registration fragment -> dependency injection wiring
9
+ 3. An appsettings.json fragment -> configuration keys
10
+
11
+ Each package is a self-contained folder under ``ndsdk/packages/<name>/`` owned
12
+ by the ND-SDK maintainers (not the end user). Adding or changing a package is a
13
+ folder edit — no generator code changes. That is the whole point: when the
14
+ package changes, you edit the fragment here and every regenerated project
15
+ picks it up.
16
+
17
+ Folder layout::
18
+
19
+ packages/observability/
20
+ package.yaml # metadata + nuget refs + defaults
21
+ Program.fragment.cs.j2 # rendered into Program.cs (optional)
22
+ appsettings.fragment.json.j2# rendered, merged into appsettings (optional)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ import yaml
33
+
34
+ PACKAGES_DIR = Path(__file__).parent / "packages"
35
+
36
+
37
+ @dataclass
38
+ class NugetRef:
39
+ name: str
40
+ version: str
41
+
42
+ @classmethod
43
+ def from_obj(cls, obj: Any) -> "NugetRef":
44
+ if isinstance(obj, str):
45
+ # allow "Newtonsoft.Json@13.0.3" or bare name
46
+ if "@" in obj:
47
+ name, version = obj.split("@", 1)
48
+ return cls(name.strip(), version.strip())
49
+ return cls(obj.strip(), "")
50
+ return cls(str(obj["name"]), str(obj.get("version", "")))
51
+
52
+
53
+ @dataclass
54
+ class Package:
55
+ """A loaded package definition."""
56
+
57
+ key: str
58
+ display_name: str
59
+ description: str
60
+ nuget: list[NugetRef] = field(default_factory=list)
61
+ defaults: dict[str, Any] = field(default_factory=dict)
62
+ interactive: list = field(default_factory=list)
63
+ program_fragment: str | None = None # raw j2 template text (service registration)
64
+ app_fragment: str | None = None # raw j2 template text (app pipeline / middleware)
65
+ appsettings_fragment: str | None = None # raw j2 template text
66
+ _dir: Path | None = None
67
+
68
+ @classmethod
69
+ def load(cls, pkg_dir: Path) -> "Package":
70
+ meta = yaml.safe_load((pkg_dir / "package.yaml").read_text()) or {}
71
+ nuget = [NugetRef.from_obj(o) for o in meta.get("nuget", [])]
72
+
73
+ prog = pkg_dir / "Program.fragment.cs.j2"
74
+ app_frag = pkg_dir / "Program.app.fragment.cs.j2"
75
+ appsettings = pkg_dir / "appsettings.fragment.json.j2"
76
+
77
+ return cls(
78
+ key=meta.get("key", pkg_dir.name),
79
+ display_name=meta.get("display_name", pkg_dir.name),
80
+ description=meta.get("description", ""),
81
+ nuget=nuget,
82
+ defaults=meta.get("defaults", {}),
83
+ interactive=meta.get("interactive", []),
84
+ program_fragment=prog.read_text() if prog.exists() else None,
85
+ app_fragment=app_frag.read_text() if app_frag.exists() else None,
86
+ appsettings_fragment=appsettings.read_text() if appsettings.exists() else None,
87
+ _dir=pkg_dir,
88
+ )
89
+
90
+
91
+ class PackageRegistry:
92
+ """Discovers and serves the packages shipped with the CLI."""
93
+
94
+ def __init__(self, packages_dir: Path = PACKAGES_DIR):
95
+ self.packages_dir = packages_dir
96
+ self._cache: dict[str, Package] = {}
97
+
98
+ def available(self) -> list[str]:
99
+ if not self.packages_dir.exists():
100
+ return []
101
+ return sorted(
102
+ p.name
103
+ for p in self.packages_dir.iterdir()
104
+ if p.is_dir() and (p / "package.yaml").exists()
105
+ )
106
+
107
+ def get(self, key: str) -> Package:
108
+ if key not in self._cache:
109
+ pkg_dir = self.packages_dir / key
110
+ if not (pkg_dir / "package.yaml").exists():
111
+ raise KeyError(
112
+ f"Unknown package '{key}'. "
113
+ f"Available: {', '.join(self.available()) or '(none)'}"
114
+ )
115
+ self._cache[key] = Package.load(pkg_dir)
116
+ return self._cache[key]
117
+
118
+ def resolve_enabled(self, pkg_config: dict[str, Any]) -> list[tuple[Package, dict]]:
119
+ """Given the merged ``packages`` block from config, return the
120
+ list of (Package, settings) pairs that are turned on.
121
+
122
+ ``settings`` is the package's own ``defaults`` deep-merged with whatever
123
+ the user supplied, so templates always see a complete settings dict.
124
+ """
125
+ resolved: list[tuple[Package, dict]] = []
126
+ for key, user_cfg in (pkg_config or {}).items():
127
+ user_cfg = user_cfg or {}
128
+ # a package is on unless explicitly disabled
129
+ if isinstance(user_cfg, dict) and user_cfg.get("enabled") is False:
130
+ continue
131
+ if user_cfg is False:
132
+ continue
133
+ try:
134
+ pkg = self.get(key)
135
+ except KeyError:
136
+ # unknown packages are surfaced by the validator; skip here
137
+ continue
138
+ settings = _deep_merge(pkg.defaults, user_cfg if isinstance(user_cfg, dict) else {})
139
+ resolved.append((pkg, settings))
140
+ return resolved
141
+
142
+
143
+ def _deep_merge(base: dict, override: dict) -> dict:
144
+ out = dict(base)
145
+ for k, v in (override or {}).items():
146
+ if isinstance(v, dict) and isinstance(out.get(k), dict):
147
+ out[k] = _deep_merge(out[k], v)
148
+ else:
149
+ out[k] = v
150
+ return out
151
+
152
+
153
+ def merge_appsettings(base: dict, fragment_json: str) -> dict:
154
+ """Merge a rendered appsettings fragment (JSON text) into the base dict."""
155
+ try:
156
+ frag = json.loads(fragment_json)
157
+ except json.JSONDecodeError as e: # pragma: no cover - defensive
158
+ raise ValueError(f"appsettings fragment is not valid JSON: {e}\n{fragment_json}")
159
+ return _deep_merge(base, frag)
160
+
161
+
162
+ def flatten_appsettings(data: dict, prefix: str = "", sep: str = "__") -> dict[str, str]:
163
+ """Flatten a nested appsettings dict into ASP.NET Core env-var keys.
164
+
165
+ Nested objects are joined with ``__`` (the .NET configuration provider's
166
+ hierarchy separator) and arrays are expanded with their numeric index, e.g.
167
+ ``ClientProvider:RetryPolicy:RetryableStatusCodes[0]`` becomes
168
+ ``ClientProvider__RetryPolicy__RetryableStatusCodes__0``. Values are
169
+ stringified so they can sit in ``launchSettings.json``'s
170
+ ``environmentVariables`` map.
171
+ """
172
+ out: dict[str, str] = {}
173
+ for key, value in (data or {}).items():
174
+ full = f"{prefix}{sep}{key}" if prefix else str(key)
175
+ if isinstance(value, dict):
176
+ out.update(flatten_appsettings(value, full, sep))
177
+ elif isinstance(value, list):
178
+ for idx, item in enumerate(value):
179
+ child = f"{full}{sep}{idx}"
180
+ if isinstance(item, (dict, list)):
181
+ out.update(flatten_appsettings({str(idx): item}, full, sep))
182
+ else:
183
+ out[child] = _stringify(item)
184
+ else:
185
+ out[full] = _stringify(value)
186
+ return out
187
+
188
+
189
+ def _stringify(value) -> str:
190
+ if isinstance(value, bool):
191
+ return "true" if value else "false"
192
+ return str(value)
@@ -0,0 +1,10 @@
1
+ // Generated by ND-SDK CLI. Business object.
2
+ namespace {{ ns.bo }}
3
+ {
4
+ public class {{ bo.name }}
5
+ {
6
+ {% for f in bo.fields | default([]) %}
7
+ public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }
8
+ {% endfor %}
9
+ }
10
+ }
@@ -0,0 +1,131 @@
1
+ // -----------------------------------------------------------------------------
2
+ // AUTO-GENERATED BY ND-SDK CLI — DO NOT EDIT.
3
+ // The controller layer is owned by the framework. Put your logic in the
4
+ // service layer ({{ service_name }}Service). Changes here are lost on regenerate.
5
+ // -----------------------------------------------------------------------------
6
+ using System.Text.Json;
7
+ using Microsoft.AspNetCore.Mvc;
8
+ {% if obs_enabled %}
9
+ using {{ obs.logger_namespace }};
10
+ {% endif %}
11
+ using Swashbuckle.AspNetCore.Annotations;
12
+ using {{ ns.service }};
13
+ {% if dtos %}
14
+ using {{ ns.dtos }};
15
+ {% endif %}
16
+ {% if models %}
17
+ using {{ ns.models }};
18
+ {% endif %}
19
+
20
+ namespace {{ ns.controller }}
21
+ {
22
+ [ApiController]
23
+ public class {{ service_name }}Controller : ControllerBase
24
+ {
25
+ private readonly IConfiguration _configuration;
26
+ {% if not use_service_locator %}
27
+ private readonly I{{ service_name }}Service _service;
28
+ {% endif %}
29
+ {% if obs_enabled %}
30
+ private readonly {{ obs.logger_type }} _logger;
31
+ private readonly string ServiceName;
32
+ {% endif %}
33
+
34
+ public {{ service_name }}Controller(
35
+ IConfiguration configuration{% if not use_service_locator %},
36
+ I{{ service_name }}Service service{% endif %}{% if obs_enabled %},
37
+ {{ obs.logger_type }} logger,
38
+ [FromKeyedServices("ServiceName")] string servicename{% endif %})
39
+ {
40
+ _configuration = configuration;
41
+ {% if not use_service_locator %}
42
+ _service = service;
43
+ {% endif %}
44
+ {% if obs_enabled %}
45
+ _logger = logger;
46
+ ServiceName = servicename;
47
+ {% endif %}
48
+ }
49
+
50
+ [HttpGet]
51
+ [Route("api/v1/health")]
52
+ [Produces("text/plain")]
53
+ [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
54
+ [SwaggerOperation(Summary = "Health check", OperationId = "HealthCheck")]
55
+ public string HealthCheck()
56
+ {
57
+ return "I'm Healthy";
58
+ }
59
+
60
+ {% for ep in business_endpoints %}
61
+ {% set method = ep.service_method | default(ep.name | pascal_case) %}
62
+ {% set request_type = ep.request_dto | default(ep.request_model | default('')) %}
63
+ {% set response_type = ep.response_dto | default(ep.response_model | default('')) %}
64
+ {% set ret = response_type if response_type else "IActionResult" %}
65
+ {% set call = "_service" if not use_service_locator else "ServiceManager.GetService<I" ~ service_name ~ "Service>()" %}
66
+ [Http{{ ep.http_method | default('GET') | capitalize }}]
67
+ [Route("{{ ep.route | default('api/v1/' ~ ep.name) }}")]
68
+ {% if ep.consumes | default('') %}
69
+ [Consumes("{{ ep.consumes }}")]
70
+ {% endif %}
71
+ [Produces("{{ ep.produces | default('application/json') }}")]
72
+ {% if response_type %}
73
+ [ProducesResponseType(typeof({{ response_type }}), StatusCodes.Status200OK)]
74
+ {% endif %}
75
+ [SwaggerOperation(Summary = "{{ ep.summary | default(ep.name) }}", OperationId = "{{ ep.name | pascal_case }}")]
76
+ {% if ep.consumes | default('') == 'multipart/form-data' %}
77
+ public {{ ret }} {{ ep.name | pascal_case }}()
78
+ {
79
+ var formData = HttpContext.Request.Form;
80
+ {% if request_type %}
81
+ {{ request_type }} input = JsonSerializer.Deserialize<{{ request_type }}>(formData["{{ ep.request_field | default('inputobj') }}"].ToString()) ?? new {{ request_type }}();
82
+ {% endif %}
83
+ {% elif request_type and ep.http_method | default('GET') | upper in ['POST', 'PUT', 'PATCH'] %}
84
+ public {{ ret }} {{ ep.name | pascal_case }}([FromBody] {{ request_type }} input)
85
+ {
86
+ {% else %}
87
+ public {{ ret }} {{ ep.name | pascal_case }}()
88
+ {
89
+ {% endif %}
90
+ {% if obs_enabled %}
91
+ string correlationId = HttpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString();
92
+ _logger.LogInformation(ServiceName, "Request received", nameof({{ ep.name | pascal_case }}), correlationId);
93
+ {% endif %}
94
+ {% if response_type %}
95
+ {{ response_type }} response = new {{ response_type }}();
96
+ {% endif %}
97
+ try
98
+ {
99
+ {% if response_type %}
100
+ response = {{ call }}.{{ method }}({% if request_type %}input, {% endif %}_configuration);
101
+ {% else %}
102
+ {{ call }}.{{ method }}({% if request_type %}input, {% endif %}_configuration);
103
+ {% endif %}
104
+ {% if obs_enabled %}
105
+ _logger.LogInformation(ServiceName, "Processing completed", nameof({{ ep.name | pascal_case }}), correlationId);
106
+ {% endif %}
107
+ }
108
+ catch (Exception ex)
109
+ {
110
+ {% if obs_enabled %}
111
+ _logger.LogError(ServiceName, "Unhandled exception", nameof({{ ep.name | pascal_case }}), correlationId, exception: ex, stackTrace: ex.StackTrace);
112
+ {% else %}
113
+ throw;
114
+ {% endif %}
115
+ }
116
+ finally
117
+ {
118
+ GC.Collect();
119
+ GC.WaitForPendingFinalizers();
120
+ }
121
+
122
+ {% if response_type %}
123
+ return response;
124
+ {% else %}
125
+ return Ok();
126
+ {% endif %}
127
+ }
128
+
129
+ {% endfor %}
130
+ }
131
+ }
@@ -0,0 +1,26 @@
1
+ // -----------------------------------------------------------------------------
2
+ // AUTO-GENERATED BY ND-SDK CLI — DO NOT EDIT.
3
+ // Abstract base for {{ provider.name }}. Method signatures are derived from the
4
+ // stored-procedure / externalization descriptors in config and are REFRESHED on
5
+ // every run. Implement the overrides in {{ provider.name }}.cs (yours, write-once).
6
+ // -----------------------------------------------------------------------------
7
+ using System.Collections.Generic;
8
+ using Microsoft.Extensions.Configuration;
9
+ {% if provider.has_relational and db_settings.types_namespace | default('') %}
10
+ using {{ db_settings.types_namespace }};
11
+ {% endif %}
12
+ {% if entities %}
13
+ using {{ ns.entities }};
14
+ {% endif %}
15
+
16
+ namespace {{ ns.provider }}
17
+ {
18
+ public abstract class {{ provider.name }}Base
19
+ {
20
+ {% for m in provider.methods %}
21
+ // {{ m.kind }} :: {% if m.relational %}sp={{ m.target }} -> {{ m.return_type_cs }}{% else %}{{ m.operation }} {{ m.target }} on {{ m.table | default('?') }}{% endif %}
22
+
23
+ public abstract {{ m.return_type_cs }} {{ m.name | pascal_case }}({% if m.entity is defined and m.entity %}{{ m.entity }} input, {% endif %}IConfiguration configuration);
24
+ {% endfor %}
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ FROM mcr.microsoft.com/dotnet/aspnet:{{ dotnet | replace('net', '') }} AS base
2
+ WORKDIR /app
3
+ EXPOSE 80
4
+ EXPOSE 443
5
+
6
+ FROM mcr.microsoft.com/dotnet/sdk:{{ dotnet | replace('net', '') }} AS build
7
+ WORKDIR /src
8
+ COPY . .
9
+ RUN dotnet restore "{{ service_name }}.csproj"
10
+ RUN dotnet publish "{{ service_name }}.csproj" -c Release -o /app/publish /p:UseAppHost=false
11
+
12
+ FROM base AS final
13
+ WORKDIR /app
14
+ COPY --from=build /app/publish .
15
+ ENTRYPOINT ["dotnet", "{{ service_name }}.dll"]
@@ -0,0 +1,16 @@
1
+ // Generated by ND-SDK CLI from config. API DTO contract.
2
+ using System.Text.Json.Serialization;
3
+
4
+ namespace {{ ns.dtos }}
5
+ {
6
+ public class {{ dto.name }}
7
+ {
8
+ {% for f in dto.fields | default([]) %}
9
+ {% if f.json_name | default('') %}
10
+ [JsonPropertyName("{{ f.json_name }}")]
11
+ {% endif %}
12
+ public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }{% if f.default is defined %} = {{ f.default }};{% endif %}
13
+
14
+ {% endfor %}
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ // Generated by ND-SDK CLI from config. Provider input/output entity contract.
2
+ using System.Text.Json.Serialization;
3
+
4
+ namespace {{ ns.entities }}
5
+ {
6
+ public class {{ entity.name }}
7
+ {
8
+ {% for f in entity.fields | default([]) %}
9
+ {% if f.json_name | default('') %}
10
+ [JsonPropertyName("{{ f.json_name }}")]
11
+ {% endif %}
12
+ public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }{% if f.default is defined %} = {{ f.default }};{% endif %}
13
+
14
+ {% endfor %}
15
+ }
16
+ }