feat: add receipt upload API and target net10
Build MoneyMap image / build-and-push (push) Failing after 10s
Build MoneyMap image / build-and-push (push) Failing after 10s
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Quick Overview
|
## Quick Overview
|
||||||
|
|
||||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application for personal finance tracking. Users can import bank transaction CSVs, categorize expenses, attach receipt images/PDFs, and parse receipts using OpenAI's Vision API.
|
MoneyMap is an ASP.NET Core 10.0 Razor Pages application for personal finance tracking. Users can import bank transaction CSVs, categorize expenses, attach receipt images/PDFs, and parse receipts using OpenAI's Vision API.
|
||||||
|
|
||||||
## Architecture Documentation
|
## Architecture Documentation
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application designed for personal finance tracking. It allows users to import bank transaction CSV files, categorize expenses, attach receipt images/PDFs, and parse receipts using AI (OpenAI GPT-4o-mini Vision API).
|
MoneyMap is an ASP.NET Core 10.0 Razor Pages application designed for personal finance tracking. It allows users to import bank transaction CSV files, categorize expenses, attach receipt images/PDFs, and parse receipts using AI (OpenAI GPT-4o-mini Vision API).
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
- **Framework**: ASP.NET Core 8.0 (Razor Pages)
|
- **Framework**: ASP.NET Core 10.0 (Razor Pages)
|
||||||
- **Database**: SQL Server with Entity Framework Core 9.0
|
- **Database**: SQL Server with Entity Framework Core 9.0
|
||||||
- **Libraries**:
|
- **Libraries**:
|
||||||
- CsvHelper (33.1.0) - CSV parsing
|
- CsvHelper (33.1.0) - CSV parsing
|
||||||
@@ -1214,6 +1214,19 @@ EF Core DbContext managing all database entities.
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
|
### Receipt Upload API
|
||||||
|
**Route:** `POST /api/receipts?transactionId={id}`
|
||||||
|
|
||||||
|
**Purpose:** Upload one receipt image or PDF, store it through `IReceiptManager`, and queue it for asynchronous parsing. Omit `transactionId` to upload an unmapped receipt; provide it to attach the receipt to an existing transaction.
|
||||||
|
|
||||||
|
**Request:** `multipart/form-data` with one required `file` field. The shared receipt service accepts JPG/JPEG, PNG, GIF, HEIC, and PDF files up to 10 MB; it validates their file signatures, calculates a SHA-256 hash, and returns duplicate warnings when applicable.
|
||||||
|
|
||||||
|
**Responses:**
|
||||||
|
- `200 OK`: uploaded receipt metadata (`id`, file metadata, `parseStatus`, optional `transactionId`, and `duplicateWarnings`).
|
||||||
|
- `400 Bad Request`: missing/invalid file, unsupported content, over-size upload, duplicate mapped receipt, or unknown transaction.
|
||||||
|
|
||||||
|
**Example:** `curl -X POST http://localhost:5010/api/receipts -F 'file=@receipt.png'`
|
||||||
|
|
||||||
### Financial Audit API
|
### Financial Audit API
|
||||||
**Route:** `GET /api/audit`
|
**Route:** `GET /api/audit`
|
||||||
|
|
||||||
@@ -1818,7 +1831,7 @@ MoneyMap demonstrates a well-architected ASP.NET Core application with clear sep
|
|||||||
|
|
||||||
**Last Updated:** 2026-02-11
|
**Last Updated:** 2026-02-11
|
||||||
**Version:** 1.5
|
**Version:** 1.5
|
||||||
**Framework:** ASP.NET Core 8.0 / EF Core 9.0
|
**Framework:** ASP.NET Core 10.0 / EF Core 9.0
|
||||||
|
|
||||||
## Recent Changes (v1.5)
|
## Recent Changes (v1.5)
|
||||||
|
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
# MoneyMap - Claude Code Context
|
|
||||||
|
|
||||||
## Quick Overview
|
|
||||||
|
|
||||||
MoneyMap is an ASP.NET Core 8.0 Razor Pages application for personal finance tracking. Users can import bank transaction CSVs, categorize expenses, attach receipt images/PDFs, and parse receipts using OpenAI's Vision API.
|
|
||||||
|
|
||||||
## Architecture Documentation
|
|
||||||
|
|
||||||
**For detailed technical documentation, see [ARCHITECTURE.md](./ARCHITECTURE.md)**
|
|
||||||
|
|
||||||
The architecture document contains:
|
|
||||||
- Complete technology stack
|
|
||||||
- Core domain models (Transaction, Receipt, Card, Account, Merchant, etc.)
|
|
||||||
- Service layer details (TransactionImporter, CardResolver, TransactionCategorizer, etc.)
|
|
||||||
- Database schema and relationships
|
|
||||||
- Key workflows and design patterns
|
|
||||||
- Security and performance considerations
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
MoneyMap/
|
|
||||||
├── Data/
|
|
||||||
│ └── MoneyMapContext.cs # EF Core DbContext
|
|
||||||
├── Models/
|
|
||||||
│ ├── Account.cs # Bank accounts
|
|
||||||
│ ├── Card.cs # Payment cards
|
|
||||||
│ ├── Merchant.cs # Merchants/vendors
|
|
||||||
│ ├── Transaction.cs # Core transaction entity
|
|
||||||
│ ├── Receipt.cs # Receipt files
|
|
||||||
│ ├── ReceiptLineItem.cs # Parsed line items
|
|
||||||
│ └── ReceiptParseLog.cs # Parse attempt logs
|
|
||||||
├── Services/
|
|
||||||
│ ├── TransactionCategorizer.cs # Auto-categorization logic
|
|
||||||
│ ├── ReceiptManager.cs # Receipt upload/storage
|
|
||||||
│ └── OpenAIReceiptParser.cs # AI-powered receipt parsing
|
|
||||||
├── Pages/
|
|
||||||
│ ├── Index.cshtml[.cs] # Dashboard
|
|
||||||
│ ├── Upload.cshtml[.cs] # CSV import
|
|
||||||
│ ├── Transactions.cshtml[.cs] # Transaction list
|
|
||||||
│ ├── EditTransaction.cshtml[.cs] # Edit transaction
|
|
||||||
│ ├── ViewReceipt.cshtml[.cs] # Receipt details
|
|
||||||
│ ├── CategoryMappings.cshtml[.cs]# Category rules
|
|
||||||
│ ├── Merchants.cshtml[.cs] # Merchant management
|
|
||||||
│ └── Recategorize.cshtml[.cs] # Bulk recategorization
|
|
||||||
└── Program.cs # DI configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Development Tasks
|
|
||||||
|
|
||||||
### Adding a New Page
|
|
||||||
1. Create `.cshtml` and `.cshtml.cs` files in `Pages/`
|
|
||||||
2. Inherit from `PageModel`
|
|
||||||
3. Add route via `@page` directive
|
|
||||||
4. Register dependencies in constructor via DI
|
|
||||||
|
|
||||||
### Adding a New Service
|
|
||||||
1. Create interface in appropriate namespace
|
|
||||||
2. Create implementation class
|
|
||||||
3. Register in `Program.cs`:
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddScoped<IMyService, MyService>();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding a Database Migration
|
|
||||||
```bash
|
|
||||||
dotnet ef migrations add MigrationName
|
|
||||||
dotnet ef database update
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modifying Domain Models
|
|
||||||
1. Update model class in `Models/`
|
|
||||||
2. Update `MoneyMapContext.OnModelCreating()` if needed (relationships, indexes)
|
|
||||||
3. Create and apply migration
|
|
||||||
|
|
||||||
## Key Design Principles
|
|
||||||
|
|
||||||
1. **Service Layer Pattern**: Business logic lives in services, not pages
|
|
||||||
2. **Result Pattern**: Services return result objects (not exceptions)
|
|
||||||
3. **Dependency Injection**: All services injected via interfaces
|
|
||||||
4. **Single Responsibility**: Each service has one clear purpose
|
|
||||||
5. **Clean Architecture**: UI → Services → Data Access
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Duplicate Prevention**: Transactions have a unique constraint on (Date, Amount, Name, Memo, AccountId, CardId)
|
|
||||||
- **Cascade Deletes**: Deleting a transaction cascades to receipts, parse logs, and line items
|
|
||||||
- **Merchant Assignment**: Category mappings can auto-assign merchants to transactions
|
|
||||||
- **Transfer Detection**: Transactions with `TransferToAccountId` are identified as transfers
|
|
||||||
- **Receipt Parsing**: OpenAI API key required for receipt parsing (env var `OPENAI_API_KEY`)
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. Read [ARCHITECTURE.md](./ARCHITECTURE.md) for technical details
|
|
||||||
2. Make changes to models, services, or pages
|
|
||||||
3. Test locally
|
|
||||||
4. Create database migration if schema changed
|
|
||||||
5. **Update [ARCHITECTURE.md](./ARCHITECTURE.md) if architecture changes** (new models, services, workflows, etc.)
|
|
||||||
6. Commit with descriptive message
|
|
||||||
|
|
||||||
## Important: Keep Documentation Updated
|
|
||||||
|
|
||||||
**When making architectural changes, always update [ARCHITECTURE.md](./ARCHITECTURE.md):**
|
|
||||||
- Adding/removing domain models
|
|
||||||
- Adding/removing services or changing their responsibilities
|
|
||||||
- Modifying database schema or relationships
|
|
||||||
- Adding new workflows or processes
|
|
||||||
- Changing design patterns or conventions
|
|
||||||
- Adding new pages or major features
|
|
||||||
|
|
||||||
This ensures both Claude Code and Codex CLI have accurate, up-to-date context.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
See `appsettings.json`:
|
|
||||||
- `ConnectionStrings:MoneyMapDb` - SQL Server connection
|
|
||||||
- `OpenAI:ApiKey` - OpenAI API key (optional, use env var instead)
|
|
||||||
- `Receipts:StoragePath` - Receipt storage location (relative to wwwroot)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- All services use interfaces for mockability
|
|
||||||
- Use in-memory database for integration tests
|
|
||||||
- Mock `IReceiptParser` to avoid OpenAI API calls in tests
|
|
||||||
|
|
||||||
## Questions?
|
|
||||||
|
|
||||||
Refer to [ARCHITECTURE.md](./ARCHITECTURE.md) for comprehensive technical documentation including:
|
|
||||||
- Detailed service descriptions
|
|
||||||
- Database schema
|
|
||||||
- Key workflows
|
|
||||||
- Security considerations
|
|
||||||
- Performance optimizations
|
|
||||||
- Troubleshooting guide
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moq;
|
||||||
|
using MoneyMap.Controllers;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
using MoneyMap.Services;
|
||||||
|
using MoneyMap.Tests.TestHelpers;
|
||||||
|
|
||||||
|
namespace MoneyMap.Tests.Controllers;
|
||||||
|
|
||||||
|
public class ReceiptsControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Upload_NoFile_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
using var context = DbContextHelper.CreateInMemoryContext();
|
||||||
|
var manager = new Mock<IReceiptManager>();
|
||||||
|
var controller = CreateController(context, manager.Object);
|
||||||
|
|
||||||
|
var result = await controller.Upload(null);
|
||||||
|
|
||||||
|
Assert.IsType<BadRequestObjectResult>(result);
|
||||||
|
manager.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Upload_UnmappedFile_QueuesReceiptAndReturnsMetadata()
|
||||||
|
{
|
||||||
|
using var context = DbContextHelper.CreateInMemoryContext();
|
||||||
|
var manager = new Mock<IReceiptManager>();
|
||||||
|
var receipt = new Receipt
|
||||||
|
{
|
||||||
|
Id = 42,
|
||||||
|
FileName = "receipt.png",
|
||||||
|
ContentType = "image/png",
|
||||||
|
FileSizeBytes = 3,
|
||||||
|
UploadedAtUtc = new DateTime(2026, 9, 11, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
ParseStatus = ReceiptParseStatus.Queued
|
||||||
|
};
|
||||||
|
manager.Setup(m => m.UploadUnmappedReceiptAsync(It.IsAny<IFormFile>()))
|
||||||
|
.ReturnsAsync(ReceiptUploadResult.Success(receipt));
|
||||||
|
var controller = CreateController(context, manager.Object);
|
||||||
|
|
||||||
|
var result = await controller.Upload(CreateFormFile("receipt.png"));
|
||||||
|
|
||||||
|
var ok = Assert.IsType<OkObjectResult>(result);
|
||||||
|
using var json = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
|
||||||
|
Assert.Equal(42, json.RootElement.GetProperty("Id").GetInt64());
|
||||||
|
Assert.Equal("Queued", json.RootElement.GetProperty("ParseStatus").GetString());
|
||||||
|
Assert.Equal(JsonValueKind.Null, json.RootElement.GetProperty("TransactionId").ValueKind);
|
||||||
|
manager.Verify(m => m.UploadUnmappedReceiptAsync(It.IsAny<IFormFile>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Upload_WithTransactionId_DelegatesToMappedUpload()
|
||||||
|
{
|
||||||
|
using var context = DbContextHelper.CreateInMemoryContext();
|
||||||
|
var manager = new Mock<IReceiptManager>();
|
||||||
|
manager.Setup(m => m.UploadReceiptAsync(99, It.IsAny<IFormFile>()))
|
||||||
|
.ReturnsAsync(ReceiptUploadResult.Failure("Transaction not found."));
|
||||||
|
var controller = CreateController(context, manager.Object);
|
||||||
|
|
||||||
|
var result = await controller.Upload(CreateFormFile("receipt.png"), 99);
|
||||||
|
|
||||||
|
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||||
|
using var json = JsonDocument.Parse(JsonSerializer.Serialize(badRequest.Value));
|
||||||
|
Assert.Equal("Transaction not found.", json.RootElement.GetProperty("message").GetString());
|
||||||
|
manager.Verify(m => m.UploadReceiptAsync(99, It.IsAny<IFormFile>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReceiptsController CreateController(
|
||||||
|
MoneyMap.Data.MoneyMapContext context,
|
||||||
|
IReceiptManager receiptManager) =>
|
||||||
|
new(context, new TestReceiptStorageOptions(), receiptManager);
|
||||||
|
|
||||||
|
private static IFormFile CreateFormFile(string fileName)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes("png");
|
||||||
|
return new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", fileName)
|
||||||
|
{
|
||||||
|
Headers = new HeaderDictionary(),
|
||||||
|
ContentType = "image/png"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestReceiptStorageOptions : IReceiptStorageOptions
|
||||||
|
{
|
||||||
|
public string ReceiptsBasePath => Path.GetTempPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|||||||
@@ -13,11 +13,44 @@ public class ReceiptsController : ControllerBase
|
|||||||
{
|
{
|
||||||
private readonly MoneyMapContext _db;
|
private readonly MoneyMapContext _db;
|
||||||
private readonly IReceiptStorageOptions _storageOptions;
|
private readonly IReceiptStorageOptions _storageOptions;
|
||||||
|
private readonly IReceiptManager _receiptManager;
|
||||||
|
|
||||||
public ReceiptsController(MoneyMapContext db, IReceiptStorageOptions storageOptions)
|
public ReceiptsController(
|
||||||
|
MoneyMapContext db,
|
||||||
|
IReceiptStorageOptions storageOptions,
|
||||||
|
IReceiptManager receiptManager)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_storageOptions = storageOptions;
|
_storageOptions = storageOptions;
|
||||||
|
_receiptManager = receiptManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
public async Task<IActionResult> Upload(IFormFile? file, [FromQuery] long? transactionId = null)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return BadRequest(new { message = "No file selected." });
|
||||||
|
|
||||||
|
var result = transactionId.HasValue
|
||||||
|
? await _receiptManager.UploadReceiptAsync(transactionId.Value, file)
|
||||||
|
: await _receiptManager.UploadUnmappedReceiptAsync(file);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return BadRequest(new { message = result.ErrorMessage });
|
||||||
|
|
||||||
|
var receipt = result.Receipt!;
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
receipt.Id,
|
||||||
|
receipt.FileName,
|
||||||
|
receipt.ContentType,
|
||||||
|
receipt.FileSizeBytes,
|
||||||
|
receipt.UploadedAtUtc,
|
||||||
|
ParseStatus = receipt.ParseStatus.ToString(),
|
||||||
|
receipt.TransactionId,
|
||||||
|
DuplicateWarnings = result.DuplicateWarnings
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<!-- .NET SDK 10.0.112 cannot resolve the Web SDK's wwwroot glob on Linux. -->
|
||||||
|
<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Utility\**" />
|
<Content Include="wwwroot/favicon.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<Compile Remove="wwwroot\receipts\**" />
|
<Content Include="wwwroot/css/site.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<Content Remove="Utility\**" />
|
<Content Include="wwwroot/js/category-mappings.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<Content Remove="wwwroot\receipts\**" />
|
<Content Include="wwwroot/js/edit-transaction.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<EmbeddedResource Remove="Utility\**" />
|
<Content Include="wwwroot/js/merchants.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<EmbeddedResource Remove="wwwroot\receipts\**" />
|
<Content Include="wwwroot/js/site.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<None Remove="Utility\**" />
|
<Content Include="wwwroot/js/transactions.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<None Remove="wwwroot\receipts\**" />
|
<Content Include="wwwroot/js/upload.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<Content Include="wwwroot/lib/bootstrap/css/bootstrap.min.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<Content Include="wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<Content Include="wwwroot/lib/jquery/jquery.min.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user