# Technology Stack

**Project:** SharePoint Toolbox v2
**Researched:** 2026-04-08 (updated for v2.2 milestone)

---

## v2.2 Stack Additions

This section covers only the NEW capability needs for v2.2 (Report Branding + User Directory). The full existing stack is documented in the section below. The short answer: **no new NuGet packages are needed for either feature.**

---

### Feature 1: HTML Report Branding (Logo Embedding)

**Requirement:** Embed MSP logo (global) and client logo (per-tenant) into the self-contained HTML reports that already exist.

#### Approach: Base64 data URI — BCL only

The existing HTML export services (`HtmlExportService`, `UserAccessHtmlExportService`, etc.) produce fully self-contained HTML files using `StringBuilder` with all CSS and JS inlined. Logo images follow the same pattern: convert image bytes to a Base64 string and embed as an HTML `<img>` data URI.

```csharp
// In a LogoEmbedHelper or directly in each export service:
byte[] bytes = await File.ReadAllBytesAsync(logoFilePath, ct);
string mime  = Path.GetExtension(logoFilePath).ToLowerInvariant() switch
{
    ".png"  => "image/png",
    ".jpg"  => "image/jpeg",
    ".jpeg" => "image/jpeg",
    ".gif"  => "image/gif",
    ".svg"  => "image/svg+xml",
    ".webp" => "image/webp",
    _       => "image/png"
};
string dataUri = $"data:{mime};base64,{Convert.ToBase64String(bytes)}";
// In HTML: <img src="{dataUri}" alt="Logo" style="height:48px;" />
```

**Why this approach:**
- Zero new dependencies. `File.ReadAllBytesAsync`, `Convert.ToBase64String`, and `Path.GetExtension` are all BCL.
- The existing "no external dependencies" constraint on HTML reports is preserved.
- Self-contained EXE constraint is preserved — no logo file paths can break because the bytes are embedded in the HTML at export time.
- Base64 increases image size by ~33% but logos are small (< 50 KB typical); the impact on HTML file size is negligible.

**Logo storage strategy — store file path, embed at export time:**

Store the logo file path (not the base64) in `AppSettings` (global MSP logo) and `TenantProfile` (per-client logo). At export time, the export service reads the file and embeds it. This keeps JSON settings files small and lets the user swap logos without re-entering settings.

- `AppSettings.MspLogoPath: string?` — path to MSP logo file
- `TenantProfile.ClientLogoPath: string?` — path to client logo file for this tenant

The settings UI uses WPF `OpenFileDialog` (already used in multiple ViewModels) to browse for image files — filter to `*.png;*.jpg;*.jpeg;*.gif;*.svg`.

**Logo preview in WPF UI:** Use `BitmapImage` (built into `System.Windows.Media.Imaging`, already in scope for any WPF project). Bind a WPF `Image` control's `Source` to a `BitmapImage` loaded from the file path.

```csharp
// In ViewModel — logo preview
[ObservableProperty]
private BitmapImage? _mspLogoPreview;

partial void OnMspLogoPathChanged(string? value)
{
    if (string.IsNullOrWhiteSpace(value) || !File.Exists(value))
    {
        MspLogoPreview = null;
        return;
    }
    var bmp = new BitmapImage();
    bmp.BeginInit();
    bmp.UriSource    = new Uri(value, UriKind.Absolute);
    bmp.CacheOption  = BitmapCacheOption.OnLoad; // close file handle immediately
    bmp.EndInit();
    MspLogoPreview = bmp;
}
```

**No new library needed:** `BitmapImage` lives in the WPF `PresentationCore` assembly, which is already a transitive dependency of any `<UseWPF>true</UseWPF>` project.

---

### Feature 2: User Directory Browse Mode (Graph API)

**Requirement:** In the User Access Audit tab, add a "Browse" mode alternative to the people-picker search. Shows a paginated list of all users in the tenant — no search query, just the full directory — allowing the admin to pick users by scrolling/filtering locally.

#### Graph API endpoint: GET /users (no filter)

The existing `GraphUserSearchService` calls `GET /users?$filter=startsWith(...)` with `ConsistencyLevel: eventual`. Full directory listing removes the `$filter` and uses `$select` for the fields needed.

**Minimum required fields for directory browse:**

```
displayName, userPrincipalName, mail, jobTitle, department, userType, accountEnabled
```

- `userType`: distinguish `"Member"` from `"Guest"` — useful for MSP admin context
- `accountEnabled`: allow filtering out disabled accounts
- `jobTitle` / `department`: helps admin identify the right user in large directories

**Permissions required (confirmed from Microsoft Learn):**

| Scope type | Minimum permission |
|---|---|
| Delegated (work/school) | `User.Read.All` |

The existing auth uses `https://graph.microsoft.com/.default` which resolves to whatever scopes the Azure AD app registration has consented. If the MSP's app has `User.Read.All` consented (required for the existing people-picker to work), no new permission is needed — `GET /users` without `$filter` uses the same `User.Read.All` scope.

**Pagination — PageIterator pattern:**

`GET /users` returns a default page size of 100 with a maximum of 999 via `$top`. For tenants with hundreds or thousands of users, pagination via `@odata.nextLink` is mandatory.

The `Microsoft.Graph` 5.x SDK (already installed at 5.74.0) includes `PageIterator<TEntity, TCollectionResponse>` in `Microsoft.Graph.Core`. No version upgrade required.

```csharp
// In a new IGraphUserDirectoryService / GraphUserDirectoryService:
var firstPage = await graphClient.Users.GetAsync(config =>
{
    config.QueryParameters.Select = new[]
    {
        "displayName", "userPrincipalName", "mail",
        "jobTitle", "department", "userType", "accountEnabled"
    };
    config.QueryParameters.Top   = 999; // max page size
    config.QueryParameters.Orderby = new[] { "displayName" };
    config.Headers.Add("ConsistencyLevel", "eventual");
    config.QueryParameters.Count = true; // required for $orderby with eventual
}, ct);

var allUsers = new List<DirectoryUserResult>();

var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(
    graphClient,
    firstPage,
    user =>
    {
        if (user.AccountEnabled == true) // optionally skip disabled
            allUsers.Add(new DirectoryUserResult(
                user.DisplayName ?? user.UserPrincipalName ?? string.Empty,
                user.UserPrincipalName ?? string.Empty,
                user.Mail,
                user.JobTitle,
                user.Department,
                user.UserType == "Guest"));
        return true; // continue iteration
    });

await pageIterator.IterateAsync(ct);
```

**Why PageIterator over manual nextLink loop:**

The Graph SDK's `PageIterator` correctly handles the `DirectoryPageTokenNotFoundException` pitfall — it uses the token from the last successful non-retry response for the next page request. Manual loops using `withUrl(nextLink)` are susceptible to this error if any retry occurs mid-iteration. The SDK pattern is the documented recommendation (Microsoft Learn, updated 2025-08-06).

**Performance consideration — large tenants:**

A tenant with 5,000 users fetching `$top=999` requires 5 API round-trips. At ~300-500 ms per call, this is 1.5–2.5 seconds total. This is acceptable for a browse-on-demand operation with a loading indicator. Do NOT load the directory automatically on tab open — require an explicit "Load Directory" button click.

**Local filtering after load:**

Once the full directory is in memory (as an `ObservableCollection<DirectoryUserResult>`), use `ICollectionView` with a `Filter` predicate for instant local text-filter — the same pattern already used in the `PermissionsViewModel` and `StorageViewModel`. No server round-trips needed for filtering once the list is loaded. This is already in-process for the existing ViewModels and requires no new library.

**New model record:**

```csharp
// Core/Models/DirectoryUserResult.cs — or extend GraphUserResult
public record DirectoryUserResult(
    string DisplayName,
    string UserPrincipalName,
    string? Mail,
    string? JobTitle,
    string? Department,
    bool IsGuest);
```

**New service interface:**

```csharp
// Services/IGraphUserDirectoryService.cs
public interface IGraphUserDirectoryService
{
    Task<IReadOnlyList<DirectoryUserResult>> GetAllUsersAsync(
        string clientId,
        bool includeGuests = true,
        bool includeDisabled = false,
        CancellationToken ct = default);
}
```

The implementation follows the same `GraphClientFactory` + `GraphServiceClient` pattern as `GraphUserSearchService`. Wire it in DI alongside the existing search service.

---

## No New NuGet Packages Required (v2.2)

| Feature | What's needed | How provided |
|---|---|---|
| Logo file → Base64 data URI | `Convert.ToBase64String`, `File.ReadAllBytesAsync` | BCL (.NET 10) |
| Logo preview in WPF settings | `BitmapImage`, `Image` control | WPF / PresentationCore |
| Logo file picker | `OpenFileDialog` | WPF / Microsoft.Win32 |
| Store logo path in settings | `AppSettings.MspLogoPath`, `TenantProfile.ClientLogoPath` | Extend existing models |
| User directory listing | `graphClient.Users.GetAsync()` + `PageIterator` | Microsoft.Graph 5.74.0 (already installed) |
| Local filtering of directory list | `ICollectionView.Filter` | WPF / System.Windows.Data |

**Do NOT add:**
- Any HTML template engine (Razor, Scriban, Handlebars) — `StringBuilder` is sufficient for logo injection
- Any image processing library (ImageSharp, SkiaSharp standalone, Magick.NET) — no image transformation is needed, only raw bytes → Base64
- Any new Graph SDK packages — `Microsoft.Graph` 5.74.0 already includes `PageIterator`

---

## Impact on Existing Services (v2.2)

### HTML Export Services

Each existing export service (`HtmlExportService`, `UserAccessHtmlExportService`, `StorageHtmlExportService`, `DuplicatesHtmlExportService`, `SearchHtmlExportService`) needs a logo injection point. Two options:

**Option A (recommended): `ReportBrandingContext` parameter**

Introduce a small record carrying resolved logo data URIs. Export services accept it as an optional parameter; when null, no logo header is rendered. This keeps the services testable without file I/O.

```csharp
public record ReportBrandingContext(
    string? MspLogoDataUri,    // "data:image/png;base64,..." or null
    string? ClientLogoDataUri, // "data:image/png;base64,..." or null
    string? MspName,
    string? ClientName);
```

A `ReportBrandingService` converts file paths to data URIs. ViewModels call it before invoking the export service.

**Option B: Inject branding directly into all BuildHtml signatures**

Less clean — modifies every export service signature and every call site.

Option A is preferred: it isolates file I/O from HTML generation and keeps existing tests passing without changes.

### UserAccessAuditViewModel

Add a `BrowseMode` boolean property (bound to a RadioButton or ToggleButton). When `true`, show the directory list panel instead of the people-picker search box. The `IGraphUserDirectoryService` is injected alongside the existing `IGraphUserSearchService`.

---

---

## v2.3 Stack Additions

**Researched:** 2026-04-09
**Scope:** Only what is NEW vs the validated v2.2 stack. The short answer: **no new NuGet packages are required for any v2.3 feature.**

---

### Feature 1: App Registration on Target Tenant (Auto + Guided Fallback)

**What is needed:** Create an Entra app registration in a *target* (client) tenant from within the app, using the already-authenticated delegated token.

**No new packages required.** The existing `Microsoft.Graph` SDK (currently 5.74.0, latest stable is 5.103.0 as of 2026-02-20) already supports this via:

- `graphClient.Applications.PostAsync(new Application { DisplayName = "...", RequiredResourceAccess = [...] })` — creates the app object; returns the new `appId`
- `graphClient.ServicePrincipals.PostAsync(new ServicePrincipal { AppId = newAppId })` — instantiates the enterprise app in the target tenant so it can be consented
- `graphClient.Applications["{objectId}"].DeleteAsync()` — removes the registration (soft-delete, 30-day recycle bin in Entra)

All three operations are Graph v1.0 endpoints confirmed in official Microsoft Learn documentation (HIGH confidence).

**Required delegated permissions for these Graph calls:**

| Operation | Minimum delegated scope |
|-----------|------------------------|
| Create application (`POST /applications`) | `Application.ReadWrite.All` |
| Create service principal (`POST /servicePrincipals`) | `Application.ReadWrite.All` |
| Delete application (`DELETE /applications/{id}`) | `Application.ReadWrite.All` |
| Grant app role consent (`POST /servicePrincipals/{id}/appRoleAssignments`) | `AppRoleAssignment.ReadWrite.All` |

The calling user must also hold the **Application Administrator** or **Cloud Application Administrator** Entra role on the target tenant (or Global Administrator). Without the role, the delegated call returns 403 regardless of scope consent.

**Integration point — `GraphClientFactory` scope extension:**

The existing `GraphClientFactory.CreateClientAsync` uses `["https://graph.microsoft.com/.default"]`, relying on pre-consented `.default` resolution. For app registration, add an overload:

```csharp
// New method — only used by AppRegistrationService
public async Task<GraphServiceClient> CreateRegistrationClientAsync(
    string clientId, CancellationToken ct)
{
    var pca = await _msalFactory.GetOrCreateAsync(clientId);
    var accounts = await pca.GetAccountsAsync();
    var account = accounts.FirstOrDefault();

    // Explicit scopes trigger incremental consent on first call
    var scopes = new[]
    {
        "Application.ReadWrite.All",
        "AppRoleAssignment.ReadWrite.All"
    };
    var tokenProvider = new MsalTokenProvider(pca, account, scopes);
    var authProvider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
    return new GraphServiceClient(authProvider);
}
```

MSAL will prompt for incremental consent if not yet granted. This keeps the default `CreateClientAsync` scopes unchanged and avoids over-permissioning all Graph calls throughout the app.

**`TenantProfile` model extension:**

```csharp
// Add to TenantProfile.cs
public string? AppObjectId { get; set; }  // Entra object ID of the registered app
                                           // null until registration completes
                                           // used for deletion
```

Stored in JSON (existing ProfileService persistence). No schema migration needed — `System.Text.Json` deserializes missing properties as their default value (`null`).

**Guided fallback path:** If the automated registration fails (user lacks Application Administrator role, or consent blocked by tenant policy), open a browser to the Entra admin center app registration URL. No additional API calls needed for the fallback path.

---

### Feature 2: App Removal from Target Tenant

**Same stack as Feature 1.** `graphClient.Applications["{objectId}"].DeleteAsync()` is the Graph v1.0 `DELETE /applications/{id}` endpoint. Returns 204 on success. `AppObjectId` stored in `TenantProfile` provides the handle.

Deletion behavior: apps go to Entra's 30-day deleted items container and can be restored via the admin center. The app does not need to handle restoration — that is admin-center territory.

---

### Feature 3: Auto-Take Ownership of Sites on Access Denied (Global Toggle)

**No new packages required.** PnP Framework 1.18.0 already exposes the SharePoint tenant admin API via the `Tenant` class, which can add a site collection admin without requiring existing access to that site:

```csharp
// Requires ClientContext pointed at the tenant admin site
// (e.g., https://contoso-admin.sharepoint.com)
var tenant = new Tenant(adminCtx);
tenant.SetSiteAdmin(siteUrl, userLogin, isAdmin: true);
adminCtx.ExecuteQueryRetry();
```

**Critical constraint (HIGH confidence):** `Tenant.SetSiteAdmin` calls the SharePoint admin tenant API. This bypasses the site-level permission check — it does NOT require the authenticated user to already be a member of the site. It DOES require the user to hold the **SharePoint Administrator** or **Global Administrator** Entra role. If the user lacks this role, the call throws `ServerException` with "Access denied."

**Integration point — `SessionManager` admin context:**

The existing `SessionManager.GetOrCreateContextAsync` accepts a `TenantProfile` and uses `profile.TenantUrl` as the site URL. For tenant admin operations, a second context is needed pointing at the admin URL. Add:

```csharp
// New method in SessionManager (no new library, same PnP auth path)
public async Task<ClientContext> GetOrCreateAdminContextAsync(
    TenantProfile profile, CancellationToken ct = default)
{
    // Derive admin URL: https://contoso.sharepoint.com -> https://contoso-admin.sharepoint.com
    var adminUrl = profile.TenantUrl
        .TrimEnd('/')
        .Replace(".sharepoint.com", "-admin.sharepoint.com",
                 StringComparison.OrdinalIgnoreCase);

    var adminProfile = profile with { TenantUrl = adminUrl };
    return await GetOrCreateContextAsync(adminProfile, ct);
}
```

**Global toggle storage:** Add `AutoTakeOwnership: bool` to `AppSettings` (existing JSON settings file). No new model needed.

---

### Feature 4: Expand Groups in HTML Reports (Clickable to Show Members)

**No new packages required.** Pure C# + inline JavaScript.

**Server-side group member resolution:** SharePoint group members are already loaded during the permissions scan via CSOM `RoleAssignment.Member`. For SharePoint groups, `Member` is a `Group` object. Load its `Users` collection:

```csharp
// Already inside the permissions scan loop — extend it
if (roleAssignment.Member is Group spGroup)
{
    ctx.Load(spGroup.Users);
    // ExecuteQueryRetry already called in the scan loop
    // Members available as spGroup.Users
}
```

No additional API calls beyond what the existing scan already performs (the Users collection is a CSOM lazy-load — one additional batch per group, amortized over the scan).

**HTML export change:** Pass group member lists into the export service as part of the existing `PermissionEntry` model (extend with `IReadOnlyList<string>? GroupMembers`). The export service renders members as a collapsible `<details>/<summary>` HTML element inline with each group-access row — pure HTML5, no JS library, no external dependency.

**Report consolidation pre-processing:** Consolidation is a LINQ step before export. No new model or service.

---

### Feature 5: Report Consolidation Toggle (Merge Duplicate User Entries)

**No new packages required.** Standard LINQ aggregation on the existing `IReadOnlyList<UserAccessEntry>` before handing off to any export service.

Consolidation merges rows with the same `(UserLogin, SiteUrl, PermissionLevel)` key, collecting distinct `GrantedThrough` values into a semicolon-joined string. Add a `ConsolidateEntries(IReadOnlyList<UserAccessEntry>)` static helper in a shared location (e.g., `UserAccessEntryExtensions`). Toggle stored in `AppSettings` or passed as a flag at export time.

---

## No New NuGet Packages Required (v2.3)

| Feature | What's needed | How provided |
|---|---|---|
| Create app registration | `graphClient.Applications.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
| Create service principal | `graphClient.ServicePrincipals.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
| Delete app registration | `graphClient.Applications[id].DeleteAsync` | Microsoft.Graph 5.74.0 (existing) |
| Grant app role consent | `graphClient.ServicePrincipals[id].AppRoleAssignments.PostAsync` | Microsoft.Graph 5.74.0 (existing) |
| Add site collection admin | `new Tenant(ctx).SetSiteAdmin(...)` | PnP.Framework 1.18.0 (existing) |
| Admin site context | `SessionManager.GetOrCreateAdminContextAsync` — new method, no new lib | PnP.Framework + MSAL (existing) |
| Group member loading | `ctx.Load(spGroup.Users)` + `ExecuteQueryRetry` | PnP.Framework 1.18.0 (existing) |
| HTML group expansion | `<details>/<summary>` HTML5 element | Plain HTML, BCL StringBuilder |
| Consolidation logic | `GroupBy` + LINQ | BCL (.NET 10) |
| Incremental Graph scopes | `MsalTokenProvider` with explicit scopes | MSAL 4.83.3 (existing) |

**Do NOT add:**

| Package | Reason to Skip |
|---------|---------------|
| `Azure.Identity` | App uses `Microsoft.Identity.Client` (MSAL) directly via `BaseBearerTokenAuthenticationProvider`. Azure.Identity would duplicate auth and conflict with the existing PCA + `MsalCacheHelper` token-cache-sharing pattern. |
| PnP Core SDK | Distinct from PnP Framework (CSOM-based). Adding both creates confusion, ~15 MB extra weight, and no benefit since `Tenant.SetSiteAdmin` already exists in PnP.Framework 1.18.0. |
| Any HTML template engine (Razor, Scriban) | StringBuilder pattern is established and sufficient. Template engines add complexity with no gain for server-side HTML generation. |
| SignalR / polling / background service | Auto-ownership is a synchronous, on-demand CSOM call triggered by an access-denied event. No push infrastructure needed. |

---

## Version Bump Consideration (v2.3)

| Package | Current | Latest Stable | Recommendation |
|---------|---------|--------------|----------------|
| `Microsoft.Graph` | 5.74.0 | 5.103.0 | Optional. All new Graph API calls work on 5.74.0. Bump only if a specific bug is encountered. All 5.x versions maintain API compatibility. |
| `PnP.Framework` | 1.18.0 | Check NuGet before bumping | Hold. `Tenant.SetSiteAdmin` works in 1.18.0. PnP Framework version bumps have historically introduced CSOM interop issues. Bump only with explicit testing. |

---

## Existing Stack (Unchanged)

The full stack as validated through v2.2:

| Technology | Version | Purpose |
|---|---|---|
| .NET 10 | 10.x | Target runtime (LTS until Nov 2028) |
| WPF | built-in | UI framework |
| C# 13 | built-in | Language |
| PnP.Framework | 1.18.0 | SharePoint CSOM, provisioning engine |
| Microsoft.Graph | 5.74.0 | Graph API (users, Teams, Groups, app management) |
| Microsoft.Identity.Client (MSAL) | 4.83.3 | Multi-tenant auth, token acquisition |
| Microsoft.Identity.Client.Extensions.Msal | 4.83.3 | Token cache persistence |
| Microsoft.Identity.Client.Broker | 4.82.1 | Windows broker (WAM) |
| CommunityToolkit.Mvvm | 8.4.2 | MVVM base classes, source generators |
| Microsoft.Extensions.Hosting | 10.0.0 | DI container, app lifetime |
| LiveCharts2 (SkiaSharpView.WPF) | 2.0.0-rc5.4 | Storage charts |
| Serilog | 4.3.1 | Structured logging |
| Serilog.Extensions.Hosting | 10.0.0 | ILogger<T> bridge |
| Serilog.Sinks.File | 7.0.0 | Rolling file output |
| CsvHelper | 33.1.0 | CSV export |
| System.Text.Json | built-in | JSON settings/profiles/templates |
| xUnit | 2.9.3 | Unit tests |
| Moq | 4.20.72 | Test mocking |

---

## Sources

**v2.2 sources:**
- Microsoft Learn — List users (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0 — permissions, $top max 999, $orderby with ConsistencyLevel (HIGH confidence)
- Microsoft Learn — Page through a collection (Graph SDKs): https://learn.microsoft.com/en-us/graph/sdks/paging — PageIterator C# pattern (HIGH confidence)

**v2.3 sources:**
- Microsoft Learn — Create application (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-1.0 — C# SDK 5.x pattern confirmed, `Application.ReadWrite.All` required (HIGH confidence, updated 2026-03-14)
- Microsoft Learn — Create servicePrincipal (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/serviceprincipal-post-serviceprincipals?view=graph-rest-1.0 — `Application.ReadWrite.All` required for multi-tenant apps (HIGH confidence, updated 2026-03-14)
- Microsoft Learn — Delete application (Graph v1.0): https://learn.microsoft.com/en-us/graph/api/application-delete?view=graph-rest-1.0 — `graphClient.Applications[id].DeleteAsync()`, 204 response, 30-day soft-delete (HIGH confidence)
- Microsoft Learn — Grant permissions programmatically: https://learn.microsoft.com/en-us/graph/permissions-grant-via-msgraph — `AppRoleAssignment.ReadWrite.All` for consent grants, C# SDK 5.x examples (HIGH confidence, updated 2026-03-21)
- Microsoft Learn — Choose authentication providers: https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers — Interactive provider pattern for desktop apps confirmed (HIGH confidence, updated 2025-08-06)
- PnP Core SDK docs — Security/SetSiteCollectionAdmins: https://pnp.github.io/pnpcore/using-the-sdk/admin-sharepoint-security.html — tenant API bypasses site-level permission check, requires SharePoint Admin role (MEDIUM confidence — PnP Core docs; maps to PnP Framework `Tenant.SetSiteAdmin` behavior)
- Microsoft.Graph NuGet package: https://www.nuget.org/packages/Microsoft.Graph/ — latest stable 5.103.0 confirmed 2026-02-20 (HIGH confidence)
- Codebase — GraphClientFactory.cs, SessionManager.cs, MsalClientFactory.cs — confirmed existing `BaseBearerTokenAuthenticationProvider` + MSAL PCA integration pattern (HIGH confidence, source read directly)
