Tag: CSharp

  • Dependency Inversion Principle

    Title: Our Renewal Reminders Went to Expired Customers, and We Couldn’t Test It

    The task sounded simple: email customers 3 days before their subscription expires.

    A few weeks after launch, support forwarded us angry messages. Customers whose subscriptions had already expired were getting “renew soon” emails every single day.

    The bug was one missing condition. What hurt more was the reason we missed it: the code was impossible to unit test.

    What Dependency Inversion really means

    The Dependency Inversion Principle says that high-level business logic should not depend on low-level details like databases, email servers, or the system clock. Both should depend on abstractions, which are usually interfaces.

    Here is the code we shipped:

    public class RenewalReminderService
    {
        public void SendReminders()
        {
            var repository = new SqlSubscriptionRepository();
            var smtp = new SmtpClient("smtp.company.com");
    
            var expiring = repository.GetAll()
                .Where(s => (s.ExpiresOn - DateTime.Now).TotalDays <= 3);
    
            foreach (var subscription in expiring)
            {
                smtp.Send("no-reply@company.com", subscription.Email,
                    "Renew soon", "Your plan expires soon.");
            }
        }
    }

    Look at the condition. If a subscription expired two weeks ago, the difference is negative, and a negative number is always less than or equal to 3. Expired customers were included.

    Why nobody caught it

    To test this method, you would need:

    • A real SQL database with test data
    • A real SMTP server, or you’d email real people
    • A way to control what DateTime.Now returns, which is impossible

    So nobody wrote a test. The high-level rule, “who gets a reminder,” was welded to three low-level details.

    The fix: depend on abstractions

    First we define what the business logic needs, next to the business logic:

    public interface ISubscriptionRepository
    {
        Task<IReadOnlyList<Subscription>> GetActiveAsync(CancellationToken ct);
    }
    
    public interface IEmailSender
    {
        Task SendAsync(string to, string subject, string body, CancellationToken ct);
    }

    For time, .NET 8 and later has TimeProvider built in, so we don’t need to invent our own clock interface. Now the service:

    public class RenewalReminderService
    {
        private readonly ISubscriptionRepository _subscriptions;
        private readonly IEmailSender _email;
        private readonly TimeProvider _time;
    
        public RenewalReminderService(
            ISubscriptionRepository subscriptions,
            IEmailSender email,
            TimeProvider time)
        {
            _subscriptions = subscriptions;
            _email = email;
            _time = time;
        }
    
        public async Task SendRemindersAsync(CancellationToken ct)
        {
            var now = _time.GetUtcNow();
            var subscriptions = await _subscriptions.GetActiveAsync(ct);
    
            foreach (var s in subscriptions)
            {
                var daysLeft = (s.ExpiresOn - now).TotalDays;
    
                if (daysLeft is >= 0 and <= 3)
                    await _email.SendAsync(s.Email, "Renew soon", "Your plan expires soon.", ct);
            }
        }
    }

    Now the rule is clear and readable, and the missing lower bound (>= 0) is obvious.

    The test we couldn’t write before

    [Fact]
    public async Task Does_not_remind_expired_subscribers()
    {
        var time = new FakeTimeProvider(new DateTimeOffset(2026, 10, 13, 0, 0, 0, TimeSpan.Zero));
        var repository = new FakeSubscriptionRepository(
            new Subscription { Email = "a@x.com", ExpiresOn = time.GetUtcNow().AddDays(-1) });
        var email = new FakeEmailSender();
        var service = new RenewalReminderService(repository, email, time);
    
        await service.SendRemindersAsync(CancellationToken.None);
    
        Assert.Empty(email.Sent);
    }

    No database, no mail server, and the clock is under our control. FakeTimeProvider comes from the Microsoft.Extensions.TimeProvider.Testing package.

    Wiring it up

    In production, the real implementations are plugged in at startup:

    builder.Services.AddSingleton(TimeProvider.System);
    builder.Services.AddScoped<ISubscriptionRepository, SqlSubscriptionRepository>();
    builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
    builder.Services.AddScoped<RenewalReminderService>();

    Where the interfaces live matters

    ISubscriptionRepository and IEmailSender belong with the business logic, not with the SQL or SMTP code. SqlSubscriptionRepository and SmtpEmailSender depend on those interfaces, not the other way around. That is the “inversion”: the details now point toward the business rules.

    Three terms people mix up

    • Dependency Inversion is the principle: depend on abstractions
    • Dependency Injection is the technique: pass dependencies in through the constructor
    • The IoC container is the tool that does the passing for you

    When not to do this

    You don’t need an interface for every class. Invert dependencies at the boundaries: databases, network calls, the file system, the clock, and randomness. A small helper that formats a string doesn’t need one.

    The lesson

    If you can’t write a fast unit test for your most important logic, it’s usually because it creates its own dependencies. Make the business rule depend on interfaces, inject the details from outside, and the code becomes testable and replaceable at the same time.

  • Interface Segregation Principle

    Title: We Needed a Read-Only Repository. The Interface Forced Us to Fake Seven Methods.

    Our reporting queries were slowing down the main database. The fix was clear: send report queries to a read replica.

    So I created ReadReplicaUserRepository. Then I opened the interface it had to implement and my heart sank. IUserRepository had seven methods, and five of them were about writing data.

    A read-only database was being forced to implement Add, Update, Delete, and BulkImport.

    What Interface Segregation really means

    The Interface Segregation Principle says that no class should be forced to depend on methods it doesn’t use. In practice, it means: don’t build one big interface that serves everybody. Build smaller ones shaped around what each client actually needs.

    Here is the interface we had:

    public interface IUserRepository
    {
        User? GetById(int id);
        IReadOnlyList<User> GetAll();
        void Add(User user);
        void Update(User user);
        void Delete(int id);
        void BulkImport(IEnumerable<User> users);
        string ExportToCsv();
    }

    And the report service that used it:

    public class UserReportService
    {
        private readonly IUserRepository _users;
    
        public UserReportService(IUserRepository users) => _users = users;
    
        public int CountActiveUsers()
            => _users.GetAll().Count(u => u.IsActive);
    }

    UserReportService reads one method. But it depends on all seven. That causes real problems:

    • The read replica class has to implement write methods it can’t support, usually by throwing exceptions
    • Hand-written test fakes must implement all seven methods just to test one
    • A change to BulkImport can force recompiling and retesting code that only reads

    The fix: split by who uses it

    public interface IUserReader
    {
        User? GetById(int id);
        IReadOnlyList<User> GetAll();
    }
    
    public interface IUserWriter
    {
        void Add(User user);
        void Update(User user);
        void Delete(int id);
    }
    
    public interface IUserImporter
    {
        void BulkImport(IEnumerable<User> users);
    }

    ExportToCsv didn’t belong in a repository at all, so it moved to a CsvUserExporter class. That was a separate responsibility hiding inside the interface.

    The main database class can still implement everything:

    public class SqlUserRepository : IUserReader, IUserWriter, IUserImporter
    {
        // all methods
    }

    And the read replica now only promises what it can really do:

    public class ReadReplicaUserRepository : IUserReader
    {
        public User? GetById(int id) { /* query the replica */ }
        public IReadOnlyList<User> GetAll() { /* query the replica */ }
    }

    The report service asks only for what it needs:

    public class UserReportService
    {
        private readonly IUserReader _users;
    
        public UserReportService(IUserReader users) => _users = users;
    
        public int CountActiveUsers()
            => _users.GetAll().Count(u => u.IsActive);
    }

    Wiring it up

    This is where the payoff shows. Reports read from the replica, while writes go to the primary:

    builder.Services.AddScoped<SqlUserRepository>();
    builder.Services.AddScoped<IUserWriter>(sp => sp.GetRequiredService<SqlUserRepository>());
    builder.Services.AddScoped<IUserImporter>(sp => sp.GetRequiredService<SqlUserRepository>());
    builder.Services.AddScoped<IUserReader, ReadReplicaUserRepository>();

    The first line registers the SQL class itself, and the next two lines forward the interfaces to that same instance. That way you get one SqlUserRepository per request, not one per interface.

    Testing gets easier too

    public class FakeUserReader : IUserReader
    {
        private readonly List<User> _users;
        public FakeUserReader(params User[] users) => _users = users.ToList();
    
        public User? GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
        public IReadOnlyList<User> GetAll() => _users;
    }

    Two methods instead of seven. The test says exactly what the report service needs, and nothing else.

    When not to overdo it

    Don’t split into one-method interfaces by reflex. Group methods by the client that uses them. Two or three well-chosen role interfaces beat fifteen tiny ones.

    The lesson

    A fat interface couples unrelated clients together and forces implementers to lie. Shape interfaces around what the caller needs, and you also get honest implementations and simpler tests.

  • Liskov Substitution Principle

    Title: Support Clicked “Refund.” The Server Threw an Exception.

    We added gift cards as a payment method. Quick job: create a GiftCardPayment class, inherit from our PaymentMethod base class, done.

    Two weeks later, a support agent clicked Refund on an order paid with a gift card, and the page returned a 500 error.

    The code compiled. The tests passed. It still crashed in production, because the subclass didn’t keep the promises its parent made.

    What Liskov Substitution really means

    The Liskov Substitution Principle says that a subclass must be usable anywhere its base class is used, without the program breaking or behaving strangely.

    Think of inheritance as a promise. The base class tells every caller, “anything of this type can do these things.” If a subclass can’t keep that promise, every caller that trusted it is now in danger.

    Here is what we wrote:

    public abstract class PaymentMethod
    {
        public abstract void Charge(decimal amount);
        public abstract void Refund(decimal amount);
    }
    
    public class CreditCardPayment : PaymentMethod
    {
        public override void Charge(decimal amount) { /* charge the card */ }
        public override void Refund(decimal amount) { /* refund to the card */ }
    }
    
    public class GiftCardPayment : PaymentMethod
    {
        public override void Charge(decimal amount) { /* deduct from balance */ }
    
        public override void Refund(decimal amount)
            => throw new NotSupportedException("Gift cards cannot be refunded.");
    }

    And the code that used it:

    public class RefundService
    {
        public void RefundOrder(Order order)
        {
            order.PaymentMethod.Refund(order.Total); // crashes for gift cards
        }
    }

    RefundService did nothing wrong. It trusted that any PaymentMethod can refund, because the base class said so. The gift card class broke that promise.

    How to spot a Liskov violation

    • An override that throws NotImplementedException or NotSupportedException
    • An override that silently does nothing
    • Callers that check “is” or “as” before using a method, because they don’t trust the type
    • A subclass that rejects inputs the base class accepts

    Fix 1: make the contract something every type can honor

    Ask what the business actually wants. For us, refunding a gift card payment means giving the customer store credit. That is valid behavior, so we can make the contract say so:

    public enum RefundOutcome
    {
        RefundedToOriginalMethod,
        StoreCreditIssued
    }
    
    public abstract class PaymentMethod
    {
        public abstract void Charge(decimal amount);
        public abstract RefundOutcome Refund(decimal amount);
    }
    
    public class GiftCardPayment : PaymentMethod
    {
        public override void Charge(decimal amount) { /* deduct from balance */ }
    
        public override RefundOutcome Refund(decimal amount)
        {
            // add the amount back as store credit
            return RefundOutcome.StoreCreditIssued;
        }
    }

    Now RefundService works for every payment type, and the UI can show a different message depending on the outcome.

    Fix 2: if a type truly can’t do it, don’t give it the method

    Sometimes there is no sensible behavior. Then the operation shouldn’t be on the shared type at all:

    public interface IPaymentMethod
    {
        void Charge(decimal amount);
    }
    
    public interface IRefundablePayment : IPaymentMethod
    {
        void Refund(decimal amount);
    }
    
    public class CreditCardPayment : IRefundablePayment { /* ... */ }
    public class GiftCardPayment : IPaymentMethod { /* Charge only */ }

    A method that needs refunds now asks for an IRefundablePayment. The compiler stops anyone from passing a gift card to it, so the mistake becomes a build error and not a production error.

    The lesson

    “Is-a” in code means “behaves-as,” not “sounds like it in English.” A Square inheriting from Rectangle sounds right, until someone sets width and height separately. Before you inherit, ask one question: can every caller of the base type safely use my subclass without knowing which one they got? If the honest answer is no, the design needs to change, not the caller.

  • Single Responsibility Principle

    Title: One Class, Four Reasons to Change: How a VAT Update Broke Our Order Emails

    Finance asked for a small change: round the VAT on invoices differently. One line, in one method. I made the change, the tests passed, and I shipped it.

    The next morning, customers were not getting their order confirmation emails.

    The invoice change had nothing to do with email. Except in our code, it did, because both lived inside the same method.

    What Single Responsibility really means

    Most people read the Single Responsibility Principle as “a class should do only one thing.” That’s not quite it.

    The real idea: a class should have only one reason to change. Or put another way, it should answer to only one group of people.

    Here is the code we had:

    public class OrderService
    {
        public void PlaceOrder(Order order)
        {
            // 1. Validation
            if (order.Items.Count == 0)
                throw new InvalidOperationException("Order is empty.");
    
            // 2. Persistence
            using var connection = new SqlConnection("Server=...;Database=Shop;");
            connection.Open();
            // INSERT order and items ...
    
            // 3. Invoice
            var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
            var vat = Math.Round(subtotal * 0.15m, 2);
            var invoicePdf = PdfBuilder.Create(order, subtotal, vat);
    
            // 4. Email
            var message = new MailMessage("no-reply@shop.com", order.CustomerEmail, "Your order", "Thanks!");
            message.Attachments.Add(new Attachment(new MemoryStream(invoicePdf), "invoice.pdf"));
            new SmtpClient("smtp.company.com").Send(message);
        }
    }

    Count the reasons this class has to change:

    • The business team changes validation rules
    • The database team changes the schema
    • Finance changes tax or invoice rules
    • Marketing changes the email provider or template

    Four groups of people, one file. Whenever any of them asks for a change, they edit the same method that also runs everyone else’s logic. Our VAT edit introduced a failure for orders containing a zero-price item. The exception was thrown before the email line, so the email never ran. A tax change broke an email.

    The fix

    We split the class by reason to change. Each part gets its own class, and OrderService only coordinates:

    public class OrderService
    {
        private readonly IOrderValidator _validator;
        private readonly IOrderRepository _repository;
        private readonly IInvoiceGenerator _invoices;
        private readonly IOrderEmailSender _email;
    
        public OrderService(
            IOrderValidator validator,
            IOrderRepository repository,
            IInvoiceGenerator invoices,
            IOrderEmailSender email)
        {
            _validator = validator;
            _repository = repository;
            _invoices = invoices;
            _email = email;
        }
    
        public async Task PlaceOrderAsync(Order order, CancellationToken ct)
        {
            _validator.Validate(order);
            await _repository.SaveAsync(order, ct);
            var invoice = _invoices.Generate(order);
            await _email.SendConfirmationAsync(order, invoice, ct);
        }
    }

    The VAT logic now lives in one small class that finance owns:

    public class InvoiceGenerator : IInvoiceGenerator
    {
        private const decimal VatRate = 0.15m;
    
        public byte[] Generate(Order order)
        {
            var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
            var vat = Math.Round(subtotal * VatRate, 2);
            return PdfBuilder.Create(order, subtotal, vat);
        }
    }

    The wiring is one line per class in Program.cs. ASP.NET Core’s container reads the OrderService constructor and supplies each dependency:

    builder.Services.AddScoped<IOrderValidator, OrderValidator>();
    builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
    builder.Services.AddScoped<IInvoiceGenerator, InvoiceGenerator>();
    builder.Services.AddScoped<IOrderEmailSender, SmtpOrderEmailSender>();
    builder.Services.AddScoped<OrderService>();

    What changed in practice

    The next VAT change touches one file. We can unit test InvoiceGenerator with a zero-price order in two lines, which would have caught our bug before it shipped.

    To be fair, SRP doesn’t decide what happens when invoice generation fails. That is still a workflow decision in OrderService. What SRP gives you is that the failure now lives in a small, testable class instead of hiding inside a 200-line method.

    A quick test you can use

    Describe the class in one sentence. If you need the word “and,” it probably has more than one responsibility. “Validates orders and saves them and emails customers” is three classes.

    When not to split

    If a class has two small methods that always change together, leave them together. Splitting everything into one-method classes gives you a codebase nobody can follow. Split when the reasons to change are genuinely different.

  • Our CSV Export Took 9 Seconds. Reflection Was Doing the Same Work 50,000 Times.

    A customer reported that exporting their order history to CSV felt “frozen.” About 50,000 rows, and the request sat there for nearly nine seconds.

    The database query was fast. The network was fine. I spent half a day profiling before the answer showed up in a flame graph, in a place I hadn’t thought to look: reflection.

    What reflection actually is

    Reflection lets your code inspect itself at runtime. You can ask a type what properties it has, read a property’s value by name, or call a method you only know by a string. It’s how serializers, ORMs, dependency injection containers, and mappers work under the hood.

    It’s powerful because your code no longer needs to know the types at compile time. It costs something because the runtime has to look up metadata every time you ask.

    Real-world example: before

    Our exporter was generic. It took any list of objects and a list of column names, and wrote them out. Here’s the simplified version:

    public string Export<T>(IEnumerable<T> rows, string[] columns)
    {
        var sb = new StringBuilder();
    
        foreach (var row in rows)
        {
            foreach (var column in columns)
            {
                var prop = row.GetType().GetProperty(column);
                var value = prop.GetValue(row);
                sb.Append(value).Append(',');
            }
            sb.AppendLine();
        }
    
        return sb.ToString();
    }

    It looks harmless. But with 50,000 rows and 8 columns, GetProperty ran 400,000 times. Every call asked the runtime to search the type’s metadata for a property it had already found on the previous row.

    Real-world example: after

    The property doesn’t change between rows. Only the object does. So the lookup belongs outside the loop:

    public string Export<T>(IEnumerable<T> rows, string[] columns)
    {
        var props = columns
            .Select(c => typeof(T).GetProperty(c))
            .ToArray();
    
        var sb = new StringBuilder();
    
        foreach (var row in rows)
        {
            foreach (var prop in props)
            {
                sb.Append(prop.GetValue(row)).Append(',');
            }
            sb.AppendLine();
        }
    
        return sb.ToString();
    }

    Eight metadata lookups instead of 400,000. The export dropped from about nine seconds to under one.

    If you need to go further

    PropertyInfo.GetValue still has overhead on every call, because it’s a late-bound invocation. For truly hot paths, you can compile the getter once into a delegate and reuse it:

    var param = Expression.Parameter(typeof(T), "x");
    var getter = Expression.Lambda<Func<T, object>>(
        Expression.Convert(Expression.Property(param, "Name"), typeof(object)),
        param).Compile();
    
    var value = getter(row); // close to a normal property read

    Compiling costs something up front, so it only pays off when the same property is read many thousands of times. That is exactly why libraries like AutoMapper and many serializers build and cache these delegates internally. In newer .NET versions, source generators take this further by moving the work to compile time, so there is no runtime reflection at all.

    When reflection is perfectly fine

    Reflection isn’t the villain. Reading metadata once at startup, scanning assemblies to register services, or building a one-off admin tool are all reasonable uses. The problem is reflection inside a hot loop, where a cost you’d never notice once gets multiplied by the size of your data.

    The lesson

    Reflection works the way a phone book works. Looking up one number is trivial. Looking up the same number 400,000 times is a design problem. If the answer won’t change between iterations, find it once, keep it, and reuse it.

  • Generics in C#: Write Reusable, Type-Safe Code

    Have you ever written the same logic multiple times because you needed to support different data types?

    C# Generics help solve this problem by allowing developers to create classes, interfaces, methods, and delegates that work with different types while maintaining compile-time type safety.

    1. What are Generics?

    Generics let you define a type or method using a type parameter, commonly represented by T.

    For example:

    public T GetValue<T>(T value)
    {
        return value;
    }

    You can call this method with different types:

    int number = GetValue(10);
    string name = GetValue("Faiz");

    The same method works with both values without requiring separate implementations.

    2. Why are Generics useful?

    Type safety: The compiler checks that you use compatible types, reducing the need for unsafe casts.

    Code reusability: Instead of duplicating logic for integers, strings, and other types, you can reuse a generic implementation.

    Performance: Generic collections avoid the boxing and unboxing overhead associated with storing value types in non-generic collections such as ArrayList.

    Maintainability: One well-designed generic class can replace several nearly identical classes.

    3. Where will you find Generics in .NET?

    You probably already use generics in your daily work:

    • List<T> — stores a collection of a specific type.
    • Dictionary<TKey, TValue> — stores key-value pairs.
    • Task<T> — represents an asynchronous operation that produces a result.
    • IEnumerable<T> — provides a strongly typed way to enumerate values.

    For example:

    List<int> numbers = [1, 2, 3];
    List<string> names = ["Faiz", "Ahmed"];

    Both collections use the same generic collection type but enforce different element types.

    4. What are Generic Constraints?

    Sometimes, a generic method needs to restrict which types it accepts.

    public class Repository<T> where T : class
    {
        // T must be a reference type.
    }

    The where T : class constraint tells the compiler that T must be a reference type. Other constraints can require an interface, a base class, or a parameterless constructor.

    Final Thoughts

    Generics are essential for building reusable libraries, maintainable services, and type-safe .NET applications.

    Don’t just learn the syntax. Look at the generic types you already use in ASP.NET Core and understand how they help reduce duplication and improve type safety.

    📌 Save this post
    ♻️ Share if it is helpful for you and others
    🛎️ Follow me for more software engineering insights

  • High Concurrency Handling: What Happens When Thousands of Users Hit Your API at the Same Time?

    High Concurrency Handling: What Happens When Thousands of Users Hit Your API at the Same Time?

    Imagine your application is running smoothly.

    Then your company launches a flash sale.

    Or a marketing campaign goes live.

    Suddenly, thousands of users start sending requests to the same endpoint at the same time.

    The question is no longer:

    “Does my API work?”

    It’s:

    “Can my API still work under heavy load?”

    Handling high concurrency isn’t about a single optimization. It’s about building a system that remains reliable as traffic grows.

    Here are some techniques I rely on.


    1. Avoid Blocking Threads

    In ASP.NET Core, every blocked thread reduces your application’s ability to handle incoming requests.

    Prefer asynchronous APIs whenever possible.

    await repository.GetOrdersAsync();
    

    Using async and await allows the thread to serve other requests while waiting for I/O operations.


    2. Optimize Database Access

    The database is often the first bottleneck.

    Reduce unnecessary work by:

    • Selecting only the required columns.
    • Adding proper indexes.
    • Using AsNoTracking() for read-only queries.
    • Avoiding the N+1 query problem.

    A faster query means every request spends less time waiting.


    3. Cache Frequently Requested Data

    Not every request needs to reach the database.

    Cache data that changes infrequently using solutions such as Redis or the in-memory cache.

    Reducing database traffic significantly improves throughput.


    4. Limit Expensive Operations

    Some endpoints perform CPU-intensive or long-running work.

    Protect your application with:

    • Rate limiting.
    • Background processing.
    • Queues for non-critical tasks.

    This prevents a small number of requests from consuming all available resources.


    5. Use Optimistic Concurrency

    When multiple users update the same record simultaneously, conflicts can occur.

    EF Core supports optimistic concurrency using concurrency tokens such as RowVersion.

    This helps prevent accidental overwrites.


    6. Scale Horizontally

    Sometimes optimization isn’t enough.

    Run multiple application instances behind a load balancer so incoming requests are distributed across servers.

    This increases capacity while improving availability.


    7. Monitor Before You Optimize

    Use tools such as:

    • Grafana
    • Prometheus
    • Application Insights
    • OpenTelemetry

    Measure:

    • Response time
    • Error rate
    • CPU usage
    • Database latency
    • Throughput

    You can’t improve what you don’t measure.


    Final Thoughts

    High concurrency isn’t just about surviving traffic spikes.

    It’s about designing systems that continue to respond quickly, remain reliable, and protect shared resources under pressure.

    Performance isn’t achieved through one big optimization.

    It’s the result of many small engineering decisions working together.

  • CORS in ASP.NET Core: It’s More Than Just Fixing a Browser Error

    CORS in ASP.NET Core: It’s More Than Just Fixing a Browser Error

    I’ll admit something.

    For a long time, whenever I saw this error:

    “Access to fetch at ‘https://api.example.com‘ from origin ‘https://localhost:3000‘ has been blocked by CORS policy.”

    My first reaction was simple:

    “Let me add AllowAnyOrigin() and move on.”

    The application worked.

    The error disappeared.

    Problem solved… or so I thought.

    Later, I decided to understand what CORS was actually doing.

    And I realized something important:

    CORS isn’t a server feature. It’s a browser security feature.

    Your API might happily return a response.

    But the browser blocks JavaScript from accessing it if the request violates the server’s CORS policy.

    That’s why Postman works while the browser fails.


    What Is CORS?

    CORS (Cross-Origin Resource Sharing) is a mechanism that allows a server to specify which origins can access its resources.

    An origin consists of:

    • Protocol (https)
    • Domain (example.com)
    • Port (5001)

    If any of these differ, the browser considers it a different origin.

    For example:

    https://localhost:3000
    

    and

    https://localhost:5001
    

    are different origins.


    Configuring CORS in ASP.NET Core

    Register a policy:

    builder.Services.AddCors(options =>
    {
        options.AddPolicy("FrontendPolicy", policy =>
        {
            policy.WithOrigins("https://localhost:3000")
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
    });
    

    Apply the policy:

    app.UseCors("FrontendPolicy");
    

    Avoid This in Production

    .AllowAnyOrigin()
    

    While it’s convenient during development, allowing every origin in production can expose your API unnecessarily.

    Instead, explicitly specify the trusted origins your application should allow.


    Middleware Order Matters

    A common mistake is placing UseCors() in the wrong position.

    A typical pipeline looks like this:

    app.UseRouting();
    
    app.UseCors("FrontendPolicy");
    
    app.UseAuthentication();
    
    app.UseAuthorization();
    
    app.MapControllers();
    

    If CORS isn’t applied at the correct point in the pipeline, the browser may still block requests.


    Final Thoughts

    Today, I don’t configure CORS just to remove an error message.

    I configure it knowing why the browser enforces it and how it protects users.

    Understanding the reason behind a feature is far more valuable than memorizing the code to make an error disappear.

  • Change Tracking in EF Core: What It Is and When Should You Use It?

    Change Tracking in EF Core: What It Is and When Should You Use It?

    One of the most powerful features of Entity Framework Core is Change Tracking.

    It allows EF Core to automatically detect changes made to entities and persist them to the database.

    But while it’s incredibly useful, it’s not always necessary.

    Understanding when to use Change Tracking—and when to disable it—can significantly improve your application’s performance.


    What Is Change Tracking?

    When EF Core retrieves an entity, it keeps track of its original state.

    If you modify one of its properties, EF Core detects the change.

    When you call SaveChanges(), only the modified values are sent to the database.

    Example:

    var product = await context.Products
        .FirstAsync(p => p.Id == 1);
    
    product.Price = 1200;
    
    await context.SaveChangesAsync();
    

    Notice that we never call Update().

    EF Core already knows that Price changed.


    How Does It Work?

    When an entity is loaded:

    • EF Core stores the original values.
    • It monitors property changes.
    • During SaveChanges(), it compares the current values with the original values.
    • Only the necessary SQL UPDATE statement is generated.

    This makes updates simple and reduces boilerplate code.


    When Should You Use Change Tracking?

    Change Tracking is ideal when:

    • Updating existing records.
    • Inserting related entities.
    • Managing relationships.
    • Performing CRUD operations where data will be modified.

    When Should You Avoid It?

    If you’re only reading data, Change Tracking adds unnecessary overhead.

    In those cases, use:

    var products = await context.Products
        .AsNoTracking()
        .ToListAsync();
    

    This avoids tracking every entity and can improve performance, especially for large result sets.


    Change Tracking vs AsNoTracking()

    With Change Tracking

    var product = await context.Products
        .FirstAsync();
    
    product.Price = 1200;
    
    await context.SaveChangesAsync();
    

    ✅ Changes are automatically detected and saved.


    With AsNoTracking()

    var product = await context.Products
        .AsNoTracking()
        .FirstAsync();
    

    The entity is not tracked.

    If you modify it and call SaveChanges(), nothing happens unless you explicitly attach or update the entity.


    Performance Considerations

    Tracking every entity consumes:

    • Memory
    • CPU
    • Change detection time

    For read-heavy APIs or reporting queries, disabling tracking can provide noticeable performance improvements.

    However, don’t disable tracking if you intend to modify the entities afterwards.


    Best Practices

    ✅ Use Change Tracking for create, update, and delete operations.

    ✅ Use AsNoTracking() for read-only queries.

    ✅ Avoid tracking thousands of entities unnecessarily.

    ✅ Understand whether your query needs tracked or untracked entities before optimizing.


    Final Thoughts

    Change Tracking is one of the reasons EF Core makes data updates so convenient.

    But like any feature, it should be used intentionally.

    A simple rule I follow is:

    • Reading data? → Use AsNoTracking().
    • Updating data? → Let EF Core track the entity.

    Choosing the right approach helps you build applications that are both clean and efficient.

  • 7 EF Core Query Optimization Tips Every .NET Developer Should Know

    7 EF Core Query Optimization Tips Every .NET Developer Should Know

    Entity Framework Core makes database access simple, but it’s also easy to write queries that look correct while performing poorly in production.

    As applications grow, inefficient queries can become one of the biggest performance bottlenecks.

    Here are seven practical techniques I regularly consider when working with EF Core.


    1. Select Only the Columns You Need

    Avoid loading an entire entity if you only need a few properties.

    ❌ Less efficient:

    var users = await context.Users.ToListAsync();
    

    ✅ Better:

    var users = await context.Users
        .Select(u => new
        {
            u.Id,
            u.Name
        })
        .ToListAsync();
    

    Fetching fewer columns reduces network traffic and memory usage.


    2. Use AsNoTracking() for Read-Only Queries

    By default, EF Core tracks every entity it loads.

    If you’re only reading data, disable change tracking.

    var products = await context.Products
        .AsNoTracking()
        .ToListAsync();
    

    This can noticeably improve performance for read-heavy operations.


    3. Avoid the N+1 Query Problem

    This is a common performance issue.

    Instead of triggering additional queries for related data, load it efficiently.

    var orders = await context.Orders
        .Include(o => o.Customer)
        .ToListAsync();
    

    Or project only the data you actually need.


    4. Filter Early

    Push filtering to the database.

    var activeUsers = await context.Users
        .Where(u => u.IsActive)
        .ToListAsync();
    

    Don’t retrieve unnecessary rows and filter them in memory.


    5. Paginate Large Result Sets

    Avoid returning thousands of records at once.

    var customers = await context.Customers
        .OrderBy(c => c.Id)
        .Skip(page * pageSize)
        .Take(pageSize)
        .ToListAsync();
    

    Pagination improves both application performance and user experience.


    6. Check the Generated SQL

    EF Core generates SQL for you—but you should still know what it’s executing.

    Use:

    var sql = query.ToQueryString();
    

    Reviewing the generated SQL helps identify unnecessary joins, filters, or inefficient queries before they reach production.


    7. Create the Right Database Indexes

    Even the best EF Core query can perform poorly without proper indexing.

    Frequently filtered or joined columns should be indexed appropriately.

    Remember:

    EF Core generates SQL. SQL Server executes it.

    Database design still matters.


    Final Thoughts

    Optimizing EF Core isn’t about replacing it with raw SQL.

    It’s about understanding how your LINQ queries translate into SQL and ensuring the database performs as little work as necessary.

    A few simple habits can make a significant difference:

    • Project only what you need.
    • Use AsNoTracking() for read-only queries.
    • Avoid N+1 queries.
    • Filter early.
    • Paginate large datasets.
    • Inspect generated SQL.
    • Design proper indexes.

    Small optimizations applied consistently often produce the biggest improvements.