Compare commits
89 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dee1c898b | ||
|
|
42d3c55ae3 | ||
|
|
343bdc2c5d | ||
|
|
ad00e75911 | ||
|
|
9b9ef9c100 | ||
|
|
59ec8950a3 | ||
|
|
54e3390cbf | ||
|
|
76091c0518 | ||
|
|
a20d47cab8 | ||
|
|
3ec9a94860 | ||
|
|
ed143c01d0 | ||
|
|
1423865a8c | ||
|
|
c33926d6e5 | ||
|
|
8be8fef972 | ||
|
|
4bf6b4c257 | ||
|
|
fd8d2ef73a | ||
|
|
6f756f33d0 | ||
|
|
63653dd974 | ||
|
|
43e3aeebd0 | ||
|
|
2e4e089e4a | ||
|
|
e80fc2337c | ||
|
|
3b93cd3895 | ||
|
|
a0491f9db8 | ||
|
|
a1a76b1a95 | ||
|
|
3db82df964 | ||
|
|
7c9934f56e | ||
|
|
9e99475a3e | ||
|
|
e237cacc2a | ||
|
|
47d9a20998 | ||
|
|
7a0839a3f6 | ||
|
|
0941b9143b | ||
|
|
3622764923 | ||
|
|
4d7aaef901 | ||
|
|
78bc20b645 | ||
|
|
c4de55d459 | ||
|
|
7ec77c551f | ||
|
|
462dd20edb | ||
|
|
1fc7111da2 | ||
|
|
a3667539d6 | ||
|
|
a670d2e966 | ||
|
|
91de623a85 | ||
|
|
ae7ddf6c7f | ||
|
|
9981d5b19a | ||
|
|
422e32489f | ||
|
|
e4cecc8a62 | ||
|
|
9de6b1d05c | ||
|
|
ec6b35d6d5 | ||
|
|
b1dac82d56 | ||
|
|
0d0834efaf | ||
|
|
9e362f6671 | ||
|
|
f744d18f83 | ||
|
|
b13f0b6a57 | ||
|
|
baef565dd4 | ||
|
|
90a66562e7 | ||
|
|
4c09de9ad0 | ||
|
|
7186cb0ab0 | ||
|
|
870c98690c | ||
|
|
4003dc1845 | ||
|
|
61234dec3f | ||
|
|
690da79b87 | ||
|
|
6da6654ec4 | ||
|
|
7659dbdaa1 | ||
|
|
003e74101f | ||
|
|
ece4ee9783 | ||
|
|
eb77285e59 | ||
|
|
010594ac27 | ||
|
|
3aa74318c2 | ||
|
|
3152cd681b | ||
|
|
7870ebb7bc | ||
|
|
a0121f8e11 | ||
|
|
f5ba73f875 | ||
|
|
9a2d88b93b | ||
|
|
c993b77b5d | ||
|
|
39ad5cab12 | ||
|
|
b517b7346e | ||
|
|
853ad0240e | ||
|
|
abece2b33e | ||
|
|
c2f1b203ab | ||
|
|
b4c8fc3a2b | ||
|
|
cedf788787 | ||
|
|
c1a74bc245 | ||
|
|
f372c17249 | ||
|
|
b5cba69e28 | ||
|
|
cced417ede | ||
|
|
ee121db521 | ||
|
|
f949d0ae78 | ||
|
|
0abf181a9c | ||
|
|
534dcce6da | ||
|
|
f292b6b6a5 |
1125 changed files with 99926 additions and 50059 deletions
309
.github/instructions/ai.instructions.md
vendored
309
.github/instructions/ai.instructions.md
vendored
|
|
@ -14,14 +14,25 @@ Primary principle:
|
||||||
|
|
||||||
- Configuration first, code last.
|
- Configuration first, code last.
|
||||||
|
|
||||||
|
Companion documents (this file wins on conflict):
|
||||||
|
|
||||||
|
| Document | Scope |
|
||||||
|
| --- | --- |
|
||||||
|
| `lowcode.instructions.md` | **Authoring reference**: exact schemas, enum values and worked examples for producing wizard screens, editor scripts, custom components, SQL objects and endpoints. Read it before writing any artifact. |
|
||||||
|
| `lowcode-reference.instructions.md` | **Capability inventory**: field-by-field contract of every ListForm/ListFormField JSON column, chart and pivot options, SQL command hooks, Visual Designer toolbox, the scheduled-mail module and (§12) every ready-made screen under the Saas and Administration menus. Sweep its §0 checklist before producing a screen or component; a request whose answer already exists as a platform screen (§12.5) is configured there, not rebuilt. |
|
||||||
|
| `dotnet.instructions.md` | Binding code standard for everything under `api/` |
|
||||||
|
| `list.instructions.md` | Step-by-step procedure for adding a module / list through seeders |
|
||||||
|
| `../../README.md` | Technical map: what exists, where it lives, how it is wired |
|
||||||
|
| `../../CLAUDE.md` | Operating rules and standing defaults for AI agents in this repo |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Platform Scope
|
## 2. Platform Scope
|
||||||
|
|
||||||
Core stack:
|
Core stack:
|
||||||
|
|
||||||
- Backend: C# .NET 9 + ABP 9.x
|
- Backend: C# .NET 10 (C# 14) + ABP 10.x — kod standardı: `dotnet.instructions.md`
|
||||||
- Frontend: React 18 + DevExtreme
|
- Frontend: React 19 + DevExtreme
|
||||||
- Database: SQL Server (dynamic datasource support)
|
- Database: SQL Server (dynamic datasource support)
|
||||||
- Cache: Redis
|
- Cache: Redis
|
||||||
- Background jobs: Hangfire + ABP Background Workers
|
- Background jobs: Hangfire + ABP Background Workers
|
||||||
|
|
@ -41,11 +52,25 @@ System nature:
|
||||||
3. Every proposal must include tenant and permission design.
|
3. Every proposal must include tenant and permission design.
|
||||||
4. Never bypass platform authorization patterns.
|
4. Never bypass platform authorization patterns.
|
||||||
5. Never hardcode secrets, tenant ids, or connection strings.
|
5. Never hardcode secrets, tenant ids, or connection strings.
|
||||||
|
6. **Stay inside the framework; ask before stepping out.** The platform may extend itself through
|
||||||
|
its own mechanisms (seed artifacts; adding a missing event, prop, or small backward-compatible
|
||||||
|
capability to an existing UI component — when Developer Kit tooling builds ~70% of the
|
||||||
|
requested component, the rest is completed with such small additions, never with a new
|
||||||
|
component; backward-compatible additions to script recipes / editor options / tokens).
|
||||||
|
Anything beyond that — a new React component or page, a new toolbox family, core engine changes (ListForm
|
||||||
|
runtime, seeders, code generator, auth, DataSourceManager, dispatcher/compiler), new ABP
|
||||||
|
entity/module/migration, platform table schema changes, new NuGet/npm dependencies, `Routes`
|
||||||
|
in `MenusData.json`, new external integrations — is **out of frame**: finish everything that
|
||||||
|
fits inside the frame first, then ask the user with a single question (why configuration is
|
||||||
|
insufficient, the smallest change that works, files touched, rollback) and proceed **only with
|
||||||
|
explicit approval**. Approval is per request and does not carry over. Full lists:
|
||||||
|
`lowcode.instructions.md` §0.8.
|
||||||
|
|
||||||
Exception policy:
|
Exception policy:
|
||||||
|
|
||||||
- Code-level React or backend development can be considered only if user explicitly requests code implementation and configuration path is insufficient.
|
- Code-level React or backend development can be considered only if user explicitly requests code implementation and configuration path is insufficient.
|
||||||
- If exception is required, AI must explain why configuration-based options are not enough.
|
- If exception is required, AI must explain why configuration-based options are not enough.
|
||||||
|
- Never say "cannot be done": the answer is either an in-frame build or an approval question.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -116,7 +141,7 @@ Capabilities:
|
||||||
- Query
|
- Query
|
||||||
- WebService
|
- WebService
|
||||||
- Cascading lookup parent-child behavior
|
- Cascading lookup parent-child behavior
|
||||||
- Dynamic validation, editor options, editor scripts
|
- Dynamic validation, editor options, editor scripts (see 7.1)
|
||||||
- Conditional formatting and style injection
|
- Conditional formatting and style injection
|
||||||
- Grid state save/load/reset
|
- Grid state save/load/reset
|
||||||
- User filter save/apply/delete flows
|
- User filter save/apply/delete flows
|
||||||
|
|
@ -146,6 +171,34 @@ Supported editor types:
|
||||||
- dxTextArea
|
- dxTextArea
|
||||||
- dxTextBox
|
- dxTextBox
|
||||||
|
|
||||||
|
### 7.1 Editor Script and Editor Options (builder-backed)
|
||||||
|
|
||||||
|
`EditorScript` and `EditorOptions` are produced from a shared vocabulary, in two places that
|
||||||
|
must stay identical:
|
||||||
|
|
||||||
|
| Surface | Location |
|
||||||
|
| --- | --- |
|
||||||
|
| Visual builder (UI) | `ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts` and `.../editor-options/optionSpecs.ts` + `presets.ts` |
|
||||||
|
| Seeder API (C#) | `api/src/Sozsoft.Platform.Domain.Shared/Editors/` — `EditorScript`, `EditorScriptRule`, `EditorScriptCondition`, `EditorOptions`, `EditorOptionsBuilder` |
|
||||||
|
|
||||||
|
Rules AI must follow:
|
||||||
|
|
||||||
|
- Prefer a recipe over hand-written JS. Recipes cover arithmetic (`Multiply`, `Subtract`,
|
||||||
|
`Percent`, `Sum`, `Formula`), dates (`Today`, `Days`, `Hours`), data movement (`Copy`,
|
||||||
|
`SetValue`, `Clear`, `ApiToField`), and view/notify actions (`ReadOnly`, `Notify`, `Ask`,
|
||||||
|
`OpenUrl`). Free-form JS is `Custom` and is the last option.
|
||||||
|
- Generated script carries a `// @builder {...}` header on the first line that stores the rules,
|
||||||
|
plus `// @runOnOpen` when any rule runs on form open. **Never edit the header by hand and never
|
||||||
|
emit a script that does not match what the builder would emit** — the dialog then treats the
|
||||||
|
script as manually edited and closes the rule editor.
|
||||||
|
- Rule triggers: `change` (default), `open` (default-value generation), `both`. Conditions join
|
||||||
|
with `and` / `or`.
|
||||||
|
- When a recipe changes on one side, change its port on the other side in the same task.
|
||||||
|
- In seeders write `EditorOptions = EditorOptions.Number(2).ShowClearButton()` style expressions
|
||||||
|
instead of raw JSON strings. Options marked `platform: true` in `optionSpecs.ts` deserialize
|
||||||
|
into typed DTOs (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`); wrong types
|
||||||
|
are silently ignored.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Dynamic Form System
|
## 8. Dynamic Form System
|
||||||
|
|
@ -172,6 +225,8 @@ Supported component families:
|
||||||
- TreeList / Tree view
|
- TreeList / Tree view
|
||||||
- SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart)
|
- SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart)
|
||||||
- Widget Group (dashboard KPI cards)
|
- Widget Group (dashboard KPI cards)
|
||||||
|
- TodoBoard — a Kanban board: columns come from `statusExpr` values, cards change status by drag
|
||||||
|
and drop, new columns (statuses) can be added from the board
|
||||||
|
|
||||||
Runtime UI capabilities:
|
Runtime UI capabilities:
|
||||||
|
|
||||||
|
|
@ -212,6 +267,54 @@ If user requests custom code explicitly:
|
||||||
- Provide a warning that low-code path is preferred.
|
- Provide a warning that low-code path is preferred.
|
||||||
- Offer configuration-first alternative first.
|
- Offer configuration-first alternative first.
|
||||||
|
|
||||||
|
### 10.1 Custom Component path (still not hand-written React)
|
||||||
|
|
||||||
|
When a screen genuinely cannot be expressed as a ListForm, the next step is **not** a physical
|
||||||
|
React page. It is a Custom Component built in the Visual Designer and attached to a menu through
|
||||||
|
the Wizard's `Custom` path.
|
||||||
|
|
||||||
|
- The component is stored in the database (`Name`, `RoutePath`, `Code`, `Props`, `Dependencies`,
|
||||||
|
`DataSources`) and compiled in the browser with `@babel/standalone`.
|
||||||
|
- The designer document lives in `Props.visualDesigner` **and** in the
|
||||||
|
`/*__SOZSOFT_VISUAL_DESIGNER__…__*/` header of the generated code. `sourceMode` is `visual` or
|
||||||
|
`code`; switching to `code` is one-way — the canvas cannot be restored afterwards, so do not
|
||||||
|
propose it casually.
|
||||||
|
- Toolbox families: `layout` (`PageContainer`, `FlexRow`, `Spacer`), `data` (`Form` — owns the
|
||||||
|
four CRUD endpoints and acts as a container), `platform` (`ListView`, `DataGridView`,
|
||||||
|
`TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` —
|
||||||
|
each embeds a ListForm screen through `listFormCode`), `html`/`ui`, and other custom
|
||||||
|
components as `custom`.
|
||||||
|
- **Reuse before drawing:** if the requirement is a list/tree/chart that a ListForm already
|
||||||
|
serves, drop the matching `platform` node with its `listFormCode` instead of rebuilding it out
|
||||||
|
of primitives.
|
||||||
|
- Data binding goes through CRUD endpoints. Filter operators map to the CrudEndpoint `GetList`
|
||||||
|
contract: `eq` is a bare query parameter, every other operator carries its name as a suffix
|
||||||
|
(`?Name.contains=…`).
|
||||||
|
- Permissions are two-layered and mandatory: node-level `designerPermission` for visibility, and
|
||||||
|
per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`,
|
||||||
|
`deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for
|
||||||
|
the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes.
|
||||||
|
- List screen permissions: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`.
|
||||||
|
- Every save/delete is mirrored into `configs/seeds/{scope}/data/App.DeveloperKit.CustomComponents.json`; the endpoints it uses are
|
||||||
|
referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done.
|
||||||
|
|
||||||
|
### 10.2 SQL View Designer
|
||||||
|
|
||||||
|
For shaping data before a screen binds to it, prefer a designed view over an inline query:
|
||||||
|
`SqlViewDesignerDialog` produces T-SQL from a diagram (sources, joins, `CROSS/OUTER APPLY`,
|
||||||
|
derived subqueries) plus an SSMS-style criteria grid (Column / Alias / Output / Group By / Sort /
|
||||||
|
Filter / Or…). It is **one-way** — model to T-SQL. A view that does not parse back into the
|
||||||
|
canonical shape drops the dialog to raw SQL mode, so hand-editing a designed view costs the
|
||||||
|
designer. Aggregations available in the Group By column: `GroupBy`, `Where`, `SUM`, `COUNT`,
|
||||||
|
`COUNT_DISTINCT`, `AVG`, `MIN`, `MAX`.
|
||||||
|
|
||||||
|
Persistence: only the **table designer's** deploy writes a seed file automatically
|
||||||
|
(`{sql|postgres}/object/{TableName}.sql`, full CREATE script, via `save-table-script`). View
|
||||||
|
Designer deploys and any DDL run from the SQL editor (views, procedures, functions) reach the
|
||||||
|
database only — their seed files under `{sql|postgres}/object/` must be written explicitly.
|
||||||
|
Full mechanics and the SP/function production patterns: `lowcode.instructions.md` §8.1–§8.2.1
|
||||||
|
and §9.5.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Integrations
|
## 11. Integrations
|
||||||
|
|
@ -342,7 +445,8 @@ When configuration cannot satisfy requirement:
|
||||||
1. Try SQL-based design
|
1. Try SQL-based design
|
||||||
2. Try Custom Endpoint
|
2. Try Custom Endpoint
|
||||||
3. Try Dynamic Service
|
3. Try Dynamic Service
|
||||||
4. Propose minimal code change with explicit justification
|
4. Propose minimal code change with explicit justification — this step is out of frame:
|
||||||
|
**ask and wait for approval before touching code** (rule 3.6).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -540,13 +644,39 @@ When in doubt, AI must choose the path that preserves:
|
||||||
|
|
||||||
## 31. Seeder-Driven Low-Code Development Guide (Authoritative)
|
## 31. Seeder-Driven Low-Code Development Guide (Authoritative)
|
||||||
|
|
||||||
AI must learn and teach implementation flow primarily from these seed assets:
|
There are **two seed surfaces**. AI must not mix them.
|
||||||
|
|
||||||
- api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs
|
**A. Repository seeds (shipped with code)** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
|
||||||
- api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json
|
Platform-owned screens, menus, permissions and language texts. Changing them requires a build and
|
||||||
- api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json
|
a deploy. AI must learn and teach implementation flow primarily from these assets:
|
||||||
- api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json
|
|
||||||
- api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json
|
- ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_{Modul}.cs
|
||||||
|
- MenusData.json
|
||||||
|
- PermissionsData.json
|
||||||
|
- HostData.json
|
||||||
|
- LanguagesData.json
|
||||||
|
- WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
|
||||||
|
surface B below
|
||||||
|
- ListFormSeedDataApplier, CrudEndpointSeedApplier (Domain/Data/) — the single home of the apply
|
||||||
|
rules; both the migrate/seed flow and the wizard import's completion call the same services.
|
||||||
|
`data/` rows are inserted-only on migrate (a possibly stale file must not overwrite live data);
|
||||||
|
`crud/` entries are timestamp-guarded — an existing record is updated or deleted only when the
|
||||||
|
file's `GeneratedAt` is newer than the record's `LastModificationTime`/`CreationTime`. **Migrate
|
||||||
|
adds; it does not overwrite.** Follow this rule in any new seeder.
|
||||||
|
|
||||||
|
**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
|
||||||
|
`SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer, the CRUD
|
||||||
|
Endpoint Manager and every ListForm screen with a `SeedFilePath` write here; the
|
||||||
|
seeders above restore them when the database is recreated. Scope folders mirror the CDN layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
configs/seeds/host/{wizard,data,crud,sql/{object,execute},postgres/{object,execute}}
|
||||||
|
configs/seeds/tenants/{tenantId}/… same layout, tenant scope
|
||||||
|
configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: anything produced at runtime must land in surface B. **A feature that disappears when the
|
||||||
|
database is recreated is not delivered.**
|
||||||
|
|
||||||
If user asks "how to add a new module/screen", AI must answer with this exact operational sequence.
|
If user asks "how to add a new module/screen", AI must answer with this exact operational sequence.
|
||||||
|
|
||||||
|
|
@ -659,6 +789,39 @@ When user asks for a screen/module, AI must answer in this order:
|
||||||
|
|
||||||
AI should produce practical, copy-adaptable artifact definitions and avoid abstract-only explanations.
|
AI should produce practical, copy-adaptable artifact definitions and avoid abstract-only explanations.
|
||||||
|
|
||||||
|
### 31.9 Screen brief, discovery questions and image-based requests
|
||||||
|
|
||||||
|
A request arrives either as prose or as a **screenshot / mockup / photo** of the desired screen.
|
||||||
|
Both enter the same pipeline — and for a **new screen/component the pipeline starts with
|
||||||
|
questions, not files**:
|
||||||
|
|
||||||
|
1. **Run the discovery round** (`lowcode.instructions.md` §0.6). Ask in batches (max 4 questions
|
||||||
|
per round, each with 2–4 options and a recommended default), max three rounds. Round 1 always
|
||||||
|
covers: screen kind (List/Wizard vs Custom Component vs single-record form), data source,
|
||||||
|
owning module, and tenancy scope. Round 2 uses the path-specific question set (§0.6.3 for
|
||||||
|
List, §0.6.4 for Custom). Never ask what the repo already answers — search first, confirm
|
||||||
|
findings instead of re-asking. If the user says "you decide / don't ask", skip the round and
|
||||||
|
build on defaults.
|
||||||
|
2. Turn the answers into a single screen brief — purpose, data source, columns and editors,
|
||||||
|
layouts, actions, sub-screens, widgets, approval flow, menu, permissions, tenancy, language
|
||||||
|
texts. For an image, read it first (page regions → main region type → toolbar → columns →
|
||||||
|
filters → form controls → row actions); the image answers most of round 2, so ask only what
|
||||||
|
an image cannot show (§0.7.4).
|
||||||
|
3. **Get the brief confirmed before writing any seed file.** Mark defaulted lines `(varsayılan)`
|
||||||
|
so the user sees what they are approving.
|
||||||
|
4. Pick the tool: grid-shaped work → SQL Query Manager + Wizard; free layout / dashboard /
|
||||||
|
single-record custom form → Custom Component. **When in doubt, recommend the Wizard.**
|
||||||
|
5. Produce the seed files in dependency order, then list them and restate the defaulted choices.
|
||||||
|
|
||||||
|
An image is a reference, not a pixel contract: build the closest thing the platform's own
|
||||||
|
components give, and state at the end what had no equivalent. Three things are never assumed,
|
||||||
|
always asked: which table is written to, who approves (when there is an approval flow), and the
|
||||||
|
formula of any calculation/business rule. This question-first rule applies only to producing a
|
||||||
|
**new** screen/component; fixes, field additions, debugging and explanations proceed directly.
|
||||||
|
|
||||||
|
The concrete question sets, brief template, the image-element → artifact mapping and the
|
||||||
|
visual-control → `EditorType` mapping are in `lowcode.instructions.md` §0.6–§0.7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 32. Mandatory Default Behaviors (Do Not Ask Repeatedly)
|
## 32. Mandatory Default Behaviors (Do Not Ask Repeatedly)
|
||||||
|
|
@ -701,7 +864,131 @@ Rules:
|
||||||
- Permission contract must be created/bound for both parent and child menu items.
|
- Permission contract must be created/bound for both parent and child menu items.
|
||||||
- Route/menu consistency remains mandatory (`Url` and screen contract must match).
|
- Route/menu consistency remains mandatory (`Url` and screen contract must match).
|
||||||
|
|
||||||
### 32.3 AI enforcement requirement
|
### 32.3 Wizard component kind and menu creation
|
||||||
|
|
||||||
|
`ListFormWizardDto.ComponentKind` is the wizard's fork and the first field in the seed file.
|
||||||
|
Default is `List`, so older seed files that lack it keep working.
|
||||||
|
|
||||||
|
| Kind | Produces | Menu URL | Child permissions |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `List` | ListForm + ListFormFields + Route + Menu + Permissions + language keys | `/admin/list/{ListFormCode}` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
|
||||||
|
| `Custom` | Menu + Permissions + language keys; the screen is the selected Custom Component | the component's `RoutePath` | `.Create`, `.Update`, `.Delete` only |
|
||||||
|
|
||||||
|
`Export` / `Import` / `Note` exist only in the ListForm ecosystem. Never propose them on the
|
||||||
|
`Custom` path.
|
||||||
|
|
||||||
|
`CreateMenu = false` (menu-less wizard): no menu record — and no parent menu record — is created;
|
||||||
|
ListForm, permissions and language keys are still produced. Use it when the screen is meant to be
|
||||||
|
embedded elsewhere as a SubGrid/part. Default is `true`.
|
||||||
|
|
||||||
|
### 32.4 Permission group selection
|
||||||
|
|
||||||
|
The wizard's permission group comes from a select list backed by `GetWizardPermissionGroups`.
|
||||||
|
Display names are read from the language key whose name equals the group name.
|
||||||
|
|
||||||
|
- Selecting an existing group fills `PermissionGroupEn/Tr` (seed file: `Language.PermissionGroupEn/Tr`);
|
||||||
|
if left blank the server writes the database values back, so the seed file always carries both
|
||||||
|
languages.
|
||||||
|
- Defining a new group writes those texts to the language key with the same name as the group.
|
||||||
|
- AI must never propose a permission group without EN + TR display names.
|
||||||
|
|
||||||
|
### 32.5 Wizard export/import is a package, not a file
|
||||||
|
|
||||||
|
Exporting a wizard produces a zip whose paths are relative to the scope folder
|
||||||
|
(`host` / `tenants/{tenantId}`) and which carries every dependency:
|
||||||
|
|
||||||
|
The closure is **recursive** (`WizardExportCollector`): a sub-form's wizard file is queued and its
|
||||||
|
own dependencies are collected under the same rules, so the archive stands on its own.
|
||||||
|
|
||||||
|
- `wizard/{file}.json` — the screen itself plus the wizard files of the sub-forms it references
|
||||||
|
through `SubFormsDto[].Code`, recursively
|
||||||
|
- `data/App.DeveloperKit.CustomComponents.json` — `Custom` path; the file is filtered down to the
|
||||||
|
exported component and its dependency chain, never exported whole
|
||||||
|
- `data/{ListFormCode}.json` — the screen's own data mirror when `SeedFilePath` is set
|
||||||
|
- `crud/{entity}.json` — endpoints those components use
|
||||||
|
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand`, the objects
|
||||||
|
a lookup query (`LookupDto.LookupQuery`) references through `FROM`/`JOIN`, and on the `Custom`
|
||||||
|
path the tables the CRUD endpoints sit on
|
||||||
|
|
||||||
|
Script files are never guessed from a fixed provider/folder combination: the scope's script folders
|
||||||
|
are scanned from disk, and every file matching the object name is added. A missing script is **not**
|
||||||
|
a warning — not every table is seed-managed (ABP's own tables come from EF migrations); whether the
|
||||||
|
target has it is decided by the import analysis against the real database, never assumed here.
|
||||||
|
|
||||||
|
An unresolved **recorded reference** (a sub-form, a `SeedFilePath` mirror, a `DataSources[].SeedFile`,
|
||||||
|
a component row named in a seed file but with no counterpart) does not block the download but is
|
||||||
|
never silent: `ExportAnalysis` returns the full archive contents plus what is missing, and the File
|
||||||
|
Manager shows that list before downloading.
|
||||||
|
|
||||||
|
The import screen is a four-step wizard (`Steps`): **Files** (resolve conflicts) → **Checks**
|
||||||
|
(findings; a schema finding opens the existing `SqlTableDesignerDialog`, and the analysis re-runs
|
||||||
|
after a deploy) → **Plan** (the full list of what confirming will do, in the server's completion
|
||||||
|
order) → **Apply**. Going back is always allowed; going forward is gated — unresolved conflicts
|
||||||
|
block Checks, a blocking finding blocks Plan.
|
||||||
|
|
||||||
|
Server-side it is two phases: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry
|
||||||
|
as `New` / `Identical` / `Conflict`) then `ApplyImport` (file-by-file write, previous content backed
|
||||||
|
up so `RollbackImport` can undo the whole batch), closed by `CompleteImport`.
|
||||||
|
|
||||||
|
`AnalyzeImport` also **validates** before anything is written (`WizardImportValidator`). With at
|
||||||
|
least one `Error` finding the session is marked `blocked` on the server and `ApplyImport` refuses,
|
||||||
|
so the check cannot be bypassed by calling the endpoint directly. Errors: malformed JSON; a wizard
|
||||||
|
file with no `Wizard`/`ListForm` section; a screen's or a `crud/{Entity}.json`'s table missing in
|
||||||
|
the target **and** absent from the archive's scripts; a `data/{code}.json` whose ListForm exists
|
||||||
|
neither in the archive nor in the target; an unresolvable custom-component `Dependencies` /
|
||||||
|
`DataSources[].SeedFile`; an empty component name or route on the `Custom` path; a sub-form
|
||||||
|
reference with no counterpart. Warnings: object existence could not be queried, provider mismatch
|
||||||
|
(`sql` ↔ `postgres`), a missing `SeedFilePath` data file, an empty file.
|
||||||
|
|
||||||
|
The analysis also **compares the table schema**. Generated table scripts are wrapped in
|
||||||
|
`IF OBJECT_ID(...) IS NULL BEGIN CREATE TABLE ... END`, so when the target already has the table the
|
||||||
|
`CREATE` block is skipped entirely and a new column never arrives from the script itself — the screen
|
||||||
|
would then fail with an invalid-column error. The incoming `CREATE TABLE` declaration (`SqlCreateTableParser`) is
|
||||||
|
compared against the live `INFORMATION_SCHEMA.COLUMNS` shape. The split follows the risk of loss:
|
||||||
|
**a missing column is added** — adding is lossless, so it is reported as a Warning with the statement
|
||||||
|
it will run (`WizardImportIssueDto.Statement`) and applied at completion; the one exception is a
|
||||||
|
`NOT NULL` column with no default on a non-empty table, which the database itself would reject and
|
||||||
|
where the value to write is a business decision — that blocks. **An existing column is never
|
||||||
|
changed**: a length / precision / scale / nullability difference is an Error and stops the import,
|
||||||
|
because narrowing truncates and a type conversion can fail. An extra column in the target is a
|
||||||
|
Warning and is never dropped; no path emits `DROP TABLE`, `DROP COLUMN` or `ALTER COLUMN`. The
|
||||||
|
comparison lives in one place (`SqlTableSchemaComparer`) and both the analysis and the completion
|
||||||
|
use it. Type **names** are deliberately not compared: the provider catalog reports its own
|
||||||
|
canonical name (`VARCHAR` → `character varying` on PostgreSQL), so a textual comparison would raise
|
||||||
|
a false difference on every `VARCHAR` column.
|
||||||
|
|
||||||
|
A `data/*.json` entry is compared and merged **by row, not by file**: the diff shows only the
|
||||||
|
rows whose keys the archive carries (file-level `GeneratedAt`/`Order` are excluded), so unrelated
|
||||||
|
records never surface as a difference — no matching row means `New` (insert), a matching identical
|
||||||
|
row means `Identical` (nothing is transferred), a matching different row means `Conflict` (update).
|
||||||
|
The file itself is never overwritten: rows are merged into the target by key (incoming row wins,
|
||||||
|
target-only rows are kept). `CompleteImport` then applies exactly the
|
||||||
|
`data/` files it wrote to the database through `IListFormSeedDataApplier` with
|
||||||
|
`overwriteExisting: true` — a missing row is inserted, an existing one is updated from the file
|
||||||
|
(unlike migrate/seed, where a possibly stale file must not overwrite live data), and a row that was
|
||||||
|
soft deleted is revived by clearing the columns the screen's `DeleteCommand` sets
|
||||||
|
(`IsDeleted`/`DeletionTime`/`DeleterId`), otherwise the default filter would keep hiding it — and
|
||||||
|
reports the inserted/updated/skipped counts. Data files that were already identical to the target
|
||||||
|
are applied too: there was nothing to write, but the rows may be missing or soft deleted in the
|
||||||
|
database. A file the user chose to keep (different, not written) is left alone. This step is not
|
||||||
|
covered by `RollbackImport`, which only restores files and must be run before it.
|
||||||
|
|
||||||
|
`CompleteImport` runs in **dependency order and the order is fixed**: (a) the `.sql` scripts under
|
||||||
|
`{sql|postgres}/{object,execute}` (`object` first) — a missing table is created here, otherwise the
|
||||||
|
later steps would fail on it; (b) columns missing from existing tables are added; (c) `crud/*.json`
|
||||||
|
through `ICrudEndpointSeedApplier`; (d) the `data/*.json` rows; (e) `wizard/*.json` — the screens
|
||||||
|
are installed through the same path the wizard screen uses (`DeployWizardAsync`), reusing the file's
|
||||||
|
`ListForm`/`Fields` contract as-is instead of rebuilding it from the wizard answers. A screen that
|
||||||
|
already exists is left untouched, and each screen is installed in its own unit of work. No migrate
|
||||||
|
run is needed: the screen works as soon as the import finishes.
|
||||||
|
|
||||||
|
Limits: allowed root folders `wizard`, `crud`, `data`, `sql`, `postgres` (and only `object` /
|
||||||
|
`execute` under the SQL provider folders); 5 MB per entry, 50 MB per archive, 500 entries.
|
||||||
|
Permissions: `App.Listforms.Wizard.Export` / `.Import`.
|
||||||
|
|
||||||
|
AI must not propose moving a screen between environments by copying a single json file.
|
||||||
|
|
||||||
|
### 32.6 AI enforcement requirement
|
||||||
|
|
||||||
AI must proactively apply these defaults in:
|
AI must proactively apply these defaults in:
|
||||||
|
|
||||||
|
|
|
||||||
150
.github/instructions/dotnet.instructions.md
vendored
Normal file
150
.github/instructions/dotnet.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# .NET / ABP Kod Standardı
|
||||||
|
|
||||||
|
Bu doküman `api/` altındaki tüm projeler için bağlayıcıdır. Platform davranışına dair
|
||||||
|
kurallar için `ai.instructions.md` esastır; çelişki olursa o dosya kazanır.
|
||||||
|
|
||||||
|
Standart derlemede zorlanır: `api/Directory.Build.props` analizörleri açar,
|
||||||
|
`api/.editorconfig` kural şiddetlerini tanımlar. Bu iki dosya standardın tek
|
||||||
|
kaynağıdır; proje dosyalarında tekrar edilmez.
|
||||||
|
|
||||||
|
## 1. Hedef sürümler
|
||||||
|
|
||||||
|
| Katman | Sürüm | Nerede tanımlı |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| SDK | .NET 10 (`10.0.300`) | `api/global.json` |
|
||||||
|
| TargetFramework | `net10.0` | proje dosyaları |
|
||||||
|
| Dil | C# 14 (`LangVersion=latest`) | `api/common.props` |
|
||||||
|
| ABP | 10.0.0 | `Volo.Abp.*` paket referansları |
|
||||||
|
|
||||||
|
Sürüm yükseltmesi tek noktadan yapılır: `global.json` + `common.props` + paket sürümleri.
|
||||||
|
Proje dosyalarına TFM/LangVersion tekrar yazılmaz.
|
||||||
|
|
||||||
|
## 2. Nullable
|
||||||
|
|
||||||
|
`Directory.Build.props` içinde **çözüm geneli `Nullable` ayarı yapılmaz.** Sebebi
|
||||||
|
EF Core: kolonun zorunlu olup olmadığını, entity assembly'sinin nullable annotation
|
||||||
|
bağlamından türetir. Global bir `enable`/`annotations` anahtarı, bir sonraki
|
||||||
|
`dotnet ef migrations add` çağrısında tüm `string` özelliklerini sessizce NOT NULL
|
||||||
|
kolona çevirir — seeder'ın doldurmadığı her alan da o anda kırılır.
|
||||||
|
|
||||||
|
Kural:
|
||||||
|
|
||||||
|
- Nullable, proje bazında ve bilinçli olarak açılır (`<Nullable>enable</Nullable>`).
|
||||||
|
Şu an açık olanlar: `Sozsoft.Platform.Application`, `Sozsoft.Notifications.*`,
|
||||||
|
test projeleri.
|
||||||
|
- **Entity taşıyan projelerde** (`*.Domain`) nullable açılacaksa, aynı PR'da
|
||||||
|
`dotnet ef migrations add` çıktısı gözden geçirilir; şema değişikliği isteniyorsa
|
||||||
|
kabul edilir, istenmiyorsa kolon zorunluluğu `PlatformDbContext` içinde
|
||||||
|
`.IsRequired(false)` ile açıkça sabitlenir.
|
||||||
|
- Kolon zorunluluğu tercihen NRT'ye değil, `PlatformDbContext` yapılandırmasına
|
||||||
|
yazılır; böylece derleyici anahtarından bağımsız olur.
|
||||||
|
|
||||||
|
## 3. Ölü kod politikası
|
||||||
|
|
||||||
|
Aşağıdaki kurallar ihlal edilmiş kod merge edilmez:
|
||||||
|
|
||||||
|
| Kural | Anlamı |
|
||||||
|
| --- | --- |
|
||||||
|
| IDE0005 | Kullanılmayan `using` |
|
||||||
|
| IDE0051 | Çağrılmayan private üye |
|
||||||
|
| IDE0052 | Yazılıp hiç okunmayan private alan (kullanılmayan enjekte bağımlılık) |
|
||||||
|
| IDE0059 | Gereksiz değer ataması |
|
||||||
|
| IDE0161 | `namespace X;` (file-scoped) |
|
||||||
|
|
||||||
|
Tarama ve otomatik düzeltme (kalıcı yapılandırma gerektirmez):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rapor
|
||||||
|
dotnet build api/Sozsoft.Platform.sln --no-incremental \
|
||||||
|
-p:EnforceCodeStyleInBuild=true -p:GenerateDocumentationFile=true
|
||||||
|
|
||||||
|
# Düzeltme
|
||||||
|
dotnet format api/Sozsoft.Platform.sln --diagnostics IDE0005,IDE0161 --severity warn
|
||||||
|
```
|
||||||
|
|
||||||
|
> Kurallar `.editorconfig`'te `warning` seviyesindedir ve `EnforceCodeStyleInBuild`
|
||||||
|
> açık olduğu için normal `dotnet build` de bunları raporlar.
|
||||||
|
|
||||||
|
IDE0052 düzeltilirken alan ile birlikte **constructor parametresi de** silinir; yoksa
|
||||||
|
DI hâlâ gereksiz servisi çözer.
|
||||||
|
|
||||||
|
`CS0162` (erişilemez kod) `Directory.Build.props` içinde gerekçesiyle susturulmuştur:
|
||||||
|
veritabanı sağlayıcısı derleme zamanı sabiti olduğu için diğer sağlayıcının dalları
|
||||||
|
erişilemez görünür. Bu durum dışında erişilemez kod bırakılmaz.
|
||||||
|
|
||||||
|
Ek kurallar:
|
||||||
|
|
||||||
|
- Bir tip/dosya silinmeden önce **repo genelinde** (api + ui + configs) referans
|
||||||
|
taraması yapılır. DI ile çözülen tipler (AppService, Manager, Provider, Repository,
|
||||||
|
Seeder, Localizer, Notification sağlayıcıları) düz metin aramasında "referanssız"
|
||||||
|
görünür — bunlar yalnızca modül/DI kaydı incelendikten sonra silinir.
|
||||||
|
- Yorum satırına alınmış kod bloğu bırakılmaz; ihtiyaç varsa git geçmişinden alınır.
|
||||||
|
- "İleride lazım olur" gerekçesiyle kullanılmayan DTO/Input/Consts tutulmaz.
|
||||||
|
- `Migrations/` ve `*.g.cs` üretilmiş koddur, bu kuralların dışındadır.
|
||||||
|
|
||||||
|
## 4. Modern C# kullanımı (yeni kod)
|
||||||
|
|
||||||
|
- File-scoped namespace: `namespace Sozsoft.Platform.ListForms;`
|
||||||
|
- Primary constructor: `public class XAppService(IRepository<X, Guid> repo) : PlatformAppService`
|
||||||
|
— parametre doğrudan kullanılır, `private readonly ... = param;` şeklinde
|
||||||
|
tekrar tanımlanmaz.
|
||||||
|
- Koleksiyon ifadeleri (`[]`, `[.. items]`), `is not null`, `switch` ifadesi,
|
||||||
|
desen eşleme, `Index/Range`, hedefi belli `new()`.
|
||||||
|
- `out var`, tuple deconstruction; kullanılmayan çıktı için `_`.
|
||||||
|
- `async` metotlar `Async` ile biter ve `Task`/`Task<T>` döner; `async void` yasak.
|
||||||
|
ABP'de `ConfigureAwait` kullanılmaz.
|
||||||
|
- Biçim: `api/.editorconfig` geçerlidir (4 boşluk girinti, `using` blokları namespace
|
||||||
|
dışında ve System önce, dosya sonunda satır sonu, UTF-8 BOM).
|
||||||
|
|
||||||
|
## 5. ABP kuralları
|
||||||
|
|
||||||
|
- **Katman sınırı**: Domain.Shared → Domain → Application.Contracts → Application →
|
||||||
|
HttpApi → HttpApi.Host. Ters yönde referans verilmez; `EntityFrameworkCore` yalnızca
|
||||||
|
Domain'e bakar. `DbMigrator` yalnızca `Application.Contracts` + `EntityFrameworkCore`
|
||||||
|
referansı taşır; **Application'a referans verilmez** — seeder'ın uygulama servislerine
|
||||||
|
ihtiyaç duyması, kodun yanlış katmanda olduğunun işaretidir.
|
||||||
|
- **Hem seeder'ın hem AppService'in ihtiyaç duyduğu dönüşüm**: ortak kod
|
||||||
|
`Application.Contracts`'a konur (DTO'nun kendi metodu olarak; yeni yardımcı sınıf
|
||||||
|
üretilmez). Dönüşümün entity'ye yazması gerekiyorsa entity'nin **yazılabilir kolon
|
||||||
|
sözleşmesi** Domain.Shared'de bir arayüz olarak tanımlanır (`IListFormArtifact`,
|
||||||
|
`IListFormFieldArtifact`), entity bu arayüzü uygular ve dönüşüm arayüz üzerinden yazar.
|
||||||
|
Böylece Contracts, Domain'i tanımadan entity doldurabilir ve aynı dönüşüm iki yerde
|
||||||
|
kopyalanmaz.
|
||||||
|
- **AppService**: `PlatformAppService`'ten türer, arayüzü Application.Contracts'ta
|
||||||
|
tanımlanır. Controller yazılmaz — ABP otomatik API üretir. Controller sadece ABP
|
||||||
|
konvansiyonunun karşılamadığı durumda (dosya indirme, harici webhook) eklenir.
|
||||||
|
- **Yetki**: her AppService/metot `[Authorize(PlatformConsts.AppCodes....)]` ile
|
||||||
|
korunur; izin adları `PermissionDefinitionProvider` içinde tanımlanır. Yetki
|
||||||
|
kontrolü koda gömülmez, izin adı sabitten okunur.
|
||||||
|
- **Tenant izolasyonu**: sorgular `ICurrentTenant` bağlamında çalışır; `IMultiTenant`
|
||||||
|
entity'lerde manuel `TenantId` filtresi yazılmaz, veri filtreleri devre dışı
|
||||||
|
bırakılacaksa (`ICurrentTenant.Change`, `IDataFilter`) gerekçe yorumla yazılır.
|
||||||
|
- **Repository**: özel sorgu gerekiyorsa Domain'de arayüz + EntityFrameworkCore'da
|
||||||
|
implementasyon. AppService içinde `DbContext` doğrudan kullanılmaz.
|
||||||
|
- **Dinamik SQL**: parametreli çalıştırılır; string birleştirme ile sorgu kurulmaz.
|
||||||
|
- **Mapping**: Mapperly (`[Mapper]` partial class) kullanılır; AutoMapper eklenmez.
|
||||||
|
RMG012 uyarısı "hedefte karşılığı olmayan alan" demektir; yeni mapper eklerken bu
|
||||||
|
uyarı bırakılmaz.
|
||||||
|
- **Localization**: kullanıcıya görünen metin koda gömülmez, `PlatformResource`
|
||||||
|
anahtarı üzerinden verilir.
|
||||||
|
- **Çok değerli alan taşıyan girdi DTO'su**: ekranda `dxTagBox` (ya da çoklu seçimli
|
||||||
|
`dxGridBox`) ile doldurulan bir alan tipli bir DTO'ya bağlanıyorsa property `string`
|
||||||
|
kalır ve `[JsonConverter(typeof(MultiValueStringJsonConverter))]` ile işaretlenir —
|
||||||
|
UI diziyi olduğu gibi yollar, converter `PlatformConsts.MultiValueDelimiter` ile tek
|
||||||
|
string üretir. Varsayılan `list-form-data/*` ucundan geçen ekranlarda bu iş
|
||||||
|
`QueryHelper` tarafından yapılır, DTO yoktur; okuma DTO'larına öznitelik eklenmez.
|
||||||
|
- **Sabitler**: bağlantı dizesi, tenant id, anahtar/secret koda yazılmaz; konfigürasyon
|
||||||
|
veya `SettingDefinition` üzerinden okunur.
|
||||||
|
|
||||||
|
## 6. Değişiklik öncesi kontrol listesi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build api/Sozsoft.Platform.sln --no-incremental \
|
||||||
|
-p:EnforceCodeStyleInBuild=true -p:GenerateDocumentationFile=true # 0 hata, yeni IDE00xx yok
|
||||||
|
dotnet format api/Sozsoft.Platform.sln --verify-no-changes --diagnostics IDE0005,IDE0161 --severity warn
|
||||||
|
dotnet test api/test/Sozsoft.Platform.EntityFrameworkCore.Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
> Not: EntityFrameworkCore testleri şu an SQLite şema oluşturmada
|
||||||
|
> (`nvarchar(max)` → `SQLite Error 1`) kırık; bu kırıklık bu standarttan önce de vardı
|
||||||
|
> ve ayrı bir iş olarak ele alınmalıdır.
|
||||||
8
.github/instructions/list.instructions.md
vendored
8
.github/instructions/list.instructions.md
vendored
|
|
@ -7,6 +7,10 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
> **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.**
|
||||||
|
> Çalışma zamanında Wizard / Custom Components / CRUD Endpoint Manager ile üretim yapacaksan
|
||||||
|
> `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer.
|
||||||
|
|
||||||
## General Principles
|
## General Principles
|
||||||
- **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code.
|
- **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code.
|
||||||
- **Modularization:** Each module (e.g., {Modul}) must have its own seeder, permission group, and localization entries.
|
- **Modularization:** Each module (e.g., {Modul}) must have its own seeder, permission group, and localization entries.
|
||||||
|
|
@ -24,7 +28,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
## Special File Handling Rules
|
## Special File Handling Rules
|
||||||
|
|
||||||
- **MenusData.json:**
|
- **MenusData.json:**
|
||||||
- Never modify the `Routes` section directly. Only `MenuGroups` and `Menus` sections can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to `MenuGroups` and `Menus` as required by platform design.
|
- Never modify the `Routes` section directly. Only `Modules` and `Menus` sections can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to `Modules` and `Menus` as required by platform design.
|
||||||
|
|
||||||
- **LanguagesData.json:**
|
- **LanguagesData.json:**
|
||||||
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
|
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
|
||||||
|
|
@ -35,7 +39,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
|
|
||||||
|
|
||||||
### 1. MenusData.json
|
### 1. MenusData.json
|
||||||
- **{Modul} menu** added as a top-level menu with `ShortName: {Modul}`.
|
- **{Modul} menu** added as a top-level menu with `ModuleId: {Modul}`.
|
||||||
- **{Liste}** added as a child menu under {Modul}.
|
- **{Liste}** added as a child menu under {Modul}.
|
||||||
|
|
||||||
### 2. PermissionsData.json
|
### 2. PermissionsData.json
|
||||||
|
|
|
||||||
1394
.github/instructions/lowcode-reference.instructions.md
vendored
Normal file
1394
.github/instructions/lowcode-reference.instructions.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
3746
.github/instructions/lowcode.instructions.md
vendored
Normal file
3746
.github/instructions/lowcode.instructions.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,5 +1,14 @@
|
||||||
|
# Konteyner veri klasorleri (mssql/postgres/redis vb.); seed veri dosyalari haric tutulur.
|
||||||
configs/**/data/**
|
configs/**/data/**
|
||||||
|
!configs/seeds/**/data/**
|
||||||
*.bak
|
*.bak
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
|
# Deploy sırasında git tag'lerinden üretilir (configs/change-logs/generate-change-logs.sh)
|
||||||
|
# ve API konteynerine mount edilir. Takip edilirse deploy'daki `git pull` yerel
|
||||||
|
# değişiklikle çakışır ve dosya eskir.
|
||||||
|
configs/change-logs/change-logs.json
|
||||||
|
# Wizard import staging klasoru (gecici; oturum bitince silinir)
|
||||||
|
configs/seeds/.imports/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NoWarn>$(NoWarn);CS0162;CS8321;CS8618;CS8632</NoWarn>
|
<NoWarn>$(NoWarn);CS0162;CS8321;CS8618;CS8632;CS1591;CS1573;CS1572;CS1570;CS1574</NoWarn>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<AnalysisLevel>latest</AnalysisLevel>
|
||||||
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ WORKDIR /app
|
||||||
COPY global.json ./
|
COPY global.json ./
|
||||||
COPY common.props ./
|
COPY common.props ./
|
||||||
COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/"
|
COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/common.props" "modules/Sozsoft.Mcp/"
|
||||||
COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/"
|
COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/"
|
||||||
COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/"
|
COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/"
|
||||||
COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/"
|
COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/"
|
||||||
|
|
@ -13,6 +14,12 @@ COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Sozsoft.Languages.Domai
|
||||||
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/"
|
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/"
|
||||||
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/"
|
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/"
|
||||||
COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/"
|
COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/Sozsoft.Notifications.Domain.Shared.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/Sozsoft.Notifications.Domain.Shared.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ RUN apk add nodejs~=24 npm~=11
|
||||||
COPY global.json ./
|
COPY global.json ./
|
||||||
COPY common.props ./
|
COPY common.props ./
|
||||||
COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/"
|
COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/common.props" "modules/Sozsoft.Mcp/"
|
||||||
COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/"
|
COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/"
|
||||||
COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/"
|
COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/"
|
||||||
COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/"
|
COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/"
|
||||||
|
|
@ -26,6 +27,12 @@ COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Sozsoft.Languages.Domai
|
||||||
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/"
|
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/"
|
||||||
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/"
|
COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/"
|
||||||
COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/"
|
COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/"
|
||||||
|
COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/Sozsoft.Notifications.Application.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/Sozsoft.Notifications.Application.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/"
|
||||||
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/"
|
COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/"
|
||||||
|
|
|
||||||
|
|
@ -92,144 +92,496 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.SqlQueryManager.App
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.SqlQueryManager.EntityFrameworkCore", "modules\Sozsoft.SqlQueryManager\Sozsoft.SqlQueryManager.EntityFrameworkCore\Sozsoft.SqlQueryManager.EntityFrameworkCore.csproj", "{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.SqlQueryManager.EntityFrameworkCore", "modules\Sozsoft.SqlQueryManager\Sozsoft.SqlQueryManager.EntityFrameworkCore\Sozsoft.SqlQueryManager.EntityFrameworkCore.csproj", "{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Domain.Shared", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj", "{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Domain", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj", "{E99A894F-1731-4A48-B345-1D14F70EA23F}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Application.Contracts", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj", "{30F4D3DF-D777-40C2-B434-49173AC06A4B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Application", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Application\Sozsoft.Mcp.Application.csproj", "{A4BC1880-5492-41FF-81BA-963AC4C60A02}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.EntityFrameworkCore", "modules\Sozsoft.Mcp\Sozsoft.Mcp.EntityFrameworkCore\Sozsoft.Mcp.EntityFrameworkCore.csproj", "{BE7FDBA3-58CD-41C0-A748-E636DD11F137}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.HttpApi", "modules\Sozsoft.Mcp\Sozsoft.Mcp.HttpApi\Sozsoft.Mcp.HttpApi.csproj", "{594F56D4-CDDD-451A-B487-D49EEB9F5B98}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.Build.0 = Release|Any CPU
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9020ECD8-81D6-4604-A379-260796918C28}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3E13749E-82D1-434B-B867-F7B094B66065}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.Build.0 = Release|Any CPU
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{620D9F08-EB9F-469F-806A-64E915437154}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.Build.0 = Release|Any CPU
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.Build.0 = Release|Any CPU
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -274,6 +626,12 @@ Global
|
||||||
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
{B45A3E8B-286B-4A74-9602-FC192ACEE8C4} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
||||||
{ED9C639A-A706-4ECB-9638-A15B3681BDEC} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
{ED9C639A-A706-4ECB-9638-A15B3681BDEC} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
||||||
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6} = {2889482E-64CA-4A25-91D8-5B963D83681B}
|
||||||
|
{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
|
{E99A894F-1731-4A48-B345-1D14F70EA23F} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
|
{30F4D3DF-D777-40C2-B434-49173AC06A4B} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
|
{A4BC1880-5492-41FF-81BA-963AC4C60A02} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
|
{BE7FDBA3-58CD-41C0-A748-E636DD11F137} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
|
{594F56D4-CDDD-451A-B487-D49EEB9F5B98} = {03E1C8DA-035E-4882-AF81-F392139FCF38}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F}
|
SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Volo.Abp.Application;
|
using Volo.Abp.Application;
|
||||||
using Volo.Abp.Modularity;
|
|
||||||
using Volo.Abp.Authorization;
|
using Volo.Abp.Authorization;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
namespace Sozsoft.Languages;
|
namespace Sozsoft.Languages;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Sozsoft.Languages;
|
|
||||||
|
|
||||||
public class LanguagesRemoteServiceConsts
|
|
||||||
{
|
|
||||||
public const string RemoteServiceName = "Languages";
|
|
||||||
|
|
||||||
public const string ModuleName = "languages";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Volo.Abp.Authorization.Permissions;
|
using Volo.Abp.Authorization.Permissions;
|
||||||
|
|
||||||
namespace Sozsoft.Languages.Permissions;
|
namespace Sozsoft.Languages.Permissions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using System;
|
||||||
using Sozsoft.Languages.Localization;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
|
using Sozsoft.Languages.Localization;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using System;
|
||||||
using Sozsoft.Languages.Localization;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
|
using Sozsoft.Languages.Localization;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using System;
|
||||||
using Sozsoft.Languages.Localization;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Dynamic.Core;
|
using System.Linq.Dynamic.Core;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
|
using Sozsoft.Languages.Localization;
|
||||||
|
using StackExchange.Redis;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
|
|
@ -136,7 +136,7 @@ public class LanguageTextAppService : CrudAppService<
|
||||||
var db = redis.GetDatabase();
|
var db = redis.GetDatabase();
|
||||||
await db.ExecuteAsync("FLUSHALL");
|
await db.ExecuteAsync("FLUSHALL");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual IQueryable<LanguageKey> ApplySorting(IQueryable<LanguageKey> query, LanguageTextTranslatedRequestDto input)
|
protected virtual IQueryable<LanguageKey> ApplySorting(IQueryable<LanguageKey> query, LanguageTextTranslatedRequestDto input)
|
||||||
{
|
{
|
||||||
//Try to sort query if available
|
//Try to sort query if available
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
using Sozsoft.Languages.Entities;
|
using Sozsoft.Languages.Entities;
|
||||||
using Volo.Abp.Localization;
|
using Volo.Abp.Localization;
|
||||||
using Riok.Mapperly.Abstractions;
|
|
||||||
using Volo.Abp.Mapperly;
|
using Volo.Abp.Mapperly;
|
||||||
|
|
||||||
namespace Sozsoft.Languages;
|
namespace Sozsoft.Languages;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace Sozsoft.Languages;
|
|
||||||
|
|
||||||
public static class LanguagesErrorCodes
|
|
||||||
{
|
|
||||||
//Add your business exception error codes here...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,45 +1,23 @@
|
||||||
namespace Sozsoft.Languages.Languages
|
namespace Sozsoft.Languages.Languages;
|
||||||
{
|
|
||||||
public static class LanguageCodes
|
|
||||||
{
|
|
||||||
public const string Ar = "ar";
|
|
||||||
public const string Cs = "cs";
|
|
||||||
public const string De = "de-DE";
|
|
||||||
public const string En = "en";
|
|
||||||
public const string Es = "es";
|
|
||||||
public const string Fi = "fi";
|
|
||||||
public const string Fr = "fr";
|
|
||||||
public const string Hi = "hi";
|
|
||||||
public const string Hr = "hr";
|
|
||||||
public const string Hu = "hu";
|
|
||||||
public const string It = "it";
|
|
||||||
public const string Pt = "pt-BR";
|
|
||||||
public const string Ru = "ru";
|
|
||||||
public const string Sk = "sk";
|
|
||||||
public const string Sl = "sl";
|
|
||||||
public const string Tr = "tr";
|
|
||||||
public const string Zh = "zh-Hans";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LanguageNames
|
public static class LanguageCodes
|
||||||
{
|
{
|
||||||
public const string Ar = "العربية";
|
public const string Ar = "ar";
|
||||||
public const string Cs = "Čeština";
|
public const string Cs = "cs";
|
||||||
public const string De = "Deutsch";
|
public const string De = "de-DE";
|
||||||
public const string En = "English";
|
public const string En = "en";
|
||||||
public const string Es = "Español";
|
public const string Es = "es";
|
||||||
public const string Fi = "Finnish";
|
public const string Fi = "fi";
|
||||||
public const string Fr = "Français";
|
public const string Fr = "fr";
|
||||||
public const string Hi = "Hindi";
|
public const string Hi = "hi";
|
||||||
public const string Hr = "Croatian";
|
public const string Hr = "hr";
|
||||||
public const string Hu = "Magyar";
|
public const string Hu = "hu";
|
||||||
public const string It = "Italiano";
|
public const string It = "it";
|
||||||
public const string Pt = "Português";
|
public const string Pt = "pt-BR";
|
||||||
public const string Ru = "Русский";
|
public const string Ru = "ru";
|
||||||
public const string Sk = "Slovak";
|
public const string Sk = "sk";
|
||||||
public const string Sl = "Slovenščina";
|
public const string Sl = "sl";
|
||||||
public const string Tr = "Türkçe";
|
public const string Tr = "tr";
|
||||||
public const string Zh = "繁體中文";
|
public const string Zh = "zh-Hans";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using System.Threading.Tasks;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Volo.Abp.Caching;
|
using Volo.Abp.Caching;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Entities.Events.Distributed;
|
using Volo.Abp.Domain.Entities.Events.Distributed;
|
||||||
using Volo.Abp.Domain.Repositories;
|
|
||||||
using Volo.Abp.EventBus.Distributed;
|
using Volo.Abp.EventBus.Distributed;
|
||||||
|
|
||||||
namespace Sozsoft.Languages;
|
namespace Sozsoft.Languages;
|
||||||
|
|
@ -15,14 +12,11 @@ public class LanguageTextCacheItemInvalidatorDistributed :
|
||||||
IDistributedEventHandler<EntityDeletedEto<LanguageTextEto>>,
|
IDistributedEventHandler<EntityDeletedEto<LanguageTextEto>>,
|
||||||
ITransientDependency
|
ITransientDependency
|
||||||
{
|
{
|
||||||
private readonly IRepository<LanguageText, Guid> languageTextRepository;
|
|
||||||
private readonly IDistributedCache<LanguageTextCacheItem> cache;
|
private readonly IDistributedCache<LanguageTextCacheItem> cache;
|
||||||
|
|
||||||
public LanguageTextCacheItemInvalidatorDistributed(
|
public LanguageTextCacheItemInvalidatorDistributed(
|
||||||
IRepository<LanguageText, Guid> languageTextRepository,
|
|
||||||
IDistributedCache<LanguageTextCacheItem> cache)
|
IDistributedCache<LanguageTextCacheItem> cache)
|
||||||
{
|
{
|
||||||
this.languageTextRepository = languageTextRepository;
|
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using System;
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
using Volo.Abp.Caching;
|
using Volo.Abp.Caching;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Microsoft.Extensions.Localization;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Volo.Abp.Localization;
|
using Volo.Abp.Localization;
|
||||||
|
|
||||||
namespace Sozsoft.Languages.Localization;
|
namespace Sozsoft.Languages.Localization;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Sozsoft.Languages.Entities;
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
using Volo.Abp.EntityFrameworkCore;
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.Languages.Entities;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Sozsoft.Languages.Entities;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.EntityFrameworkCore.Modeling;
|
using Volo.Abp.EntityFrameworkCore.Modeling;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
namespace Sozsoft.MailQueue.Application;
|
namespace Sozsoft.MailQueue.Application;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Sozsoft.MailQueue.FileGeneration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.MailQueue.FileGeneration;
|
||||||
using Sozsoft.MailQueue.MailGeneration.Models;
|
using Sozsoft.MailQueue.MailGeneration.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Guids;
|
using Volo.Abp.Guids;
|
||||||
using Volo.Abp.TextTemplating;
|
using Volo.Abp.TextTemplating;
|
||||||
|
|
@ -141,53 +141,53 @@ public class AttachmentGenerator : IAttachmentGenerator, ITransientDependency
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// if (model.Filtreler.ContainsKey("QUERY") && model.Filtreler.ContainsKey("QUERYSQL"))
|
// if (model.Filtreler.ContainsKey("QUERY") && model.Filtreler.ContainsKey("QUERYSQL"))
|
||||||
// {
|
// {
|
||||||
// /*
|
// /*
|
||||||
//Parametrelerden asagidaki sekilde gelen queryleri en alttaki formata donusturuyoruz
|
//Parametrelerden asagidaki sekilde gelen queryleri en alttaki formata donusturuyoruz
|
||||||
//params
|
//params
|
||||||
//0 =>
|
//0 =>
|
||||||
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
|
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
|
||||||
// QUERYSQL -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
|
// QUERYSQL -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
|
||||||
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
|
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
|
||||||
// QUERY2SQL -> KSp_Depo_Cikis_CekiListesi_Sub 252993
|
// QUERY2SQL -> KSp_Depo_Cikis_CekiListesi_Sub 252993
|
||||||
|
|
||||||
//SQL olan keyvalue'lari cikar ve sadece querylerde foreach yap
|
|
||||||
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
|
|
||||||
// dict.add(item.Value, params[item.Key + "SQL"].Value)
|
|
||||||
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
|
|
||||||
// dict.add(item.Value, params[item.Key + "SQL"].Value)
|
|
||||||
|
|
||||||
//Bunlari yeni dict'e ekle
|
//SQL olan keyvalue'lari cikar ve sadece querylerde foreach yap
|
||||||
//dict
|
// QUERY -> qryDepo_Cikis_CekiListesi_Detayli
|
||||||
//qryDepo_Cikis_CekiListesi_Detayli -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
|
// dict.add(item.Value, params[item.Key + "SQL"].Value)
|
||||||
//qryDepo_Cikis_CekiListesi_Sub -> KSp_Depo_Cikis_CekiListesi_Sub 252993
|
// QUERY2 -> qryDepo_Cikis_CekiListesi_Sub
|
||||||
// */
|
// dict.add(item.Value, params[item.Key + "SQL"].Value)
|
||||||
// //dict -> key (name), value (sql)
|
|
||||||
// var queries = model.Filtreler
|
|
||||||
// .Where(a => a.Key.StartsWith("QUERY"))
|
|
||||||
// .Where(a => !a.Key.EndsWith("SQL"))
|
|
||||||
// .Where(a => !a.Value.IsNullOrWhiteSpace())
|
|
||||||
// .ToDictionary(x => x.Value, x => model.Filtreler.GetValueOrDefault(x.Key + "SQL"));
|
|
||||||
|
|
||||||
// return MsAccessReportManager.GenerateAccessReport(
|
//Bunlari yeni dict'e ekle
|
||||||
// model.DbAdi,
|
//dict
|
||||||
// model.Index,
|
//qryDepo_Cikis_CekiListesi_Detayli -> Ksp_Depo_Cikis_CekiListesi_Detayli 252993
|
||||||
// model.RaporAdi,
|
//qryDepo_Cikis_CekiListesi_Sub -> KSp_Depo_Cikis_CekiListesi_Sub 252993
|
||||||
// model.ExportDosyaTipi,
|
// */
|
||||||
// queries,
|
// //dict -> key (name), value (sql)
|
||||||
// attachmentFolder);
|
// var queries = model.Filtreler
|
||||||
// }
|
// .Where(a => a.Key.StartsWith("QUERY"))
|
||||||
// else
|
// .Where(a => !a.Key.EndsWith("SQL"))
|
||||||
// {
|
// .Where(a => !a.Value.IsNullOrWhiteSpace())
|
||||||
// return MsAccessReportManager.GenerateAccessReport(
|
// .ToDictionary(x => x.Value, x => model.Filtreler.GetValueOrDefault(x.Key + "SQL"));
|
||||||
// model.DbAdi,
|
|
||||||
// model.Index,
|
// return MsAccessReportManager.GenerateAccessReport(
|
||||||
// model.RaporAdi,
|
// model.DbAdi,
|
||||||
// string.Join(" AND ", model.Filtreler.Select(a => $"{a.Key}={a.Value}")),
|
// model.Index,
|
||||||
// model.ExportDosyaTipi,
|
// model.RaporAdi,
|
||||||
// attachmentFolder);
|
// model.ExportDosyaTipi,
|
||||||
// }
|
// queries,
|
||||||
|
// attachmentFolder);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// return MsAccessReportManager.GenerateAccessReport(
|
||||||
|
// model.DbAdi,
|
||||||
|
// model.Index,
|
||||||
|
// model.RaporAdi,
|
||||||
|
// string.Join(" AND ", model.Filtreler.Select(a => $"{a.Key}={a.Value}")),
|
||||||
|
// model.ExportDosyaTipi,
|
||||||
|
// attachmentFolder);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> ConvertFileToBase64Async(string filePath)
|
private async Task<string> ConvertFileToBase64Async(string filePath)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.MailQueue.MailGeneration.Models;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Sozsoft.MailQueue.MailGeneration.Models;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.TextTemplating;
|
using Volo.Abp.TextTemplating;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Sozsoft.MailQueue.Domain.Entities;
|
using Sozsoft.MailQueue.Domain.Entities;
|
||||||
using Sozsoft.MailQueue.EntityFrameworkCore;
|
using Sozsoft.MailQueue.EntityFrameworkCore;
|
||||||
using Sozsoft.MailQueue.MailGeneration.Models;
|
using Sozsoft.MailQueue.MailGeneration.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.TextTemplating;
|
using Volo.Abp.TextTemplating;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using Sozsoft.Sender.Mail;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Sozsoft.MailQueue.Domain.MailGeneration;
|
using Sozsoft.MailQueue.Domain.MailGeneration;
|
||||||
using Sozsoft.MailQueue.Domain.Shared;
|
using Sozsoft.MailQueue.Domain.Shared;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Sozsoft.Sender.Mail;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Volo.Abp.BackgroundWorkers;
|
using Volo.Abp.BackgroundWorkers;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Guids;
|
using Volo.Abp.Guids;
|
||||||
|
|
@ -182,7 +182,7 @@ public class MailQueueWorker : BackgroundWorkerBase
|
||||||
}
|
}
|
||||||
queue.SendStatus = result.Success;
|
queue.SendStatus = result.Success;
|
||||||
queue.SendTime = Clock.Now;
|
queue.SendTime = Clock.Now;
|
||||||
await Repository.UpdateAsync(queue);
|
await Repository.UpdateAsync(queue, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
// Attachmentlari silebiliriz
|
// Attachmentlari silebiliriz
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
using Amazon;
|
using Amazon;
|
||||||
using Amazon.SQS;
|
using Amazon.SQS;
|
||||||
using Amazon.SQS.Model;
|
using Amazon.SQS.Model;
|
||||||
using Sozsoft.Sender.Mail.AmazonSes;
|
|
||||||
using Sozsoft.MailQueue.Domain.Entities;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.MailQueue.Domain.Entities;
|
||||||
|
using Sozsoft.Sender.Mail.AmazonSes;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.MailQueue.Domain;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Sozsoft.MailQueue.Domain;
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
using Volo.Abp.EntityFrameworkCore;
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Sozsoft.MailQueue.Domain;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.MailQueue.Domain;
|
||||||
using Sozsoft.MailQueue.Domain.Entities;
|
using Sozsoft.MailQueue.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.EntityFrameworkCore.Modeling;
|
using Volo.Abp.EntityFrameworkCore.Modeling;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using Sozsoft.MailQueue.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.MailQueue.Domain.Entities;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using ClosedXML.Excel;
|
using ClosedXML.Excel;
|
||||||
using DocumentFormat.OpenXml.Spreadsheet;
|
|
||||||
using Sozsoft.MailQueue.MailGeneration.Models;
|
using Sozsoft.MailQueue.MailGeneration.Models;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -26,7 +25,7 @@ public class XLSFileGenerator : IFileGenerator, ITransientDependency
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
workbook.SaveAs(stream);
|
workbook.SaveAs(stream);
|
||||||
stream.Close();
|
stream.Close();
|
||||||
|
|
||||||
return await Task.FromResult(stream.ToArray());
|
return await Task.FromResult(stream.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,8 +79,11 @@ public class XLSFileGenerator : IFileGenerator, ITransientDependency
|
||||||
var col = table.TabloSutunlari[j];
|
var col = table.TabloSutunlari[j];
|
||||||
var cell = sheet.Cell(i + 2, j + 1);
|
var cell = sheet.Cell(i + 2, j + 1);
|
||||||
var value = row.GetValueOrDefault(col.SutunAdi);
|
var value = row.GetValueOrDefault(col.SutunAdi);
|
||||||
var dataType = XLDataType.Text;
|
if (!Enum.TryParse(col.VeriTipi, out XLDataType dataType))
|
||||||
Enum.TryParse(col.VeriTipi, out dataType);
|
{
|
||||||
|
dataType = XLDataType.Text;
|
||||||
|
}
|
||||||
|
|
||||||
switch (dataType)
|
switch (dataType)
|
||||||
{
|
{
|
||||||
case XLDataType.Number:
|
case XLDataType.Number:
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Sozsoft.Sender;
|
|
||||||
using Sozsoft.MailQueue.Domain.Shared;
|
|
||||||
using Sozsoft.MailQueue.EntityFrameworkCore;
|
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Sozsoft.MailQueue.Domain.Shared;
|
||||||
|
using Sozsoft.MailQueue.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Sender;
|
||||||
using Volo.Abp.Application;
|
using Volo.Abp.Application;
|
||||||
using Volo.Abp.Authorization;
|
using Volo.Abp.Authorization;
|
||||||
using Volo.Abp.Mapperly;
|
|
||||||
using Volo.Abp.Domain;
|
using Volo.Abp.Domain;
|
||||||
using Volo.Abp.EntityFrameworkCore;
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
using Volo.Abp.Localization;
|
using Volo.Abp.Localization;
|
||||||
using Volo.Abp.Localization.ExceptionHandling;
|
using Volo.Abp.Localization.ExceptionHandling;
|
||||||
|
using Volo.Abp.Mapperly;
|
||||||
using Volo.Abp.Modularity;
|
using Volo.Abp.Modularity;
|
||||||
using Volo.Abp.TextTemplating.Razor;
|
using Volo.Abp.TextTemplating.Razor;
|
||||||
using Volo.Abp.VirtualFileSystem;
|
using Volo.Abp.VirtualFileSystem;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
/// <summary>Dinamik istemci kaydi (RFC 7591) istegi.</summary>
|
||||||
|
public class McpClientRegistrationRequestDto
|
||||||
|
{
|
||||||
|
public string? ClientName { get; set; }
|
||||||
|
|
||||||
|
public List<string> RedirectUris { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dinamik istemci kaydinin sonucu. Kayit basarisizsa <see cref="Error"/> RFC 7591 hata kodunu
|
||||||
|
/// tasir ve diger alanlar bos kalir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpClientRegistrationResultDto
|
||||||
|
{
|
||||||
|
public string? ClientId { get; set; }
|
||||||
|
|
||||||
|
public string? ClientName { get; set; }
|
||||||
|
|
||||||
|
public List<string> RedirectUris { get; set; } = [];
|
||||||
|
|
||||||
|
public List<string> GrantTypes { get; set; } = [];
|
||||||
|
|
||||||
|
public List<string> ResponseTypes { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Genel istemci: token ucunda gizli anahtar kullanilmaz, PKCE zorunludur.</summary>
|
||||||
|
public string TokenEndpointAuthMethod { get; set; } = "none";
|
||||||
|
|
||||||
|
public string? Scope { get; set; }
|
||||||
|
|
||||||
|
public long ClientIdIssuedAt { get; set; }
|
||||||
|
|
||||||
|
public string? Error { get; set; }
|
||||||
|
|
||||||
|
public string? ErrorDescription { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir listenin verisini ekranin kendi sorgusuyla okuma istegi. Filtre, siralama ve gruplama
|
||||||
|
/// DevExtreme veri yukleme bicimindedir; ekranin grid'i de ayni bicimi kullanir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpListQueryRequestDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Ornek: <c>["Status","=","Open"]</c> ya da <c>[["A",">",1],"and",["B","contains","x"]]</c>.</summary>
|
||||||
|
public string? Filter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Ornek: <c>[{"selector":"CreationTime","desc":true}]</c>.</summary>
|
||||||
|
public string? Sort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Ornek: <c>[{"selector":"CustomerId","desc":false,"isExpanded":false}]</c>.</summary>
|
||||||
|
public string? Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Ornek: <c>[{"selector":"Id","summaryType":"count"}]</c>.</summary>
|
||||||
|
public string? GroupSummary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Ornek: <c>[{"selector":"Amount","summaryType":"sum"}]</c>.</summary>
|
||||||
|
public string? TotalSummary { get; set; }
|
||||||
|
|
||||||
|
public int Skip { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Istenen satir sayisi; sunucu kaydindaki tavana kirpilir.</summary>
|
||||||
|
public int Take { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Liste sorgusunun sonucu.</summary>
|
||||||
|
public class McpListQueryResultDto
|
||||||
|
{
|
||||||
|
public object? Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Filtreye uyan toplam kayit sayisi (sayfalamadan once).</summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
public int GroupCount { get; set; }
|
||||||
|
|
||||||
|
public object[]? Summary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Uygulanan satir tavani; <see cref="TotalCount"/> bundan buyukse sonuc sayfalanmistir.</summary>
|
||||||
|
public int Take { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parametreli tek SELECT istegi.</summary>
|
||||||
|
public class McpSelectRequestDto
|
||||||
|
{
|
||||||
|
public string Sql { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Sorgudaki adli parametreler.</summary>
|
||||||
|
public Dictionary<string, object?> Parameters { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Istenen satir sayisi; sunucu kaydindaki tavana kirpilir.</summary>
|
||||||
|
public int MaxRows { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kayitli bir veritabani nesnesi (view, stored procedure, function) ya da Custom Endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public class McpSavedQueryDto
|
||||||
|
{
|
||||||
|
/// <summary><c>McpConsts.SavedQueryKinds</c> degerlerinden biri.</summary>
|
||||||
|
public string Kind { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? SchemaName { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Custom Endpoint'te beklenen parametre adlari; veritabani nesnelerinde bostur.</summary>
|
||||||
|
public List<string> ParameterNames { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kayitli bir nesnenin ya da Custom Endpoint'in cagrilmasi.</summary>
|
||||||
|
public class McpSavedQueryRequestDto
|
||||||
|
{
|
||||||
|
/// <summary><see cref="McpSavedQueryDto.Kind"/> ile ayni degerler.</summary>
|
||||||
|
public string Kind { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Bos birakilirsa saglayicinin varsayilan semasi kullanilir.</summary>
|
||||||
|
public string? SchemaName { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, object?> Parameters { get; set; } = [];
|
||||||
|
|
||||||
|
public int MaxRows { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sorgu sonucu.</summary>
|
||||||
|
public class McpQueryResultDto
|
||||||
|
{
|
||||||
|
public List<string> Columns { get; set; } = [];
|
||||||
|
public List<Dictionary<string, object?>> Rows { get; set; } = [];
|
||||||
|
public int RowCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Satir sayisi tavana dayandiginda true; sonuc kesilmis demektir.</summary>
|
||||||
|
public bool Truncated { get; set; }
|
||||||
|
|
||||||
|
public long ExecutionTimeMs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bir ListForm ekrani uzerinden kayit yazma/silme istegi.</summary>
|
||||||
|
public class McpRowRequestDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Alan adi → deger. Silme isleminde bos birakilir.</summary>
|
||||||
|
public Dictionary<string, object?> Values { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Guncelleme ve silmede anahtar alan degerleri.</summary>
|
||||||
|
public List<object?> Keys { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Yazma isleminin sonucu.</summary>
|
||||||
|
public class McpWriteResultDto
|
||||||
|
{
|
||||||
|
/// <summary>Insert'te uretilen anahtar; guncelleme ve silmede bostur.</summary>
|
||||||
|
public string? Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Guncelleme ve silmede etkilenen satir sayisi.</summary>
|
||||||
|
public int AffectedRows { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istegin kiracisina dusen MCP sunucu kaydinin etkin hali. /mcp ucu hangi araclari
|
||||||
|
/// listeleyecegine ve sorgularin hangi satir tavaniyla kosacagina bu kayda bakarak karar verir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpServerConfigDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Ekranda gosterilen baglanti adresi; sunucu tarafinda yonlendirme icin kullanilmaz.</summary>
|
||||||
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bos ise istegin kapsamindaki varsayilan veri kaynagi kullanilir.</summary>
|
||||||
|
public string? DataSourceCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bos liste "yazma disindaki tum araclar acik" anlamina gelir.</summary>
|
||||||
|
public List<string> AllowedTools { get; set; } = [];
|
||||||
|
|
||||||
|
public int MaxRows { get; set; }
|
||||||
|
|
||||||
|
public bool AllowWrite { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Istegin cozumlenen kiraci adi; host baglaminda bostur.</summary>
|
||||||
|
public string? TenantName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri sozlugunun tazelik bilgisi. Sozluk <c>Sas_H_DbTableColumn</c> tablosunda, sozluk
|
||||||
|
/// ureten prosedur tarafindan yazilmis JSON olarak durur.
|
||||||
|
/// </summary>
|
||||||
|
public class McpSchemaInfoDto
|
||||||
|
{
|
||||||
|
public bool Available { get; set; }
|
||||||
|
public string? CacheKey { get; set; }
|
||||||
|
public DateTime? RefreshedAt { get; set; }
|
||||||
|
public int FormCount { get; set; }
|
||||||
|
public int FieldCount { get; set; }
|
||||||
|
public string? TenantName { get; set; }
|
||||||
|
public string? DefinitionCultureName { get; set; }
|
||||||
|
public string? UiCultureName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sozluk bulunamadiginda ne yapilacagini anlatan mesaj.</summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bir listenin (ListForm) ozet tanimi.</summary>
|
||||||
|
public class McpListSummaryDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; } = string.Empty;
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Listenin arkasindaki tablo adi; SELECT tabanli listelerde bostur.</summary>
|
||||||
|
public string? TableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi.</summary>
|
||||||
|
public string? SqlSourceName { get; set; }
|
||||||
|
|
||||||
|
/// <summary><c>table</c>, <c>selectSql</c> ya da <c>unknown</c>.</summary>
|
||||||
|
public string? SourceKind { get; set; }
|
||||||
|
|
||||||
|
public string? KeyFieldName { get; set; }
|
||||||
|
public int FieldCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bir listenin sutun tanimi.</summary>
|
||||||
|
public class McpColumnDto
|
||||||
|
{
|
||||||
|
public string Field { get; set; } = string.Empty;
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? SqlDataType { get; set; }
|
||||||
|
public bool IsVisible { get; set; }
|
||||||
|
public bool IsBooleanColumn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Lookup tasiyan sutunda hedef tablo; yoksa bostur.</summary>
|
||||||
|
public string? LookupTableName { get; set; }
|
||||||
|
public string? LookupValueColumn { get; set; }
|
||||||
|
public string? LookupDisplayColumn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Parola/token/anahtar gibi sutunlarda true; sorgu araclari bunlari onermez.</summary>
|
||||||
|
public bool IsSensitive { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Varsayilan SELECT listesine girmesi uygun olan sutunlarda true.</summary>
|
||||||
|
public bool IsDefaultSelectColumn { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bir listenin sutunlariyla birlikte tam tanimi.</summary>
|
||||||
|
public class McpListDetailDto : McpListSummaryDto
|
||||||
|
{
|
||||||
|
public List<McpColumnDto> Columns { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Serbest metin aramasinin tek sonucu.</summary>
|
||||||
|
public class McpSchemaMatchDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; } = string.Empty;
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? TableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Eslesme yalnizca liste duzeyindeyse bos; sutun duzeyindeyse eslesen sutunlar.</summary>
|
||||||
|
public List<McpColumnDto> MatchedColumns { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP veri yazma yuzeyi. Yazma her zaman bir ListForm ekraninin insert/update/delete
|
||||||
|
/// sozlesmesinden gecer; ekran yetkisi ve tenant filtresi aynen uygulanir. Sunucu kaydinda
|
||||||
|
/// <c>AllowWrite</c> kapali oldugunda bu araclar hic listelenmez.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpDataAppService : IApplicationService
|
||||||
|
{
|
||||||
|
Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input);
|
||||||
|
|
||||||
|
Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input);
|
||||||
|
|
||||||
|
Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) OAuth kaydi. Kayit yeni bir
|
||||||
|
/// OpenIddict uygulamasi uretmez: istek, seed edilmis tek MCP istemcisinin izinli geri donus
|
||||||
|
/// adresleriyle dogrulanir ve o istemcinin kimligi dondurulur. Boylece herkese acik kayit ucu
|
||||||
|
/// veritabaninda denetimsiz uygulama biriktiremez.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpOAuthAppService : IApplicationService
|
||||||
|
{
|
||||||
|
Task<McpClientRegistrationResultDto> RegisterClientAsync(McpClientRegistrationRequestDto input);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP salt-okunur sorgu yuzeyi. Her sorgu istegin kapsamindaki veri kaynaginda, sunucu
|
||||||
|
/// kaydindaki satir tavaniyla ve parametreli olarak kosar.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpQueryAppService : IApplicationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bir listenin verisini ekranin kendi sorgusuyla okur: liste okuma yetkisi, alan bazli
|
||||||
|
/// okuma yetkisi, kiraci/sube filtreleri ve kullanici ozellestirmeleri uygulamadaki gibi
|
||||||
|
/// gecerlidir. Normal kullanicinin veri okuma yolu budur.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpListQueryResultDto> QueryListAsync(McpListQueryRequestDto input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tek bir SELECT ifadesi kosturur. Coklu ifade, DDL ve DML reddedilir; satir sayisi
|
||||||
|
/// sunucu kaydindaki tavana kirpilir.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input);
|
||||||
|
|
||||||
|
/// <summary>Veri kaynagindaki view/procedure/function nesneleri ve Custom Endpoint'ler.</summary>
|
||||||
|
Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null);
|
||||||
|
|
||||||
|
/// <summary>Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir.</summary>
|
||||||
|
Task<McpQueryResultDto> RunSavedQueryAsync(McpSavedQueryRequestDto input);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP sema kesfi. Tablo ve sutun bilgisi canli katalogdan degil, platformun ekran
|
||||||
|
/// tanimlarindan uretilen JSON veri sozlugunden gelir; boylece modelin gordugu adlar
|
||||||
|
/// kullanicinin ekranda gordugu adlarla ayni olur.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpSchemaAppService : IApplicationService
|
||||||
|
{
|
||||||
|
/// <summary>Istegin kiracisina dusen etkin MCP sunucu kaydi; kayit yoksa null.</summary>
|
||||||
|
Task<McpServerConfigDto?> GetServerConfigAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bu kullanicinin bu baglantida cagirabilecegi araclar: sunucu kaydinin actiklari ile
|
||||||
|
/// kullanicinin uygulamadaki yetkilerinin kesisimi.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<string>> GetAvailableToolsAsync();
|
||||||
|
|
||||||
|
/// <summary>Veri sozlugunun bulunup bulunmadigi ve ne zaman tazelendigi.</summary>
|
||||||
|
Task<McpSchemaInfoDto> GetSchemaInfoAsync();
|
||||||
|
|
||||||
|
/// <param name="search">Bos birakilirsa tum listeler doner.</param>
|
||||||
|
/// <param name="maxResults">Donecek en fazla liste sayisi.</param>
|
||||||
|
Task<List<McpListSummaryDto>> GetListsAsync(string? search = null, int maxResults = 200);
|
||||||
|
|
||||||
|
/// <summary>Tek bir listenin sutunlariyla birlikte tanimi; bulunamazsa null.</summary>
|
||||||
|
Task<McpListDetailDto?> GetListAsync(string listFormCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<McpSchemaMatchDto>> SearchAsync(string term, int maxResults = 50);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Volo.Abp.Application;
|
||||||
|
using Volo.Abp.Authorization;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
[DependsOn(
|
||||||
|
typeof(McpDomainSharedModule),
|
||||||
|
typeof(AbpDddApplicationContractsModule),
|
||||||
|
typeof(AbpAuthorizationModule)
|
||||||
|
)]
|
||||||
|
public class McpApplicationContractsModule : AbpModule
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Volo.Abp.Authorization" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Sozsoft.Platform.Localization;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP uygulama servislerinin ortak tabani. Kullaniciya gorunen metinler platformun
|
||||||
|
/// <c>PlatformResource</c> kaynagindan okunur; sunucu kaydini cozme ve arac kapisi her
|
||||||
|
/// serviste ayni davranir.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class McpAppService : ApplicationService
|
||||||
|
{
|
||||||
|
protected IMcpServerConfigProvider ServerConfigProvider { get; }
|
||||||
|
|
||||||
|
protected McpAppService(IMcpServerConfigProvider serverConfigProvider)
|
||||||
|
{
|
||||||
|
ServerConfigProvider = serverConfigProvider;
|
||||||
|
LocalizationResource = typeof(PlatformResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Etkin sunucu kaydinin DTO hali; kayit yoksa null.</summary>
|
||||||
|
protected async Task<McpServerConfigDto?> ResolveServerConfigAsync()
|
||||||
|
{
|
||||||
|
var server = await ServerConfigProvider.GetEffectiveServerAsync();
|
||||||
|
if (server is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new McpServerConfigDto
|
||||||
|
{
|
||||||
|
Name = server.Name,
|
||||||
|
Url = server.Url,
|
||||||
|
DataSourceCode = server.DataSourceCode,
|
||||||
|
AllowedTools = [.. ServerConfigProvider.SplitMultiValue(server.AllowedTools)],
|
||||||
|
MaxRows = server.MaxRows,
|
||||||
|
AllowWrite = server.AllowWrite,
|
||||||
|
Description = server.Description,
|
||||||
|
TenantName = CurrentTenant.Name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kaydi zorunlu kilan islemler icin. Kayit yoksa istemcinin nedenini gorebilecegi bir
|
||||||
|
/// hata atar.
|
||||||
|
/// </summary>
|
||||||
|
protected async Task<McpServerConfigDto> RequireServerConfigAsync()
|
||||||
|
=> await ResolveServerConfigAsync()
|
||||||
|
?? throw new UserFriendlyException(L[McpErrorCodes.NoServerRecord]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kapali bir araca yapilan cagriyi reddeder: arac sunucu kaydinda kapaliysa ya da
|
||||||
|
/// kullanicinin uygulamada karsilik gelen yetkisi yoksa. Arac listesini suzmek tek basina
|
||||||
|
/// yeterli degildir — istemci listede olmayan bir araci da cagirabilir.
|
||||||
|
/// </summary>
|
||||||
|
protected async Task<McpServerConfigDto> EnsureToolAllowedAsync(string toolName)
|
||||||
|
{
|
||||||
|
var config = await RequireServerConfigAsync();
|
||||||
|
|
||||||
|
if (!McpToolPolicy.IsAllowed(true, config.AllowWrite, config.AllowedTools, toolName))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.ToolDisabled, toolName, config.Name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (McpToolNames.RequiredPermissions.TryGetValue(toolName, out var permission))
|
||||||
|
{
|
||||||
|
await AuthorizationService.CheckAsync(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Istenen satir sayisini sunucu kaydindaki tavana kirpar.</summary>
|
||||||
|
protected static int ResolveMaxRows(McpServerConfigDto config, int requested)
|
||||||
|
{
|
||||||
|
var ceiling = config.MaxRows > 0 ? config.MaxRows : McpConsts.DefaultMaxRows;
|
||||||
|
|
||||||
|
return requested > 0 ? Math.Min(requested, ceiling) : ceiling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
using Volo.Abp.Application;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
[DependsOn(
|
||||||
|
typeof(McpDomainModule),
|
||||||
|
typeof(McpApplicationContractsModule),
|
||||||
|
typeof(AbpDddApplicationModule)
|
||||||
|
)]
|
||||||
|
public class McpApplicationModule : AbpModule
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kayitli nesneleri listelemek icin kullanilan katalog sorgulari. Parametre almazlar;
|
||||||
|
/// sabit metin oldugu icin string birlestirme riski yoktur.
|
||||||
|
/// </summary>
|
||||||
|
internal static class McpCatalogQueries
|
||||||
|
{
|
||||||
|
public const string SqlServer = """
|
||||||
|
SELECT
|
||||||
|
SCHEMA_NAME(o.schema_id) AS SchemaName,
|
||||||
|
o.name AS ObjectName,
|
||||||
|
CASE o.type
|
||||||
|
WHEN 'V' THEN 'VIEW'
|
||||||
|
WHEN 'P' THEN 'PROCEDURE'
|
||||||
|
WHEN 'IF' THEN 'TABLE_FUNCTION'
|
||||||
|
WHEN 'TF' THEN 'TABLE_FUNCTION'
|
||||||
|
WHEN 'FN' THEN 'SCALAR_FUNCTION'
|
||||||
|
END AS ObjectKind
|
||||||
|
FROM sys.objects o
|
||||||
|
WHERE o.is_ms_shipped = 0
|
||||||
|
AND o.type IN ('V', 'P', 'IF', 'TF', 'FN')
|
||||||
|
ORDER BY SchemaName, ObjectName
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string Postgres = """
|
||||||
|
SELECT n.nspname AS "SchemaName",
|
||||||
|
c.relname AS "ObjectName",
|
||||||
|
'VIEW' AS "ObjectKind"
|
||||||
|
FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind IN ('v', 'm')
|
||||||
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
UNION ALL
|
||||||
|
SELECT n.nspname AS "SchemaName",
|
||||||
|
p.proname AS "ObjectName",
|
||||||
|
CASE
|
||||||
|
WHEN p.prokind = 'p' THEN 'PROCEDURE'
|
||||||
|
WHEN p.proretset THEN 'TABLE_FUNCTION'
|
||||||
|
ELSE 'SCALAR_FUNCTION'
|
||||||
|
END AS "ObjectKind"
|
||||||
|
FROM pg_proc p
|
||||||
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||||
|
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
ORDER BY 1, 2
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Sozsoft.Platform.ListForms.Select;
|
||||||
|
using Volo.Abp;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri yazma yuzeyi. Yazma her zaman bir ListForm ekraninin insert/update/delete
|
||||||
|
/// sozlesmesinden gecer; ekran yetkisi, tenant filtresi ve seed senkronizasyonu oradaki
|
||||||
|
/// yoldan aynen uygulanir.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Write)]
|
||||||
|
public class McpDataAppService(
|
||||||
|
IMcpServerConfigProvider serverConfigProvider,
|
||||||
|
IListFormDataAppService listFormDataAppService)
|
||||||
|
: McpAppService(serverConfigProvider), IMcpDataAppService
|
||||||
|
{
|
||||||
|
public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input)
|
||||||
|
{
|
||||||
|
var request = await BuildRequestAsync(input, McpToolNames.InsertRow, requireKeys: false, requireValues: true);
|
||||||
|
var key = await listFormDataAppService.PostInsertAsync(request);
|
||||||
|
|
||||||
|
return new McpWriteResultDto { Key = ((object?)key)?.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input)
|
||||||
|
{
|
||||||
|
var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, requireKeys: true, requireValues: true);
|
||||||
|
var affected = await listFormDataAppService.PostUpdateAsync(request);
|
||||||
|
|
||||||
|
return new McpWriteResultDto { AffectedRows = affected };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input)
|
||||||
|
{
|
||||||
|
var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, requireKeys: true, requireValues: false);
|
||||||
|
var affected = await listFormDataAppService.PostDeleteAsync(request);
|
||||||
|
|
||||||
|
return new McpWriteResultDto { AffectedRows = affected };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istegi ekranin bekledigi bicime cevirir. <c>Data</c> JSON metni olarak verilir; ayni
|
||||||
|
/// bicim ekranin kopyalama yolunda da kullanilir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<DataRequestDto> BuildRequestAsync(
|
||||||
|
McpRowRequestDto input,
|
||||||
|
string toolName,
|
||||||
|
bool requireKeys,
|
||||||
|
bool requireValues)
|
||||||
|
{
|
||||||
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
// AllowWrite kapaliysa yazma araclari McpToolPolicy tarafindan zaten reddedilir;
|
||||||
|
// burada ayri bir kontrol yok.
|
||||||
|
await EnsureToolAllowedAsync(toolName);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.ListFormCode))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.ListFormCodeRequired]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireValues && (input.Values is null || input.Values.Count == 0))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireKeys && (input.Keys is null || input.Keys.Count == 0))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.KeysRequired]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = input.ListFormCode,
|
||||||
|
Data = JsonSerializer.Serialize(input.Values ?? new Dictionary<string, object?>()),
|
||||||
|
Keys = [.. (input.Keys ?? []).Select(key => key!)]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Sozsoft.Platform.Localization;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dinamik istemci kaydi (RFC 7591). Uc kimlik dogrulamasizdir; istemci ne alirsa alsin token
|
||||||
|
/// kullanicinin kendi girisiyle ve kendi yetkileriyle uretilir. Kural ve kayit
|
||||||
|
/// <see cref="IMcpOAuthClientManager"/> icindedir.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class McpOAuthAppService : ApplicationService, IMcpOAuthAppService
|
||||||
|
{
|
||||||
|
private const string AuthorizationCodeGrant = "authorization_code";
|
||||||
|
private const string RefreshTokenGrant = "refresh_token";
|
||||||
|
|
||||||
|
private readonly IMcpOAuthClientManager clientManager;
|
||||||
|
|
||||||
|
public McpOAuthAppService(IMcpOAuthClientManager clientManager)
|
||||||
|
{
|
||||||
|
this.clientManager = clientManager;
|
||||||
|
LocalizationResource = typeof(PlatformResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpClientRegistrationResultDto> RegisterClientAsync(McpClientRegistrationRequestDto input)
|
||||||
|
{
|
||||||
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
var redirectUris = (input.RedirectUris ?? []).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
var registration = await clientManager.RegisterAsync(input.ClientName, redirectUris);
|
||||||
|
|
||||||
|
return registration.Error switch
|
||||||
|
{
|
||||||
|
McpClientRegistrationError.InvalidRedirectUri => new McpClientRegistrationResultDto
|
||||||
|
{
|
||||||
|
Error = "invalid_redirect_uri",
|
||||||
|
ErrorDescription = L[McpErrorCodes.RedirectUriNotAllowed, registration.ErrorValue ?? string.Empty]
|
||||||
|
},
|
||||||
|
McpClientRegistrationError.LimitReached => new McpClientRegistrationResultDto
|
||||||
|
{
|
||||||
|
Error = "invalid_client_metadata",
|
||||||
|
ErrorDescription = L[McpErrorCodes.RegistrationLimitReached, registration.ErrorValue ?? string.Empty]
|
||||||
|
},
|
||||||
|
_ => new McpClientRegistrationResultDto
|
||||||
|
{
|
||||||
|
ClientId = registration.ClientId,
|
||||||
|
ClientName = input.ClientName,
|
||||||
|
RedirectUris = redirectUris,
|
||||||
|
// Istemci hangi akislari isterse istesin gecerli olanlar bunlardir; RFC 7591
|
||||||
|
// sunucunun istenen degerleri daraltmasina izin verir.
|
||||||
|
GrantTypes = [AuthorizationCodeGrant, RefreshTokenGrant],
|
||||||
|
ResponseTypes = ["code"],
|
||||||
|
Scope = string.Join(' ', McpConsts.OAuthScopes),
|
||||||
|
ClientIdIssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Sozsoft.Platform.ListForms.Select;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salt-okunur sorgu yuzeyi. MCP kullaniciya yetki vermez, uygulamadaki yetkisini tasir:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>query_list</c> ekranin kendi sorgusundan gecer; liste ve alan yetkileri, kiraci ve
|
||||||
|
/// sube filtreleri aynen uygulanir.</item>
|
||||||
|
/// <item>Ham SQL ve veritabani nesneleri (view/procedure/function) liste yetkilerini asar; bu
|
||||||
|
/// yuzden yalnizca uygulamada SQL Query Manager yetkisi olan kullaniciya aciktir.</item>
|
||||||
|
/// <item>Custom Endpoint'ler endpoint dispatcher'inin yetkisini ve endpoint'in kendi
|
||||||
|
/// kullanici/rol listesini ister.</item>
|
||||||
|
/// </list>
|
||||||
|
/// SQL kurallari <see cref="IMcpSqlGuard"/>, veri kaynagi cozumu ve calistirma
|
||||||
|
/// <see cref="IMcpQueryExecutor"/> icindedir.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
|
||||||
|
public class McpQueryAppService(
|
||||||
|
IMcpServerConfigProvider serverConfigProvider,
|
||||||
|
IMcpSqlGuard sqlGuard,
|
||||||
|
IMcpQueryExecutor queryExecutor,
|
||||||
|
IRepository<CustomEndpoint, Guid> customEndpointRepository,
|
||||||
|
IListFormSelectAppService listFormSelectAppService)
|
||||||
|
: McpAppService(serverConfigProvider), IMcpQueryAppService
|
||||||
|
{
|
||||||
|
private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default;
|
||||||
|
private const string CustomEndpointPermission = PlatformConsts.AppCodes.DeveloperKits.Get;
|
||||||
|
|
||||||
|
public async Task<McpListQueryResultDto> QueryListAsync(McpListQueryRequestDto input)
|
||||||
|
{
|
||||||
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
var config = await EnsureToolAllowedAsync(McpToolNames.QueryList);
|
||||||
|
var take = ResolveMaxRows(config, input.Take);
|
||||||
|
|
||||||
|
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = input.ListFormCode,
|
||||||
|
Filter = NullIfEmpty(input.Filter),
|
||||||
|
Sort = NullIfEmpty(input.Sort),
|
||||||
|
Group = NullIfEmpty(input.Group),
|
||||||
|
GroupSummary = NullIfEmpty(input.GroupSummary),
|
||||||
|
TotalSummary = NullIfEmpty(input.TotalSummary),
|
||||||
|
Skip = Math.Max(input.Skip, 0),
|
||||||
|
Take = take,
|
||||||
|
RequireTotalCount = true,
|
||||||
|
RequireGroupCount = !string.IsNullOrWhiteSpace(input.Group)
|
||||||
|
});
|
||||||
|
|
||||||
|
return new McpListQueryResultDto
|
||||||
|
{
|
||||||
|
Data = result.Data,
|
||||||
|
TotalCount = result.TotalCount,
|
||||||
|
GroupCount = result.GroupCount,
|
||||||
|
Summary = result.Summary,
|
||||||
|
Take = take
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public async Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input)
|
||||||
|
{
|
||||||
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
var config = await EnsureToolAllowedAsync(McpToolNames.RunSelect);
|
||||||
|
|
||||||
|
var sql = (input.Sql ?? string.Empty).Trim().TrimEnd(';').Trim();
|
||||||
|
sqlGuard.EnsureReadOnlyStatement(sql);
|
||||||
|
|
||||||
|
var maxRows = ResolveMaxRows(config, input.MaxRows);
|
||||||
|
var parameters = sqlGuard.NormalizeParameters(input.Parameters);
|
||||||
|
var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode);
|
||||||
|
var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType);
|
||||||
|
|
||||||
|
return ToResult(await queryExecutor.ExecuteAsync(context, limitedSql, parameters, maxRows));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null)
|
||||||
|
{
|
||||||
|
var config = await EnsureToolAllowedAsync(McpToolNames.ListQueries);
|
||||||
|
var results = new List<McpSavedQueryDto>();
|
||||||
|
|
||||||
|
if (await AuthorizationService.IsGrantedAsync(SqlPermission))
|
||||||
|
{
|
||||||
|
results.AddRange(await GetDatabaseObjectsAsync(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await AuthorizationService.IsGrantedAsync(CustomEndpointPermission))
|
||||||
|
{
|
||||||
|
results.AddRange(await GetCallableCustomEndpointsAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
results = [.. results.Where(item =>
|
||||||
|
item.Name.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (item.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. results
|
||||||
|
.OrderBy(item => item.Kind, StringComparer.Ordinal)
|
||||||
|
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<McpSavedQueryDto>> GetDatabaseObjectsAsync(McpServerConfigDto config)
|
||||||
|
{
|
||||||
|
var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode);
|
||||||
|
var catalogSql = context.DataSourceType == DataSourceTypeEnum.Postgresql
|
||||||
|
? McpCatalogQueries.Postgres
|
||||||
|
: McpCatalogQueries.SqlServer;
|
||||||
|
|
||||||
|
// Katalog sorgusunun kendi satir siniri yoktur; nesne sayisi tavani asmaz.
|
||||||
|
var catalog = await queryExecutor.ExecuteAsync(context, catalogSql, new Dictionary<string, object>(), int.MaxValue);
|
||||||
|
|
||||||
|
var results = new List<McpSavedQueryDto>();
|
||||||
|
|
||||||
|
foreach (var row in catalog.Rows)
|
||||||
|
{
|
||||||
|
var kind = MapCatalogKind(ReadString(row, "ObjectKind"));
|
||||||
|
if (kind is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new McpSavedQueryDto
|
||||||
|
{
|
||||||
|
Kind = kind,
|
||||||
|
SchemaName = ReadString(row, "SchemaName"),
|
||||||
|
Name = ReadString(row, "ObjectName") ?? string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryResultDto> RunSavedQueryAsync(McpSavedQueryRequestDto input)
|
||||||
|
{
|
||||||
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
var config = await EnsureToolAllowedAsync(McpToolNames.RunQuery);
|
||||||
|
|
||||||
|
if (IsKind(input.Kind, McpConsts.SavedQueryKinds.CustomEndpoint))
|
||||||
|
{
|
||||||
|
await AuthorizationService.CheckAsync(CustomEndpointPermission);
|
||||||
|
return await RunCustomEndpointAsync(input, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
await AuthorizationService.CheckAsync(SqlPermission);
|
||||||
|
|
||||||
|
var maxRows = ResolveMaxRows(config, input.MaxRows);
|
||||||
|
var parameters = sqlGuard.NormalizeParameters(input.Parameters);
|
||||||
|
var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode);
|
||||||
|
|
||||||
|
var schemaName = string.IsNullOrWhiteSpace(input.SchemaName)
|
||||||
|
? sqlGuard.DefaultSchemaName(context.DataSourceType)
|
||||||
|
: input.SchemaName;
|
||||||
|
|
||||||
|
var qualifiedName = sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType);
|
||||||
|
var sql = BuildSavedQuerySql(input.Kind, qualifiedName, parameters, maxRows, context.DataSourceType);
|
||||||
|
|
||||||
|
return ToResult(await queryExecutor.ExecuteAsync(context, sql, parameters, maxRows));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<McpQueryResultDto> RunCustomEndpointAsync(
|
||||||
|
McpSavedQueryRequestDto input,
|
||||||
|
McpServerConfigDto config)
|
||||||
|
{
|
||||||
|
var endpoints = await GetCallableEndpointEntitiesAsync();
|
||||||
|
var endpoint = endpoints.Find(candidate => string.Equals(candidate.Name, input.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? throw new UserFriendlyException(L[McpErrorCodes.CustomEndpointNotFound, input.Name]);
|
||||||
|
|
||||||
|
var maxRows = ResolveMaxRows(config, input.MaxRows);
|
||||||
|
var parameters = sqlGuard.NormalizeParameters(input.Parameters);
|
||||||
|
|
||||||
|
// Eksik parametre Dapper tarafinda belirsiz bir hataya donusmesin diye burada yakalanir.
|
||||||
|
var missing = endpoint.Parameters
|
||||||
|
.Where(parameter => parameter.IsRequired && !parameters.ContainsKey(parameter.Name))
|
||||||
|
.Select(parameter => parameter.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.MissingParameters, string.Join(", ", missing)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = await queryExecutor.ResolveDataSourceAsync(endpoint.DataSourceCode);
|
||||||
|
|
||||||
|
return ToResult(await queryExecutor.ExecuteAsync(context, endpoint.Sql, parameters, maxRows));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<McpSavedQueryDto>> GetCallableCustomEndpointsAsync()
|
||||||
|
{
|
||||||
|
var endpoints = await GetCallableEndpointEntitiesAsync();
|
||||||
|
|
||||||
|
return [.. endpoints.Select(endpoint => new McpSavedQueryDto
|
||||||
|
{
|
||||||
|
Kind = McpConsts.SavedQueryKinds.CustomEndpoint,
|
||||||
|
Name = endpoint.Name,
|
||||||
|
Description = endpoint.Description,
|
||||||
|
ParameterNames = [.. endpoint.Parameters.Select(parameter => parameter.Name)]
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Yalnizca okuma amacli (GET) ve istegi yapan kullaniciya acik Custom Endpoint kayitlari.
|
||||||
|
/// Yetki sozlesmesi endpoint dispatcher'indaki ile birebir aynidir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<CustomEndpoint>> GetCallableEndpointEntitiesAsync()
|
||||||
|
{
|
||||||
|
var query = await customEndpointRepository.GetQueryableAsync();
|
||||||
|
var candidates = query.Where(endpoint => endpoint.Method == "GET").ToList();
|
||||||
|
|
||||||
|
return [.. candidates.Where(endpoint => endpoint.Permissions.Any(permission =>
|
||||||
|
permission.ResourceType == "Global"
|
||||||
|
|| (permission.ResourceType == "User" && permission.ResourceId == CurrentUser.UserName)
|
||||||
|
|| (permission.ResourceType == "Role" && CurrentUser.Roles.Contains(permission.ResourceId))))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildSavedQuerySql(
|
||||||
|
string? kind,
|
||||||
|
string qualifiedName,
|
||||||
|
Dictionary<string, object> parameters,
|
||||||
|
int maxRows,
|
||||||
|
DataSourceTypeEnum dataSourceType)
|
||||||
|
{
|
||||||
|
var parameterList = string.Join(", ", parameters.Keys.Select(name => $"@{name}"));
|
||||||
|
|
||||||
|
if (IsKind(kind, McpConsts.SavedQueryKinds.View))
|
||||||
|
{
|
||||||
|
return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}", maxRows, dataSourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsKind(kind, McpConsts.SavedQueryKinds.TableFunction))
|
||||||
|
{
|
||||||
|
return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}({parameterList})", maxRows, dataSourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsKind(kind, McpConsts.SavedQueryKinds.ScalarFunction))
|
||||||
|
{
|
||||||
|
return $"SELECT {qualifiedName}({parameterList}) AS Value";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsKind(kind, McpConsts.SavedQueryKinds.Procedure))
|
||||||
|
{
|
||||||
|
// Prosedur cagrisina satir tavani yazilamaz; sonuc kumesi okunurken kesilir.
|
||||||
|
return dataSourceType == DataSourceTypeEnum.Postgresql
|
||||||
|
? $"CALL {qualifiedName}({parameterList})"
|
||||||
|
: $"EXEC {qualifiedName} {string.Join(", ", parameters.Keys.Select(name => $"@{name} = @{name}"))}".TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UserFriendlyException(L[McpErrorCodes.UnsupportedObjectKind, kind ?? string.Empty]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NullIfEmpty(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
|
||||||
|
private static McpQueryResultDto ToResult(McpQueryRows rows) => new()
|
||||||
|
{
|
||||||
|
Columns = rows.Columns,
|
||||||
|
Rows = rows.Rows,
|
||||||
|
RowCount = rows.Rows.Count,
|
||||||
|
Truncated = rows.Truncated,
|
||||||
|
ExecutionTimeMs = rows.ExecutionTimeMs
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool IsKind(string? kind, string expected)
|
||||||
|
=> string.Equals(kind, expected, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string? MapCatalogKind(string? catalogKind) => catalogKind switch
|
||||||
|
{
|
||||||
|
McpConsts.CatalogKinds.View => McpConsts.SavedQueryKinds.View,
|
||||||
|
McpConsts.CatalogKinds.Procedure => McpConsts.SavedQueryKinds.Procedure,
|
||||||
|
McpConsts.CatalogKinds.TableFunction => McpConsts.SavedQueryKinds.TableFunction,
|
||||||
|
McpConsts.CatalogKinds.ScalarFunction => McpConsts.SavedQueryKinds.ScalarFunction,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? ReadString(Dictionary<string, object?> row, string key)
|
||||||
|
=> row.TryGetValue(key, out var value) ? value?.ToString() : null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Volo.Abp.Authorization.Permissions;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sema kesfini JSON veri sozlugu uzerinden karsilar; sozlugu okuma ve arama normalizasyonu
|
||||||
|
/// domain servisindedir (<see cref="IMcpSchemaDictionary"/>). Kullanici yalnizca uygulamada
|
||||||
|
/// okuma yetkisi olan listeleri gorur (<see cref="IMcpListFormAccessFilter"/>).
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
|
||||||
|
public class McpSchemaAppService(
|
||||||
|
IMcpServerConfigProvider serverConfigProvider,
|
||||||
|
IMcpSchemaDictionary dictionary,
|
||||||
|
IMcpListFormAccessFilter accessFilter,
|
||||||
|
IPermissionChecker permissionChecker)
|
||||||
|
: McpAppService(serverConfigProvider), IMcpSchemaAppService
|
||||||
|
{
|
||||||
|
public async Task<McpServerConfigDto?> GetServerConfigAsync()
|
||||||
|
=> await ResolveServerConfigAsync();
|
||||||
|
|
||||||
|
public async Task<List<string>> GetAvailableToolsAsync()
|
||||||
|
{
|
||||||
|
var config = await ResolveServerConfigAsync();
|
||||||
|
var candidates = McpToolPolicy.VisibleTools(
|
||||||
|
config is not null,
|
||||||
|
config?.AllowWrite ?? false,
|
||||||
|
config?.AllowedTools ?? []);
|
||||||
|
|
||||||
|
var required = candidates
|
||||||
|
.Where(McpToolNames.RequiredPermissions.ContainsKey)
|
||||||
|
.Select(tool => McpToolNames.RequiredPermissions[tool])
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var granted = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
if (required.Length > 0)
|
||||||
|
{
|
||||||
|
var grants = await permissionChecker.IsGrantedAsync(required);
|
||||||
|
granted.UnionWith(grants.Result
|
||||||
|
.Where(result => result.Value == PermissionGrantResult.Granted)
|
||||||
|
.Select(result => result.Key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. candidates.Where(tool =>
|
||||||
|
!McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpSchemaInfoDto> GetSchemaInfoAsync()
|
||||||
|
{
|
||||||
|
var snapshot = await dictionary.GetSnapshotAsync();
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
return new McpSchemaInfoDto
|
||||||
|
{
|
||||||
|
Available = false,
|
||||||
|
TenantName = dictionary.ResolveTenantName(),
|
||||||
|
Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new McpSchemaInfoDto
|
||||||
|
{
|
||||||
|
Available = true,
|
||||||
|
CacheKey = snapshot.CacheKey,
|
||||||
|
RefreshedAt = snapshot.RefreshedAt,
|
||||||
|
FormCount = snapshot.FormCount,
|
||||||
|
FieldCount = snapshot.FieldCount,
|
||||||
|
TenantName = snapshot.TenantName,
|
||||||
|
DefinitionCultureName = snapshot.DefinitionCultureName,
|
||||||
|
UiCultureName = snapshot.UiCultureName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McpListSummaryDto>> GetListsAsync(string? search = null, int maxResults = 200)
|
||||||
|
{
|
||||||
|
await EnsureToolAllowedAsync(McpToolNames.ListLists);
|
||||||
|
|
||||||
|
var forms = await GetReadableListFormsAsync();
|
||||||
|
var normalizedSearch = dictionary.Normalize(search);
|
||||||
|
|
||||||
|
var matches = forms.Where(form => normalizedSearch.Length == 0
|
||||||
|
|| dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
return [.. matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpListDetailDto?> GetListAsync(string listFormCode)
|
||||||
|
{
|
||||||
|
await EnsureToolAllowedAsync(McpToolNames.DescribeList);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(listFormCode))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var forms = await GetReadableListFormsAsync();
|
||||||
|
var form = forms.FirstOrDefault(candidate =>
|
||||||
|
string.Equals(candidate.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (form is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var detail = new McpListDetailDto();
|
||||||
|
CopySummary(form, detail);
|
||||||
|
detail.Columns = [.. (form.Fields ?? []).Select(ToColumn)];
|
||||||
|
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McpSchemaMatchDto>> SearchAsync(string term, int maxResults = 50)
|
||||||
|
{
|
||||||
|
await EnsureToolAllowedAsync(McpToolNames.SearchSchema);
|
||||||
|
|
||||||
|
var normalized = dictionary.Normalize(term);
|
||||||
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var limit = NormalizeLimit(maxResults, 50);
|
||||||
|
var forms = await GetReadableListFormsAsync();
|
||||||
|
var results = new List<McpSchemaMatchDto>();
|
||||||
|
|
||||||
|
foreach (var form in forms)
|
||||||
|
{
|
||||||
|
var formMatches = dictionary.Normalize(form.SearchText).Contains(normalized, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
var columnMatches = (form.Fields ?? [])
|
||||||
|
.Where(field => dictionary.Normalize(field.Field).Contains(normalized, StringComparison.Ordinal)
|
||||||
|
|| dictionary.Normalize(field.DisplayName).Contains(normalized, StringComparison.Ordinal))
|
||||||
|
.Select(ToColumn)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!formMatches && columnMatches.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new McpSchemaMatchDto
|
||||||
|
{
|
||||||
|
ListFormCode = form.ListFormCode ?? string.Empty,
|
||||||
|
DisplayName = form.DisplayName,
|
||||||
|
TableName = NullIfEmpty(form.TableName),
|
||||||
|
MatchedColumns = columnMatches
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.Count >= limit)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<McpDictionaryListForm>> GetReadableListFormsAsync()
|
||||||
|
{
|
||||||
|
var forms = await dictionary.GetListFormsAsync();
|
||||||
|
var readable = await accessFilter.GetReadableListFormCodesAsync();
|
||||||
|
|
||||||
|
return [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static McpListSummaryDto ToSummary(McpDictionaryListForm form)
|
||||||
|
{
|
||||||
|
var summary = new McpListSummaryDto();
|
||||||
|
CopySummary(form, summary);
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopySummary(McpDictionaryListForm form, McpListSummaryDto target)
|
||||||
|
{
|
||||||
|
target.ListFormCode = form.ListFormCode ?? string.Empty;
|
||||||
|
target.DisplayName = NullIfEmpty(form.DisplayName);
|
||||||
|
target.Title = NullIfEmpty(form.Title);
|
||||||
|
target.Description = NullIfEmpty(form.Description);
|
||||||
|
target.TableName = NullIfEmpty(form.TableName);
|
||||||
|
target.SqlSourceName = NullIfEmpty(form.SqlSourceName);
|
||||||
|
target.SourceKind = NullIfEmpty(form.SourceKind);
|
||||||
|
target.KeyFieldName = NullIfEmpty(form.KeyFieldName);
|
||||||
|
target.FieldCount = form.Fields?.Count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static McpColumnDto ToColumn(McpDictionaryField field) => new()
|
||||||
|
{
|
||||||
|
Field = field.Field ?? string.Empty,
|
||||||
|
DisplayName = NullIfEmpty(field.DisplayName),
|
||||||
|
SqlDataType = NullIfEmpty(field.SqlDataType),
|
||||||
|
IsVisible = field.IsVisible,
|
||||||
|
IsBooleanColumn = field.IsBooleanColumn,
|
||||||
|
LookupTableName = NullIfEmpty(field.LookupTableName),
|
||||||
|
LookupValueColumn = NullIfEmpty(field.LookupValueColumn),
|
||||||
|
LookupDisplayColumn = NullIfEmpty(field.LookupDisplayColumn),
|
||||||
|
IsSensitive = field.IsSensitive,
|
||||||
|
IsDefaultSelectColumn = field.IsDefaultSelectColumn
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int NormalizeLimit(int requested, int fallback)
|
||||||
|
=> requested <= 0 ? fallback : Math.Min(requested, fallback * 5);
|
||||||
|
|
||||||
|
private static string? NullIfEmpty(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Volo.Abp.Ddd.Application" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj" />
|
||||||
|
<!-- Veri yazma ekranin insert/update/delete sozlesmesinden gecer. -->
|
||||||
|
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Application.Contracts\Sozsoft.Platform.Application.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
104
api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
Normal file
104
api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>MCP modulunun sabit sozlesme degerleri.</summary>
|
||||||
|
public static class McpConsts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Veri sozlugu satirlarinin turu. <c>Sas_H_DbTableColumn</c> tablosunda ayni deger
|
||||||
|
/// <c>DictionaryType</c> kolonunda durur.
|
||||||
|
/// </summary>
|
||||||
|
public const string DictionaryType = "ListFormJson";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sozluk ureten prosedurun varsayilan <c>@TenantName</c> degeri. Host baglaminda sozluk
|
||||||
|
/// bu adla yazilir.
|
||||||
|
/// </summary>
|
||||||
|
public const string DefaultDictionaryTenantName = "Sozsoft";
|
||||||
|
|
||||||
|
/// <summary>Sunucu kaydinda satir tavani verilmediginde (ya da sifir oldugunda) uygulanan deger.</summary>
|
||||||
|
public const int DefaultMaxRows = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>CustomEndpoint.DataSourceCode</c> bu degeri tasidiginda istegi yapan kullanicinin
|
||||||
|
/// kiraci veritabani kullanilir.
|
||||||
|
/// </summary>
|
||||||
|
public const string TenantDataSourceMarker = "!Tenant";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sozluk ureten prosedurun adi. Sozluk bulunamadiginda istemciye ne yapacagi bu adla
|
||||||
|
/// bildirilir.
|
||||||
|
/// </summary>
|
||||||
|
public const string DictionaryProcedureName = "dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) OAuth istemci kimligi. OpenIddict
|
||||||
|
/// uygulamasi DbMigrator'da <c>OpenIddict:Applications:Platform_Mcp</c> ayarindan seed edilir;
|
||||||
|
/// dinamik istemci kaydi yeni uygulama uretmez, bu kimligi dondurur.
|
||||||
|
/// </summary>
|
||||||
|
public const string OAuthClientId = "Platform_Mcp";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dinamik kayitla (RFC 7591) uretilen istemcilerin kimlik oneki. Bilinen saglayicilarin
|
||||||
|
/// (Claude, ChatGPT) disindaki MCP istemcileri bu onekle kendi OpenIddict uygulamasini alir.
|
||||||
|
/// </summary>
|
||||||
|
public const string DynamicClientIdPrefix = "mcp_";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dinamik istemci sayisinin tavani. Kayit ucu kimlik dogrulamasizdir; tavan, ucun
|
||||||
|
/// veritabaninda sinirsiz uygulama biriktirmesini onler.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxDynamicClients = 500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP istemcilerinin yenileme token omru. Web istemcisinin kisa omru (dakikalar) connector
|
||||||
|
/// icin uygun degil: kullanici birkac saat soru sormadiginda baglanti kopardi. Yenileme
|
||||||
|
/// token'i her kullanimda yenilenir; omur, son kullanimdan itibaren sayilir.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly System.TimeSpan RefreshTokenLifetime = System.TimeSpan.FromDays(30);
|
||||||
|
|
||||||
|
/// <summary>Istemci kimligi bir MCP istemcisine (bilinen ya da dinamik) mi ait.</summary>
|
||||||
|
public static bool IsMcpClient(string? clientId)
|
||||||
|
=> clientId is not null
|
||||||
|
&& (string.Equals(clientId, OAuthClientId, System.StringComparison.Ordinal)
|
||||||
|
|| clientId.StartsWith(DynamicClientIdPrefix, System.StringComparison.Ordinal));
|
||||||
|
|
||||||
|
/// <summary>MCP ucunun sunucu adresine gore yolu.</summary>
|
||||||
|
public const string EndpointPath = "/mcp";
|
||||||
|
|
||||||
|
/// <summary>Dinamik istemci kaydi (RFC 7591) ucunun yolu.</summary>
|
||||||
|
public const string RegistrationEndpointPath = "/connect/register";
|
||||||
|
|
||||||
|
/// <summary>Korumali kaynak meta verisi (RFC 9728) ucunun yolu.</summary>
|
||||||
|
public const string ProtectedResourceMetadataPath = "/.well-known/oauth-protected-resource";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemcinin istemesi gereken kapsamlar. <c>offline_access</c> yenileme token'i icindir;
|
||||||
|
/// olmadan baglanti her saat yeniden giris ister.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string[] OAuthScopes = [Sozsoft.Platform.PlatformConsts.AppName, "offline_access"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>run_query</c> araciyla cagrilabilen nesne turleri. Degerler istemciye oldugu gibi
|
||||||
|
/// gosterilir; degistirilirse arac sozlesmesi kirilir.
|
||||||
|
/// </summary>
|
||||||
|
public static class SavedQueryKinds
|
||||||
|
{
|
||||||
|
public const string View = "view";
|
||||||
|
public const string Procedure = "procedure";
|
||||||
|
public const string TableFunction = "tableFunction";
|
||||||
|
public const string ScalarFunction = "scalarFunction";
|
||||||
|
public const string CustomEndpoint = "customEndpoint";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veritabanı katalog sorgusunun dondurdugu nesne turu etiketleri.
|
||||||
|
/// <see cref="SavedQueryKinds"/> degerlerine eslenir.
|
||||||
|
/// </summary>
|
||||||
|
public static class CatalogKinds
|
||||||
|
{
|
||||||
|
public const string View = "VIEW";
|
||||||
|
public const string Procedure = "PROCEDURE";
|
||||||
|
public const string TableFunction = "TABLE_FUNCTION";
|
||||||
|
public const string ScalarFunction = "SCALAR_FUNCTION";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using Volo.Abp.Domain;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
using Volo.Abp.Validation;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi
|
||||||
|
/// <c>PlatformResource</c> kaynagindan (LanguagesData.json) okunur; modul ikinci bir dil
|
||||||
|
/// kaynagi acmaz.
|
||||||
|
/// </summary>
|
||||||
|
[DependsOn(
|
||||||
|
typeof(AbpValidationModule),
|
||||||
|
typeof(AbpDddDomainSharedModule)
|
||||||
|
)]
|
||||||
|
public class McpDomainSharedModule : AbpModule
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>LanguagesData.json</c>
|
||||||
|
/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
|
||||||
|
/// </summary>
|
||||||
|
public static class McpErrorCodes
|
||||||
|
{
|
||||||
|
private const string Prefix = "App.Mcp.Error.";
|
||||||
|
|
||||||
|
public const string NoServerRecord = Prefix + "NoServerRecord";
|
||||||
|
public const string ToolDisabled = Prefix + "ToolDisabled";
|
||||||
|
public const string EmptyQuery = Prefix + "EmptyQuery";
|
||||||
|
public const string SingleStatementOnly = Prefix + "SingleStatementOnly";
|
||||||
|
public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired";
|
||||||
|
public const string ForbiddenKeyword = Prefix + "ForbiddenKeyword";
|
||||||
|
public const string InvalidIdentifier = Prefix + "InvalidIdentifier";
|
||||||
|
public const string InvalidParameterName = Prefix + "InvalidParameterName";
|
||||||
|
public const string UnsupportedObjectKind = Prefix + "UnsupportedObjectKind";
|
||||||
|
public const string CustomEndpointNotFound = Prefix + "CustomEndpointNotFound";
|
||||||
|
public const string MissingParameters = Prefix + "MissingParameters";
|
||||||
|
public const string ListFormCodeRequired = Prefix + "ListFormCodeRequired";
|
||||||
|
public const string ValuesRequired = Prefix + "ValuesRequired";
|
||||||
|
public const string KeysRequired = Prefix + "KeysRequired";
|
||||||
|
public const string DictionaryNotFound = Prefix + "DictionaryNotFound";
|
||||||
|
public const string RegistrationLimitReached = Prefix + "RegistrationLimitReached";
|
||||||
|
public const string RedirectUriNotAllowed = Prefix + "RedirectUriNotAllowed";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /mcp ucunda yayinlanan arac adlari. Ayni degerler MCP Servers ekranindaki
|
||||||
|
/// <c>AllowedTools</c> alaninin secenek listesini besler; buraya bir arac eklenirse ekranin
|
||||||
|
/// statik lookup listesi de guncellenir.
|
||||||
|
/// </summary>
|
||||||
|
public static class McpToolNames
|
||||||
|
{
|
||||||
|
public const string ServerInfo = "server_info";
|
||||||
|
public const string ListLists = "list_lists";
|
||||||
|
public const string DescribeList = "describe_list";
|
||||||
|
public const string SearchSchema = "search_schema";
|
||||||
|
public const string QueryList = "query_list";
|
||||||
|
public const string RunSelect = "run_select";
|
||||||
|
public const string ListQueries = "list_queries";
|
||||||
|
public const string RunQuery = "run_query";
|
||||||
|
public const string InsertRow = "insert_row";
|
||||||
|
public const string UpdateRow = "update_row";
|
||||||
|
public const string DeleteRow = "delete_row";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri yazan araclar. Sunucu kaydinda <c>AllowWrite</c> kapaliysa bunlar arac listesinde
|
||||||
|
/// hic yer almaz.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string[] WriteTools = [InsertRow, UpdateRow, DeleteRow];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sunucu kaydi olmasa da acik kalan araclar. Istemcinin baglantisinin neden bos
|
||||||
|
/// gorundugunu anlayabilmesi icin gereklidir.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string[] AlwaysAvailableTools = [ServerInfo];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kullanicinin uygulamada da sahip olmasi gereken yetki. MCP yetki vermez: ham SQL'i
|
||||||
|
/// uygulamada SQL Query Manager'i kullanabilen, veri yazmayi yazma yetkisi olan kullanici
|
||||||
|
/// gorur. Listede olmayan araclar kendi icinde liste bazli yetkiyle suzulur.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly IReadOnlyDictionary<string, string> RequiredPermissions = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[RunSelect] = PlatformConsts.AppCodes.SqlQueryManagers.Default,
|
||||||
|
[InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
|
[UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
|
[DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Yayinlanan tum araclar, arac listesindeki siralarina gore.</summary>
|
||||||
|
public static readonly string[] All =
|
||||||
|
[
|
||||||
|
ServerInfo,
|
||||||
|
ListLists,
|
||||||
|
DescribeList,
|
||||||
|
SearchSchema,
|
||||||
|
QueryList,
|
||||||
|
RunSelect,
|
||||||
|
ListQueries,
|
||||||
|
RunQuery,
|
||||||
|
InsertRow,
|
||||||
|
UpdateRow,
|
||||||
|
DeleteRow
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>AllowedTools</c> alaninda secilebilen araclar. Her zaman acik olanlar secenek
|
||||||
|
/// olarak sunulmaz; onlari kapatmak mumkun degildir.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<string> SelectableTools
|
||||||
|
=> All.Where(name => !AlwaysAvailableTools.Contains(name));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir aracin bir sunucu kaydina gore acik olup olmadigina karar veren kural. Karar iki yerde
|
||||||
|
/// kullanilir: arac listesi suzulurken ve arac cagrilirken. Listeyi suzmek tek basina bir
|
||||||
|
/// yetki siniri degildir — istemci listede olmayan bir araci da cagirabilir.
|
||||||
|
/// </summary>
|
||||||
|
public static class McpToolPolicy
|
||||||
|
{
|
||||||
|
/// <param name="hasServerRecord">Istegin kiracisina dusen aktif bir sunucu kaydi var mi.</param>
|
||||||
|
/// <param name="allowWrite">Sunucu kaydinda veri yazma acik mi.</param>
|
||||||
|
/// <param name="allowedTools">Acik arac adlari; bos liste "yazma disindaki tumu acik" demektir.</param>
|
||||||
|
public static bool IsAllowed(
|
||||||
|
bool hasServerRecord,
|
||||||
|
bool allowWrite,
|
||||||
|
IReadOnlyCollection<string> allowedTools,
|
||||||
|
string toolName)
|
||||||
|
{
|
||||||
|
if (McpToolNames.AlwaysAvailableTools.Contains(toolName, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasServerRecord)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowWrite && McpToolNames.WriteTools.Contains(toolName, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedTools.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedTools.Contains(toolName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> VisibleTools(
|
||||||
|
bool hasServerRecord,
|
||||||
|
bool allowWrite,
|
||||||
|
IReadOnlyCollection<string> allowedTools)
|
||||||
|
=> [.. McpToolNames.All.Where(name => IsAllowed(hasServerRecord, allowWrite, allowedTools, name))];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Volo.Abp.Ddd.Domain.Shared" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Volo.Abp.Validation" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Yetki adlari ve tablo adi platformun tek kayit defterinde durur
|
||||||
|
(PlatformConsts.AppCodes, TableNameResolver); modul bunlari kopyalamaz. -->
|
||||||
|
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Domain.Shared\Sozsoft.Platform.Domain.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.Mcp.Services;
|
||||||
|
using Volo.Abp.Data;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bilinen MCP istemcisini (<see cref="McpConsts.OAuthClientId"/>) seed eder. Geri donus
|
||||||
|
/// adresleri <c>OpenIddict:Applications:Platform_Mcp:RedirectUris</c> ayarindadir; yeni bir
|
||||||
|
/// saglayici eklemek bu listeye callback eklemektir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpOAuthClientDataSeedContributor(
|
||||||
|
IConfiguration configuration,
|
||||||
|
IMcpOAuthClientManager clientManager) : IDataSeedContributor, ITransientDependency
|
||||||
|
{
|
||||||
|
private const string RedirectUrisKey = "OpenIddict:Applications:Platform_Mcp:RedirectUris";
|
||||||
|
|
||||||
|
public async Task SeedAsync(DataSeedContext context)
|
||||||
|
{
|
||||||
|
// OpenIddict uygulamalari host kapsamindadir; kiraci seed turlarinda tekrarlanmaz.
|
||||||
|
if (context.TenantId is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirectUris = configuration.GetSection(RedirectUrisKey).Get<string[]>();
|
||||||
|
if (redirectUris is null || redirectUris.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await clientManager.EnsureKnownClientAsync(redirectUris);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP (Model Context Protocol) sunucu tanimi. Kayit host kapsamindadir; hangi kiracilarin
|
||||||
|
/// kullanabilecegi <see cref="Tenants"/> ile belirlenir. Istegin kiracisina dusen kayit, /mcp
|
||||||
|
/// ucunda hangi araclarin gorunecegini ve sorgularin hangi veri kaynaginda hangi satir
|
||||||
|
/// tavaniyla kosacagini belirler.
|
||||||
|
/// </summary>
|
||||||
|
public class McpServer : FullAuditedEntity<Guid>
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemciye verilen baglanti adresi (ornek: https://api.sozsoft.com/mcp). Yonlendirme icin
|
||||||
|
/// kullanilmaz; ekranin kopyalanabilir baglanti komutu uretebilmesi icin tutulur.
|
||||||
|
/// </summary>
|
||||||
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kaydin gecerli oldugu kiraci adlari, <c>PlatformConsts.MultiValueDelimiter</c> ile
|
||||||
|
/// ayrilir. Bos ise tum kiracilara aciktir.
|
||||||
|
/// </summary>
|
||||||
|
public string? Tenants { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorgu araclarinin kosacagi DataSource kodu. Bos ise istegin kapsamindaki varsayilan
|
||||||
|
/// veri kaynagi kullanilir.
|
||||||
|
/// </summary>
|
||||||
|
public string? DataSourceCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acik arac adlari, <c>PlatformConsts.MultiValueDelimiter</c> ile ayrilir. Bos ise yazma
|
||||||
|
/// disindaki tum araclar aciktir.
|
||||||
|
/// </summary>
|
||||||
|
public string? AllowedTools { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Tek sorgunun dondurebilecegi en fazla satir sayisi.</summary>
|
||||||
|
public int MaxRows { get; set; } = McpConsts.DefaultMaxRows;
|
||||||
|
|
||||||
|
/// <summary>Kapali oldugunda veri yazma araclari arac listesinde hic yer almaz.</summary>
|
||||||
|
public bool AllowWrite { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Volo.Abp.Domain;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
using Volo.Abp.OpenIddict;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
[DependsOn(
|
||||||
|
typeof(AbpDddDomainModule),
|
||||||
|
typeof(AbpOpenIddictDomainModule),
|
||||||
|
typeof(McpDomainSharedModule)
|
||||||
|
)]
|
||||||
|
public class McpDomainModule : AbpModule
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.Permissions;
|
||||||
|
using Volo.Abp.Authorization.Permissions;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kullanicinin okuyabildigi listeleri belirler. Karar ekranin kendisiyle aynidir: ListForm'un
|
||||||
|
/// <c>PermissionJson.R</c> yetkisi (bkz. <c>ListFormAuthorizationManager</c>). MCP bu kurala
|
||||||
|
/// ek bir yetki tanimlamaz; uygulamada acamadigin listeyi MCP'de de goremezsin.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpListFormAccessFilter
|
||||||
|
{
|
||||||
|
Task<IReadOnlySet<string>> GetReadableListFormCodesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpListFormAccessFilter(
|
||||||
|
IRepository<ListForm, Guid> listFormRepository,
|
||||||
|
IPermissionChecker permissionChecker)
|
||||||
|
: DomainService, IMcpListFormAccessFilter
|
||||||
|
{
|
||||||
|
public async Task<IReadOnlySet<string>> GetReadableListFormCodesAsync()
|
||||||
|
{
|
||||||
|
// ListFormAuthorizationManager.CanAccess liste basina bir sorgu ve bir yetki kontrolu
|
||||||
|
// yapar; sozlukteki yuzlerce liste icin bu tek sorgu + tek toplu kontrole indirilir.
|
||||||
|
var query = await listFormRepository.GetQueryableAsync();
|
||||||
|
var definitions = query
|
||||||
|
.Select(listForm => new { listForm.ListFormCode, listForm.PermissionJson })
|
||||||
|
.ToList()
|
||||||
|
.GroupBy(definition => definition.ListFormCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var readPermissions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var definition in definitions)
|
||||||
|
{
|
||||||
|
var permission = ReadPermissionOf(definition.PermissionJson);
|
||||||
|
if (permission is not null)
|
||||||
|
{
|
||||||
|
readPermissions[definition.ListFormCode] = permission;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var distinctPermissions = readPermissions.Values.Distinct(StringComparer.Ordinal).ToArray();
|
||||||
|
if (distinctPermissions.Length == 0)
|
||||||
|
{
|
||||||
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var grants = await permissionChecker.IsGrantedAsync(distinctPermissions);
|
||||||
|
var granted = grants.Result
|
||||||
|
.Where(result => result.Value == PermissionGrantResult.Granted)
|
||||||
|
.Select(result => result.Key)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
return readPermissions
|
||||||
|
.Where(entry => granted.Contains(entry.Value))
|
||||||
|
.Select(entry => entry.Key)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadPermissionOf(string? permissionJson)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(permissionJson))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var permissions = JsonSerializer.Deserialize<PermissionCrudValueObject>(permissionJson);
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Bozuk yetki tanimi ekranda da erisimi kapatir (CanAccess false doner).
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenIddict.Abstractions;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
using Volo.Abp.OpenIddict.Applications;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uzak MCP istemcilerinin OpenIddict uygulamalari. Iki tur istemci vardir:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>Bilinen istemci</b> (<see cref="McpConsts.OAuthClientId"/>): geri donus adresleri
|
||||||
|
/// DbMigrator ayarinda sabit olan saglayicilar (Claude, ChatGPT). Onay ekrani gostermez.</item>
|
||||||
|
/// <item><b>Dinamik istemci</b> (<see cref="McpConsts.DynamicClientIdPrefix"/>): baska bir MCP
|
||||||
|
/// istemcisi (Cursor, VS Code, Claude Code, ...) kayit ucundan kendi uygulamasini alir. Kayit
|
||||||
|
/// herkese acik oldugu icin bu istemciler <b>acik onay</b> ister: kullanici giris yaptiktan sonra
|
||||||
|
/// hangi uygulamanin erisim istedigini gorup onaylar.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Iki turde de token kullanicinin kendi yetkileriyle uretilir; istemci yetki kazanmaz.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpOAuthClientManager
|
||||||
|
{
|
||||||
|
/// <summary>Bilinen istemciyi olusturur ya da geri donus adreslerini gunceller.</summary>
|
||||||
|
Task EnsureKnownClientAsync(IReadOnlyCollection<string> redirectUris);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemci kaydi. Adresler bilinen istemcide kayitliysa onu, degilse ayni adres kumesine
|
||||||
|
/// sahip dinamik istemciyi dondurur; yoksa yenisini uretir.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpClientRegistration> RegisterAsync(string? clientName, IReadOnlyCollection<string> redirectUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpOAuthClientManager(
|
||||||
|
IAbpApplicationManager applicationManager,
|
||||||
|
IRepository<OpenIddictApplication, Guid> applicationRepository,
|
||||||
|
ICurrentTenant currentTenant)
|
||||||
|
: DomainService, IMcpOAuthClientManager
|
||||||
|
{
|
||||||
|
private const int MaxClientNameLength = 100;
|
||||||
|
|
||||||
|
/// <summary>Tarayici disi calistirilabilir icerik tasiyan, geri donus adresi olamayacak semalar.</summary>
|
||||||
|
private static readonly string[] ForbiddenSchemes = ["javascript", "data", "file", "vbscript", "about", "blob"];
|
||||||
|
|
||||||
|
public async Task EnsureKnownClientAsync(IReadOnlyCollection<string> redirectUris)
|
||||||
|
{
|
||||||
|
// OpenIddict uygulamalari host kapsamindadir.
|
||||||
|
using (currentTenant.Change(null))
|
||||||
|
{
|
||||||
|
var existing = await applicationManager.FindByClientIdAsync(McpConsts.OAuthClientId);
|
||||||
|
var descriptor = BuildDescriptor(
|
||||||
|
McpConsts.OAuthClientId,
|
||||||
|
"MCP Connectors",
|
||||||
|
redirectUris,
|
||||||
|
OpenIddictConstants.ConsentTypes.Implicit);
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
await applicationManager.CreateAsync(descriptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanim tamamen dosyadan gelir; ayar degistiyse (yeni saglayici callback'i, token omru)
|
||||||
|
// kayit yeniden yazilir. Kimlik ve istemci turu degismez.
|
||||||
|
await applicationManager.PopulateAsync(existing, descriptor);
|
||||||
|
await applicationManager.UpdateAsync(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpClientRegistration> RegisterAsync(string? clientName, IReadOnlyCollection<string> redirectUris)
|
||||||
|
{
|
||||||
|
if (redirectUris.Count == 0)
|
||||||
|
{
|
||||||
|
return McpClientRegistration.Rejected(McpClientRegistrationError.InvalidRedirectUri, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalid = redirectUris.FirstOrDefault(uri => !IsAcceptableRedirectUri(uri));
|
||||||
|
if (invalid is not null)
|
||||||
|
{
|
||||||
|
return McpClientRegistration.Rejected(McpClientRegistrationError.InvalidRedirectUri, invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (currentTenant.Change(null))
|
||||||
|
{
|
||||||
|
var known = await applicationManager.FindByClientIdAsync(McpConsts.OAuthClientId);
|
||||||
|
if (known is not null)
|
||||||
|
{
|
||||||
|
var knownUris = await applicationManager.GetRedirectUrisAsync(known);
|
||||||
|
if (redirectUris.All(uri => knownUris.Any(registered => SameUri(registered, uri))))
|
||||||
|
{
|
||||||
|
return McpClientRegistration.Accepted(McpConsts.OAuthClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicClients = await GetDynamicClientsAsync();
|
||||||
|
|
||||||
|
// Ayni istemci her baglantida yeniden kayit olabilir; ayni adres kumesi tek uygulamaya
|
||||||
|
// baglanir, tablo buyumez.
|
||||||
|
var match = dynamicClients.FirstOrDefault(client => SameUriSet(client.RedirectUris, redirectUris));
|
||||||
|
if (match.ClientId is not null)
|
||||||
|
{
|
||||||
|
return McpClientRegistration.Accepted(match.ClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicClients.Count >= McpConsts.MaxDynamicClients)
|
||||||
|
{
|
||||||
|
return McpClientRegistration.Rejected(McpClientRegistrationError.LimitReached, McpConsts.MaxDynamicClients.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientId = McpConsts.DynamicClientIdPrefix + GuidGenerator.Create().ToString("N");
|
||||||
|
await applicationManager.CreateAsync(BuildDescriptor(
|
||||||
|
clientId,
|
||||||
|
DisplayNameOf(clientName, redirectUris.First()),
|
||||||
|
redirectUris,
|
||||||
|
OpenIddictConstants.ConsentTypes.Explicit));
|
||||||
|
|
||||||
|
return McpClientRegistration.Accepted(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<(string? ClientId, IReadOnlyList<string> RedirectUris)>> GetDynamicClientsAsync()
|
||||||
|
{
|
||||||
|
var query = await applicationRepository.GetQueryableAsync();
|
||||||
|
var rows = query
|
||||||
|
.Where(application => application.ClientId!.StartsWith(McpConsts.DynamicClientIdPrefix))
|
||||||
|
.Select(application => new { application.ClientId, application.RedirectUris })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return [.. rows.Select(row => ((string?)row.ClientId, ParseUris(row.RedirectUris)))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AbpApplicationDescriptor BuildDescriptor(
|
||||||
|
string clientId,
|
||||||
|
string displayName,
|
||||||
|
IEnumerable<string> redirectUris,
|
||||||
|
string consentType)
|
||||||
|
{
|
||||||
|
var descriptor = new AbpApplicationDescriptor
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
ClientType = OpenIddictConstants.ClientTypes.Public,
|
||||||
|
ConsentType = consentType,
|
||||||
|
DisplayName = displayName
|
||||||
|
};
|
||||||
|
|
||||||
|
descriptor.Permissions.UnionWith(
|
||||||
|
[
|
||||||
|
OpenIddictConstants.Permissions.Endpoints.Authorization,
|
||||||
|
OpenIddictConstants.Permissions.Endpoints.Token,
|
||||||
|
OpenIddictConstants.Permissions.Endpoints.Revocation,
|
||||||
|
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
|
||||||
|
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
|
||||||
|
OpenIddictConstants.Permissions.ResponseTypes.Code,
|
||||||
|
OpenIddictConstants.Permissions.Prefixes.Scope + PlatformConsts.AppName
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Genel istemci gizli anahtar tasiyamaz; yetkilendirme kodunun calinmasina karsi PKCE
|
||||||
|
// zorunludur.
|
||||||
|
descriptor.Requirements.Add(OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange);
|
||||||
|
|
||||||
|
descriptor.Settings[OpenIddictConstants.Settings.TokenLifetimes.RefreshToken] =
|
||||||
|
McpConsts.RefreshTokenLifetime.ToString("c", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
foreach (var redirectUri in redirectUris)
|
||||||
|
{
|
||||||
|
descriptor.RedirectUris.Add(new Uri(redirectUri, UriKind.Absolute));
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kabul edilen geri donus adresleri: HTTPS, yerel makineye donen HTTP (RFC 8252 §7.3 —
|
||||||
|
/// masaustu istemciler rastgele portla dinler) ve masaustu uygulamalarin ozel semalari
|
||||||
|
/// (RFC 8252 §7.1, ornek: <c>cursor://...</c>). Yerel olmayan duz HTTP reddedilir.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsAcceptableRedirectUri(string value)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || !string.IsNullOrEmpty(uri.Fragment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.Scheme == Uri.UriSchemeHttps)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.Scheme == Uri.UriSchemeHttp)
|
||||||
|
{
|
||||||
|
return uri.IsLoopback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !ForbiddenSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Onay ekraninda gorunen ad. Istemci adi kayit isteginden gelir ve dogrulanamaz (herkes
|
||||||
|
/// kendine "Claude" diyebilir); bu yuzden token'in gidecegi adresin sahibi de gosterilir.
|
||||||
|
/// </summary>
|
||||||
|
private static string DisplayNameOf(string? clientName, string redirectUri)
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(clientName) ? "MCP Client" : clientName.Trim();
|
||||||
|
if (name.Length > MaxClientNameLength)
|
||||||
|
{
|
||||||
|
name = name[..MaxClientNameLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = new Uri(redirectUri, UriKind.Absolute);
|
||||||
|
var owner = uri.IsLoopback ? "localhost" : string.IsNullOrEmpty(uri.Host) ? uri.Scheme : uri.Host;
|
||||||
|
|
||||||
|
return $"{name} ({owner})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ParseUris(string? json)
|
||||||
|
=> string.IsNullOrWhiteSpace(json) ? [] : JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||||
|
|
||||||
|
private static bool SameUriSet(IReadOnlyCollection<string> left, IReadOnlyCollection<string> right)
|
||||||
|
=> left.Count == right.Count && right.All(uri => left.Any(candidate => SameUri(candidate, uri)));
|
||||||
|
|
||||||
|
/// <summary>OpenIddict adresleri <see cref="Uri"/> bicimiyle saklar; karsilastirma ayni bicimle yapilir.</summary>
|
||||||
|
private static bool SameUri(string left, string right)
|
||||||
|
=> Uri.TryCreate(left, UriKind.Absolute, out var a)
|
||||||
|
&& Uri.TryCreate(right, UriKind.Absolute, out var b)
|
||||||
|
&& string.Equals(a.AbsoluteUri, b.AbsoluteUri, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum McpClientRegistrationError
|
||||||
|
{
|
||||||
|
InvalidRedirectUri,
|
||||||
|
LimitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kayit sonucu: basariliysa istemci kimligi, degilse hata ve hataya konu deger.</summary>
|
||||||
|
public sealed record McpClientRegistration(string? ClientId, McpClientRegistrationError? Error, string? ErrorValue)
|
||||||
|
{
|
||||||
|
public static McpClientRegistration Accepted(string clientId) => new(clientId, null, null);
|
||||||
|
|
||||||
|
public static McpClientRegistration Rejected(McpClientRegistrationError error, string value) => new(null, error, value);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
|
using Sozsoft.Platform.DynamicData;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorguyu istegin kapsamindaki veri kaynaginda kosturur ve satir tavanini uygular.
|
||||||
|
/// Veri kaynagi cozumu tek yerde kalsin diye burada toplanmistir.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpQueryExecutor
|
||||||
|
{
|
||||||
|
/// <param name="dataSourceCode">
|
||||||
|
/// Sunucu kaydindaki kod. Bos ya da <c>!Tenant</c> ise veri kaynagi istegin kiracisindan
|
||||||
|
/// cozulur; host baglaminda varsayilan kaynaga duser.
|
||||||
|
/// </param>
|
||||||
|
Task<McpDataSourceContext> ResolveDataSourceAsync(string? dataSourceCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorguyu kosturur. Sonuc kumesi <paramref name="maxRows"/> satirda kesilir; kesilme
|
||||||
|
/// olduysa sonuc bunu bildirir.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryRows> ExecuteAsync(
|
||||||
|
McpDataSourceContext context,
|
||||||
|
string sql,
|
||||||
|
IReadOnlyDictionary<string, object> parameters,
|
||||||
|
int maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpQueryExecutor(IDynamicDataManager dynamicDataManager) : DomainService, IMcpQueryExecutor
|
||||||
|
{
|
||||||
|
public async Task<McpDataSourceContext> ResolveDataSourceAsync(string? dataSourceCode)
|
||||||
|
{
|
||||||
|
var useTenantScope = string.IsNullOrWhiteSpace(dataSourceCode)
|
||||||
|
|| dataSourceCode == McpConsts.TenantDataSourceMarker;
|
||||||
|
|
||||||
|
var resolvedCode = useTenantScope
|
||||||
|
? SeedConsts.DataSources.DefaultCode
|
||||||
|
: dataSourceCode!;
|
||||||
|
|
||||||
|
var (repository, connectionString, dataSourceType) =
|
||||||
|
await dynamicDataManager.GetAsync(useTenantScope, resolvedCode);
|
||||||
|
|
||||||
|
return new McpDataSourceContext(repository, connectionString, dataSourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryRows> ExecuteAsync(
|
||||||
|
McpDataSourceContext context,
|
||||||
|
string sql,
|
||||||
|
IReadOnlyDictionary<string, object> parameters,
|
||||||
|
int maxRows)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var rows = await context.Repository.QueryAsync(
|
||||||
|
sql,
|
||||||
|
context.ConnectionString,
|
||||||
|
parameters.ToDictionary(entry => entry.Key, entry => entry.Value));
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var columns = new List<string>();
|
||||||
|
var materialized = new List<Dictionary<string, object?>>();
|
||||||
|
var truncated = false;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (materialized.Count >= maxRows)
|
||||||
|
{
|
||||||
|
truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row is not IDictionary<string, object> values)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columns.Count == 0)
|
||||||
|
{
|
||||||
|
columns = [.. values.Keys];
|
||||||
|
}
|
||||||
|
|
||||||
|
materialized.Add(values.ToDictionary(entry => entry.Key, entry => (object?)entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new McpQueryRows(columns, materialized, truncated, stopwatch.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cozumlenmis veri kaynagi: hangi depo, hangi baglanti, hangi saglayici.</summary>
|
||||||
|
public sealed record McpDataSourceContext(
|
||||||
|
IDynamicDataRepository Repository,
|
||||||
|
string ConnectionString,
|
||||||
|
DataSourceTypeEnum DataSourceType);
|
||||||
|
|
||||||
|
/// <summary>Sorgu sonucunun ham hali.</summary>
|
||||||
|
public sealed record McpQueryRows(
|
||||||
|
List<string> Columns,
|
||||||
|
List<Dictionary<string, object?>> Rows,
|
||||||
|
bool Truncated,
|
||||||
|
long ExecutionTimeMs);
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sema bilgisinin tek kaynagi: <c>Sas_H_DbTableColumn</c> tablosundaki JSON veri sozlugu.
|
||||||
|
/// Sozluk kullaniciya gorunen basliklari ve lookup iliskilerini birlikte tasidigi icin canli
|
||||||
|
/// katalog yerine bu kullanilir — modelin gordugu adlar ekranda gorunen adlarla ayni olur.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpSchemaDictionary
|
||||||
|
{
|
||||||
|
/// <summary>Sozlugun bulunup bulunmadigi ve ne zaman tazelendigi; yoksa null.</summary>
|
||||||
|
Task<McpDictionarySnapshot?> GetSnapshotAsync();
|
||||||
|
|
||||||
|
/// <summary>Sozlukteki listeler; sozluk yoksa bos liste.</summary>
|
||||||
|
Task<IReadOnlyList<McpDictionaryListForm>> GetListFormsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istegin kapsamina dusen sozluk kiraci adi. Sozluk ureten prosedurun
|
||||||
|
/// <c>@TenantName</c> parametresiyle ayni degeri tasimasi gerekir.
|
||||||
|
/// </summary>
|
||||||
|
string ResolveTenantName();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aksan ve buyuk/kucuk harf duyarsiz karsilastirma anahtari; "Musteri" aramasi
|
||||||
|
/// "Müşteri" basligini da bulur.
|
||||||
|
/// </summary>
|
||||||
|
string Normalize(string? value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpSchemaDictionary(
|
||||||
|
IRepository<DbTableColumn, Guid> dictionaryRepository,
|
||||||
|
ICurrentTenant currentTenant)
|
||||||
|
: DomainService, IMcpSchemaDictionary
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions DictionaryJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly CultureInfo TurkishCulture = CultureInfo.GetCultureInfo("tr-TR");
|
||||||
|
|
||||||
|
public string ResolveTenantName()
|
||||||
|
=> currentTenant.Name ?? McpConsts.DefaultDictionaryTenantName;
|
||||||
|
|
||||||
|
public async Task<McpDictionarySnapshot?> GetSnapshotAsync()
|
||||||
|
{
|
||||||
|
// Tazelik bilgisi icin JsonData (buyuk sozluklerde megabaytlar) okunmaz.
|
||||||
|
using (currentTenant.Change(null))
|
||||||
|
{
|
||||||
|
var query = await ActiveRowsAsync(ResolveTenantName());
|
||||||
|
|
||||||
|
return query
|
||||||
|
.Select(row => new McpDictionarySnapshot(
|
||||||
|
row.CacheKey,
|
||||||
|
row.RefreshedAt,
|
||||||
|
row.FormCount,
|
||||||
|
row.FieldCount,
|
||||||
|
row.TenantName,
|
||||||
|
row.DefinitionCultureName,
|
||||||
|
row.UiCultureName))
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<McpDictionaryListForm>> GetListFormsAsync()
|
||||||
|
{
|
||||||
|
var json = await GetJsonDataAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = JsonSerializer.Deserialize<DictionaryDocument>(json, DictionaryJsonOptions);
|
||||||
|
|
||||||
|
return document?.ListForms ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decomposed = value.ToLower(TurkishCulture).Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(decomposed.Length);
|
||||||
|
|
||||||
|
foreach (var character in decomposed)
|
||||||
|
{
|
||||||
|
if (CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(character switch
|
||||||
|
{
|
||||||
|
'ı' => 'i',
|
||||||
|
'ğ' => 'g',
|
||||||
|
'ş' => 's',
|
||||||
|
_ => character
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetJsonDataAsync()
|
||||||
|
{
|
||||||
|
// Sozluk host kapsamli bir tabloda tutulur; kiraci ayrimi TenantName kolonundadir.
|
||||||
|
using (currentTenant.Change(null))
|
||||||
|
{
|
||||||
|
var query = await ActiveRowsAsync(ResolveTenantName());
|
||||||
|
|
||||||
|
return query.Select(row => row.JsonData).FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IQueryable<DbTableColumn>> ActiveRowsAsync(string tenantName)
|
||||||
|
{
|
||||||
|
var query = await dictionaryRepository.GetQueryableAsync();
|
||||||
|
|
||||||
|
return query
|
||||||
|
.Where(row => row.DictionaryType == McpConsts.DictionaryType
|
||||||
|
&& row.IsActive
|
||||||
|
&& row.TenantName == tenantName)
|
||||||
|
.OrderByDescending(row => row.RefreshedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sozluk JSON'unun kok nesnesi.</summary>
|
||||||
|
private sealed class DictionaryDocument
|
||||||
|
{
|
||||||
|
public List<McpDictionaryListForm>? ListForms { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sozluk satirinin tazelik bilgisi.</summary>
|
||||||
|
public sealed record McpDictionarySnapshot(
|
||||||
|
string? CacheKey,
|
||||||
|
DateTime RefreshedAt,
|
||||||
|
int FormCount,
|
||||||
|
int FieldCount,
|
||||||
|
string? TenantName,
|
||||||
|
string? DefinitionCultureName,
|
||||||
|
string? UiCultureName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sozlukteki bir liste. Alan adlari <c>dbo.Sas_H_ListFormFieldsDictionaryJson</c> prosedurunun
|
||||||
|
/// urettigi JSON ile birebir ayni olmak zorundadir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpDictionaryListForm
|
||||||
|
{
|
||||||
|
public string? ListFormCode { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? TableName { get; set; }
|
||||||
|
public string? SqlSourceName { get; set; }
|
||||||
|
public string? SourceKind { get; set; }
|
||||||
|
public string? KeyFieldName { get; set; }
|
||||||
|
public string? SearchText { get; set; }
|
||||||
|
public List<McpDictionaryField>? Fields { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sozlukteki bir sutun.</summary>
|
||||||
|
public class McpDictionaryField
|
||||||
|
{
|
||||||
|
public string? Field { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
public string? SqlDataType { get; set; }
|
||||||
|
public bool IsVisible { get; set; }
|
||||||
|
public bool IsBooleanColumn { get; set; }
|
||||||
|
public string? LookupTableName { get; set; }
|
||||||
|
public string? LookupValueColumn { get; set; }
|
||||||
|
public string? LookupDisplayColumn { get; set; }
|
||||||
|
public bool IsSensitive { get; set; }
|
||||||
|
public bool IsDefaultSelectColumn { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Mcp.Entities;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istegin kiracisina dusen etkin sunucu kaydini cozer.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpServerConfigProvider
|
||||||
|
{
|
||||||
|
/// <summary>Etkin kayit; kiraciya acik aktif bir kayit yoksa null.</summary>
|
||||||
|
Task<McpServer?> GetEffectiveServerAsync();
|
||||||
|
|
||||||
|
/// <summary>Coklu deger tasiyan kolonu (kiraci/arac listesi) ogelerine ayirir.</summary>
|
||||||
|
IReadOnlyList<string> SplitMultiValue(string? value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpServerConfigProvider(
|
||||||
|
IRepository<McpServer, Guid> serverRepository,
|
||||||
|
ICurrentTenant currentTenant)
|
||||||
|
: DomainService, IMcpServerConfigProvider
|
||||||
|
{
|
||||||
|
public async Task<McpServer?> GetEffectiveServerAsync()
|
||||||
|
{
|
||||||
|
var tenantName = currentTenant.Name;
|
||||||
|
|
||||||
|
// Sunucu tanimlari host kapsaminda yasar; kiraci baglamindaki istek de ayni tabloyu
|
||||||
|
// okur, hangi kiracilara acik oldugu Tenants kolonunda durur.
|
||||||
|
List<McpServer> servers;
|
||||||
|
using (currentTenant.Change(null))
|
||||||
|
{
|
||||||
|
var query = await serverRepository.GetQueryableAsync();
|
||||||
|
servers = [.. query.Where(server => server.IsActive).OrderBy(server => server.Name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers.Find(server => IsAvailableForTenant(server.Tenants, tenantName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> SplitMultiValue(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value)
|
||||||
|
? []
|
||||||
|
: [.. value.Split(
|
||||||
|
PlatformConsts.MultiValueDelimiter,
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kaydin kiraci listesi bos ise tum kiracilara aciktir; doluysa kiraci adi listede
|
||||||
|
/// gecmelidir (AiBot ekraniyla ayni sozlesme).
|
||||||
|
/// </summary>
|
||||||
|
private bool IsAvailableForTenant(string? tenants, string? tenantName)
|
||||||
|
{
|
||||||
|
var allowed = SplitMultiValue(tenants);
|
||||||
|
if (allowed.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenantName is not null
|
||||||
|
&& allowed.Any(name => string.Equals(name, tenantName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Sozsoft.Platform.Localization;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salt-okunur sorgu kurali. Hem serbest SELECT hem kayitli nesne cagrisi buradan gecer:
|
||||||
|
/// ifadenin gercekten okuma oldugu dogrulanir, tanimlayici ve parametre adlari kalibi gecer,
|
||||||
|
/// satir tavani sorguya yazilir.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpSqlGuard
|
||||||
|
{
|
||||||
|
/// <summary>Tek bir okuma ifadesi oldugunu dogrular; degilse anlasilir bir hata atar.</summary>
|
||||||
|
void EnsureReadOnlyStatement(string sql);
|
||||||
|
|
||||||
|
/// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary>
|
||||||
|
string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType);
|
||||||
|
|
||||||
|
/// <summary>Saglayicinin varsayilan semasi.</summary>
|
||||||
|
string DefaultSchemaName(DataSourceTypeEnum dataSourceType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Satir tavanini sorgunun kendisine yazar: SQL Server'da <c>TOP (n)</c>, PostgreSQL'de
|
||||||
|
/// <c>LIMIT n</c>. Sorgu kendi sinirini zaten tasiyorsa dokunulmaz.
|
||||||
|
/// </summary>
|
||||||
|
string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemciden gelen parametre adlarini dogrular ve bastaki @ isaretini duserek Dapper'in
|
||||||
|
/// bekledigi bicime cevirir.
|
||||||
|
/// </summary>
|
||||||
|
Dictionary<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard
|
||||||
|
{
|
||||||
|
/// <summary>Tek parcali SQL tanimlayicisi; sema ve nesne adlari bu kalibi gecmek zorundadir.</summary>
|
||||||
|
private static readonly Regex SafeIdentifierRegex = new(
|
||||||
|
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex SafeParameterNameRegex = new(
|
||||||
|
@"^@?[A-Za-z_][A-Za-z0-9_]{0,127}$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex SqlCommentRegex = new(
|
||||||
|
@"--[^\r\n]*|/\*[\s\S]*?\*/",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex SqlStringLiteralRegex = new(
|
||||||
|
@"'(?:[^']|'')*'",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Okuma sozde kalmasi gereken ifadelerde yasakli anahtar kelimeler. Kontrol yorumlar ve
|
||||||
|
/// metin sabitleri cikarildiktan sonra yapilir; aksi halde bir yorum blogu ya da tirnak
|
||||||
|
/// icindeki metin yasakli bir kelimeyi gizleyebilir.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Regex ForbiddenStatementRegex = new(
|
||||||
|
@"\b(INSERT|UPDATE|DELETE|MERGE|UPSERT|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|DENY|BACKUP|RESTORE|SHUTDOWN|RECONFIGURE|EXEC|EXECUTE|WAITFOR|OPENROWSET|OPENDATASOURCE|OPENQUERY|BULK|INTO)\b",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex SelectPrefixRegex = new(
|
||||||
|
@"^\s*SELECT\s+(?:(?:DISTINCT|ALL)\s+)?",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex ExistingTopRegex = new(
|
||||||
|
@"^\s*SELECT\s+(?:(?:DISTINCT|ALL)\s+)?TOP\s*[(\s]",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex ExistingLimitRegex = new(
|
||||||
|
@"\bLIMIT\s+\d+",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
public void EnsureReadOnlyStatement(string sql)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sql))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(localizer[McpErrorCodes.EmptyQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stripped = SqlStringLiteralRegex.Replace(SqlCommentRegex.Replace(sql, " "), "''");
|
||||||
|
|
||||||
|
if (stripped.Contains(';', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(localizer[McpErrorCodes.SingleStatementOnly]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = stripped.TrimStart();
|
||||||
|
var startsWithRead = trimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| trimmed.StartsWith("WITH", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!startsWithRead)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(localizer[McpErrorCodes.ReadOnlyStatementRequired]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var forbidden = ForbiddenStatementRegex.Match(stripped);
|
||||||
|
if (forbidden.Success)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(
|
||||||
|
localizer[McpErrorCodes.ForbiddenKeyword, forbidden.Value.ToUpperInvariant()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType)
|
||||||
|
{
|
||||||
|
EnsureSafeIdentifier(schemaName);
|
||||||
|
EnsureSafeIdentifier(objectName);
|
||||||
|
|
||||||
|
return dataSourceType == DataSourceTypeEnum.Postgresql
|
||||||
|
? $"\"{schemaName}\".\"{objectName}\""
|
||||||
|
: $"[{schemaName}].[{objectName}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DefaultSchemaName(DataSourceTypeEnum dataSourceType)
|
||||||
|
=> dataSourceType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
|
||||||
|
|
||||||
|
public string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType)
|
||||||
|
{
|
||||||
|
if (dataSourceType == DataSourceTypeEnum.Postgresql)
|
||||||
|
{
|
||||||
|
return ExistingLimitRegex.IsMatch(sql) ? sql : $"{sql} LIMIT {maxRows}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExistingTopRegex.IsMatch(sql))
|
||||||
|
{
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = SelectPrefixRegex.Match(sql);
|
||||||
|
|
||||||
|
// WITH ile baslayan ya da beklenmeyen bicimdeki sorgularda sarmalama yapilmaz: SQL
|
||||||
|
// Server'da ORDER BY tasiyan bir alt sorgu gecersizdir. Sonuc kumesi okunurken yine
|
||||||
|
// maxRows satirda kesilir.
|
||||||
|
return match.Success
|
||||||
|
? string.Concat(sql.AsSpan(0, match.Length), $"TOP ({maxRows}) ", sql.AsSpan(match.Length))
|
||||||
|
: sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters)
|
||||||
|
{
|
||||||
|
var normalized = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (parameters is null)
|
||||||
|
{
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (key, value) in parameters)
|
||||||
|
{
|
||||||
|
if (key is null || !SafeParameterNameRegex.IsMatch(key))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidParameterName, key ?? string.Empty]);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized[key.TrimStart('@')] = value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureSafeIdentifier(string identifier)
|
||||||
|
{
|
||||||
|
if (!SafeIdentifierRegex.IsMatch(identifier ?? string.Empty))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier ?? string.Empty]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj" />
|
||||||
|
<!-- Veri sozlugu (DbTableColumn) ve dinamik veri erisimi platform domain'inde yasar. -->
|
||||||
|
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Domain\Sozsoft.Platform.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Mcp.Entities;
|
||||||
|
using Volo.Abp.Data;
|
||||||
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
[ConnectionStringName(McpDbProperties.ConnectionStringName)]
|
||||||
|
public class McpDbContext(DbContextOptions<McpDbContext> options)
|
||||||
|
: AbpDbContext<McpDbContext>(options)
|
||||||
|
{
|
||||||
|
public DbSet<McpServer> McpServers { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
builder.ConfigureMcp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Mcp.Entities;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.EntityFrameworkCore.Modeling;
|
||||||
|
using static Sozsoft.Platform.PlatformConsts;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public static class McpDbContextModelCreatingExtensions
|
||||||
|
{
|
||||||
|
public static void ConfigureMcp(this ModelBuilder builder)
|
||||||
|
{
|
||||||
|
Check.NotNull(builder, nameof(builder));
|
||||||
|
|
||||||
|
builder.Entity<McpServer>(b =>
|
||||||
|
{
|
||||||
|
// Tablo adi platformun tek kayit defterinden gelir (TableNameEnum.McpServer);
|
||||||
|
// modul ikinci bir adlandirma kurali tanimlamaz.
|
||||||
|
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.McpServer)), Prefix.DbSchema);
|
||||||
|
b.ConfigureByConvention();
|
||||||
|
|
||||||
|
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||||
|
b.Property(x => x.Url).HasMaxLength(512);
|
||||||
|
// Kiraci ve arac listeleri tek kolonda MultiValueDelimiter ile saklanir; ekranda
|
||||||
|
// dxTagBox ile doldurulur.
|
||||||
|
b.Property(x => x.Tenants).HasMaxLength(2048);
|
||||||
|
b.Property(x => x.AllowedTools).HasMaxLength(1024);
|
||||||
|
b.Property(x => x.DataSourceCode).HasMaxLength(64);
|
||||||
|
b.Property(x => x.MaxRows).HasDefaultValue(McpConsts.DefaultMaxRows);
|
||||||
|
b.Property(x => x.AllowWrite).HasDefaultValue(false);
|
||||||
|
b.Property(x => x.Description).HasMaxLength(1024);
|
||||||
|
|
||||||
|
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Sozsoft.Mcp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public static class McpDbProperties
|
||||||
|
{
|
||||||
|
public const string ConnectionStringName = "Mcp";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
[DependsOn(
|
||||||
|
typeof(McpDomainModule),
|
||||||
|
typeof(AbpEntityFrameworkCoreModule)
|
||||||
|
)]
|
||||||
|
public class McpEntityFrameworkCoreModule : AbpModule
|
||||||
|
{
|
||||||
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
context.Services.AddAbpDbContext<McpDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.AddDefaultRepositories(includeAllEntities: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
using Volo.Abp.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uzak MCP istemcilerinin baglanti kesfi. Istemci /mcp'den 401 alir, <c>WWW-Authenticate</c>
|
||||||
|
/// basligindaki meta veri adresinden yetkilendirme sunucusunu ogrenir, kayit ucundan istemci
|
||||||
|
/// kimligini alir ve kullaniciyi platformun giris ekranina yonlendirir.
|
||||||
|
/// Adresler ve yanit bicimi RFC'lerle sabittir; bu yuzden ABP'nin konvansiyonel uclari yerine
|
||||||
|
/// acik rotali bir controller kullanilir.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[IgnoreAntiforgeryToken]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public class McpOAuthController(
|
||||||
|
IOptions<McpOAuthOptions> options,
|
||||||
|
IMcpOAuthAppService oAuthAppService) : AbpControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>Korumali kaynak meta verisi (RFC 9728).</summary>
|
||||||
|
[HttpGet(McpConsts.ProtectedResourceMetadataPath)]
|
||||||
|
[HttpGet(McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath)]
|
||||||
|
public IActionResult GetProtectedResourceMetadata()
|
||||||
|
{
|
||||||
|
var value = options.Value;
|
||||||
|
|
||||||
|
return new JsonResult(new ProtectedResourceMetadata(
|
||||||
|
value.ResourceUrl,
|
||||||
|
[value.AuthorizationServer],
|
||||||
|
McpConsts.OAuthScopes,
|
||||||
|
["header"],
|
||||||
|
McpServerInfoDefaults.DisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dinamik istemci kaydi (RFC 7591).</summary>
|
||||||
|
[HttpPost(McpConsts.RegistrationEndpointPath)]
|
||||||
|
public async Task<IActionResult> RegisterAsync([FromBody] ClientRegistrationRequest request)
|
||||||
|
{
|
||||||
|
var result = await oAuthAppService.RegisterClientAsync(new McpClientRegistrationRequestDto
|
||||||
|
{
|
||||||
|
ClientName = request.ClientName,
|
||||||
|
RedirectUris = request.RedirectUris ?? []
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.Error is not null)
|
||||||
|
{
|
||||||
|
return new JsonResult(new ClientRegistrationError(result.Error, result.ErrorDescription))
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResult(new ClientRegistrationResponse(
|
||||||
|
result.ClientId!,
|
||||||
|
result.ClientIdIssuedAt,
|
||||||
|
result.ClientName,
|
||||||
|
result.RedirectUris,
|
||||||
|
result.GrantTypes,
|
||||||
|
result.ResponseTypes,
|
||||||
|
result.TokenEndpointAuthMethod,
|
||||||
|
result.Scope))
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status201Created
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protokol sozlesmeleri snake_case alan adlariyla sabittir; ABP'nin camelCase JSON
|
||||||
|
// politikasina birakilmaz.
|
||||||
|
|
||||||
|
public sealed record ClientRegistrationRequest(
|
||||||
|
[property: JsonPropertyName("client_name")] string? ClientName,
|
||||||
|
[property: JsonPropertyName("redirect_uris")] List<string>? RedirectUris);
|
||||||
|
|
||||||
|
private sealed record ClientRegistrationResponse(
|
||||||
|
[property: JsonPropertyName("client_id")] string ClientId,
|
||||||
|
[property: JsonPropertyName("client_id_issued_at")] long ClientIdIssuedAt,
|
||||||
|
[property: JsonPropertyName("client_name")] string? ClientName,
|
||||||
|
[property: JsonPropertyName("redirect_uris")] IReadOnlyList<string> RedirectUris,
|
||||||
|
[property: JsonPropertyName("grant_types")] IReadOnlyList<string> GrantTypes,
|
||||||
|
[property: JsonPropertyName("response_types")] IReadOnlyList<string> ResponseTypes,
|
||||||
|
[property: JsonPropertyName("token_endpoint_auth_method")] string TokenEndpointAuthMethod,
|
||||||
|
[property: JsonPropertyName("scope")] string? Scope);
|
||||||
|
|
||||||
|
private sealed record ClientRegistrationError(
|
||||||
|
[property: JsonPropertyName("error")] string Error,
|
||||||
|
[property: JsonPropertyName("error_description")] string? ErrorDescription);
|
||||||
|
|
||||||
|
private sealed record ProtectedResourceMetadata(
|
||||||
|
[property: JsonPropertyName("resource")] string Resource,
|
||||||
|
[property: JsonPropertyName("authorization_servers")] IReadOnlyList<string> AuthorizationServers,
|
||||||
|
[property: JsonPropertyName("scopes_supported")] IReadOnlyList<string> ScopesSupported,
|
||||||
|
[property: JsonPropertyName("bearer_methods_supported")] IReadOnlyList<string> BearerMethodsSupported,
|
||||||
|
[property: JsonPropertyName("resource_name")] string ResourceName);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /mcp'den donen 401 yanitina korumali kaynak meta verisinin adresini ekler (RFC 9728 §5.1).
|
||||||
|
/// Uzak MCP istemcisi OAuth akisini bu basliktan baslatir; baslik yoksa baglanti formu
|
||||||
|
/// "kimlik dogrulama desteklenmiyor" diye durur. Kimlik dogrulamadan once calismalidir:
|
||||||
|
/// basligi yanit baslamadan hemen once yazar, 401'i hangi katman uretmis olursa olsun.
|
||||||
|
/// </summary>
|
||||||
|
public class McpAuthenticationChallengeMiddleware(IOptions<McpOAuthOptions> options)
|
||||||
|
: IMiddleware, ITransientDependency
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||||
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments(McpConsts.EndpointPath))
|
||||||
|
{
|
||||||
|
context.Response.OnStarting(() =>
|
||||||
|
{
|
||||||
|
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
|
||||||
|
{
|
||||||
|
context.Response.Headers[HeaderNames.WWWAuthenticate] =
|
||||||
|
$"Bearer resource_metadata=\"{options.Value.ProtectedResourceMetadataUrl}\", " +
|
||||||
|
$"scope=\"{string.Join(' ', McpConsts.OAuthScopes)}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ModelContextProtocol.AspNetCore;
|
||||||
|
using Sozsoft.Mcp.Tools;
|
||||||
|
using Volo.Abp.AspNetCore.Mvc;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /mcp ucunun sunum katmani. Araclar yalnizca uygulama sozlesmesini tanir; kimlik dogrulama,
|
||||||
|
/// kiraci cozumu, unit of work ve yetki kontrolu istegin gectigi ABP ardisik duzeninden gelir.
|
||||||
|
/// Ucun kendisi host tarafinda <c>MapMcp</c> ile baglanir.
|
||||||
|
/// </summary>
|
||||||
|
[DependsOn(
|
||||||
|
typeof(McpApplicationContractsModule),
|
||||||
|
typeof(AbpAspNetCoreMvcModule)
|
||||||
|
)]
|
||||||
|
public class McpHttpApiModule : AbpModule
|
||||||
|
{
|
||||||
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
var configuration = context.Services.GetConfiguration();
|
||||||
|
|
||||||
|
Configure<McpOAuthOptions>(options =>
|
||||||
|
{
|
||||||
|
options.BaseUrl = (configuration["App:SelfUrl"] ?? string.Empty).TrimEnd('/');
|
||||||
|
options.AuthorizationServer = (configuration["AuthServer:Authority"] ?? string.Empty).TrimEnd('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
context.Services
|
||||||
|
.AddMcpServer(options =>
|
||||||
|
{
|
||||||
|
options.ServerInfo = new() { Name = McpServerInfoDefaults.Name, Version = McpServerInfoDefaults.Version };
|
||||||
|
options.ServerInstructions = McpServerInfoDefaults.Instructions;
|
||||||
|
})
|
||||||
|
.WithHttpTransport(options =>
|
||||||
|
{
|
||||||
|
// Durumsuz kip: her istek kendi ABP kapsaminda kosar, boylece kimlik, kiraci
|
||||||
|
// cozumu ve unit of work istekle birlikte dogru kurulur. Durumlu kipte oturum
|
||||||
|
// kapsami initialize istegine baglanir ve sonraki cagrilar yanlis kiracinin
|
||||||
|
// baglaminda kalabilir; ayrica ters vekil arkasinda oturum yapisakligi gerekmez.
|
||||||
|
options.SessionMode = HttpServerSessionMode.Stateless;
|
||||||
|
})
|
||||||
|
.WithTools<McpSchemaTools>()
|
||||||
|
.WithTools<McpQueryTools>()
|
||||||
|
.WithTools<McpDataTools>()
|
||||||
|
.WithRequestFilters(filters => filters.AddListToolsFilter(next => async (requestContext, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var result = await next(requestContext, cancellationToken);
|
||||||
|
|
||||||
|
// Arac listesi kiracinin sunucu kaydina ve kullanicinin uygulamadaki yetkilerine
|
||||||
|
// gore suzulur. Cagri aninda da ayni kural uygulanir
|
||||||
|
// (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da
|
||||||
|
// cagirabilir, suzme tek basina bir yetki siniri degildir.
|
||||||
|
var schemaAppService = requestContext.Services!.GetRequiredService<IMcpSchemaAppService>();
|
||||||
|
var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal);
|
||||||
|
|
||||||
|
result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /mcp ucunun OAuth adresleri. Degerler ortam yapilandirmasindan gelir (<c>App:SelfUrl</c>,
|
||||||
|
/// <c>AuthServer:Authority</c>); localhost, dev ve production ayni kodla calisir.
|
||||||
|
/// </summary>
|
||||||
|
public class McpOAuthOptions
|
||||||
|
{
|
||||||
|
/// <summary>API'nin disaridan erisilen kok adresi, sonunda egik cizgi olmadan.</summary>
|
||||||
|
public string BaseUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Token'i veren yetkilendirme sunucusu (OpenIddict), sonunda egik cizgi olmadan.</summary>
|
||||||
|
public string AuthorizationServer { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Korumali kaynagin tanimlayicisi: istemcinin baglandigi /mcp adresi.</summary>
|
||||||
|
public string ResourceUrl => BaseUrl + McpConsts.EndpointPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kaynaga ozgu meta veri adresi (RFC 9728 §3.1: yol, well-known onekinin arkasina eklenir).
|
||||||
|
/// </summary>
|
||||||
|
public string ProtectedResourceMetadataUrl => BaseUrl + McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemciye <c>initialize</c> yanitinda bildirilen sunucu kimligi ve yonerge metni.
|
||||||
|
/// Yonerge modele hitap eder, son kullaniciya degil; bu yuzden dil anahtari uzerinden
|
||||||
|
/// gecmez — protokol metnidir, arayuz metni degil.
|
||||||
|
/// </summary>
|
||||||
|
internal static class McpServerInfoDefaults
|
||||||
|
{
|
||||||
|
public const string Name = "sozsoft-platform";
|
||||||
|
|
||||||
|
/// <summary>Connector ekraninda gorunen kaynak adi.</summary>
|
||||||
|
public const string DisplayName = "Sozsoft Platform";
|
||||||
|
|
||||||
|
public const string Version = "1.0.0";
|
||||||
|
|
||||||
|
public const string Instructions =
|
||||||
|
"Sozsoft Platform data access. Do not guess table or column names: first find the list with " +
|
||||||
|
"list_lists or search_schema, read its columns with describe_list, then query it with run_select " +
|
||||||
|
"using named @parameters. Never inline literal values into SQL. Columns marked isSensitive must " +
|
||||||
|
"not be selected. Call server_info first when a tool behaves unexpectedly.";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sozsoft.Mcp</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- /mcp ucunun Streamable HTTP tasiyicisi. -->
|
||||||
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Sunum katmani: araclar yalnizca uygulama sozlesmesini tanir, Domain'i gormez. -->
|
||||||
|
<ProjectReference Include="..\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri yazma araclari. Yazma bir ListForm ekraninin sozlesmesinden gecer; ekran yetkisi ve
|
||||||
|
/// tenant filtresi aynen uygulanir. Sunucu kaydinda <c>AllowWrite</c> kapaliysa bu araclar
|
||||||
|
/// arac listesinde hic gorunmez.
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public sealed class McpDataTools(IMcpDataAppService dataAppService)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = McpToolNames.InsertRow, Destructive = false, Idempotent = false)]
|
||||||
|
[Description("Bir listeye yeni kayit ekler. Alan adlari describe_list cikisindaki field degerleridir.")]
|
||||||
|
public async Task<McpWriteResultDto> InsertRowAsync(
|
||||||
|
[Description("Kaydin eklenecegi listenin kodu.")]
|
||||||
|
string listFormCode,
|
||||||
|
[Description("Alan adi -> deger eslemesi.")]
|
||||||
|
Dictionary<string, object?> values)
|
||||||
|
=> await dataAppService.InsertRowAsync(new McpRowRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = listFormCode,
|
||||||
|
Values = values ?? []
|
||||||
|
});
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.UpdateRow, Destructive = true, Idempotent = true)]
|
||||||
|
[Description("Bir listedeki kaydi anahtar degerine gore gunceller. Yalnizca verilen alanlar yazilir.")]
|
||||||
|
public async Task<McpWriteResultDto> UpdateRowAsync(
|
||||||
|
[Description("Kaydin bulundugu listenin kodu.")]
|
||||||
|
string listFormCode,
|
||||||
|
[Description("Guncellenecek kaydin anahtar alan degeri; describe_list cikisindaki keyFieldName alanina karsilik gelir.")]
|
||||||
|
List<object?> keys,
|
||||||
|
[Description("Alan adi -> yeni deger eslemesi.")]
|
||||||
|
Dictionary<string, object?> values)
|
||||||
|
=> await dataAppService.UpdateRowAsync(new McpRowRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = listFormCode,
|
||||||
|
Keys = keys ?? [],
|
||||||
|
Values = values ?? []
|
||||||
|
});
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.DeleteRow, Destructive = true, Idempotent = true)]
|
||||||
|
[Description("Bir listedeki kaydi anahtar degerine gore siler. Ekranin kendi silme komutu kullanilir; soft delete tasiyan tablolarda kayit pasife cekilir.")]
|
||||||
|
public async Task<McpWriteResultDto> DeleteRowAsync(
|
||||||
|
[Description("Kaydin bulundugu listenin kodu.")]
|
||||||
|
string listFormCode,
|
||||||
|
[Description("Silinecek kaydin anahtar alan degeri.")]
|
||||||
|
List<object?> keys)
|
||||||
|
=> await dataAppService.DeleteRowAsync(new McpRowRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = listFormCode,
|
||||||
|
Keys = keys ?? []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>Salt-okunur sorgu araclari.</summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = McpToolNames.QueryList, ReadOnly = true)]
|
||||||
|
[Description("Bir listenin verisini uygulamadaki ekranla ayni sorguyla okur; kullanicinin liste ve alan yetkileri, kiraci ve sube filtreleri gecerlidir. Veri okumak icin once bunu kullan. Alan adlari describe_list cikisindaki field degerleridir. Filtre/siralama/gruplama DevExtreme bicimindedir.")]
|
||||||
|
public async Task<McpListQueryResultDto> QueryListAsync(
|
||||||
|
[Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")]
|
||||||
|
string listFormCode,
|
||||||
|
[Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")]
|
||||||
|
string? filter = null,
|
||||||
|
[Description("Siralama, JSON metni. Ornek: [{\"selector\":\"CreationTime\",\"desc\":true}].")]
|
||||||
|
string? sort = null,
|
||||||
|
[Description("Gruplama, JSON metni. Ornek: [{\"selector\":\"CustomerId\",\"desc\":false,\"isExpanded\":false}]. Gruplu sorguda satirlar yerine grup anahtarlari ve ozetleri doner.")]
|
||||||
|
string? group = null,
|
||||||
|
[Description("Grup ozetleri, JSON metni. Ornek: [{\"selector\":\"Id\",\"summaryType\":\"count\"},{\"selector\":\"Amount\",\"summaryType\":\"sum\"}]. Tipler: count, sum, min, max, avg.")]
|
||||||
|
string? groupSummary = null,
|
||||||
|
[Description("Tum sonuc icin ozet, JSON metni. Ornek: [{\"selector\":\"Amount\",\"summaryType\":\"sum\"}].")]
|
||||||
|
string? totalSummary = null,
|
||||||
|
[Description("Atlanacak kayit sayisi (sayfalama).")]
|
||||||
|
int skip = 0,
|
||||||
|
[Description("Istenen kayit sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
|
||||||
|
int take = 0)
|
||||||
|
=> await queryAppService.QueryListAsync(new McpListQueryRequestDto
|
||||||
|
{
|
||||||
|
ListFormCode = listFormCode,
|
||||||
|
Filter = filter,
|
||||||
|
Sort = sort,
|
||||||
|
Group = group,
|
||||||
|
GroupSummary = groupSummary,
|
||||||
|
TotalSummary = totalSummary,
|
||||||
|
Skip = skip,
|
||||||
|
Take = take
|
||||||
|
});
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.RunSelect, ReadOnly = true)]
|
||||||
|
[Description("Yalnizca uygulamada SQL Query Manager yetkisi olan kullanicilar icin: tek bir parametreli SELECT ifadesi kosturur. Liste verisi icin query_list tercih edilir. Coklu ifade, DDL ve DML reddedilir; satir sayisi sunucu kaydindaki tavana kirpilir. Sorguya deger gomme, @ad parametresi kullan.")]
|
||||||
|
public async Task<McpQueryResultDto> RunSelectAsync(
|
||||||
|
[Description("Tek bir SELECT (ya da WITH ... SELECT) ifadesi. Degerler @ad bicimindeki parametrelerle verilir.")]
|
||||||
|
string sql,
|
||||||
|
[Description("Sorgudaki adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")]
|
||||||
|
Dictionary<string, object?>? parameters = null,
|
||||||
|
[Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
|
||||||
|
int maxRows = 0)
|
||||||
|
=> await queryAppService.RunSelectAsync(new McpSelectRequestDto
|
||||||
|
{
|
||||||
|
Sql = sql,
|
||||||
|
Parameters = parameters ?? [],
|
||||||
|
MaxRows = maxRows
|
||||||
|
});
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.ListQueries, ReadOnly = true)]
|
||||||
|
[Description("Veri kaynagindaki kayitli nesneleri (view, stored procedure, function) ve cagrilabilir Custom Endpoint'leri dondurur. run_query bunlardan birini cagirir.")]
|
||||||
|
public async Task<List<McpSavedQueryDto>> ListQueriesAsync(
|
||||||
|
[Description("Nesne adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")]
|
||||||
|
string? search = null)
|
||||||
|
=> await queryAppService.GetSavedQueriesAsync(search);
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.RunQuery, ReadOnly = true)]
|
||||||
|
[Description("Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir. Nesne turu ve adi list_queries cikisindan alinir.")]
|
||||||
|
public async Task<McpQueryResultDto> RunQueryAsync(
|
||||||
|
[Description("Nesne turu: view, procedure, tableFunction, scalarFunction ya da customEndpoint.")]
|
||||||
|
string kind,
|
||||||
|
[Description("Nesnenin ya da Custom Endpoint'in adi.")]
|
||||||
|
string name,
|
||||||
|
[Description("Nesnenin semasi. Bos birakilirsa SQL Server'da dbo, PostgreSQL'de public kullanilir.")]
|
||||||
|
string? schemaName = null,
|
||||||
|
[Description("Adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")]
|
||||||
|
Dictionary<string, object?>? parameters = null,
|
||||||
|
[Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
|
||||||
|
int maxRows = 0)
|
||||||
|
=> await queryAppService.RunSavedQueryAsync(new McpSavedQueryRequestDto
|
||||||
|
{
|
||||||
|
Kind = kind,
|
||||||
|
Name = name,
|
||||||
|
SchemaName = schemaName,
|
||||||
|
Parameters = parameters ?? [],
|
||||||
|
MaxRows = maxRows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using Sozsoft.Mcp.Dtos;
|
||||||
|
|
||||||
|
namespace Sozsoft.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sema kesfi araclari. Tablo ve sutun bilgisi canli katalogdan degil, platformun ekran
|
||||||
|
/// tanimlarindan uretilen veri sozlugunden gelir; boylece modelin gordugu adlar kullanicinin
|
||||||
|
/// ekranda gordugu adlarla ayni olur.
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = McpToolNames.ServerInfo, ReadOnly = true)]
|
||||||
|
[Description("Bu baglantinin kiracisini, acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")]
|
||||||
|
public async Task<McpServerInfoResult> ServerInfoAsync()
|
||||||
|
{
|
||||||
|
var config = await schemaAppService.GetServerConfigAsync();
|
||||||
|
var schema = await schemaAppService.GetSchemaInfoAsync();
|
||||||
|
var availableTools = await schemaAppService.GetAvailableToolsAsync();
|
||||||
|
|
||||||
|
return new McpServerInfoResult(config, schema, availableTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)]
|
||||||
|
[Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner.")]
|
||||||
|
public async Task<List<McpListSummaryDto>> ListListsAsync(
|
||||||
|
[Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")]
|
||||||
|
string? search = null,
|
||||||
|
[Description("Donecek en fazla liste sayisi. Varsayilan 200.")]
|
||||||
|
int maxResults = 200)
|
||||||
|
=> await schemaAppService.GetListsAsync(search, maxResults);
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.DescribeList, ReadOnly = true)]
|
||||||
|
[Description("Tek bir listenin sutunlarini dondurur: alan adi, kullaniciya gorunen basligi, SQL tipi, lookup iliskisi ve sutunun hassas olup olmadigi.")]
|
||||||
|
public async Task<McpListDetailDto?> DescribeListAsync(
|
||||||
|
[Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")]
|
||||||
|
string listFormCode)
|
||||||
|
=> await schemaAppService.GetListAsync(listFormCode);
|
||||||
|
|
||||||
|
[McpServerTool(Name = McpToolNames.SearchSchema, ReadOnly = true)]
|
||||||
|
[Description("Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama yapar; hangi listenin hangi sutununun arandigini bulmak icin kullanilir.")]
|
||||||
|
public async Task<List<McpSchemaMatchDto>> SearchSchemaAsync(
|
||||||
|
[Description("Aranacak metin. Turkce karakterler ve aksanlar onemsizdir.")]
|
||||||
|
string term,
|
||||||
|
[Description("Donecek en fazla eslesme sayisi. Varsayilan 50.")]
|
||||||
|
int maxResults = 50)
|
||||||
|
=> await schemaAppService.SearchAsync(term, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="Server">Istegin kiracisina dusen sunucu kaydi; yoksa null.</param>
|
||||||
|
/// <param name="Schema">Veri sozlugunun tazelik bilgisi.</param>
|
||||||
|
/// <param name="AvailableTools">Bu baglantida cagrilabilen araclar.</param>
|
||||||
|
public sealed record McpServerInfoResult(
|
||||||
|
McpServerConfigDto? Server,
|
||||||
|
McpSchemaInfoDto Schema,
|
||||||
|
IReadOnlyList<string> AvailableTools);
|
||||||
8
api/modules/Sozsoft.Mcp/common.props
Normal file
8
api/modules/Sozsoft.Mcp/common.props
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
<AbpProjectType>module</AbpProjectType>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -10,6 +10,6 @@ public class NotificationFilterRequestDto : PagedAndSortedResultRequestDto
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// İstek; aktivite sayfasındaki liste ekranından mı geliyor yoksa bildirimleri kontrol etmek için mi
|
/// İstek; aktivite sayfasındaki liste ekranından mı geliyor yoksa bildirimleri kontrol etmek için mi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsListRequest { get; set; } = false;
|
public bool IsListRequest { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Sozsoft.Notifications.Application;
|
|
||||||
|
|
||||||
public class NotificationRemoteServiceConsts
|
|
||||||
{
|
|
||||||
public const string RemoteServiceName = "Notifications";
|
|
||||||
|
|
||||||
public const string ModuleName = "notifications";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Volo.Abp.Authorization.Permissions;
|
using Volo.Abp.Authorization.Permissions;
|
||||||
|
|
||||||
namespace Sozsoft.Notifications.Permissions;
|
namespace Sozsoft.Notifications.Permissions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Sozsoft.Notifications.Domain;
|
using Sozsoft.Notifications.Domain;
|
||||||
using Sozsoft.Notifications.Entities;
|
using Sozsoft.Notifications.Entities;
|
||||||
using Sozsoft.Notifications.Enums;
|
using Sozsoft.Notifications.Enums;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using Sozsoft.Notifications.Entities;
|
using Riok.Mapperly.Abstractions;
|
||||||
using Sozsoft.Notifications.Domain;
|
using Sozsoft.Notifications.Domain;
|
||||||
|
using Sozsoft.Notifications.Entities;
|
||||||
using Sozsoft.Notifications.NotificationRules;
|
using Sozsoft.Notifications.NotificationRules;
|
||||||
using Riok.Mapperly.Abstractions;
|
|
||||||
using Volo.Abp.Mapperly;
|
using Volo.Abp.Mapperly;
|
||||||
|
|
||||||
namespace Sozsoft.Notifications.Application;
|
namespace Sozsoft.Notifications.Application;
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Sozsoft.Notifications.Application;
|
||||||
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
|
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
|
||||||
public partial class NotificationTypeToNotificationTypeDtoMapper : MapperBase<NotificationType, NotificationTypeDto>
|
public partial class NotificationTypeToNotificationTypeDtoMapper : MapperBase<NotificationType, NotificationTypeDto>
|
||||||
{
|
{
|
||||||
public override partial NotificationTypeDto Map(NotificationType source);
|
public override partial NotificationTypeDto Map(NotificationType source);
|
||||||
|
|
||||||
public override partial void Map(NotificationType source, NotificationTypeDto destination);
|
public override partial void Map(NotificationType source, NotificationTypeDto destination);
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ public override partial NotificationTypeDto Map(NotificationType source);
|
||||||
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
|
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
|
||||||
public partial class NotificationRuleToNotificationRuleDtoMapper : MapperBase<NotificationRule, NotificationRuleDto>
|
public partial class NotificationRuleToNotificationRuleDtoMapper : MapperBase<NotificationRule, NotificationRuleDto>
|
||||||
{
|
{
|
||||||
public override partial NotificationRuleDto Map(NotificationRule source);
|
public override partial NotificationRuleDto Map(NotificationRule source);
|
||||||
|
|
||||||
public override partial void Map(NotificationRule source, NotificationRuleDto destination);
|
public override partial void Map(NotificationRule source, NotificationRuleDto destination);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.Notifications.Domain;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Sozsoft.Notifications.Domain;
|
||||||
using Volo.Abp.Application;
|
using Volo.Abp.Application;
|
||||||
using Volo.Abp.Mapperly;
|
using Volo.Abp.Mapperly;
|
||||||
using Volo.Abp.Modularity;
|
using Volo.Abp.Modularity;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Sozsoft.Notifications.Domain;
|
|
||||||
using Sozsoft.Notifications.Entities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Notifications.Domain;
|
||||||
|
using Sozsoft.Notifications.Entities;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
@ -23,22 +23,16 @@ public class NotificationRuleAppService : CrudAppService<
|
||||||
INotificationRuleAppService
|
INotificationRuleAppService
|
||||||
{
|
{
|
||||||
private readonly IRepository<NotificationType, Guid> repositoryType;
|
private readonly IRepository<NotificationType, Guid> repositoryType;
|
||||||
private readonly IOrganizationUnitRepository repositoryOrganizationUnit;
|
|
||||||
private readonly IdentityUserManager userManager;
|
|
||||||
private readonly IIdentityUserRepository repositoryUser;
|
private readonly IIdentityUserRepository repositoryUser;
|
||||||
|
|
||||||
public NotificationRuleAppService(
|
public NotificationRuleAppService(
|
||||||
IRepository<NotificationType, Guid> repositoryType,
|
IRepository<NotificationType, Guid> repositoryType,
|
||||||
IRepository<NotificationRule, Guid> repository,
|
IRepository<NotificationRule, Guid> repository,
|
||||||
IdentityUserManager userManager,
|
IIdentityUserRepository repositoryUser
|
||||||
IIdentityUserRepository repositoryUser,
|
|
||||||
IOrganizationUnitRepository repositoryOrganizationUnit
|
|
||||||
) : base(repository)
|
) : base(repository)
|
||||||
{
|
{
|
||||||
this.repositoryType = repositoryType;
|
this.repositoryType = repositoryType;
|
||||||
this.userManager = userManager;
|
|
||||||
this.repositoryUser = repositoryUser;
|
this.repositoryUser = repositoryUser;
|
||||||
this.repositoryOrganizationUnit = repositoryOrganizationUnit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string[]> GetNotificationTypes()
|
public async Task<string[]> GetNotificationTypes()
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ public static class NotificationChannels
|
||||||
public const string Sms = "Sms"; //SMS (ABP Sms + Posta Guvercini)
|
public const string Sms = "Sms"; //SMS (ABP Sms + Posta Guvercini)
|
||||||
public const string Mail = "Mail"; //Email (ABP Emailing + Amazon SES)
|
public const string Mail = "Mail"; //Email (ABP Emailing + Amazon SES)
|
||||||
public const string Rocket = "Rocket"; //Rocket.Chat (HTTP API)
|
public const string Rocket = "Rocket"; //Rocket.Chat (HTTP API)
|
||||||
public const string Desktop = "Desktop";
|
public const string Desktop = "Desktop";
|
||||||
public const string UiActivity = "UiActivity"; //UI Activity (ABP UI Activity)
|
public const string UiActivity = "UiActivity"; //UI Activity (ABP UI Activity)
|
||||||
public const string UiToast = "UiToast"; //UI Toast (ABP UI Toast) Ayarlar/Sistem/Bildirimler/Chrome açık olması gerekiyor.
|
public const string UiToast = "UiToast"; //UI Toast (ABP UI Toast) Ayarlar/Sistem/Bildirimler/Chrome açık olması gerekiyor.
|
||||||
public const string WhatsApp = "WhatsApp"; //WhatsApp (HTTP API, template-based)
|
public const string WhatsApp = "WhatsApp"; //WhatsApp (HTTP API, template-based)
|
||||||
// public const string Telegram = "Telegram";
|
public const string Telegram = "Telegram"; //Telegram (Bot API, chat based)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace Sozsoft.Notifications.Domain;
|
|
||||||
|
|
||||||
public static class NotificationErrorCodes
|
|
||||||
{
|
|
||||||
//Add your business exception error codes here...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Domain.Entities.Auditing;
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
|
||||||
namespace Sozsoft.Notifications.Entities;
|
namespace Sozsoft.Notifications.Entities;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ namespace Sozsoft.Notifications.Entities;
|
||||||
public class NotificationRule : FullAuditedEntity<Guid>
|
public class NotificationRule : FullAuditedEntity<Guid>
|
||||||
{
|
{
|
||||||
public required Guid NotificationTypeId { get; set; }
|
public required Guid NotificationTypeId { get; set; }
|
||||||
public NotificationType NotificationType { get; set; }
|
public NotificationType NotificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alıcı Tipi: User, Role, Organization, All, Custom
|
/// Alıcı Tipi: User, Role, Organization, All, Custom
|
||||||
/// User ise RecipientId UserId olacak
|
/// User ise RecipientId UserId olacak
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class NotificationIdentifierProvider : INotificationIdentifierProvider
|
||||||
NotificationChannels.UiActivity => user.Id.ToString(),
|
NotificationChannels.UiActivity => user.Id.ToString(),
|
||||||
NotificationChannels.UiToast => user.Id.ToString(),
|
NotificationChannels.UiToast => user.Id.ToString(),
|
||||||
NotificationChannels.WhatsApp => user.PhoneNumber,
|
NotificationChannels.WhatsApp => user.PhoneNumber,
|
||||||
// NotificationChannels.Telegram => user.PhoneNumber,
|
NotificationChannels.Telegram => user.GetProperty<string?>("TelegramChatId"),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Sozsoft.Notifications.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Notifications.Entities;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Identity;
|
using Volo.Abp.Identity;
|
||||||
|
|
@ -192,9 +192,9 @@ public class NotificationManager : INotificationManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationType = await repositoryType.FirstOrDefaultAsync(a => a.Name == NotificationType)
|
var notificationType = await repositoryType.FirstOrDefaultAsync(a => a.Name == NotificationType)
|
||||||
?? throw new Exception($"NotificationType '{NotificationType}' not found.");
|
?? throw new Exception($"NotificationType '{NotificationType}' not found.");
|
||||||
|
|
||||||
await repositoryNotification.InsertAsync(new Notification
|
await repositoryNotification.InsertAsync(new Notification
|
||||||
{
|
{
|
||||||
NotificationTypeId = notificationType.Id,
|
NotificationTypeId = notificationType.Id,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Sozsoft.Notifications.Domain;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Sozsoft.Notifications.Domain;
|
||||||
using Sozsoft.Notifications.Entities;
|
using Sozsoft.Notifications.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
using Volo.Abp.EntityFrameworkCore;
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Sozsoft.Notifications.Domain;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Sozsoft.Notifications.Domain;
|
||||||
using Volo.Abp.EntityFrameworkCore;
|
using Volo.Abp.EntityFrameworkCore;
|
||||||
using Volo.Abp.Modularity;
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
using Volo.Abp.Localization;
|
using Volo.Abp.Localization;
|
||||||
|
|
||||||
namespace Sozsoft.Sender.Localization
|
namespace Sozsoft.Sender.Localization;
|
||||||
|
|
||||||
|
[LocalizationResourceName("SozsoftSender")]
|
||||||
|
public class ErpSenderResource
|
||||||
{
|
{
|
||||||
[LocalizationResourceName("SozsoftSender")]
|
|
||||||
public class ErpSenderResource
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue