ASP.NET Core  

SaccoShare Management System — Sample Project (ASP.NET Core 8)

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):

  • Member 1--* ShareTransaction

  • Member 1--* Contribution

  • Member 1--* LoanApplication 1--* LoanRepayment

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

  • Use built-in logging + Serilog for structured logs.

  • Add health checks: builder.Services.AddHealthChecks() and map app.MapHealthChecks("/health").

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