Multi-Tenant Application Design

What Is a Multi-Tenant Application A multi-tenant application is a software architecture design that allows a single instance of software to serve multiple customers. Each customer is called a tenant; data across tenants is automatically isolated, so one tenant’s data never affects another’s. Search for “multi-tenancy” on the ZeroDegree Programming website to watch the multi-tenant design video tutorial. Isolating Tenant Data with a Shared Table and a Discriminator Column The shared-table multi-tenant design stores data for multiple tenants in a single table, distinguishing each tenant’s rows with an added TenantId column. ...

March 10, 2026

Orchestration with the Aspire App Host

Adding Application Components via Add Methods Using methods such as AddProject, AddContainer, AddExecutable, AddParameter, and AddConnectionString, you can orchestrate the app host into a complete application. Configuring Explicit Resource Startup builder.AddProject<Projects.MyApp>("dbmigration").WithExplicitStart(); Using the WithExplicitStart method, you can configure explicit resource startup; resources configured for explicit startup need to be started manually. Referencing Resources with the WithReference Method builder.AddProject<Projects.MyApp>("myapp").WithReference(postgresdb); The referenced resource can be an endpoint, a connection string, or another resource. The referenced resource is passed to the referencing resource via environment variables. ...

March 10, 2026

Ordering Microservice Architecture Overview

The ordering part covers many technical topics, including the CQRS pattern, MediatR for local command dispatch, local event handling, abstraction and implementation of distributed events, the outbox pattern, distributed locks, and more. Ordering Service Architecture Diagram Microservice Communication Client Side The client side mainly covers order creation, payment, cancellation, and querying. The client calls the ordering microservice through the API Gateway; the ordering microservice processes commands via MediatR, then publishes events through the event bus; the event bus dispatches the events to event handlers, which process them and then persist the events to the database. ...

March 10, 2026

Paging, Sorting, and Multi-Condition Queries

Paging Parameters Requests should use the GET method http://localhost:8080/api/products?keyword=test&pagenumber=1&pagesize=5&orderby=id desc,price asc The response is as follows { "totalCount": 100, "items": [ { "id": 1, "name": "test", "price": 100 }, { "id": 2, "name": "test", "price": 200 } ] } Paged Request Model public class PagedAndSortedRequest : PagedRequest { public string? OrderBy { get; init; } } Paged Response Model public class PagedResponse<T>(IReadOnlyList<T> items, int totalCount) { public IReadOnlyList<T> Items { get; init; } = items; public int TotalCount { get; init; } = totalCount; } Extending IQueryable to Sort by Property Name QueryableOrderByExtensions.cs Convert a string condition into a sort expression IOrderedQueryable<TSource> OrderBy(IQueryable<TSource> source, string propertyName) Extending IQueryable for Searching and Sorting QueryableExtensions.cs Extension methods for paging and sorting ...

March 10, 2026

Policy-based and Resource-based Authorization

After authentication, identity data is available in HttpContext.User. However, identity is not equal to permission. Permissions should be evaluated by the authorization system. Authorization in ASP.NET Core ASP.NET Core provides a policy-based authorization model. Policies can be defined by role, resource, or custom conditions. Use IAuthorizationService IAuthorizationService is the core authorization service in ASP.NET Core: public interface IAuthorizationService { Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, AuthorizationPolicy policy); } AuthorizationResult contains success/failure information and reasons. Implement a custom policy provider You can use OperationAuthorizationRequirement to model permission requirements and inject them into policies. ...

March 10, 2026

PostgreSQL High Availability and Read/Write Splitting

Basic Concepts A single operating system can run multiple PostgreSQL instances, each with its own configuration file, data directory, and port. Each instance can host multiple databases, and each database can contain multiple tables. Docker makes it easy to create multiple PostgreSQL instances. Installing the Primary and Replica Servers docker run --name postgres1 -e POSTGRES_PASSWORD=postgres -d -p 5431:5432 -v ${pwd}/postgres1/data:/var/lib/postgresql/data postgres docker run --name postgres2 -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 -v ${pwd}/postgres2/data:/var/lib/postgresql/data postgres Logical Replication Logical replication is a method of replicating data objects and their changes based on a replication identity (usually the primary key). We use the term “logical” in contrast to physical replication. Logical replication uses a publish and subscribe model, in which one or more subscribers subscribe to one or more publications on a publisher node. Subscribers pull data from the publications they subscribe to, and may subsequently re-publish the data to allow cascading replication or more complex configurations. ...

March 10, 2026

Provider used to generate initial data

Data seeding is the process of populating a database with a set of initial data. It is usually performed when the database is first created. These data are usually static and will not change over time. Data seeding is usually used to fill in some basic data, such as users, roles, permissions, products, classifications, etc. Universal interface design public interface IDataSeedingProvider { Task SeedingAsync(IServiceProvider serviceProvider); } Implement specific interface public class DataSeedingProvider(UserManager<User> userManager) : IDataSeedingProvider { public async Task SeedingAsync(IServiceProvider ServiceProvider) { var user = await userManager.FindByNameAsync("admin"); if (user == null) { await userManager.CreateAsync(new User { UserName = "admin", Email = "admin@test.com" }, "admin"); } } } Register in the container builder.Services.AddTransient<IDataSeedingProvider, DataSeedingProvider>(); Automated execution of extensions public static class DataSeedingExtensions { public static IApplicationBuilder UseDataSeedingProviders(this IApplicationBuilder app) { using var serviceScope = app.ApplicationServices.CreateScope(); var dataSeedingProviders = serviceScope.ServiceProvider.GetServices<IDataSeedingProvider>(); foreach (IDataSeedingProvider dataSeedingProvider in dataSeedingProviders) { dataSeedingProvider.SeedingAsync(serviceScope.ServiceProvider).Wait(); } return app; } } Automatic registration implementation public static class DataSeedingExtensions { public static IServiceCollection AddDataSeedingProviders(this IServiceCollection services, Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); var dataSeedProviders = assembly.ExportedTypes.Where(t => t.IsAssignableTo(typeof(IDataSeedingProvider)) && t.IsClass); dataSeedProviders.ToList().ForEach(t => services.AddTransient(typeof(IDataSeedingProvider), t)); return services; } } Execution order problem The execution order of automatically registered providers is uncertain. If you need to determine the execution order, you can design the Order attribute, and then order execution according to the Order attribute in the UseDataSeedingProviders method. ...

March 10, 2026

Readme

Friends, the new generation of the Zero framework officially starts being built today. We are very excited to announce that the new framework is named the HelloShop project. This framework will demonstrate a development architecture built on the new generation of the .NET technology stack. The name HelloShop was chosen because a simple shop application can demonstrate all the technologies of a system. Of course, this shop system could just as well be any other system — as long as you understand the design philosophy of this framework, you can quickly build a system of your own. The following document was generated by AI and may contain some grammatical errors, which we will correct later. ...

March 10, 2026

Solving Concurrency Problems with Distributed Locks

Locks in Single-Process Applications In a single-machine environment, we can use thread locks to solve concurrency problems. In a distributed system, however, thread locks cannot solve concurrency problems, because a thread lock in a distributed system can only lock the current process, not other processes. In .NET, we can use the lock keyword to implement a thread lock. https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/lock private static readonly object _lock = new object(); public void DoSomething() { lock (_lock) { // Business logic } } C# 13 introduces a new thread synchronization type, System.Threading.Lock, which simplifies lock usage through scope management, making the code clearer and more reliable. ...

March 10, 2026

Starting Distributed Microservices with Aspire

Preparing the Database docker run --name postgres -e POSTGRES_PASSWORD=postgres -e TZ=Asia/Shanghai -d -p 5432:5432 postgres docker run --name pgadmin -e PGADMIN_DEFAULT_EMAIL=test@test.com -e PGADMIN_DEFAULT_PASSWORD=test -e TZ=Asia/Shanghai -d -p 5050:80 dpage/pgadmin4 Initializing the Dapr Runtime dapr init -s --from-dir C:\dapr Extending the Wait Time for RabbitMQ Startup public static IResourceBuilder<IDaprSidecarResource> WithReference(this IResourceBuilder<IDaprSidecarResource> builder, IResourceBuilder<IResourceWithConnectionString> resourceBuilder, int waitInSeconds = 10) Updating MediatR Dependency Injection Previously, behaviors were registered with the options.AddBehavior method; now they are registered with the options.AddOpenBehavior method. builder.Services.AddMediatR(options => { options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); options.AddOpenBehavior(typeof(LoggingBehavior<,>)); options.AddOpenBehavior(typeof(ValidatorBehavior<,>)); options.AddOpenBehavior(typeof(TransactionBehavior<,>)); }); Microservices Using Distributed Events Need the HttpContext Injected builder.Services.AddHttpContextAccessor(); Dropping a Database in Azure Data Studio -- Terminate all sessions connected to the target database SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'my_database' AND pid <> pg_backend_pid(); -- Drop the database DROP DATABASE my_database; Automatically Creating the Database Before IdentityService Demo Data Generation await serviceProvider.GetRequiredService<IdentityServiceDbContext>().Database.EnsureCreatedAsync(cancellationToken); Consistently Use DateTimeOffset Instead of DateTime The advantage of using UTC time is that it can be converted between time zones without losing time information, which helps programs handle time better and benefits the internationalization and localization of cross-time-zone applications. ...

March 10, 2026