Introduction#
If you are a C# developer familiar with ASP.NET Core, picking up Spring Boot feels like learning a parallel universe. The concepts are largely the same — dependency injection, ORM, REST APIs, configuration — but the names, annotations, and conventions differ significantly.
This guide maps Spring Boot concepts directly to their ASP.NET Core equivalents, so you can leverage your existing knowledge and focus only on what is genuinely different.
The Concept Map at a Glance#
Before diving in, here is a high-level mapping:
| ASP.NET Core Concept | Spring Boot Equivalent |
|---|---|
Program.cs + builder.Services |
@SpringBootApplication + Auto Configuration |
appsettings.json |
application.properties / application.yml |
IOptions<T> |
@ConfigurationProperties |
IServiceCollection DI |
@Component, @Service, @Repository, @Bean |
| Entity Framework Core | Spring Data JPA (Hibernate) |
DbContext |
EntityManager / JPA Repositories |
| EF Core Migrations | Flyway / Liquibase |
[ApiController] / ControllerBase |
@RestController |
[HttpGet], [HttpPost] |
@GetMapping, @PostMapping |
[FromBody], [FromQuery] |
@RequestBody, @RequestParam |
| ASP.NET Core Middleware | Spring Boot Filters / Interceptors |
| Action Filters | @Aspect (Spring AOP) |
IHostedService |
@Scheduled / CommandLineRunner |
Auto Configuration: The Biggest Mental Shift#
How ASP.NET Core Does It#
In ASP.NET Core, you explicitly register everything in Program.cs:
1
2
3
4
5
6
7
8
9
10
11
var builder = WebApplication.CreateBuilder(args);
// You explicitly add what you need
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
app.MapControllers();
app.Run();
You are always in control. Nothing happens unless you explicitly wire it up.
How Spring Boot Does It#
Spring Boot inverts this with Auto Configuration. You add a dependency (called a Starter) to your pom.xml (Maven) or build.gradle (Gradle), and Spring Boot configures it automatically based on what it finds on the classpath.
1
2
3
4
5
6
@SpringBootApplication // This single annotation does a LOT
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication combines three annotations:
@SpringBootConfiguration— marks this as a configuration source (likeStartup.cs)@EnableAutoConfiguration— tells Spring to auto-configure based on dependencies@ComponentScan— scans the package for annotated classes (like scanning for controllers)
The Auto Configuration Flow#
flowchart TD
A[Application Starts] --> B[Reads pom.xml or build.gradle / Starters on Classpath]
B --> C{spring-boot-autoconfigure}
C --> D[Reads META-INF/spring/AutoConfiguration.imports]
D --> E{Check @Conditional Annotations}
E -->|Class present on classpath| F[Apply Auto Configuration]
E -->|Property set in application.properties| F
E -->|Bean NOT already defined| F
E -->|Condition false| G[Skip Auto Configuration]
F --> H[Bean registered in Application Context]
G --> I[Bean not registered]
C# Equivalent of Spring Boot Starters#
In ASP.NET Core, you install a NuGet package and manually call an extension method:
1
2
3
// NuGet: Microsoft.EntityFrameworkCore.SqlServer
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlServer(connectionString));
In Spring Boot, you add the starter dependency and configuration happens automatically.
Maven (pom.xml):
1
2
3
4
5
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Gradle (build.gradle):
1
2
3
4
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
Gradle Kotlin DSL (build.gradle.kts):
1
2
3
4
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}
1
2
3
4
5
6
7
8
9
# application.yml (equivalent to appsettings.json)
spring:
datasource:
url: jdbc:sqlserver://localhost:1433;databaseName=mydb
username: sa
password: secret
jpa:
hibernate:
ddl-auto: update
Spring Boot sees spring-boot-starter-data-jpa on the classpath and automatically configures DataSource, EntityManagerFactory, and transaction management.
Configuration Binding: IOptions vs @ConfigurationProperties #
ASP.NET Core:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// appsettings.json
// {
// "PaymentSettings": { "ApiUrl": "https://...", "Timeout": 30 }
// }
public class PaymentSettings
{
public string ApiUrl { get; set; }
public int Timeout { get; set; }
}
// Program.cs
builder.Services.Configure<PaymentSettings>(
builder.Configuration.GetSection("PaymentSettings"));
// Usage via constructor injection
public class PaymentService
{
private readonly PaymentSettings _settings;
public PaymentService(IOptions<PaymentSettings> options)
{
_settings = options.Value;
}
}
Spring Boot equivalent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// application.yml
// payment:
// api-url: https://...
// timeout: 30
@ConfigurationProperties(prefix = "payment")
@Component
public class PaymentSettings {
private String apiUrl;
private int timeout;
// getters and setters required
}
// Usage via constructor injection (same concept)
@Service
public class PaymentService {
private final PaymentSettings settings;
public PaymentService(PaymentSettings settings) {
this.settings = settings;
}
}
@Conditional: Spring Boot’s Equivalent of Conditional Registration#
ASP.NET Core does not have a direct built-in equivalent, but you can conditionally register services:
1
2
3
4
5
6
7
8
9
// ASP.NET Core - conditional registration
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IEmailSender, MockEmailSender>();
}
else
{
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
}
Spring Boot provides this as a first-class concept using @Conditional annotations:
1
2
3
4
5
6
7
8
9
// Registers the bean only if a specific class is on the classpath
@ConditionalOnClass(MailSender.class)
// Registers the bean only if a property is set
@ConditionalOnProperty(name = "feature.email.enabled", havingValue = "true")
// Registers the bean only if NO other bean of this type is registered
// (this is the key safety mechanism in auto configuration)
@ConditionalOnMissingBean(IEmailSender.class)
The @ConditionalOnMissingBean pattern is why auto configuration does not override your custom beans — if you define your own DataSource bean, Spring Boot’s auto-configured one is skipped automatically.
Dependency Injection: Familiar Concept, Different Syntax#
ASP.NET Core DI#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Registration
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// Usage - constructor injection
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
}
Spring Boot DI#
Spring Boot uses annotations on the class itself to register beans:
1
2
3
4
5
6
7
8
@Service // equivalent to AddScoped (request-scoped by default for web)
public class OrderService implements IOrderService { }
@Repository // same as @Service but adds exception translation for data access
public class OrderRepository { }
@Component // generic bean - equivalent to AddScoped
public class EmailSender { }
Spring Boot lifetimes mapped to ASP.NET Core:
| ASP.NET Core | Spring Boot | Notes |
|---|---|---|
AddScoped |
@Scope("request") or default for @Service |
Per-request |
AddSingleton |
@Scope("singleton") (default) |
One instance |
AddTransient |
@Scope("prototype") |
New instance each time |
1
2
3
4
5
6
7
8
9
10
// Constructor injection works identically
@RestController
public class OrderController {
private final OrderService orderService;
// @Autowired is optional when there is only one constructor
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
Database Connections: EF Core vs Spring Data JPA#
Startup Configuration#
flowchart LR
subgraph ASP.NET Core
A1[Program.cs] --> B1[AddDbContext]
B1 --> C1[DbContext]
C1 --> D1[DbSet per Entity]
end
subgraph Spring Boot
A2[application.yml] --> B2[Auto-configured DataSource]
B2 --> C2[EntityManagerFactory]
C2 --> D2[JpaRepository per Entity]
end
Defining Entities#
EF Core (C#):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Entity class - EF Core uses conventions or Fluent API
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<OrderItem> Items { get; set; }
}
// DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasMany(o => o.Items)
.WithOne(i => i.Order);
}
}
Spring Data JPA (Java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Entity class - JPA uses annotations directly on the class
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerName;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
// Getters and setters required (or use Lombok @Data)
}
Repositories: DbContext vs JpaRepository#
This is where Spring Data JPA is noticeably more concise than EF Core.
EF Core — you write the query methods yourself:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(int id)
=> await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
public async Task<List<Order>> GetByCustomerAsync(string customerName)
=> await _context.Orders
.Where(o => o.CustomerName == customerName)
.ToListAsync();
public async Task<Order> CreateAsync(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order;
}
}
Spring Data JPA — the interface generates queries from method names:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Spring generates the implementation automatically
// No boilerplate code needed
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Spring generates: SELECT * FROM orders WHERE customer_name = ?
List<Order> findByCustomerName(String customerName);
// Spring generates: SELECT * FROM orders WHERE total_amount > ?
List<Order> findByTotalAmountGreaterThan(BigDecimal amount);
// Custom JPQL query when method name is not enough
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
}
The query derivation from method names is one of Spring Data JPA’s most powerful features. findByCustomerName is equivalent to a LINQ Where(o => o.CustomerName == name) query — no code required.
Transactions: SaveChanges() vs @Transactional#
EF Core — explicit SaveChanges:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public async Task<Order> ProcessOrderAsync(OrderDto dto)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var order = new Order { CustomerName = dto.CustomerName };
_context.Orders.Add(order);
var inventory = await _context.Inventory.FindAsync(dto.ItemId);
inventory.Stock -= dto.Quantity;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return order;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
Spring Boot — declarative with @Transactional:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
// Constructor injection
public OrderService(OrderRepository orderRepository,
InventoryRepository inventoryRepository) {
this.orderRepository = orderRepository;
this.inventoryRepository = inventoryRepository;
}
// Transaction starts before method, commits on success, rolls back on exception
@Transactional
public Order processOrder(OrderDto dto) {
Order order = new Order();
order.setCustomerName(dto.getCustomerName());
orderRepository.save(order);
Inventory inventory = inventoryRepository.findById(dto.getItemId()).orElseThrow();
inventory.setStock(inventory.getStock() - dto.getQuantity());
inventoryRepository.save(inventory);
return order;
// Transaction commits here automatically
}
}
Database Migrations#
| ASP.NET Core | Spring Boot |
|---|---|
EF Core Migrations (dotnet ef migrations add) |
Flyway or Liquibase |
dotnet ef database update |
Runs automatically on startup |
_EFMigrationsHistory table |
flyway_schema_history table |
EF Core migrations are code-first and generate C# migration files. Flyway uses SQL migration files:
1
2
3
4
5
6
7
-- V1__create_orders_table.sql (Flyway naming convention)
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(255) NOT NULL,
total_amount DECIMAL(19, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
REST APIs: Controllers Side by Side#
Spring Boot’s @RestController maps almost directly to ASP.NET Core’s [ApiController].
flowchart TD
subgraph ASP.NET Core
R1[HTTP Request] --> M1[Middleware Pipeline]
M1 --> AF1[Action Filters]
AF1 --> C1["[ApiController] Method"]
C1 --> S1[Service Layer]
S1 --> C1
C1 --> AF1
AF1 --> R2[HTTP Response]
end
subgraph Spring Boot
R3[HTTP Request] --> M2[Filter Chain]
M2 --> IN1[HandlerInterceptor]
IN1 --> C2["@RestController Method"]
C2 --> S2[Service Layer]
S2 --> C2
C2 --> IN1
IN1 --> R4[HTTP Response]
end
Controller Comparison#
ASP.NET Core:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> GetById([FromRoute] int id)
{
var order = await _orderService.GetByIdAsync(id);
if (order is null)
return NotFound();
return Ok(order);
}
[HttpGet]
public async Task<ActionResult<List<OrderDto>>> GetAll(
[FromQuery] string? customerName)
{
var orders = await _orderService.GetAllAsync(customerName);
return Ok(orders);
}
[HttpPost]
public async Task<ActionResult<OrderDto>> Create([FromBody] CreateOrderDto dto)
{
var order = await _orderService.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
[FromRoute] int id, [FromBody] UpdateOrderDto dto)
{
await _orderService.UpdateAsync(id, dto);
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id)
{
await _orderService.DeleteAsync(id);
return NoContent();
}
}
Spring Boot equivalent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public ResponseEntity<OrderDto> getById(@PathVariable Long id) {
return orderService.getById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<OrderDto>> getAll(
@RequestParam(required = false) String customerName) {
return ResponseEntity.ok(orderService.getAll(customerName));
}
@PostMapping
public ResponseEntity<OrderDto> create(@RequestBody @Valid CreateOrderDto dto) {
OrderDto created = orderService.create(dto);
URI location = URI.create("/api/orders/" + created.getId());
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Void> update(
@PathVariable Long id,
@RequestBody @Valid UpdateOrderDto dto) {
orderService.update(id, dto);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
orderService.delete(id);
return ResponseEntity.noContent().build();
}
}
Annotation Mapping Reference#
| ASP.NET Core | Spring Boot | Purpose |
|---|---|---|
[ApiController] |
@RestController |
Mark as REST controller, combines @Controller + @ResponseBody |
[Route("api/[controller]")] |
@RequestMapping("/api/orders") |
Base route |
[HttpGet("{id}")] |
@GetMapping("/{id}") |
GET with path param |
[HttpPost] |
@PostMapping |
POST |
[HttpPut] |
@PutMapping |
PUT |
[HttpDelete] |
@DeleteMapping |
DELETE |
[FromRoute] |
@PathVariable |
Bind from URL path |
[FromQuery] |
@RequestParam |
Bind from query string |
[FromBody] |
@RequestBody |
Bind from request body |
[FromHeader] |
@RequestHeader |
Bind from header |
[Required], [Range] |
@NotNull, @Min, @Max |
Validation annotations |
ModelState.IsValid |
@Valid on parameter |
Trigger validation |
Validation#
ASP.NET Core:
1
2
3
4
5
6
7
8
9
10
11
public class CreateOrderDto
{
[Required]
[StringLength(100)]
public string CustomerName { get; set; }
[Range(0.01, double.MaxValue)]
public decimal TotalAmount { get; set; }
}
// [ApiController] validates automatically and returns 400 if invalid
Spring Boot:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CreateOrderDto {
@NotBlank
@Size(max = 100)
private String customerName;
@DecimalMin("0.01")
private BigDecimal totalAmount;
// getters and setters
}
// @Valid in method parameter triggers validation
// Returns 400 automatically if validation fails (with MethodArgumentNotValidException)
Middleware vs Filters#
ASP.NET Core Middleware:
1
2
3
4
5
6
7
8
9
10
11
12
// Global middleware in Program.cs
app.Use(async (context, next) =>
{
// Before request
var start = Stopwatch.GetTimestamp();
await next(context);
// After response
var elapsed = Stopwatch.GetElapsedTime(start);
logger.LogInformation("Request took {Ms}ms", elapsed.TotalMilliseconds);
});
Spring Boot Filter (same concept):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class RequestTimingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestTimingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
long start = System.currentTimeMillis();
chain.doFilter(request, response); // equivalent to next(context)
long elapsed = System.currentTimeMillis() - start;
logger.info("Request took {}ms", elapsed);
}
}
Exception Handling: IExceptionFilter vs @ControllerAdvice#
ASP.NET Core:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is NotFoundException ex)
{
context.Result = new NotFoundObjectResult(new { error = ex.Message });
context.ExceptionHandled = true;
}
}
}
// Or use minimal API exception handler
app.UseExceptionHandler(appError =>
{
appError.Run(async context => { /* handle */ });
});
Spring Boot:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestControllerAdvice // applies to all @RestController classes
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(new ErrorResponse(message));
}
}
Project Structure Comparison#
flowchart LR
subgraph ASP.NET Core
P1[Program.cs] --> P2[Controllers/]
P2 --> P3[Services/]
P3 --> P4[Repositories/]
P4 --> P5[Data/AppDbContext.cs]
P5 --> P6[Models/]
P6 --> P7[appsettings.json]
end
subgraph Spring Boot
S1[Application.java] --> S2[controller/]
S2 --> S3[service/]
S3 --> S4[repository/]
S4 --> S5[EntityManager via JPA]
S5 --> S6[entity/]
S6 --> S7[application.yml]
end
What a C# Developer Should Focus on to Learn Spring Boot Fast#
1. Master the Annotation System#
In C#, you use attributes sparingly. In Spring Boot, annotations drive almost everything. Understanding what each annotation does and when to use it is the single most important skill.
Key annotations to know first:
@SpringBootApplication— entry point@RestController,@GetMapping,@PostMapping— controllers@Service,@Repository,@Component— bean registration@Autowired(optional with constructor injection) — dependency injection@Entity,@Id,@GeneratedValue— JPA entities@Transactional— transaction management@ConfigurationProperties— configuration binding@Beaninside@Configuration— explicit bean registration (like registering inIServiceCollection)
2. Understand Spring’s Application Context#
The Application Context is Spring’s equivalent of ASP.NET Core’s DI container (the IServiceProvider). Everything registered with @Component, @Service, @Repository, or @Bean lives in the Application Context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// You can even query the application context directly (like GetService<T>() in C#)
@Component
public class StartupRunner implements CommandLineRunner {
private final ApplicationContext context;
public StartupRunner(ApplicationContext context) {
this.context = context;
}
@Override
public void run(String... args) {
// Equivalent to serviceProvider.GetService<OrderService>()
OrderService service = context.getBean(OrderService.class);
}
}
3. Learn Maven/Gradle (Your New NuGet + MSBuild)#
Spring Boot uses Maven or Gradle for dependency management and build automation. Both serve the same role as your *.csproj + packages.config combined. New projects commonly use Gradle with the Kotlin DSL (build.gradle.kts), but Maven is still widely used in enterprise codebases.
Maven (pom.xml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- pom.xml — equivalent of .csproj -->
<dependencies>
<!-- Spring Boot Web (MVC + Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA + Hibernate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- SQL Server Driver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Gradle Groovy DSL (build.gradle):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// build.gradle
plugins {
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.7'
id 'java'
}
dependencies {
// Spring Boot Web (MVC + Tomcat)
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Data JPA + Hibernate
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// SQL Server Driver
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Gradle Kotlin DSL (build.gradle.kts) — preferred for new projects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// build.gradle.kts
plugins {
id("org.springframework.boot") version "3.4.0"
id("io.spring.dependency-management") version "1.1.7"
kotlin("jvm") version "2.1.0"
}
dependencies {
// Spring Boot Web (MVC + Tomcat)
implementation("org.springframework.boot:spring-boot-starter-web")
// Spring Data JPA + Hibernate
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// SQL Server Driver
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Build tool command comparison:
| Task | Maven | Gradle |
|---|---|---|
| Build | ./mvnw package |
./gradlew build |
| Run | ./mvnw spring-boot:run |
./gradlew bootRun |
| Test | ./mvnw test |
./gradlew test |
| Clean | ./mvnw clean |
./gradlew clean |
| Skip tests | ./mvnw package -DskipTests |
./gradlew build -x test |
| Show dependencies | ./mvnw dependency:tree |
./gradlew dependencies |
4. Recognize These Key Differences#
Java lacks properties — use getters/setters or Lombok
C# properties (public string Name { get; set; }) do not exist in Java. Every field needs explicit getter and setter methods. Use Lombok to eliminate this boilerplate:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Without Lombok — verbose
public class Order {
private String customerName;
public String getCustomerName() { return customerName; }
public void setCustomerName(String value) { customerName = value; }
}
// With Lombok — equivalent to C# auto-property
@Data // generates all getters, setters, equals, hashCode, toString
public class Order {
private String customerName;
}
**Optional
1
2
3
4
5
// Java - explicit Optional wrapping
Optional<Order> order = orderRepository.findById(id);
// C# equivalent
Order? order = await _repository.GetByIdAsync(id);
Checked exceptions are common
Java has checked exceptions that the compiler forces you to handle or declare. This has no C# equivalent — just be aware that throws declarations in method signatures are normal.
Interfaces in Java have no direct C# equivalent for default implementations until Java 8+
Spring commonly uses interface-based design just like ASP.NET Core, so this part will feel familiar.
5. Recommended Learning Path#
flowchart TD
A[Start: Familiar with ASP.NET Core] --> B[Set up Spring Initializr project]
B --> C[Learn Maven/Gradle basics]
C --> D[Build a basic REST API with @RestController]
D --> E[Add Spring Data JPA + H2 in-memory DB]
E --> F[Replace H2 with SQL Server / PostgreSQL]
F --> G[Add @Transactional and understand rollback behavior]
G --> H[Add Bean Validation with @Valid]
H --> I[Add @ControllerAdvice for global error handling]
I --> J[Learn Spring Security basics]
J --> K[Understand Auto Configuration and Starters]
K --> L[Production-ready: Actuator, Micrometer metrics]
Quick Reference: Common Tasks#
Setting Up a New Project#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ASP.NET Core
dotnet new webapi -n MyApi
# Spring Boot — use Spring Initializr (browser UI)
# https://start.spring.io
# Or with Spring CLI — Maven (default)
spring init --build=maven --dependencies=web,data-jpa,sqlserver my-api
# Or with Spring CLI — Gradle Groovy DSL
spring init --build=gradle --dependencies=web,data-jpa,sqlserver my-api
# Or with Spring CLI — Gradle Kotlin DSL
spring init --build=gradle-project-kotlin --dependencies=web,data-jpa,sqlserver my-api
Reading Configuration Values#
1
2
3
// ASP.NET Core
var connectionString = builder.Configuration.GetConnectionString("Default");
var timeout = builder.Configuration.GetValue<int>("AppSettings:Timeout");
1
2
3
4
5
6
// Spring Boot
@Value("${spring.datasource.url}")
private String connectionString;
@Value("${app.settings.timeout:30}") // 30 is the default
private int timeout;
Running the Application#
1
2
3
4
5
6
7
8
9
10
11
12
# ASP.NET Core
dotnet run
# Spring Boot — Maven
./mvnw spring-boot:run
# Spring Boot — Gradle
./gradlew bootRun
# Run the packaged JAR directly (works regardless of build tool)
java -jar target/my-api-0.0.1-SNAPSHOT.jar # Maven output path
java -jar build/libs/my-api-0.0.1-SNAPSHOT.jar # Gradle output path
Integration Testing#
ASP.NET Core:
1
2
3
4
5
6
7
8
9
10
[Fact]
public async Task GetOrder_ReturnsOrder_WhenExists()
{
var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders/1");
response.EnsureSuccessStatusCode();
}
Spring Boot:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void getOrder_returnsOrder_whenExists() {
ResponseEntity<OrderDto> response =
restTemplate.getForEntity("/api/orders/1", OrderDto.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
Conclusion#
Spring Boot and ASP.NET Core solve the same problems with remarkably similar architectures. As a C# developer, your biggest adjustments will be:
- Accepting annotation-driven configuration over explicit registration
- Learning Maven (
pom.xml) or Gradle (build.gradle/build.gradle.kts) and the Starter ecosystem - Getting comfortable with Java’s verbosity (and Lombok as a remedy)
- Understanding that
@Transactionalreplaces explicitSaveChanges()and transaction management - Recognizing that Spring Data JPA’s repository method derivation eliminates much of the LINQ-style query code you write manually in EF Core
The core patterns — layered architecture, dependency injection, ORM, REST controllers, middleware/filters — are identical. Focus on learning the Spring-specific mechanics of those patterns and you will be productive quickly.