diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..558fe14d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,428 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+*.env
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+
+[Dd]ebug/x64/
+[Dd]ebugPublic/x64/
+[Rr]elease/x64/
+[Rr]eleases/x64/
+bin/x64/
+obj/x64/
+
+[Dd]ebug/x86/
+[Dd]ebugPublic/x86/
+[Rr]elease/x86/
+[Rr]eleases/x86/
+bin/x86/
+obj/x86/
+
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+[Aa][Rr][Mm]64[Ee][Cc]/
+bld/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Build results on 'Bin' directories
+**/[Bb]in/*
+# Uncomment if you have tasks that rely on *.refresh files to move binaries
+# (https://github.com/github/gitignore/pull/3736)
+#!**/[Bb]in/*.refresh
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+*.trx
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Approval Tests result files
+*.received.*
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.idb
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+**/.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+**/.fake/
+
+# CodeRush personal settings
+**/.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+**/__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+#tools/**
+#!tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+MSBuild_Logs/
+
+# AWS SAM Build and Temporary Artifacts folder
+.aws-sam
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+**/.mfractor/
+
+# Local History for Visual Studio
+**/.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+**/.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
\ No newline at end of file
diff --git a/InvestmentPerformanceWebAPI.Tests/InvestmentPerformanceWebAPI.Tests.csproj b/InvestmentPerformanceWebAPI.Tests/InvestmentPerformanceWebAPI.Tests.csproj
new file mode 100644
index 00000000..30b71300
--- /dev/null
+++ b/InvestmentPerformanceWebAPI.Tests/InvestmentPerformanceWebAPI.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/InvestmentPerformanceWebAPI.Tests/UserInvestmentServiceTests.cs b/InvestmentPerformanceWebAPI.Tests/UserInvestmentServiceTests.cs
new file mode 100644
index 00000000..b95c73fc
--- /dev/null
+++ b/InvestmentPerformanceWebAPI.Tests/UserInvestmentServiceTests.cs
@@ -0,0 +1,116 @@
+using InvestmentPerformanceWebAPI.Database.Contexts;
+using InvestmentPerformanceWebAPI.Database.Models;
+using InvestmentPerformanceWebAPI.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace InvestmentPerformanceWebAPI.Tests
+{
+ public class UserInvestmentServiceTests
+ {
+ private static InvestmentDbContext CreateInMemoryContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ var context = new InvestmentDbContext(options);
+
+ context.Investments.Add(new Investment
+ {
+ InvestmentId = 1,
+ UserId = 1,
+ Name = "Test",
+ Ticker = "TST",
+ NumberOfShares = 10m,
+ CostBasisPerShare = 100m,
+ CurrentPrice = 150m,
+ TimeOfPurchase = DateTime.UtcNow.AddDays(-100)
+ });
+
+ context.SaveChanges();
+ return context;
+ }
+
+ [Fact]
+ public async Task GetInvestment_Computes_Performance_Correctly()
+ {
+ // Arrange
+ using var context = CreateInMemoryContext();
+ var service = new UserInvestmentService(context);
+
+ // Act
+ var result = await service.GetInvestmentAsync(1, 1);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(1500m, result!.CurrentValue); // 10 * 150
+ Assert.Equal(500m, result.TotalGainLoss); // 1500 - 1000
+ Assert.Equal("ShortTerm", result.Term); // 100 days < 365
+ }
+
+ [Fact]
+ public async Task GetInvestment_ReturnsNull_For_WrongUser()
+ {
+ using var context = CreateInMemoryContext();
+
+ // Add same investment for different user
+ context.Investments.Add(new Investment
+ {
+ InvestmentId = 2,
+ UserId = 2,
+ Name = "ForeignInvestment",
+ Ticker = "FI",
+ NumberOfShares = 10m,
+ CostBasisPerShare = 20m,
+ CurrentPrice = 22m,
+ TimeOfPurchase = DateTime.UtcNow.AddDays(-10)
+ });
+
+ context.SaveChanges();
+
+ var service = new UserInvestmentService(context);
+
+ // Act - user 1 tries to access investment 2 (owned by user 2)
+ var result = await service.GetInvestmentAsync(1, 2);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+
+ [Fact]
+ public async Task GetInvestment_ReturnsNull_If_NotFound()
+ {
+ using var context = CreateInMemoryContext();
+ var service = new UserInvestmentService(context);
+
+ // Act
+ var result = await service.GetInvestmentAsync(1, 999);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task GetInvestment_Computes_LongTerm_Correctly()
+ {
+ using var context = CreateInMemoryContext();
+
+ // Modify existing seed to be long-term (> 365 days)
+ var investment = context.Investments.First();
+ investment.TimeOfPurchase = DateTime.UtcNow.AddDays(-400);
+ context.SaveChanges();
+
+ var service = new UserInvestmentService(context);
+
+ // Act
+ var result = await service.GetInvestmentAsync(1, 1);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("LongTerm", result!.Term);
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/InvestmentPerformanceWebAPI/Controllers/UserInvestments.cs b/InvestmentPerformanceWebAPI/Controllers/UserInvestments.cs
new file mode 100644
index 00000000..a6a961d4
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Controllers/UserInvestments.cs
@@ -0,0 +1,66 @@
+using InvestmentPerformanceWebAPI.DTOs;
+using InvestmentPerformanceWebAPI.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace InvestmentPerformanceWebAPI.Controllers
+{
+ ///
+ /// API controller that exposes endpoints for querying user investments and investment details.
+ ///
+ [ApiController]
+ [Route("api/users/{userId:int}/investments")]
+ public class UserInvestmentsController(ILogger logger, IUserInvestmentService userInvestments) : ControllerBase
+ {
+ private readonly ILogger _logger = logger;
+ private readonly IUserInvestmentService _userInvestments = userInvestments;
+
+ ///
+ /// Gets a list of investments for a given user (id + name only).
+ ///
+ /// Identifier of the user whose investments to retrieve.
+ [HttpGet]
+ public async Task> GetUserInvestments(int userId)
+ {
+ try
+ {
+ var dto = await _userInvestments.GetUserInvestmentsAsync(userId);
+ return Ok(dto);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while fetching investments for userId {userId}", userId);
+ return Problem("Internal server error", statusCode: 500);
+ }
+ }
+
+ ///
+ /// Gets detailed information about a single investment for a given user.
+ ///
+ /// Identifier of the owning user.
+ /// Identifier of the investment to retrieve.
+ [HttpGet("{investmentId:int}")]
+ public async Task> GetInvestment(int userId, int investmentId)
+ {
+ try
+ {
+ var dto = await _userInvestments.GetInvestmentAsync(userId, investmentId);
+
+ if (dto is null)
+ {
+ if (_logger.IsEnabled(LogLevel.Warning))
+ {
+ _logger.LogWarning("Investment {investmentId} for user {userId} not found", investmentId, userId);
+ }
+ return NotFound();
+ }
+
+ return Ok(dto);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while fetching investment {investmentId} for userId {userId}", investmentId, userId);
+ return Problem("Internal server error", statusCode: 500);
+ }
+ }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/DTOs/InvestmentDTO.cs b/InvestmentPerformanceWebAPI/DTOs/InvestmentDTO.cs
new file mode 100644
index 00000000..3fa143bb
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/DTOs/InvestmentDTO.cs
@@ -0,0 +1,21 @@
+namespace InvestmentPerformanceWebAPI.DTOs
+{
+ public class InvestmentDTO
+ {
+ public int InvestmentId { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Ticker { get; set; } = string.Empty;
+ public decimal NumberOfShares { get; set; }
+ public decimal CostBasisPerShare { get; set; }
+ public decimal CurrentPrice { get; set; }
+
+ // Current total value of the position (NumberOfShares * CurrentPrice).
+ public decimal CurrentValue { get; set; }
+
+ // Investment term classification: "ShortTerm" or "LongTerm".
+ public string Term { get; set; } = string.Empty;
+
+ // CurrentValue - (NumberOfShares * CostBasisPerShare).
+ public decimal TotalGainLoss { get; set; }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/DTOs/UserInvestmentsDTO.cs b/InvestmentPerformanceWebAPI/DTOs/UserInvestmentsDTO.cs
new file mode 100644
index 00000000..f9653ea4
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/DTOs/UserInvestmentsDTO.cs
@@ -0,0 +1,19 @@
+namespace InvestmentPerformanceWebAPI.DTOs
+{
+ ///
+ /// Response DTO representing a user's list of investments.
+ ///
+ public class UserInvestmentsDTO
+ {
+ public List Investments { get; set; } = [];
+ }
+
+ ///
+ /// Summary view for a single investment, used in list endpoints.
+ ///
+ public class UserInvestmentSummaryDTO
+ {
+ public int InvestmentId { get; set; }
+ public string Name { get; set; } = string.Empty;
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/Database/Contexts/InvestmentDbContext.cs b/InvestmentPerformanceWebAPI/Database/Contexts/InvestmentDbContext.cs
new file mode 100644
index 00000000..545921a7
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Database/Contexts/InvestmentDbContext.cs
@@ -0,0 +1,143 @@
+using InvestmentPerformanceWebAPI.Database.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace InvestmentPerformanceWebAPI.Database.Contexts
+{
+ public class InvestmentDbContext(DbContextOptions options) : DbContext(options)
+ {
+ public DbSet Users { get; set; } = null!;
+ public DbSet Investments { get; set; } = null!;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasMany(u => u.Investments)
+ .WithOne(i => i.User!)
+ .HasForeignKey(i => i.UserId);
+
+
+ // manually create some data entities
+ modelBuilder.Entity().HasData(
+ new User { UserId = 1, Name = "Daniel Rivers" },
+ new User { UserId = 2, Name = "Sophia Bennett" },
+ new User { UserId = 3, Name = "Marcus Holt" },
+ new User { UserId = 4, Name = "Emma Caldwell" },
+ new User { UserId = 5, Name = "Jonathan Pierce" }
+ );
+
+
+ modelBuilder.Entity().HasData(
+ new Investment
+ {
+ InvestmentId = 1,
+ UserId = 1,
+ Name = "Berkshire Hathaway Inc.",
+ Ticker = "BRK.B",
+ NumberOfShares = 4,
+ CostBasisPerShare = 290.75m,
+ CurrentPrice = 365.40m,
+ TimeOfPurchase = new DateTime(2022, 4, 12)
+ },
+ new Investment
+ {
+ InvestmentId = 2,
+ UserId = 1,
+ Name = "Procter & Gamble Co.",
+ Ticker = "PG",
+ NumberOfShares = 18,
+ CostBasisPerShare = 130.10m,
+ CurrentPrice = 153.25m,
+ TimeOfPurchase = new DateTime(2023, 11, 3)
+ },
+ new Investment
+ {
+ InvestmentId = 3,
+ UserId = 2,
+ Name = "Costco Wholesale Corporation",
+ Ticker = "COST",
+ NumberOfShares = 6,
+ CostBasisPerShare = 470.20m,
+ CurrentPrice = 680.50m,
+ TimeOfPurchase = new DateTime(2021, 8, 19)
+ },
+ new Investment
+ {
+ InvestmentId = 4,
+ UserId = 2,
+ Name = "Walt Disney Company",
+ Ticker = "DIS",
+ NumberOfShares = 22,
+ CostBasisPerShare = 92.15m,
+ CurrentPrice = 101.85m,
+ TimeOfPurchase = new DateTime(2024, 1, 7)
+ },
+ new Investment
+ {
+ InvestmentId = 5,
+ UserId = 3,
+ Name = "Intel Corporation",
+ Ticker = "INTC",
+ NumberOfShares = 30,
+ CostBasisPerShare = 28.00m,
+ CurrentPrice = 36.75m,
+ TimeOfPurchase = new DateTime(2023, 5, 16)
+ },
+ new Investment
+ {
+ InvestmentId = 6,
+ UserId = 3,
+ Name = "Chevron Corporation",
+ Ticker = "CVX",
+ NumberOfShares = 12,
+ CostBasisPerShare = 148.50m,
+ CurrentPrice = 159.95m,
+ TimeOfPurchase = new DateTime(2022, 2, 27)
+ },
+ new Investment
+ {
+ InvestmentId = 7,
+ UserId = 4,
+ Name = "UnitedHealth Group Incorporated",
+ Ticker = "UNH",
+ NumberOfShares = 3,
+ CostBasisPerShare = 460.00m,
+ CurrentPrice = 529.40m,
+ TimeOfPurchase = new DateTime(2023, 8, 24)
+ },
+ new Investment
+ {
+ InvestmentId = 8,
+ UserId = 4,
+ Name = "PepsiCo, Inc.",
+ Ticker = "PEP",
+ NumberOfShares = 14,
+ CostBasisPerShare = 165.25m,
+ CurrentPrice = 172.45m,
+ TimeOfPurchase = new DateTime(2021, 12, 10)
+ },
+ new Investment
+ {
+ InvestmentId = 9,
+ UserId = 5,
+ Name = "AbbVie Inc.",
+ Ticker = "ABBV",
+ NumberOfShares = 9,
+ CostBasisPerShare = 112.80m,
+ CurrentPrice = 157.60m,
+ TimeOfPurchase = new DateTime(2024, 3, 5)
+ },
+ new Investment
+ {
+ InvestmentId = 10,
+ UserId = 5,
+ Name = "Ford Motor Company",
+ Ticker = "F",
+ NumberOfShares = 40,
+ CostBasisPerShare = 11.45m,
+ CurrentPrice = 13.90m,
+ TimeOfPurchase = new DateTime(2022, 9, 22)
+ }
+ );
+ }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/Database/Models/Investment.cs b/InvestmentPerformanceWebAPI/Database/Models/Investment.cs
new file mode 100644
index 00000000..b402bb45
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Database/Models/Investment.cs
@@ -0,0 +1,21 @@
+namespace InvestmentPerformanceWebAPI.Database.Models
+{
+ ///
+ /// Represents a single investment position in a user's portfolio.
+ ///
+ public class Investment
+ {
+ public int InvestmentId { get; set; }
+ public int UserId { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+ public string Ticker { get; set; } = string.Empty;
+
+ public decimal NumberOfShares { get; set; }
+ public decimal CostBasisPerShare { get; set; }
+ public decimal CurrentPrice { get; set; }
+ public DateTime TimeOfPurchase { get; set; }
+
+ public User? User { get; set; }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/Database/Models/User.cs b/InvestmentPerformanceWebAPI/Database/Models/User.cs
new file mode 100644
index 00000000..dcba57fe
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Database/Models/User.cs
@@ -0,0 +1,12 @@
+namespace InvestmentPerformanceWebAPI.Database.Models
+{
+ ///
+ /// Represents a user of the investment platform.
+ ///
+ public class User
+ {
+ public int UserId { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public ICollection Investments { get; set; } = [];
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/Interfaces/IUserInvestmentService.cs b/InvestmentPerformanceWebAPI/Interfaces/IUserInvestmentService.cs
new file mode 100644
index 00000000..5f1882ce
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Interfaces/IUserInvestmentService.cs
@@ -0,0 +1,21 @@
+using InvestmentPerformanceWebAPI.DTOs;
+
+namespace InvestmentPerformanceWebAPI.Interfaces
+{
+ ///
+ /// Abstraction for querying investment data for users.
+ ///
+ public interface IUserInvestmentService
+ {
+ ///
+ /// Returns a summary list of investments for the given user.
+ ///
+ Task GetUserInvestmentsAsync(int userId);
+
+ ///
+ /// Returns detailed information for a single investment owned by the given user.
+ /// Returns null if the investment is not found.
+ ///
+ Task GetInvestmentAsync(int userId, int investmentId);
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.csproj b/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.csproj
new file mode 100644
index 00000000..7c2255bd
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.sln b/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.sln
new file mode 100644
index 00000000..ff5b4ac6
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/InvestmentPerformanceWebAPI.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11217.181 d18.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceWebAPI", "InvestmentPerformanceWebAPI.csproj", "{5CA0090A-5C1D-4362-AA50-598E5FC6D34B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceWebAPI.Tests", "..\InvestmentPerformanceWebAPI.Tests\InvestmentPerformanceWebAPI.Tests.csproj", "{398C4B37-404A-4E64-BF8C-244EFD2526B9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5CA0090A-5C1D-4362-AA50-598E5FC6D34B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5CA0090A-5C1D-4362-AA50-598E5FC6D34B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5CA0090A-5C1D-4362-AA50-598E5FC6D34B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5CA0090A-5C1D-4362-AA50-598E5FC6D34B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {398C4B37-404A-4E64-BF8C-244EFD2526B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {398C4B37-404A-4E64-BF8C-244EFD2526B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {398C4B37-404A-4E64-BF8C-244EFD2526B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {398C4B37-404A-4E64-BF8C-244EFD2526B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {05C36DCB-225B-4E6A-B55F-4AA0A6B5C707}
+ EndGlobalSection
+EndGlobal
diff --git a/InvestmentPerformanceWebAPI/Program.cs b/InvestmentPerformanceWebAPI/Program.cs
new file mode 100644
index 00000000..aae49f08
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Program.cs
@@ -0,0 +1,42 @@
+using InvestmentPerformanceWebAPI.Database.Contexts;
+using InvestmentPerformanceWebAPI.Interfaces;
+using InvestmentPerformanceWebAPI.Services;
+using Microsoft.EntityFrameworkCore;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+// EF Core InMemory DB for this exercise
+builder.Services.AddDbContext(options =>
+ options.UseInMemoryDatabase("InvestmentsDb"));
+
+// Register the application service responsible for investment-related business logic.
+builder.Services.AddScoped();
+
+var app = builder.Build();
+
+// Force database creation & seeding
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.EnsureCreated();
+}
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/InvestmentPerformanceWebAPI/Properties/launchSettings.json b/InvestmentPerformanceWebAPI/Properties/launchSettings.json
new file mode 100644
index 00000000..392a2489
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:11292",
+ "sslPort": 44322
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5121",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7015;http://localhost:5121",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/README.md b/InvestmentPerformanceWebAPI/README.md
new file mode 100644
index 00000000..15061a45
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/README.md
@@ -0,0 +1,32 @@
+## About:
+
+Built with VS Community 2026 using C# 10.
+
+## To Run:
+
+- Install VS2026 + .NET 10 SDK
+- Build and start debugging in VS via the UI/F5
+
+Alternatively using the .NET CLI:
+- Install .NET 10 SDK
+- From the project directory, run:
+
+```bash
+dotnet restore
+dotnet build
+dotnet run
+```
+
+If it does not open automatically, open Swagger UI at the address given in the console output (eg. https://localhost:5001/swagger).
+
+## Endpoint:
+
+`GET /api/users/{userId}/investments`
+- Returns a list of user's investments
+`GET /api/users/{userId}/investments/{investmentId}`
+- Returns details for a given investment
+
+## Assumptions
+
+- In-memory db for the sake of time
+- Current prices are stored and fixed with the investments. In practice a current price would be sourced at runtime.
\ No newline at end of file
diff --git a/InvestmentPerformanceWebAPI/Services/UserInvestmentService.cs b/InvestmentPerformanceWebAPI/Services/UserInvestmentService.cs
new file mode 100644
index 00000000..3dc96811
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/Services/UserInvestmentService.cs
@@ -0,0 +1,78 @@
+using InvestmentPerformanceWebAPI.Database.Contexts;
+using InvestmentPerformanceWebAPI.DTOs;
+using InvestmentPerformanceWebAPI.Interfaces;
+using Microsoft.EntityFrameworkCore;
+
+namespace InvestmentPerformanceWebAPI.Services
+{
+ ///
+ /// Service that encapsulates the business logic for retrieving user investments and computing performance metrics.
+ ///
+ public class UserInvestmentService(InvestmentDbContext dbContext) : IUserInvestmentService
+ {
+ private readonly InvestmentDbContext _dbContext = dbContext;
+
+ ///
+ /// Returns a summary list of all investments for the specified user.
+ ///
+ public async Task GetUserInvestmentsAsync(int userId)
+ {
+ var investments = await _dbContext.Investments
+ .Where(i => i.UserId == userId)
+ .ToListAsync();
+
+ return new UserInvestmentsDTO
+ {
+ Investments = investments.Select(i => new UserInvestmentSummaryDTO
+ {
+ InvestmentId = i.InvestmentId,
+ Name = i.Name
+ }).ToList()
+ };
+ }
+
+ ///
+ /// Returns detailed information about a specific investment owned by the user.
+ /// Computes derived values such as current value, term, and total gain/loss.
+ ///
+ public async Task GetInvestmentAsync(int userId, int investmentId)
+ {
+ var investment = await _dbContext.Investments
+ .FirstOrDefaultAsync(i => i.UserId == userId && i.InvestmentId == investmentId);
+
+ if (investment is null)
+ {
+ return null;
+ }
+
+ var currentValue = investment.NumberOfShares * investment.CurrentPrice;
+ var amountPaid = investment.NumberOfShares * investment.CostBasisPerShare;
+ var gainLoss = currentValue - amountPaid;
+
+ var term = CalculateTerm(investment.TimeOfPurchase, DateTime.UtcNow);
+
+ return new InvestmentDTO
+ {
+ InvestmentId = investment.InvestmentId,
+ Name = investment.Name,
+ Ticker = investment.Ticker,
+ NumberOfShares = investment.NumberOfShares,
+ CostBasisPerShare = investment.CostBasisPerShare,
+ CurrentPrice = investment.CurrentPrice,
+ CurrentValue = currentValue,
+ Term = term,
+ TotalGainLoss = gainLoss
+ };
+ }
+
+ ///
+ /// Calculates the investment term classification ("ShortTerm" or "LongTerm")
+ /// based on the difference between the purchase date and the current date.
+ ///
+ private static string CalculateTerm(DateTime purchaseDate, DateTime nowUtc)
+ {
+ var daysHeld = (nowUtc.Date - purchaseDate.Date).TotalDays;
+ return daysHeld <= 365 ? "ShortTerm" : "LongTerm";
+ }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/appsettings.Development.json b/InvestmentPerformanceWebAPI/appsettings.Development.json
new file mode 100644
index 00000000..ff66ba6b
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/InvestmentPerformanceWebAPI/appsettings.json b/InvestmentPerformanceWebAPI/appsettings.json
new file mode 100644
index 00000000..4d566948
--- /dev/null
+++ b/InvestmentPerformanceWebAPI/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}