C#进阶:项目中Service类与Repository类的应用方法问询
Hey there! Let's break this down clearly—since you're diving into loose coupling and dependency injection (DI) in C#, understanding the divide between Service and Repository classes is key to keeping your code clean, maintainable, and scalable.
Core Responsibilities: What Each Class Does
First, let's nail down the distinct roles of these two types of classes—mixing them up is a common pitfall, so clarity here goes a long way.
Repository Classes
Repos are your data access layer—they’re the only part of your code that should talk directly to databases, file systems, APIs, or any other data store. Their sole job is to encapsulate CRUD (Create, Read, Update, Delete) operations for a specific entity (like your Player class).
- They should never contain business logic—only logic related to fetching, saving, or manipulating data.
- Example: A
PlayerRepositorymight have methods likeGetPlayerById(int id),AddPlayer(Player player), orCheckIfUsernameExists(string username). - Always implement an interface (e.g.,
IPlayerRepository)—this lets you swap data stores later (say, from SQL to MongoDB) without touching any other code, which is perfect for loose coupling.
Service Classes
Services are where your business logic lives. They use repositories to pull the data they need, then apply rules, transformations, or orchestration to that data to fulfill business requirements.
- For example, a
PlayerServicemight have aRegisterNewPlayermethod that:- Uses
PlayerRepositoryto check if the username/email is already taken - Hashes the user's password (using a separate password-hashing service)
- Creates a valid
Playerobject with default values (like a creation timestamp) - Calls
PlayerRepository.AddPlayer()to save the new user - Triggers a welcome email (via another service, if you’re splitting responsibilities further)
- Uses
- Services can also coordinate multiple repositories—like updating a player’s stats and logging the change in an
AuditRepository. - Just like repos, services should implement interfaces (e.g.,
IPlayerService) to enable mocking for testing and easy implementation swaps.
Putting It All Together with Dependency Injection
Since you’re focused on loose coupling, DI is how you wire these layers together without hardcoding dependencies. Here’s a concrete C# example:
1. Define Interfaces (Critical for Loose Coupling)
// Repository interface public interface IPlayerRepository { Player GetById(int id); void Add(Player player); bool UsernameExists(string username); } // Service interface public interface IPlayerService { bool RegisterPlayer(string username, string email, string password); }
2. Implement the Repository (SQL Example)
public class PlayerRepository : IPlayerRepository { private readonly MyDbContext _dbContext; // Inject DbContext via constructor (DI handles this) public PlayerRepository(MyDbContext dbContext) { _dbContext = dbContext; } public Player GetById(int id) { return _dbContext.Players.Find(id); } public void Add(Player player) { _dbContext.Players.Add(player); _dbContext.SaveChanges(); } public bool UsernameExists(string username) { return _dbContext.Players.Any(p => p.Username == username); } }
3. Implement the Service (Inject the Repository)
public class PlayerService : IPlayerService { private readonly IPlayerRepository _playerRepository; private readonly IPasswordHasher _passwordHasher; // Another injected dependency // Inject the repository interface (not the concrete class!) public PlayerService(IPlayerRepository playerRepository, IPasswordHasher passwordHasher) { _playerRepository = playerRepository; _passwordHasher = passwordHasher; } public bool RegisterPlayer(string username, string email, string password) { // Business rule: Username must be unique if (_playerRepository.UsernameExists(username)) return false; // Apply business logic (hash password, set defaults) var newPlayer = new Player { Username = username, Email = email, PasswordHash = _passwordHasher.Hash(password), CreatedAt = DateTime.UtcNow }; // Delegate data saving to the repository _playerRepository.Add(newPlayer); return true; } }
4. Register Dependencies in Your DI Container (ASP.NET Core Example)
In Program.cs, tell the DI system which implementations to use for each interface:
builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); builder.Services.AddScoped<IPlayerRepository, PlayerRepository>(); builder.Services.AddScoped<IPlayerService, PlayerService>(); builder.Services.AddScoped<IPasswordHasher, PasswordHasher>();
5. Use the Service in Your Controller/UI Layer
public class PlayerController : ControllerBase { private readonly IPlayerService _playerService; // Inject the service interface public PlayerController(IPlayerService playerService) { _playerService = playerService; } [HttpPost("register")] public IActionResult Register([FromBody] RegisterRequest request) { bool success = _playerService.RegisterPlayer(request.Username, request.Email, request.Password); return success ? Ok("Player registered!") : BadRequest("Username already exists."); } }
Key Best Practices to Follow
- Keep repos focused: One repo per entity (or aggregate root)—don’t make a single "AllInOneRepository" that handles every table.
- Keep services cohesive: A service should handle a specific business domain (e.g.,
PlayerServicefor player logic,MatchServicefor match logic) instead of a single "GodService" that does everything. - Depend on interfaces, not concrete classes: This is the backbone of loose coupling—you can mock
IPlayerRepositoryin unit tests forPlayerServicewithout hitting a real database. - No business logic in repos: Repos are for data access only. Keeping business rules in services ensures they’re centralized and easy to modify.
内容的提问来源于stack exchange,提问作者cardnc

