C#  

Building a User Subscription Module in ASP.NET MVC with C# 14

Introduction

In modern web applications, subscription-based features are essential for managing user access, monetizing content, or offering tiered services. In this guide, we will build a comprehensive User Subscription Module using ASP.NET MVC (Model-View-Controller) and C# 14. The module will support:

  • User registration and login
  • Subscription plan selection
  • Payment integration (mocked)
  • Role-based access based on subscription level
  • Subscription management (upgrade/downgrade/cancel)

We’ll also take advantage of new C# 14 features such as primary constructors, collection expressions, field-backed properties, and extension members to make our code more concise and modern. These features help streamline the codebase and make it easier to read and maintain, especially in large applications with many interrelated components.

By the end of this tutorial, you'll have a functioning module that you can integrate into an existing ASP.NET MVC project. Whether you're building a SaaS platform or a content subscription service, the patterns and practices here will provide a strong foundation for building reliable, scalable subscription systems.

1. Project Setup

The first step in building our subscription module is creating a new ASP.NET MVC application using Visual Studio. This will serve as our foundation, and we'll build our data models, business logic, and UI components on top of it. When prompted during project creation, choose the MVC template and configure your authentication settings as needed for your user base.

After creating the project, we need to install a few NuGet packages to support Entity Framework and ASP.NET Identity, which are core to our user and subscription management functionality. For example, Microsoft.EntityFramework will allow us to build our database access layer, and Microsoft.AspNet.Identity.EntityFramework provides user authentication capabilities.

Install-Package Microsoft.EntityFramework -Version 6.4.4
Install-Package Microsoft.AspNet.Identity.EntityFramework
Install-Package Stripe.net // Optional for real payment integration

These packages lay the groundwork for our entire system. With Entity Framework, we can interact with the database using strongly typed models, and Identity gives us a comprehensive system for managing users and roles. The Stripe package is optional but included for future scalability and integration.

Using NuGet simplifies the setup process and ensures that you get the latest, most secure versions of each dependency. This modular approach also makes the project easier to maintain, since each package can be updated independently of the others.

2. Define Models

To store and manage subscription data, we need to define our data models. These include SubscriptionPlan for defining available plans, UserSubscription to represent a user’s active or past subscriptions, and ApplicationUser which extends ASP.NET Identity’s user model. Each of these models plays a key role in organizing how data is stored and retrieved.

public class SubscriptionPlan
{
    public int Id { get; set; }

    public string Name
    {
        get => field;
        set => field = value ?? throw new ArgumentNullException(nameof(value));
    }

    public decimal Price { get; set; }
    public string Description { get; set; }
    public int DurationInDays { get; set; }
}

public class UserSubscription
{
    public int Id { get; set; }
    public string UserId { get; set; }
    public int PlanId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public bool IsActive => DateTime.UtcNow <= EndDate;

    public virtual ApplicationUser User { get; set; }
    public virtual SubscriptionPlan Plan { get; set; }
}

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<UserSubscription> Subscriptions { get; set; }
}

These classes map to tables in our SQL database and help organize our business logic. The use of navigation properties (virtual) allows us to navigate related entities easily when using Entity Framework. The UserSubscription model is especially critical for tracking start and end dates, which will be used to determine access.

In C# 14, field-backed properties like Name in SubscriptionPlan help ensure data integrity with built-in validation. This allows us to enforce rules such as requiring names to be non-null directly in the model, rather than repeating this logic throughout the codebase.

3. Add Subscription Logic to DbContext

Next, we extend our DbContext to include our new models. This allows us to use EF to manage our tables and data.

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<SubscriptionPlan> SubscriptionPlans { get; set; }
    public DbSet<UserSubscription> UserSubscriptions { get; set; }

    public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { }

    public static ApplicationDbContext Create() => new();
}

This DbContext class bridges your application to the database. Entity Framework uses it to perform CRUD operations and generate SQL queries under the hood. The DbSet<T> properties tell EF which entities should be tracked and mapped to database tables.

The constructor and factory method (Create) show how to simplify context initialization, and using the latest syntax from C# 14 makes it even more concise. Keeping the context clean and modular helps when unit testing or transitioning to more advanced patterns like repository or unit-of-work.

4. Seed Subscription Plans

Add seed data to initialize plans in development or production environments.

context.SubscriptionPlans.AddOrUpdate(p => p.Name,
    new SubscriptionPlan { Name = "Free", Price = 0, Description = "Basic access", DurationInDays = 30 },
    new SubscriptionPlan { Name = "Pro", Price = 9.99M, Description = "Pro features", DurationInDays = 30 },
    new SubscriptionPlan { Name = "Enterprise", Price = 29.99M, Description = "All features", DurationInDays = 90 }
);

Seeding is essential to provide default options without requiring manual input every time the database is recreated. This is useful not only in development but also in CI/CD pipelines for automated deployments. These plans can be adjusted or extended later to suit business needs.

This code ensures that users always have valid plans to choose from. It's especially useful when the app is first launched, ensuring a smooth experience from the very first interaction.

5. Controllers and Views

Here’s a controller that uses a primary constructor:

[Authorize]
public class SubscriptionController(ApplicationDbContext db) : Controller
{
    public ActionResult Index() => View(db.SubscriptionPlans.ToList());

    public ActionResult Subscribe(int id) => View(db.SubscriptionPlans.Find(id));

    [HttpPost]
    public ActionResult SubscribeConfirmed(int id)
    {
        var userId = User.Identity.GetUserId();
        var plan = db.SubscriptionPlans.Find(id);

        var subscription = new UserSubscription
        {
            UserId = userId,
            PlanId = plan.Id,
            StartDate = DateTime.UtcNow,
            EndDate = DateTime.UtcNow.AddDays(plan.DurationInDays)
        };

        db.UserSubscriptions.Add(subscription);
        db.SaveChanges();

        return RedirectToAction("MySubscription");
    }

    public ActionResult MySubscription()
    {
        var userId = User.Identity.GetUserId();
        var activeSub = db.UserSubscriptions
            .Include("Plan")
            .Where(s => s.UserId == userId && s.IsActive)
            .OrderByDescending(s => s.EndDate)
            .FirstOrDefault();

        return View(activeSub);
    }
}

This controller manages the user's interaction with subscription plans. It retrieves available plans, handles subscription actions, and displays the user's current subscription. Using C# 14 primary constructors simplifies dependency injection, especially when there are few dependencies.

The controller is tied to Identity for user management, ensuring that actions like subscribing are tied to the correct user context. This integration of business logic and authentication is central to secure and personalized application flows.

6. Access Control Based on Subscription

Create a custom authorization attribute:

public class SubscriptionAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var userId = httpContext.User.Identity.GetUserId();
        var db = new ApplicationDbContext();
        var sub = db.UserSubscriptions.FirstOrDefault(x => x.UserId == userId && x.EndDate >= DateTime.UtcNow);
        return sub != null;
    }
}

Use it like this:

[SubscriptionAuthorize]
public ActionResult PremiumContent()
{
    return View();
}

This custom attribute checks if the logged-in user has an active subscription. It’s a lightweight and reusable solution to protect controller actions or entire controllers. It works well for scenarios where only subscribers should access certain parts of the application.

By encapsulating this logic into an attribute, we keep our controller actions clean and focused on what they need to do, rather than how authorization should be enforced. This improves both readability and maintainability.

7. Optional: Mock Payment Service

Handling payments is a critical part of any subscription system, but during development or for MVP testing, it's useful to mock this functionality. A mock payment service allows us to simulate successful transactions without integrating a third-party provider like Stripe or PayPal.

Below is a simple mock PaymentService class. It returns true to simulate a successful payment transaction, regardless of input. This allows you to develop and test your system logic without incurring real charges or managing payment API credentials.

public class PaymentService
{
    public bool ChargeUser(string userId, decimal amount)
    {
        // Simulate a successful payment
        Console.WriteLine($"Charging user {userId} for ${amount}");
        return true;
    }
}

You can integrate this into your subscription flow by calling it before saving the subscription. For example:

var paymentService = new PaymentService();
if (paymentService.ChargeUser(userId, plan.Price))
{
    var subscription = new UserSubscription
    {
        UserId = userId,
        PlanId = plan.Id,
        StartDate = DateTime.UtcNow,
        EndDate = DateTime.UtcNow.AddDays(plan.DurationInDays)
    };

    db.UserSubscriptions.Add(subscription);
    db.SaveChanges();
}

Using a mock now allows you to fully test your end-to-end subscription logic while making it simple to switch to a real provider later. When you're ready for live payments, just replace it PaymentService with a real implementation that connects to your gateway of choice.

8. Extension Members in C# 14

Use extension members to clean check the premium status:

public static class SubscriptionExtensions
{
    extension(UserSubscription subscription)
    {
        public bool IsPremium => ["Pro", "Enterprise"].Contains(subscription.Plan?.Name);
    }
}

Then in your code:

if (userSubscription.IsPremium)
{
    // Show premium features
}

Extension members let you encapsulate logic related to a class without modifying the class itself. This is ideal for computed properties like determining plan tier. It keeps your domain model lean while making your code more expressive.

This feature is especially useful in scenarios where your business logic spans multiple types or you need utility-style operations across many parts of the app. You avoid cluttering your core model with too much logic.

9. Views

Here's a simple Index.cshtml:

@model IEnumerable<SubscriptionPlan>
<h2>Choose a Plan</h2>
@foreach (var plan in Model)
{
    <div>
        <h3>@plan.Name</h3>
        <p>@plan.Description</p>
        <p>Price: [email protected]</p>
        @Html.ActionLink("Subscribe", "Subscribe", new { id = plan.Id })
    </div>
}

This view lists all available subscription plans. Each plan includes a button that links to the subscribe confirmation page. The use of Razor syntax makes it easy to iterate through models and generate dynamic HTML.

Views like this help users quickly understand the value proposition of each plan. With Bootstrap or Tailwind CSS, you could further enhance the design for a polished, responsive experience.

Conclusion

We built a user subscription module using ASP.NET MVC and C# 14, leveraging modern language features for better performance and readability. We explored models, controllers, access control, extension methods, and mocked payment processing. This provides a solid foundation for a SaaS or subscription-based application.

Next steps might include adding webhook support, real payment integration (e.g., Square, Stripe), user dashboards, or analytics.