Meet The Author

I'm Ethan Jackson, An 25 years old blogger Currently living in London, United Kingdom. I'm a Skilled Blogger, Part Time web Developer And Creating new things as a web Designer.

author

Smart Query Caching in C#: Auto-Invalidate on Database Changes

Leave a Comment

A useful manual for maintaining cache freshness with distributed invalidation patterns (MemoryCache / Redis), version stamps, and SqlDependency.

Why is caching database-aware?
Performance is improved and database I/O is reduced when query results are cached. Serving outdated data poses a danger. The solution entails linking basic database modifications to cache invalidation.

  • reduced DB load and delay.
  • Freshness can be predicted using automatic invalidation.
  • expands from distributed programs to single instances.

Patterns at a glance

  • SqlDependency: push notifications from SQL Server for supported queries.
  • Version-Stamps: compare a “last updated” token; refresh only when changed.
  • Event-Driven: clear caches when your app writes to the DB.
  • Redis Pub/Sub: broadcast invalidations across instances.


1. SQL Server Query Notifications with SqlDependency
Requirements: SQL Server Service Broker enabled; query must follow notification rules (no SELECT *, no temp tables, etc.). Suitable for small-to-medium workloads.

// Install: System.Data.SqlClient (for SqlDependency)
// Add at startup (once per process):
SqlDependency.Start(connectionString);

// Service
public sealed class ProductsQueryCache
{
    private readonly IMemoryCache _cache;
    private readonly string _cs;

    public ProductsQueryCache(IMemoryCache cache, string connectionString)
    {
        _cache = cache;
        _cs = connectionString;
    }

    public IReadOnlyList<ProductDto> GetTopProducts()
    {
        const string cacheKey = "products:top";
        if (_cache.TryGetValue(cacheKey, out IReadOnlyList<ProductDto> cached))
            return cached;

        using var conn = new SqlConnection(_cs);
        using var cmd = new SqlCommand(@"
            SELECT p.Id, p.Name, p.Price
            FROM dbo.Products AS p
            WHERE p.IsActive = 1
            ORDER BY p.Sales DESC;", conn);

        // Wire dependency BEFORE executing:
        var dep = new SqlDependency(cmd);
        dep.OnChange += (s, e) => _cache.Remove(cacheKey);

        conn.Open();
        using var rdr = cmd.ExecuteReader();

        var items = new List<ProductDto>();
        while (rdr.Read())
        {
            items.Add(new ProductDto
            {
                Id = rdr.GetGuid(0),
                Name = rdr.GetString(1),
                Price = rdr.GetDecimal(2)
            });
        }

        // Cache with sensible absolute cap as a fallback
        _cache.Set(cacheKey, items, TimeSpan.FromMinutes(30));
        return items;
    }
}

// On shutdown:
SqlDependency.Stop(connectionString);

public record ProductDto
{
    public Guid Id { get; init; }
    public string Name { get; init; } = "";
    public decimal Price { get; init; }
}


Tip: Wrap each dependent query in a small service. Re-execute the query on cache miss to re-subscribe after an invalidation.

2. Version-Stamp (Polling-Light) Strategy

Maintain a single monotonic version (e.g., DATETIME2 or ROWVERSION) per entity or aggregate. The cache stores both the data and the version; if the DB version changes, refresh the cache.

-- One row per entity/aggregate
CREATE TABLE dbo.CacheVersion
(
  EntityName   sysname       NOT NULL PRIMARY KEY,
  LastUpdated  DATETIME2(3)  NOT NULL DEFAULT SYSUTCDATETIME()
);

-- Keep it up to date (example trigger)
CREATE OR ALTER TRIGGER dbo.trg_Products_VersionBump
ON dbo.Products
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
  SET NOCOUNT ON;
  MERGE dbo.CacheVersion AS cv
  USING (SELECT N'Products' AS EntityName) AS s
  ON cv.EntityName = s.EntityName
  WHEN MATCHED THEN
     UPDATE SET LastUpdated = SYSUTCDATETIME()
  WHEN NOT MATCHED THEN
     INSERT(EntityName, LastUpdated) VALUES (s.EntityName, SYSUTCDATETIME());
END;

public sealed class VersionedCache
{
    private readonly IMemoryCache _cache;
    private readonly string _cs;

    public VersionedCache(IMemoryCache cache, string connectionString)
    {
        _cache = cache;
        _cs = connectionString;
    }

    public async Task<IReadOnlyList<ProductDto>> GetTopProductsAsync(CancellationToken ct = default)
    {
        const string key = "products:top";
        var dbVersion = await GetVersionAsync("Products", ct);

        if (_cache.TryGetValue<(DateTime version, IReadOnlyList<ProductDto> data)>(
            key, out var cached) && cached.version == dbVersion)
        {
            return cached.data;
        }

        var data = await LoadFromDbAsync(ct);
        _cache.Set(key, (dbVersion, data), TimeSpan.FromMinutes(30));
        return data;
    }

    private async Task<DateTime> GetVersionAsync(string entity, CancellationToken ct)
    {
        const string sql = "SELECT LastUpdated FROM dbo.CacheVersion WHERE EntityName = @e";
        await using var cn = new SqlConnection(_cs);
        await using var cmd = new SqlCommand(sql, cn);
        cmd.Parameters.AddWithValue("@e", entity);
        await cn.OpenAsync(ct);
        var val = await cmd.ExecuteScalarAsync(ct);
        return (val == null || val == DBNull.Value) ? DateTime.MinValue : (DateTime)val;
    }

    private async Task<IReadOnlyList<ProductDto>> LoadFromDbAsync(CancellationToken ct)
    {
        const string sql = @"
            SELECT TOP (50) Id, Name, Price
            FROM dbo.Products
            WHERE IsActive = 1
            ORDER BY Sales DESC;";

        await using var cn = new SqlConnection(_cs);
        await using var cmd = new SqlCommand(sql, cn);
        await cn.OpenAsync(ct);
        await using var rdr = await cmd.ExecuteReaderAsync(ct);

        var list = new List<ProductDto>();
        while (await rdr.ReadAsync(ct))
        {
            list.Add(new ProductDto{
                Id = rdr.GetGuid(0),
                Name = rdr.GetString(1),
                Price = rdr.GetDecimal(2)
            });
        }
        return list;
    }
}


Suitable for: cross-DB compatibility, complex queries, and large deployments. Overhead is a tiny metadata read per cache check.

3. Event-Driven Invalidation (Application Layer)
When all writes go through your app, clear relevant cache keys immediately after INSERT/UPDATE/DELETE. No polling, no DB features needed.

public interface ICacheBus
{
    void Invalidate(params string[] keys);
}

public sealed class MemoryCacheBus(IMemoryCache cache) : ICacheBus
{
    public void Invalidate(params string[] keys)
    {
        foreach (var k in keys) cache.Remove(k);
    }
}

public sealed class ProductsService
{
    private readonly string _cs;
    private readonly ICacheBus _bus;

    public ProductsService(string cs, ICacheBus bus)
    {
        _cs = cs; _bus = bus;
    }

    public async Task UpdatePriceAsync(Guid id, decimal price, CancellationToken ct)
    {
        const string sql = "UPDATE dbo.Products SET Price=@p WHERE Id=@id";
        await using var cn = new SqlConnection(_cs);
        await using var cmd = new SqlCommand(sql, cn);
        cmd.Parameters.AddWithValue("@p", price);
        cmd.Parameters.AddWithValue("@id", id);
        await cn.OpenAsync(ct);
        await cmd.ExecuteNonQueryAsync(ct);

        // Immediately invalidate related keys:
        _bus.Invalidate("products:top", $"product:{id}");
    }
}


Note. If other systems write to the DB, pair this with Version-Stamps or database triggers to catch external changes.

4. Distributed Cache + Redis Pub/Sub
In multi-instance setups, store data in Redis and broadcast invalidation events with Pub/Sub so all instances drop stale entries simultaneously.

// Using StackExchange.Redis
public sealed class RedisInvalidator
{
    private readonly IConnectionMultiplexer _mux;
    private const string Channel = "cache:invalidate";

    public RedisInvalidator(IConnectionMultiplexer mux) { _mux = mux; }

    public void Publish(params string[] keys)
    {
        var sub = _mux.GetSubscriber();
        sub.Publish(Channel, string.Join(",", keys));
    }

    public void Subscribe(IMemoryCache localCache)
    {
        var sub = _mux.GetSubscriber();
        sub.Subscribe(Channel, (_, msg) =>
        {
            foreach (var key in msg.ToString().Split(',', StringSplitOptions.RemoveEmptyEntries))
                localCache.Remove(key);
        });
    }
}

Combine this with Version-Stamps for safety: even if a node misses a message (rare), the next read detects version mismatch and refreshes.

5. Drop-in Cache Wrapper (Interface + DI)

A small abstraction to centralize caching and invalidation logic.
public interface IQueryCache
{
    Task<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, Task<T>> factory, TimeSpan ttl,
        CancellationToken ct = default);
    void Remove(params string[] keys);
}

public sealed class DefaultQueryCache(IMemoryCache cache) : IQueryCache
{
    public async Task<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, Task<T>> factory, TimeSpan ttl,
        CancellationToken ct = default)
    {
        if (cache.TryGetValue(key, out T value)) return value;
        value = await factory(ct);
        cache.Set(key, value, ttl);
        return value;
    }

    public void Remove(params string[] keys)
    {
        foreach (var k in keys) cache.Remove(k);
    }
}

// Usage inside a repository:
public sealed class ProductsReadModel(IQueryCache qc, string cs)
{
    public Task<IReadOnlyList<ProductDto>> GetTopAsync(CancellationToken ct)
        => qc.GetOrCreateAsync("products:top", _ => Load(ct), TimeSpan.FromMinutes(30), ct);

    private async Task<IReadOnlyList<ProductDto>> Load(CancellationToken ct)
    {
        // ... DB query as shown earlier ...
        return new List<ProductDto>();
    }
}


Best Practices & Pitfalls

  • Pick a key strategy: prefix by aggregate (products:*) so you can invalidate in groups.
  • Set a safety TTL: even with perfect invalidation, It protects you from edge cases.
  • Cache shape matters: cache the final DTO you serve, not raw rows.
  • Avoid “stampedes”: add jitter to TTL or use a single-flight mutex on rebuild.
  • Warm hot paths on app start if latency spikes matter.
  • Measure: track hit ratio, rebuild latency, DB load before/after.
  • SqlDependency limits: ensure Service Broker is on; keep queries notification-compatible.

Windows Hosting Recommendation

HostForLIFEASP.NET receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2022 R2, SQL Server 2022, ASP.NET Core 7.0.10 , ASP.NET MVC, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More

Minimal APIs Cheatsheet (in ASP.NET Core)

Leave a Comment

Minimal APIs were introduced in .NET 6 to simplify the creation of small, fast, and lightweight web APIs. They allow developers to express API logic with minimal code and configuration, without having controllers or properties like in typical ASP.NET MVC. They are best used when constructing small services, microservices, or proof-of-concept systems.

1. Create a New Minimal API Project

To get started

dotnet new web -n MinimalApiDemo
  • This template sets up a basic minimal API.
  • No controllers or Views folder.
2. Basic Program.cs Structure
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
  • MapGet defines a GET endpoint.
  • Everything is written in Program.cs.
3. MapGet() – Handle GET Requests
app.MapGet("/hello", () => "Hi there!");
  • Used to return data or simple messages.
  • Can return strings, objects, or JSON.
4. MapPost() – Handle POST Requests
app.MapPost("/add", (int x, int y) => x + y);
  • Accepts parameters directly from query or body.
  • Ideal for submitting data.
5. MapPut() – Handle PUT Requests
app.MapPut("/update", (int id, string name) => $"Updated {id} to {name}");
  • Used to update existing resources.

6. MapDelete() – Handle DELETE Requests
app.MapDelete("/delete/{id}", (int id) => $"Deleted item {id}");
  • Deletes a resource based on the ID.

7. Return JSON Response
app.MapGet("/user", () => new { Name = "John", Age = 25 });
  • Automatically serializes object to JSON.
  • No need for extra configuration.
8. Reading From Query Strings
app.MapGet("/greet", (string name) => $"Hello, {name}!");
  • Pass like /greet?name=Diksha.

9. Reading From Route Parameters
app.MapGet("/product/{id}", (int id) => $"Product ID: {id}");
  • Route parameters are defined inside {}.

10. Read JSON Body in POST
app.MapPost("/person", (Person person) =>
{
    return $"Name: {person.Name}, Age: {person.Age}";
});

record Person(string Name, int Age);
  • Automatically binds the JSON request body to an object.
  • Use record or class.
11. Dependency Injection (DI)

Register service

builder.Services.AddSingleton<MyService>();

Use it in the route

app.MapGet("/info", (MyService svc) => svc.GetInfo());
  • Supports constructor-less DI in lambda.

12. Validation Example
app.MapPost("/validate", (User user) =>
{
    if (string.IsNullOrWhiteSpace(user.Email))
        return Results.BadRequest("Email is required");

    return Results.Ok("Valid User");
});

record User(string Email);
  • Manual validation can be added easily.

13. Using Results Object
app.MapGet("/status", () => Results.Ok("Working"));
app.MapGet("/fail", () => Results.BadRequest("Something went wrong"));
  • Explicit control over HTTP response type and status.

14. Grouping Routes
var userGroup = app.MapGroup("/users");

userGroup.MapGet("/", () => "All users");
userGroup.MapPost("/", (User u) => $"User {u.Email} added");
  • Helps organize related endpoints under a common path.

15. Middleware Usage
app.Use(async (context, next) =>
{
    Console.WriteLine("Request coming in");
    await next();
    Console.WriteLine("Response going out");
});
  • Minimal APIs support middlewares like normal APIs.

16. Enable Swagger (OpenAPI)

Add services

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Use Swagger UI

app.UseSwagger();
app.UseSwaggerUI();
  • Helps with testing and documentation.

17. Route Constraints
app.MapGet("/item/{id:int}", (int id) => $"Item ID: {id}");
  • :int ensures only integer is accepted.

18. Allow CORS
builder.Services.AddCors();

app.UseCors(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
  • Enables cross-origin API calls.

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
  • Add environment-specific behaviors.

20. Error Handling
app.UseExceptionHandler("/error");

app.Map("/error", () => Results.Problem("Unexpected error occurred"));
  • Define a global error route.

21. Run on Custom URL/Port

Update launchSettings.json or in code:

builder.WebHost.UseUrls("http://localhost:5001");
22. Route Prefix with [MapGroup] for Versioning
var v1 = app.MapGroup("/api/v1");

v1.MapGet("/products", () => "v1 products");
  • Helps version your API easily (/api/v1/products).

23. Use Authorization
builder.Services.AddAuthorization();
app.UseAuthorization();

app.MapGet("/secure", () => "Secure Data")
   .RequireAuthorization();
  • Supports role-based or policy-based authorization.

24. Async Endpoints
app.MapGet("/delay", async () =>
{
    await Task.Delay(1000);
    return "Done after delay";
});
  • All endpoints can be asynchronous using async/await.

25. Binding From Headers / Claims
app.MapGet("/agent", (HttpRequest req) =>
{
    var userAgent = req.Headers["User-Agent"];
    return $"Your agent: {userAgent}";
});
  • Use HttpRequest or HttpContext to access headers, claims, cookies, etc.

26. Custom Response Codes
app.MapGet("/notfound", () => Results.StatusCode(404));
  • Fine-grained control over HTTP response codes.

27. Conditional Endpoints (MapWhen)
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/admin"), appBuilder =>
{
    appBuilder.Run(async ctx => await ctx.Response.WriteAsync("Admin section"));
});
  • Handle advanced routing logic manually.

28. Bind Form Data
app.MapPost("/upload", async (HttpRequest req) =>
{
    var form = await req.ReadFormAsync();
    var file = form.Files["file"];
    return $"Uploaded: {file?.FileName}";
});
  • Useful for uploading files or handling multipart/form-data.

29. Handle Query, Route, and Body Together
app.MapPost("/mixed/{id}", (int id, string name, MyData data) =>
{
    return $"ID: {id}, Name: {name}, Data: {data.Value}";
});

record MyData(string Value);
  • You can mix route, query, and body parameters.

Comparison: Minimal APIs vs Traditional RESTful APIs (Controllers)
Feature Minimal APIs RESTful APIs (Controllers)
Setup Very low setup, all in Program.cs Needs controllers, attributes, and routing setup
Performance Slightly better (fewer layers) Slightly heavier due to abstraction layers
Best For Small services, microservices, and quick APIs Large-scale apps with many endpoints
Testability Less structured, harder to unit test Easier to test with controllers/services
Routing Code-based (inline) Attribute routing or convention-based
Dependency Injection Direct in lambda Via constructor injection in controllers
Maintainability (Large apps) Harder to scale/organize Better separation of concerns
Swagger Support Fully supported Fully supported
Custom Filters / Middleware Limited (no filters, but use middleware) Full support (filters, middleware, etc.)
Validation (FluentValidation, etc.) Manual or via packages Built-in integration via ModelState
Extensibility Limited to what's in lambdas/middleware Highly extensible (Filters, Bindings, etc.)

When to Use What?
Use Case Recommendation
Quick prototype or POC Minimal API
Microservices / serverless Minimal API
Full-blown business app RESTful API (Controllers)
App needing filters/attributes RESTful API
Highly structured/maintainable code RESTful API

Conclusion
Minimal APIs are a powerful tool for quickly building web APIs with clean and minimal code. They reduce boilerplate and work great for microservices or simple services. However, RESTful APIs with controllers continue to provide greater structure and long-term scalability for complex systems.

Use Minimal APIs when you want speed and simplicity, but switch to RESTful APIs when you need organization, structure, testability, or layered architecture. 

Windows Hosting Recommendation

HostForLIFE.eu receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2022 R2, SQL Server 2022, ASP.NET Core 10.0 , ASP.NET MVC, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More

.NET Core Dependency Injection with IServiceCollection

Leave a Comment

Modern applications may easily manage dependencies thanks to.NET Core's built-in support for Dependency Injection (DI), a design pattern that facilitates the creation of loosely connected code. Using the IServiceCollection interface, we will examine how DI functions in.NET Core in this post and go over a basic example. 

What Is Dependency Injection?

Dependency Injection is a technique where an object receives its dependencies from an external source rather than creating them itself. This makes the application easier to maintain, test, and scale.

Key Components in .NET Core DI

  • IServiceCollection – Used to register dependencies (services).
  • ServiceProvider – Used to resolve dependencies.
  • Service Lifetimes
    • Singleton: One instance for the application lifetime.
    • Scoped: One instance per request.
    • Transient: A new instance every time.

Example. Setting Up DI in a .NET Core Console Application

Let’s create a simple console app that demonstrates DI.

Step 1. Define Interfaces and Implementations

// IGreeter.cs
public interface IGreeter
{
    void Greet(string name);
}
// ConsoleGreeter.cs
public class ConsoleGreeter : IGreeter
{
    public void Greet(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
}

Step 2. Set Up DI with IServiceCollection

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using System;

class Program
{
    static void Main(string[] args)
    {
        // Step 1: Create a new service collection
        var serviceCollection = new ServiceCollection();

        // Step 2: Register services
        serviceCollection.AddTransient<IGreeter, ConsoleGreeter>();

        // Step 3: Build the service provider
        var serviceProvider = serviceCollection.BuildServiceProvider();

        // Step 4: Resolve and use the service
        var greeter = serviceProvider.GetRequiredService<IGreeter>();
        greeter.Greet("Alice");
    }
}

Explanation

  • AddTransient<IGreeter, ConsoleGreeter>(): Registers ConsoleGreeter as the implementation for IGreeter. A new instance is created every time it’s requested.
  • BuildServiceProvider(): Compiles the service registrations into a container that can resolve services.
  • GetRequiredService<IGreeter>(): Requests an instance of IGreeter. The DI container automatically injects the correct implementation.

When to Use Which Lifetime?

Lifetime Use When...
Singleton Service should be shared for the app lifetime
Scoped Per web request (mostly in ASP.NET Core apps)
Transient Lightweight, stateless services

Bonus: Use DI in ASP.NET Core

In ASP.NET Core, you don’t need to manually build the ServiceProvider. Just use ConfigureServices in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IGreeter, ConsoleGreeter>();
}

Then, inject via the constructor:

public class HomeController : Controller
{
    private readonly IGreeter _greeter;

    public HomeController(IGreeter greeter)
    {
        _greeter = greeter;
    }

    public IActionResult Index()
    {
        _greeter.Greet("User");
        return View();
    }
}
Conclusion

Dependency Injection in .NET Core is simple and powerful. Using IServiceCollection, you can register and manage dependencies with different lifetimes to keep your code clean and maintainable. Whether you're building a console app or a large web application, understanding and applying DI is a must-have skill in .NET Core development.

Windows Hosting Recommendation

HostForLIFEASP.NET receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2022 R2, SQL Server 2022, ASP.NET Core 10.0, ASP.NET MVC, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More

Real-World Example of CRUD Operations in ASP.NET Core Using Entity Framework

Leave a Comment

You want to manage Customer records with typical operations:

  • Create customer records

  • Get all customers or by ID

  • Update customer details

  • Delete customer records

Step-by-step implementation

1. Create Project

dotnet new webapi -n CustomerServiceApi
cd CustomerServiceApi

2. Define Customer Model

Create Models/Customer.cs

namespace CustomerServiceApi.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string FullName { get; set; } = string.Empty;
        public string Email { get; set; } = string.Empty;
        public string PhoneNumber { get; set; } = string.Empty;
        public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;
    }
}

3. Setup DbContext

Create Data/AppDbContext.cs

using Microsoft.EntityFrameworkCore;
using CustomerServiceApi.Models;

namespace CustomerServiceApi.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

        public DbSet<Customer> Customers { get; set; }
    }
}

4. Configure EF Core and Services in Program.cs

using CustomerServiceApi.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.Run();

Add connection string in appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=CustomerServiceDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

5. Create CustomersController

Create Controllers/CustomersController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using CustomerServiceApi.Data;
using CustomerServiceApi.Models;

namespace CustomerServiceApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly AppDbContext _context;

        public CustomersController(AppDbContext context)
        {
            _context = context;
        }

        // GET: api/customers
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Customer>>> GetCustomers()
        {
            return await _context.Customers.ToListAsync();
        }

        // GET: api/customers/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Customer>> GetCustomer(int id)
        {
            var customer = await _context.Customers.FindAsync(id);
            if (customer == null) return NotFound();
            return customer;
        }

        // POST: api/customers
        [HttpPost]
        public async Task<ActionResult<Customer>> CreateCustomer(Customer customer)
        {
            _context.Customers.Add(customer);
            await _context.SaveChangesAsync();
            return CreatedAtAction(nameof(GetCustomer), new { id = customer.Id }, customer);
        }

        // PUT: api/customers/5
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateCustomer(int id, Customer customer)
        {
            if (id != customer.Id) return BadRequest();

            _context.Entry(customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!_context.Customers.Any(c => c.Id == id))
                    return NotFound();
                else throw;
            }

            return NoContent();
        }

        // DELETE: api/customers/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteCustomer(int id)
        {
            var customer = await _context.Customers.FindAsync(id);
            if (customer == null) return NotFound();

            _context.Customers.Remove(customer);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}

6. Run EF Core Migration

Install EF CLI if needed

dotnet tool install --global dotnet-ef

Create and apply migration

dotnet ef migrations add InitialCreate
dotnet ef database update

7. Run and Test

dotnet run

Go to: https://localhost:5001/swagger

Test your Customer API endpoints with Swagger UI:

  • GET /api/customers

  • POST /api/customers

  • GET /api/customers/{id}

  • PUT /api/customers/{id}

  • DELETE /api/customers/{id}

Summary

You now have a real-time Customer Service REST API managing customer records with full CRUD using Entity Framework Core in ASP.NET Core. This can be extended with authentication, paging, validation, etc.

If you want, I can help with those next! Would you like that?

Windows Hosting Recommendation

HostForLIFE.eu receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2022 R2, SQL Server 2022, ASP.NET Core 9.0, ASP.NET MVC, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More