This is a complete, efficient starter scaffold and explanation for a SaccoShare Management System built with C# ASP.NET Core 8 (MVC) and Entity Framework Core. It includes:
Project overview and architecture
Database design (ER diagram description and DDL)
Key models, DbContext, and migrations notes
Controllers and sample actions (C#)
Razor view examples and layout
Authentication & Authorization (Identity)
Background jobs, logging, and deployment tips (Docker)
Sample seed data and unit-test suggestions
Use this scaffold as a starting point — adapt business rules and UI per your Sacco's needs.
1. Project Overview
Goal: Build a clean, maintainable Sacco (Savings and Credit Cooperative) management web app for handling members, share purchases, contribution collections, loans, and reporting. Focus on performance, secure code, and automation.
Tech stack
ASP.NET Core 8 (MVC)
EF Core 8 (Code First)
SQL Server (or Azure SQL)
Identity for auth
Razor views with Bootstrap 5
Background jobs: IHostedService or Hangfire (optional)
Docker for containerization
Core modules
Membership (CRUD, KYC)
Shares & Contributions (Buy shares, record payments)
Loans (apply, approve, schedule) — scaffolded but not fully implemented
Reports & Dashboards (real-time summaries)
Admin (users, roles, settings)
2. Database Design (Simplified)
Tables (main): Members, SaccoShares, ShareTransactions, Contributions, LoanApplications, LoanRepayments, Users (Identity), AuditLogs.
Sample ER sketch (text):
Simplified DDL (SQL)
CREATE TABLE Members (
MemberId INT IDENTITY PRIMARY KEY,
MemberCode NVARCHAR(50) NOT NULL UNIQUE,
FirstName NVARCHAR(100) NOT NULL,
LastName NVARCHAR(100) NOT NULL,
Email NVARCHAR(200),
Phone NVARCHAR(50),
DateOfBirth DATE NULL,
JoinedOn DATETIME2 DEFAULT SYSUTCDATETIME(),
IsActive BIT DEFAULT 1
);
CREATE TABLE SaccoShares (
SaccoShareId INT IDENTITY PRIMARY KEY,
Name NVARCHAR(150),
SharePrice DECIMAL(18,2) NOT NULL,
CreatedOn DATETIME2 DEFAULT SYSUTCDATETIME()
);
CREATE TABLE ShareTransactions (
ShareTransactionId INT IDENTITY PRIMARY KEY,
MemberId INT NOT NULL FOREIGN KEY REFERENCES Members(MemberId),
SaccoShareId INT NOT NULL FOREIGN KEY REFERENCES SaccoShares(SaccoShareId),
Quantity INT NOT NULL,
TotalAmount DECIMAL(18,2) NOT NULL,
TransactionDate DATETIME2 DEFAULT SYSUTCDATETIME(),
TransactionType NVARCHAR(20) -- Purchase / Refund
);
CREATE TABLE Contributions (
ContributionId INT IDENTITY PRIMARY KEY,
MemberId INT NOT NULL FOREIGN KEY REFERENCES Members(MemberId),
Amount DECIMAL(18,2) NOT NULL,
ContributionDate DATETIME2 DEFAULT SYSUTCDATETIME(),
Source NVARCHAR(100)
);
3. Project Structure (Folders)
/SaccoShare
/Controllers
/Models
/ViewModels
/Views
/Data
ApplicationDbContext.cs
Migrations/
/Services
/Infrastructure
/wwwroot
/Areas/Admin
appsettings.json
Program.cs
SaccoShare.csproj
4. Key Models (C#)
// Models/Member.cs
public class Member
{
public int MemberId { get; set; }
public string MemberCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public DateTime? DateOfBirth { get; set; }
public DateTime JoinedOn { get; set; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
public ICollection<ShareTransaction> ShareTransactions { get; set; }
public ICollection<Contribution> Contributions { get; set; }
}
// Models/SaccoShare.cs
public class SaccoShare
{
public int SaccoShareId { get; set; }
public string Name { get; set; }
public decimal SharePrice { get; set; }
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
}
// Models/ShareTransaction.cs
public class ShareTransaction
{
public int ShareTransactionId { get; set; }
public int MemberId { get; set; }
public Member Member { get; set; }
public int SaccoShareId { get; set; }
public SaccoShare SaccoShare { get; set; }
public int Quantity { get; set; }
public decimal TotalAmount { get; set; }
public DateTime TransactionDate { get; set; } = DateTime.UtcNow;
public string TransactionType { get; set; }
}
// Models/Contribution.cs
public class Contribution
{
public int ContributionId { get; set; }
public int MemberId { get; set; }
public Member Member { get; set; }
public decimal Amount { get; set; }
public DateTime ContributionDate { get; set; } = DateTime.UtcNow;
public string Source { get; set; }
}
5. DbContext and EF Core Setup
// Data/ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<Member> Members { get; set; }
public DbSet<SaccoShare> SaccoShares { get; set; }
public DbSet<ShareTransaction> ShareTransactions { get; set; }
public DbSet<Contribution> Contributions { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Member>().HasIndex(m => m.MemberCode).IsUnique();
builder.Entity<ShareTransaction>().Property(s => s.TotalAmount).HasPrecision(18,2);
builder.Entity<SaccoShare>().Property(s => s.SharePrice).HasPrecision(18,2);
}
}
Program.cs (minimal hosting model)
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
6. Sample Controller (Members)
// Controllers/MembersController.cs
[Authorize(Roles = "Admin,Clerk")]
public class MembersController : Controller
{
private readonly ApplicationDbContext _db;
public MembersController(ApplicationDbContext db) { _db = db; }
public async Task<IActionResult> Index(string search, int page = 1)
{
var query = _db.Members.AsQueryable();
if (!string.IsNullOrEmpty(search))
{
query = query.Where(m => m.FirstName.Contains(search) || m.LastName.Contains(search) || m.MemberCode.Contains(search));
}
var pageSize = 20;
var members = await query.OrderByDescending(m => m.JoinedOn)
.Skip((page - 1) * pageSize)
.Take(pageSize).ToListAsync();
return View(members);
}
public IActionResult Create() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Member model)
{
if (!ModelState.IsValid) return View(model);
model.MemberCode = "MEM" + DateTime.UtcNow.Ticks.ToString().Substring(10);
_db.Members.Add(model);
await _db.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
7. Razor View Example (Create Member)
@model Member
@{
ViewData["Title"] = "Create Member";
}
<form asp-action="Create" method="post">
<div class="mb-3">
<label asp-for="FirstName" class="form-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="LastName" class="form-label"></label>
<input asp-for="LastName" class="form-control" />
</div>
<div class="mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
8. Share Purchase Flow (Service)
public class ShareService
{
private readonly ApplicationDbContext _db;
public ShareService(ApplicationDbContext db) { _db = db; }
public async Task<ShareTransaction> PurchaseSharesAsync(int memberId, int saccoShareId, int qty)
{
var share = await _db.SaccoShares.FindAsync(saccoShareId);
if (share == null) throw new Exception("Share not found");
var total = share.SharePrice * qty;
var tx = new ShareTransaction
{
MemberId = memberId,
SaccoShareId = saccoShareId,
Quantity = qty,
TotalAmount = total,
TransactionType = "Purchase"
};
_db.ShareTransactions.Add(tx);
await _db.SaveChangesAsync();
return tx;
}
}
Register service in Program.cs
builder.Services.AddScoped<ShareService>();
9. Authentication and Roles
Use ASP.NET Core Identity for user authentication. Create roles: Admin, Clerk, Viewer.
Seed an admin user at startup.
public static async Task SeedRolesAndAdminAsync(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roles = new[] { "Admin", "Clerk", "Viewer" };
foreach (var role in roles) if (!await roleManager.RoleExistsAsync(role)) await roleManager.CreateAsync(new IdentityRole(role));
var adminEmail = "[email protected]";
var admin = await userManager.FindByEmailAsync(adminEmail);
if (admin == null)
{
admin = new ApplicationUser { UserName = "[email protected]", Email = adminEmail, EmailConfirmed = true };
await userManager.CreateAsync(admin, "Admin@12345");
await userManager.AddToRoleAsync(admin, "Admin");
}
}
Call this method during app startup (after app.Build() and before app.Run()).
10. Background Jobs & Reporting
Use IHostedService for simple recurring tasks (e.g., nightly reconciliation).
Or use Hangfire for advanced scheduling and a dashboard.
Example minimal hosted service
public class ReconciliationService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public ReconciliationService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// perform nightly reconciliation logic if TimeSpan matches
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
Register
builder.Services.AddHostedService<ReconciliationService>();
11. Logging, Health Checks, and Diagnostics
12. Dockerfile (simple)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["SaccoShare.csproj", "./"]
RUN dotnet restore "SaccoShare.csproj"
COPY . ./
RUN dotnet publish "SaccoShare.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "SaccoShare.dll"]
13. Sample Seed Data (EF Core)
public static void SeedSampleData(ApplicationDbContext db)
{
if (!db.SaccoShares.Any())
{
db.SaccoShares.AddRange(new[] {
new SaccoShare { Name = "Basic Share", SharePrice = 100m },
new SaccoShare { Name = "Premium Share", SharePrice = 500m }
});
db.SaveChanges();
}
if (!db.Members.Any())
{
db.Members.Add(new Member { MemberCode = "MEM1001", FirstName = "John", LastName = "Doe", Email = "[email protected]" });
db.SaveChanges();
}
}
Call SeedSampleData after building a service provider at the startup.
14. Tips for Efficiency & Best Practices
Use pagination on list pages and server-side filtering.
Avoid SELECT * — use projection into viewmodels.
Use proper indexing on MemberCode, TransactionDate, etc.
Use AsNoTracking() for read-only queries.
Keep transactions short and use ExecuteSqlRaw for bulk operations where needed.
Run EF Core migrations in CI/CD and avoid runtime migration on production unless safe.
15. Unit & Integration Tests
Use xUnit and Microsoft.AspNetCore.Mvc.Testing for integration tests.
Mock DbContext with in-memory provider for unit tests, but prefer testcontainers/real DB for integration