←back to thread

201 points olvy0 | 8 comments | | HN request time: 1.329s | source | bottom
Show context
high_na_euv ◴[] No.41878416[source]
LINQ is so fucking useful and well designed feature of .NET ecosystem that it is unreal when you gotta use lang which doesnt have such a thing.

C# design team is/was unparalleled

replies(7): >>41878459 #>>41878543 #>>41878588 #>>41878686 #>>41879163 #>>41879194 #>>41879315 #
pjmlp ◴[] No.41878459[source]
LINQ is largely based on FP stuff, also how Smalltalk collections work.

It is relatively easy to find similar capabilities in most languages nowadays, unless one is stuck on Go, C and similar.

replies(7): >>41878547 #>>41878579 #>>41878702 #>>41878783 #>>41878792 #>>41878816 #>>41879057 #
blackoil ◴[] No.41878783[source]
One difference with LINQ is its ubiquity. It works with database, in memory data structures, on disk files. You can use your skills/code across all the system.
replies(1): >>41878827 #
John23832 ◴[] No.41878827[source]
It's just built on top of anything that is Iterable. If a language has first class iterator support, they could do something similar.
replies(2): >>41878964 #>>41879023 #
mythz ◴[] No.41879023[source]
Takes a lot more than that, LINQ providers work by accepting a LINQ Expression Syntax tree instead of an opaque function, which allows providers to inspect and traverse the Expression's AST and translate it into the data source it's implementing.

This Expression AST is constructed by the compiler, not something that can be tacked on by a library later.

replies(2): >>41879354 #>>41879511 #
1. Pxtl ◴[] No.41879511[source]
Having used it since its inception, I've come to the conclusion that the SQL translator is kind of a misfeature. It creates so many weird bugs and edge-cases and tedium.

I love LINQ, I love having a typesafe ORM as a standard feature of C#, but the convenience of being able to reuse my Pocos and some expressions for both in-memory and in-SQL don't outweigh the downsides.

If I were designing SQL/LINQ today, I'd keep the in-memory record classes and in-database record classes distinct and use some kind of codegen/automapping framework for keeping them synched up. Maybe allow predicate operators to return things other than booleans so we could make `a == b` return some kind of expression tree node.

For ad-hoc queries using anonymous classes? Support defining an interface inline in a generic so you can say

    public T MyQuery<interface {string Firstname{get;set;}; string Lastname{get;set:}} T>();
Like, to elaborate, if you were doing some kind of JSON-based codegen (alternately you could do something where you have a separate hand-written POCO Model assembly and use reflection against it to generate your DbModel classes so it's still Code First). Yes, I know MS tried and abandoned this approach, I used LinqToSQL and EF3.5 and whatnot and suffered all that pain.

like, your master datatable file would be something like

    ```json
    "tables" : [
      "persons" : {
        "dataRecordClass" : "DataRecordsNamespace.DbPerson",
        "objectClass" : "PocosNamespace.Person"
      },
      "columns : {
        "PKID" : {
          "type" = "integer",
          "isPrimaryKey" = true,
          "isAutoGenerated" = true,
        }
        "firstName" : {
          "type" : "nvarchar(255)",
          "allowNull" : true,
        }
        "lastName" : {
          "type" : "nvarchar(255)"
          "allowNull" : false
        }
      }
    ]
    ```
which would generates something like

    ```cs
    public class DataRecordsNamespace.DbPerson : DbRecord {
      public DbPerson() { throw ThisIsAFakeClassException(); }
      public DbInt PKID{
        get => throw ThisIsAFakeClassException();
        set => throw ThisIsAFakeClassException();
      }
      public DbNVarChar {
        get => throw ThisIsAFakeClassException();
        set => throw ThisIsAFakeClassException();
      }
    }

    public partial class PocosNamespace.Person {
      public AutoGenerated<int> PKID{ get; init; }
      public string FirstName { get; set; }
      public string LastName { get; set; }
    }

    public class MyDbModel : DbModel {
      public DbTable<DbPerson> Persons => DoSomeLazyStuff();
    }

    public static class MyDbContextExtensions {
      public static List<Person> Resolve(this DbQuery<DbPerson> dbPersons) 
      {
        //call code to execute the actual query.
      }
    }
    ```
Am I making sense? Then you wouldn't have the problem of "oops I used an untranslateable method or member of Person", because MyDbModel can't have any of those. You'd lose the ability to to switch from whether a query is in-memory or in-database just by removing the ToList(), but I'd argue that's a misfeature, and better-handled by having some kind of InMemory implementation. Like, having DbQuery have a simple `.ToLocalMemory()` function that is a hint that the next part should be done locally instead of in the database would be a better way to do that. Then you could still do

    ```cs
    List<Person> myPersons = connection.MyDbModel
      .Persons
      .DoSomeInDatabaseQueryStuff()
      .ToLocalMemory()
      .DoSomeLocalMemoryStuffToOffloadItFromDatabase()
      .Resolve()
      .DoSomeDotNetStuff()
      .ToList();
    ```
edits: fix some of the HN pseudomarkdown
replies(2): >>41879691 #>>41880535 #
2. mythz ◴[] No.41879691[source]
Guess everyone has their preferred style, I personally avoid code-gen data models like the plague and much prefer code-first libraries.

Here's how you'd do something similar in our OrmLite ORM [1]:

    public class Person
    {
        [AutoIncrement]
        public int Id { get; set; }
        public string? FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
    }
Create Table:

    var db = dbFactory.Open(); // Resolve ADO.NET IDbConnection
    db.CreateTable<Person>();  // Create RDBMS Table from POCO definition
Execute Query:

    // Performs SQL Query on Server that's returned in a List<Person>
    var results = db.Select<Person>(x => x.FirstName.StartsWith("A") && x.LastName == "B");

    // Use LINQ to further transform an In Memory collection
    var to = results.Where(MemoryFilter).OrderBy(MemorySort).ToList();
Everything works off the POCO, no other external tools, manual configuration mapping, or code gen needed.

[1] https://docs.servicestack.net/ormlite/

replies(1): >>41879863 #
3. Pxtl ◴[] No.41879863[source]
My problem with this approach is that this falls apart if you write:

    db.Select<Person>(x => Regex.IsMatch(x.FirstName, "^A.*"));
This would fail at run-time instead of compile-time.

That's why I'd rather see the DB classes auto-generated with a mapper to convert them. Having the "master" be POCOs instead of JSON/XML/YAML/whatever isn't something I'm convinced on in either direction, but imho the in-database classes being not real POCOs is the important part because it reduces the the problem of somebody writing Person.MyMethod() and then blowing up because it's not a SQL function.

replies(1): >>41879948 #
4. mythz ◴[] No.41879948{3}[source]
Isn't this just `.StartsWith("A")`?

How would you perform this regex query with your code generated solution? What would have to be code generated and what would the developer have to write?

As there's a lot more features available in different RDBMS's than what's available in C# expression syntax, you can use SQL Fragments whenever you need to:

    var results = db.Select(db.From<Person>()
        .Where(x => x.LastName == "B")
        .And("FirstName ~ '^A.*'"));
replies(1): >>41882485 #
5. magicalhippo ◴[] No.41880535[source]
Saw EF now supports custom SQL queries, so been considering that once we've moved to MSSQL (old db server isn't supported by EF).

We're quite accustomed to writing our own SQL select statements and would like to continue doing that to have known performance, but the update, insert and delete statements are a chore to do manually, especially for once you're 4-5 parent child levels deep.

replies(1): >>41883541 #
6. Pxtl ◴[] No.41882485{4}[source]
Yes, it's a trivial example. I'm not looking to support it, I'm looking to catch it at compile-time.

if "Person.FirstName" is a string, then that encourages users to use string-operations against it, which will fail if this expression is being translated to SQL for executing in the DB.

if "Person.FirstName" is some other type with no meaningful operations supported on it (which will get converted into a string when the query is executed) then it prevents many many classes of logic errors.

7. Pxtl ◴[] No.41883541[source]
Quick word of advice when dealing with deep parent/child reloationships:

Do not use lazy loading feature. That way lies madness.

replies(1): >>41883854 #
8. magicalhippo ◴[] No.41883854{3}[source]
We're not doing that where we come from. All child tables have the main id so we can load the data for all child rows with just one query per child table, and we load everything at once.

We were planning on sticking with this, it has worked well so far, but good to know to avoid getting tempted by the alternative.