Archive of

Efficiently Map Related Objects in SqlSugar

Introduction

When working with relational databases in .NET, one common requirement is to join related tables and map the results to objects. This can become cumbersome if done manually for each field. SqlSugar, a powerful ORM for .NET, simplifies this process by providing built-in methods for automatic mapping. This blog post will guide you through using SqlSugar’s Mapper feature to map related objects efficiently.

Setting Up Your Entities

Let’s start by defining two simple entities: Note and User. We want to map User information into each Note record based on a foreign key relationship.

public class Note
{
    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
    public long Id { get; set; }
    public long UserId { get; set; }
    public string Content { get; set; } = string.Empty;

    [SugarColumn(IsIgnore = true)]
    public User? User { get; set; }  // Navigation property for User
}

public class User
{
    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
    public long Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string? Gravatar { get; set; } = string.Empty;
}

Fetching and Mapping Data

Using SqlSugar, you can fetch data from the Note table and join it with the User table to get complete information for each note, including the associated user details. The Mapper method is key to simplifying this process.

Here’s how to write a method that retrieves paginated Note data along with related User information:

private async Task<PageData<Note>> GetPagedNotesAsync(int pageSize = 20, int pageNumber = 1,
    Expression<Func<Note, bool>>? filter = null, Expression<Func<Note, object>>? orderBy = null, bool isAsc = true)
{
    var pageData = new PageData<Note>
    {
        PageIndex = pageNumber,
        PageSize = pageSize
    };
    RefAsync<int> totalCount = 0;

    // Query with join and mapper
    var notes = await db.Queryable<Note, User>((n, u) => new JoinQueryInfos(
            JoinType.Inner, n.UserId == u.Id
        ))
        .WhereIF(filter != null, filter)
        .OrderByIF(orderBy != null, orderBy, isAsc ? OrderByType.Asc : OrderByType.Desc)
        .Mapper(n => n.User, n => n.UserId)  // Automatically map User based on UserId
        .ToPageListAsync(pageNumber, pageSize, totalCount);

    pageData.TotalCount = totalCount;
    pageData.DataList = notes;
    return pageData;
}

Key Steps Explained

  • Join Query: The Queryable method with JoinQueryInfos specifies an inner join between the Note and User tables based on UserId.

  • Conditional Clauses: WhereIF and OrderByIF conditionally apply filters and ordering based on the provided expressions, offering flexibility for dynamic queries.

  • Automatic Mapping: The Mapper method maps the User object to the Note entity. It uses the foreign key UserId to establish this relationship, making it unnecessary to manually assign each field from the User table to the Note.

Conclusion

With SqlSugar’s Mapper feature, mapping related entities becomes a straightforward task. This reduces boilerplate code and improves code maintainability. By defining navigation properties and leveraging the power of Mapper, you can effortlessly join and map complex data structures, streamlining your development workflow.

Sharing this approach with fellow developers will not only save them time but also encourage the use of efficient, modern ORM practices in .NET applications.

Maintaining Scroll Position in Flutter's ListView: A Practical Guide

If you’ve worked with Flutter’s ListView, you might have encountered a scenario where you navigate between pages, and upon returning, the scroll position isn’t where you expected it to be. Recently, I faced a similar challenge with a note-taking app. Every time I moved between pages, I ended up at the bottom of the new page. Not the best user experience, right?

The Problem

In my note-taking app, I wanted the NoteList widget to always start at the top position whenever it’s built or updated. This way, users wouldn’t have to manually scroll up to see the latest notes. The challenge was to encapsulate the scroll behavior within the NoteList widget itself, without having to manage the scroll status from its parent widget. Here’s how we achieved that.

The Initial Implementation

Initially, NoteList was a simple StatelessWidget that displayed a list of notes grouped by their creation date. However, it didn’t retain or reset the scroll position upon navigation or page refreshes, which led to an inconsistent and frustrating user experience. Here’s a simplified version of the original NoteList:

class NoteList extends StatelessWidget {
  final List<Note> notes;
  final Function(Note) onTap;

  const NoteList({required this.notes, required this.onTap});

  @override
  Widget build(BuildContext context) {
    // Group notes by date
    final notesByDate = <String, List<Note>>{};
    for (var note in notes) {
      final dateKey = note.createDate!;
      notesByDate[dateKey] = notesByDate[dateKey] ?? [];
      notesByDate[dateKey]!.add(note);
    }

    return ListView.builder(
      itemCount: notesByDate.keys.length,
      itemBuilder: (context, index) {
        final dateKey = notesByDate.keys.elementAt(index);
        final dayNotes = notesByDate[dateKey]!;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: dayNotes.map((note) => GestureDetector(
              onTap: () => onTap(note),
              child: Text(note.content),
            )).toList(),
        );
      },
    );
  }
}

The Solution

To solve this, we needed to ensure that every time NoteList was built, it would start at the top of the list. We introduced a ScrollController to manage the scroll position and added a simple logic to reset the scroll position to the top whenever the widget is built or updated.

Here's how we transformed the NoteList:

  1. Added a ScrollController: This allows us to control the scroll position programmatically.
  2. Reset Scroll Position: Using WidgetsBinding.instance.addPostFrameCallback, we ensure the scroll is reset after the widget's frame is built.

Here's the updated NoteList with these improvements:

class NoteList extends StatelessWidget {
  final List<Note> notes;
  final Function(Note) onTap;
+  final ScrollController _scrollController = ScrollController();

  NoteList({required this.notes, required this.onTap}) {
+    // Scroll to the top whenever the widget is built or updated
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _scrollController.jumpTo(0);
+    });
  }

  @override
  Widget build(BuildContext context) {
    // Group notes by date
    final notesByDate = <String, List<Note>>{};
    for (var note in notes) {
      final dateKey = note.createDate!;
      notesByDate[dateKey] = notesByDate[dateKey] ?? [];
      notesByDate[dateKey]!.add(note);
    }

    return ListView.builder(
+      controller: _scrollController,
      itemCount: notesByDate.keys.length,
      itemBuilder: (context, index) {
        final dateKey = notesByDate.keys.elementAt(index);
        final dayNotes = notesByDate[dateKey]!;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: dayNotes.map((note) => GestureDetector(
              onTap: () => onTap(note),
              child: Text(note.content),
            )).toList(),
        );
      },
    );
  }
}

Key Takeaways

  1. Encapsulation: By managing the scroll state within NoteList, we keep the parent widget cleaner and more focused on data management.
  2. Consistent User Experience: Resetting the scroll position ensures users always start at the top, providing a predictable and pleasant experience.
  3. Simple Yet Effective: Sometimes, small changes like adding a ScrollController can significantly enhance the usability of your app.

This solution should help other developers facing similar issues with managing scroll positions in Flutter. By following this approach, you can ensure that your list-based components offer a consistent and user-friendly experience.

By the way, my simple note taking app has been released at https://happynotes.shukebeta.com, and I'll improve it day by day. It is an free web application, and it has an android version on GitHub.

Feel free to use it and give me feedback on GitHub!

Navigation in Flutter: `pushReplacement` vs `pushAndRemoveUntil`

pushReplacement

Imagine you're replacing an old road sign with a new one. The old one is gone, and there's no way to know it was ever there. That's what pushReplacement does in Flutter.

Usage:

  • Navigator.pushReplacement(context, newRoute)

What It Does:

  • Replaces the current route with a new route.
  • The current route is removed from the stack, making way for the new route.
  • The removed route is completely gone—pressing the back button won’t bring it back.

When to Use It:

  • Ideal for scenarios like replacing a login screen with a home screen after a user logs in. You don’t want them to go back to the login screen, right?

Code Example:

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomeScreen()),
);

pushAndRemoveUntil

Now, think of pushAndRemoveUntil as clearing a cluttered desk, keeping only the essentials. You decide which items stay and which go. This function allows more control over which routes to remove.

Usage:

  • Navigator.pushAndRemoveUntil(context, newRoute, (route) => condition)

What It Does:

  • Pushes a new route and removes routes below it until a specified condition is met.
  • You provide a condition to determine which routes to keep or discard.

When to Use It:

  • Perfect for situations like logging out, where you want to return to a clean slate (the login screen) and remove all previous screens.

Code Example:

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => LoginScreen()),
  (Route<dynamic> route) => false, // Remove all previous routes.
);

Key Differences

  1. Scope of Operation:

    • pushReplacement swaps only the top route.
    • pushAndRemoveUntil can clear out multiple routes based on your condition.
  2. Stack Control:

    • pushReplacement provides straightforward, one-route replacement.
    • pushAndRemoveUntil allows for a more flexible navigation reset.
  3. Use Case Scenarios:

    • Use pushReplacement for simple replacement needs.
    • Use pushAndRemoveUntil for complex navigation flows requiring cleanup of multiple routes.

A sample nginx config file that redirects traffic from a domain to b domain

# This config is for old-domain.com => new-domain.com redirecting
server {
  listen 80;
  listen 443 ssl;
  server_name old-domain.com www.old-domain.com;
  location / {
    return 301 https://new-domain.com$request_uri;
  }
  #include /etc/nginx/conf.d/snippets/ssl.conf;
}


# A sample nginx config file for a reverse proxy server
server {
    server_name happynotes-img-uploader.shukebeta.com;
    client_max_body_size 10M;

    access_log  /var/log/nginx/happynotes-img-uploader.shukebeta.com.access.log;
    error_log   /var/log/nginx/happynotes-img-uploader.shukebeta.com.error.log;

    location / {
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass http://127.0.0.1:3000;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    large_client_header_buffers 4 32k;

    listen [::]:80;
    listen 80;
}

gh pr view can't find PR just created

TLDR;

The solution is git push -u

David.Wei @ /d/git/xemt-core/dfx - [feature/mt-38546-axo] $ git push -u
branch 'feature/mt-38546-axo' set up to track 'origin/feature/mt-38546-axo'.
Everything up-to-date
David.Wei @ /d/git/xemt-core/dfx - [feature/mt-38546-axo] $ gh pr view
MT-38546 Fintrac API| DFX DB changes #1355
Open • David-Wei_euronet wants to merge 6 commits into integration from feature/mt-38546-axo • about 3 days ago
...

vilmibm wrote the following on this issue and it reminds me why I couldn't find the PR:

gh pr view relies on mapping the current locally checked out branch to a remote tracking branch so that the github host can be queried appropriately for PR data; in your example it seems like you're creating PRs without any configured tracking information, making it impossible for gh to open the PR.

To avoid this issue happening in the future, you can run the following command to enable the auto tracking feature.

git config --global push.autoSetupRemote true

If your git version is lower and couldn't upgrade easily, another measure is to create an alias like the following

git config --global alias.p 'push -u origin HEAD'

Then always use git p instead of git push will do the trick.