and has 0 comments

  After watching another YouTube video with the full cinematics, I can tell you that I liked the story in Aliens: Dark Descent, but the same formula is starting to get old.

  You see, I loved the movies, I like the universe - especially if you consider Blade Runner as part of it, and I've also read a couple of books in the Alien universe and my impression is that with whatever you start with, even Prometheus and that crap, you will probably like it, too. However, you will start losing enjoyment as you go along, as almost every Alien story is exactly the same. You have to appreciate the Alien: Resurrection movie because, as weird and euro as it was, it felt different. I know it's an unpopular belief, but I really liked that film.

  Anyway, back to the game: aliens gets loose in a closed environment - even if it's big as a planet, there is no escape from it, and people try to survive, helped by marines and hindered by greedy psychopathic corporate shills. There, that's almost every Alien story. Sometimes they throw a Predator in there, for good measure, and lately they put Engineers in there, too, but the plot is the same. Dark Descent is no exception. There is a downed ship, some towns, a corporate tower, synths, automated turrets, APCs, marines, corporation stuff, Weyland Yutani and so on. The only difference might be the appearance of Cassandra, a woman that seems to be able to communicate telepathically with the aliens.

  Now, in the lore that is not movies, there is an extended version of the story described above where the aliens are not just mindless creatures, but somehow project this mental field that can sometimes attract some people to their side. There is also the royal jelly, a substance only the queen makes and that has great effects on humans as well. In the game there is no jelly and Cassandra's gift seems to be a genetic fluke. Whatever her abilities mean will have to wait for a sequel to the game.

  I can't say anything about the gameplay. Seemed to be mostly top down, like those cheap horror survival games that sometimes you play online, only slightly better. The cut scene animations, though, were pretty good and amounted to about two hours put together. It's funny when game companies just make full movies as an afterthought, to enhance the experience of the game proper.

  Now, about the story. There is this woman administrator in the corporation that sees aliens killing everybody and initializes the Cerberus protocol, which means satellites around the planet will stop anything from escaping, shooting everything down. Nothing in, nothing out. Even synths will make everything possible to enforce this. She ends up on a ship that gets almost shut down by the satellites, but it's a military vessel, so it survives reentry. It is her job (yours) to collaborate with the marines to save as many people as possible, get to the bottom of the mystery and, of course, live.

What would I change so that story feels fresh? Well, it's now canon that you have to have a sleazy corporate ass making everything harder. In fact, the main character of the game does start as one, only she's a good woman. I wouldn't do that. I would let the player decide the level of sleaziness and if they want to play it psycho or good guy or something in between. Basically Witcherize the game (talking about the game, not the books, where Geralt is a boyscout). Increase personal stakes, give her some competence other than "administrator" - which means she gets to move around talking to people all the time - and a secret to protect. Basically combine the main characters in the game: the admin, the father of Cassandra and the scientist.

  What if the main character is the mother of Cassandra? Maybe she's not her biological mother, to add some distance, so she could conceivably save her ass and sell her to the company. What if she is afraid something would come up and destroy her life, so she has incentives to leave everybody else behind, maybe even this Cassandra stuff. What is she is indebted to the company and she needs a way out, to add to the desperation? You could add a bit of romantic tension between the soldier and her, making an eventual death more meaningful. You could manipulate people, seduce them, intimidate them or even shoot them, kind of like Vampire: The Masquerade, adding to the agency of the player.

  Because in this game you only run a linear story. There are no alternative outcomes. You fail a mission, you die, and if you played the game already you know what's coming but you can't stop it. Just add a diversity of choice. And I know you will say that this would make the two hour cinematics be 10 hours. Not necessarily. Olden games managed to do wonders reusing parts of animations to construct multiple stories. The knowledge is there.

  Bottom line: a true Alien story, but bringing nothing new to the table. A linear gameplay, that provides little choice other than just go with the flow. The top-down thing makes it look like a rip-off of Starcraft playing the marines, so it didn't really captivate my imagination. A decent game, but nothing more.

and has 0 comments

  XCOM was one of my favorite games from when I was a kid. I didn't have the personal time resources to play XCOM2, though, and besides it felt quite different. You see, in the first game I liked collecting alien stuff and researching it. The tactics were fun, but I was all into the research tree. So I though, after playing the beginning of XCOM2 a few times then forgetting about it, how about I watch the entire storyline on YouTube, like a complete ass, and then comment on the game like I know what I am talking about?

  So here is goes. I think one of the major things going against XCOM2 is its timing. XCOM was first released in 1994, when telling a story like this felt fresh and being able to play through it amazing. XCOM2 was released in 2016, 22 years later, with so much sci-fi content in the form of TV, movies and games having been released since. There were a bunch of TV series covering the alien invasion and resistance angle, all of them devolving into the lazy German vs French resistance with ray guns tropes, and having yet another thing like this wasn't awesome. Add to it that the mystery was already revealed and it would explain my lack of interest at least.

  Anyway, the story here is that 20 years later "the commander", meaning you, is rescued by the resistance from an alien device that was using him as a hub for tactical information. With your brilliant leadership, they can now fight against an enemy that has conquered the world and controls it via propaganda, empty promises and ultimately violence. Somehow, after full control over the planet for 20 years, the aliens still have a lot of trouble locating and fighting you.

  The tactical fighting and upgrade system was improved dramatically. It's so complex that... it bored me to death. Tactical games afficionados loved it, though, and for good reasons. You can do all kinds of things, based on the enemy, the team composition, the tech tree, etc. However the story felt lackluster to me. A new McGuffin every stage of the game and the ending... how do American stories end? You find the source and you blow it up!... did nothing for me. Really, it felt like they were rehashing the Falling Skies story, even the end scene.

  Bottom line: if you love turn based tactical games, this should be one of your favorites, but the story is simplistic and derivative.

  P.S. and if you're wondering why XCOM3 is not out there, it's because of Marvel.

  P.S.too - "so if you are so smart, how would you have written the story?".

  Well, I am no writer, but I can tell you something after decades of consuming popularized science and a lot of Dunning Kruger: any space conquering force would have two characteristics. First, they would have limited resources. Without some very cheap space travel option that seems unlikely, it should be very expensive to come to our planet. Second: it's so easy to destroy anything on Earth from space. It's ridiculously easy if you have mastered interstellar travel. So the story would have to take that into account. XCOM2 actually used that, with the discovery of the final McGuffin, to explain and then solve the game, but it was a lazy solution.

Here is my take: alien intervention is being suspected, so X-COM is created. They have to solve the mystery of what is going on, considering they are an organization working on a hunch. This is more like X-Files than X-Com. Then the aliens are not all powerful, they are a bridgehead force, if not fugitives or a small team stranded here. They may be not malevolent outside considering humans a bunch of stupid monkeys to be used to further their goals. And when people are stranded amongst aliens, they tend to be terrified and act more psychotically than normal. The advantage of this take is that you can play both sides. So this would be more of an adventure game than a tactical shooter, although the horror of fighting what you thought were aliens, but in fact were human and animal chimeras should be there.

For the sequel, continue from the last scene of the first, when you discover that the small force on Earth is actually a small tactical team that operates behind enemy lines. The alien ship and their main force is located on some large asteroid in the Asteroid Belt or maybe a moon of Jupiter. With their presence and their source location revealed, now humanity must race to build the infrastructure required to defend and attack in space, when at any moment the desperate aliens might throw an asteroid or two at us. This would be tactical, but also strategic. We switch from a mystery adventure to a tactical space exploration and warfare game, akin to The Expanse.

Bonus: the third part, a continuation of the second where you have discovered the aliens themselves, which you may have seen as desperate and sympathetic, were actually just another version of chimeras, made out of organic and machine parts by the real culprit, a semi-sentient machine intelligence that has the mission to explore, exploit and contain any threat to its original builders. Now the enemy is a bunch of von Neumann probes that have no qualms in capturing and using for parts any of their human prisoners or craft.

The whole idea of the series is not that some malevolent Elders want to rule the world, but that a very small and resource poor alien presence can wreak havoc and endanger our very existence. It's you who has all the resources and they have the technology and relentless cunning. The terror is just as for someone with cancer. Even when you get rid of it, you don't know for sure and must remain eternally vigilant. We are the sheep and they are the wolves in the night. In each of the games you can choose to play the humans or the aliens. DO NOT create the common enemy that we would join forces to defeat, a la Starcraft, it's not that kind of story, although you can play around with altered humans and manufactured alien forces that escape their control as a third wildcard faction. And of course, as any good game, the story would change based on your approach.

This would be it. It's not like my ideas are not derivative, but they would be more fun to play outside the tactical shooter, mystery adventure or space RTS mechanics. At least they would make a good YouTube cutscenes video.

and has 0 comments

  Dopamine Nation: Finding Balance in the Age of Indulgence is not so much a scientific book as an informed opinion piece. I kind of had different expectations for a book that starts with a chemical name for a neurotransmitter.  This book is about addiction, not dopamine per se. Anna Lembke is Chief of the Stanford Addiction Medicine Dual Diagnosis Clinic at Stanford University., so she knows what she's talking about.

  I am torn between me agreeing with most of what the book says and an instinctive dislike of the author. She came off to me as a conservative American prude, talking about the positive results of the Prohibition or prosocial shame and skewing statistics to make a point. But, really, when I try to pinpoint the things she said that I feel are completely wrong, there aren't many. She is just honest with herself and with the reader. Yes, she is appalled by the patient who builds machines to masturbate him and shares this online, but she doesn't lack the empathy required to help him. Yes, she does believe the Prohibition had positive effects, presenting statistics about it, but she's aware of the organized crime effect of it. Yes, she believes shame has a positive effect, but only in a community that also supports you and guides you to get out of the situation you're in.

  I guess my instinct is to reject any social solution to one's personal problems, so that might be it. I also have a rather addictive personality, so it might be a defensive reaction. So let's discuss the book, and not how the author felt to me.

  Starting with the end, here are the 10 steps that Lembke recommends for handling addiction, defined in the book as any behavior that causes harm to you or your group that you are having difficulty stopping:

  1. The relentless pursuit of pleasure (and avoidance of pain) leads to pain. - this is something to take note of
  2. Recovery begins with abstinence. - I partly agree with this
  3. Abstinence resets the brain’s reward pathway and with it our capacity to take joy in simpler pleasures. - this is a reductive idea, contradicted by the book's thesis, since things to get addicted to are all around us and part of what is considered normal social life
  4. Self-binding creates literal and metacognitive space between desire and consumption, a modern necessity in our dopamine-overloaded world. - self-binding is putting barriers between you and the thing that addicts you. - agreed.
  5. Medications can restore homeostasis, but consider what we lose by medicating away our pain. - agreed.
  6. Pressing on the pain side resets our balance to the side of pleasure. - I agree that facing your pain opens the door to more pleasure, but depends on the context.
  7. Beware of getting addicted to pain. - this is another thing to take note of. Pain and pleasure are not antonyms inside the brain, they are closely related in a functional sense.
  8. Radical honesty promotes awareness, enhances intimacy, and fosters a plenty mindset. - this is one of her central points in the book. I fully agree.
  9. Prosocial shame affirms that we belong to the human tribe. - tribalism is something that automatically repels me.
  10. Instead of running away from the world, we can find escape by immersing ourselves in it. - I am not doing that, and I should. However, going fully in the other extreme is probably worse.

And I agree with most of what she says. We live in times of abundance, where the next fix to escape reality is right around the corner. And doesn't it feel good? Apparently... not. People are more and more dissatisfied with their lives, even when those would have appeared miraculous even to people living in the '80s. It might not be hard drugs, but alcohol, maybe weed, maybe a video game or two, maybe romance novels, maybe TV series or news watching. The act of escaping reality makes us feel less real ourselves and that is what leads to that feeling of unmoored loss.

  However, I don't agree with everything. One of the things that bothered me from the beginning was the way she presented statistics. Comparing absolute values of population size today and in the 1980s completely ignored the global population nearly doubled since then. Also showing relative percentual statistics between the same kind of values means nothing. I can't imagine someone as mature and educated like the author could make these kinds of mistakes unknowingly.

  Then there is this idea of abstinence. I personally know what this is the method that works best against addiction, too, however it works best because it is the easiest. Just like I agree with her that our hysterical overprotection of children deprives them of skills they should have learned before they go into the world by themselves, using abstinence to evade addiction is also a type of escapism. An addict dreams of two things: the thing they are addicted to and living a normal life where they are not addicted. Well, being perfectly honest with yourself and others and going to meetings and relying on others to not relapse is all nice and good, but it's not a normal life. It's still the life of an addict. And while abstinence from hard opioid drugs is obviously a good idea, I don't know what to say about stuff like reading or watching movies. Start with abstinence, but that should be the first step only.

  As for the prosocial shame, I almost agree, because in principle having people to lovingly point out your mistakes and help you get out of them is a good thing, but I don't think that the social groups Lembke was thinking about are also what I would be willing to accept.

  Bottom line: something fell a bit off to me, a bit culty, in this book. I think I reacted to the overconfidence of how the author expresses her opinions. However the content is very informative and informed, while also reenforced by personal experience as a therapist. The book is also short, you can read it in a few hours, so I recommend it, but with a personal warning of caution.

and has 0 comments

  When I started reading Dragonsbane I expected something light, young adult, typical dragons and magic and heroic women. And it was, but it was more than that. Barbara Hambly adds a lot of depth to her world, her characters, imbuing them with meaning and subtly anchoring them in real life.

  For example, our hero isn't only a mage, but also a mother, maybe a reluctant one, someone who has to always choose between her craft and her love for family. I understand how a writer might feel like that and after reading the "light book about dragons and magic", so will you. Or how gnomes are being bullied and discriminated against, but it's not empty virtue signaling when the author explores how hidden power (normal power, not magic) is being used to influence people to do that, for nefarious reasons they are not even aware of. Characters who are weak find strength, characters who seem powerful reveal their inner weakness and in the end victory is brought not by magical power, but by knowing yourself and accepting it as it is: mature strength of character.

  Yet, all of this complexity is subtle. You don't have to pay attention to it. One can just read it as a typical magical dragon quest just as well. It's not one of those books that you can reread multiple times, but I appreciate that a kid might enjoy this just as much as an adult, for very different reasons.

  I liked the book, so I might read the sequel as well. Not the best book ever, but a very pleasant surprise.

and has 0 comments

  Hah, if I'd have looked at the cover I would have probably chosen to read something else. Something that Ann Leckie absolutely loved is definitely not for me. If books had gender, A Memory Called Empire would be 100% female, as it focuses primarily on social cues, personal feelings and attachments, romantic connections, poetry, connotations of every word said, yet everything is so naive in terms of power or violence or even sexuality, like a children's story. There is almost no mention of technology or space travel, people meet face to face all the time, touching each other for whatever reasons and feeling things, and it all happens in a bureaucratic empire where social expectations are high and complex. This could just as well have been set in the 18th century Earth with some little magic McGuffin instead of the tech one Arkady Martine used, and none would have been the wiser.

  The main character is an ambassador from a tiny space station republic to the large empire of the region, which dominates, technologically, militarily, economically and culturally. Everybody dreams to be part of the empire, while they are slowly being devoured by it. So, again, could be any historical era. The ambassador is greeted by someone from the empire who is attached to her as aide. Now, they almost immediately become fast friends, with some romantic tension between them. Imagine this happening: the Chinese ambassador to the US receives an American aide who immediately befriends and helps them reach their goals, sometimes in defiance of American protocols, culture or even authority.

  The book continues in the same vein, with a lot of cultural references that mean nothing to the story, but at least add to the world building. The magical tech that the station uses is an imago, a machine that records one's experiences and personality in a chip that can then be implanted in their successor, as an advisor. The empire could use it, but their morality and laws prohibit it. A lot of intrigue around this little device that any decent security service would find out everything about in days. There are some civil war ideas, some future alien invasion hints, but mainly it is an old fashioned PG13 whodunnit given a sci-fi veneer.

  Bottom line: I found it extremely boring, fell asleep numerous times and only finished it out of spite. It wasn't bad, for sure, but it was anathema to what I enjoy. If you're a typical sci-fi reading guy or if you tried Ann Leckie and found it ridiculous, maybe you should reconsider reading this.

and has 0 comments

 I don't know if I want to play it more, but I've had fun for a few hours playing Sail Forth, managing my fleet and talking to the weird characters of the game, while navigating by hand a small sea full of islands.

  You start off as a guy with a boat, you find other stranded people, you upgrade your boat, buy cannons, fight pirates, do errands, buy new boats, create a fleet and so on. It's really entertaining! Plus it's a relatively small game that requires no hardware resources to speak of. Recommended!

and has 0 comments

  Doors - Paradox is a game where you solve puzzle to open doors, but the doors only lead to other doors. The whole concept is rather ridiculous, but one can get into it if they want to waste some time.

  The thing that really pissed me off was the messages you receive in each puzzle, suggesting this is some inner search for meaning or some comatose dream state or something like that, only it doesn't bring anything at all to the game. It doesn't have a story plot that gets resolved somehow, it changes nothing for the game play, it's just another cliché that became popular in this kind of games and we can't get rid of it.

  The graphics are fun and I can't help but think that with a good design this could have been something special. Alas, the puzzles are either ridiculously easy or annoyingly time consuming, like sliding puzzles. It gets old really quick and there is no resolution or anything for the time you spent opening stupid doors. An opportunity missed.

and has 0 comments

Beyond Blue has a short and silly story that has no consequences and leads nowhere, but the main point is that you can free dive in several ocean stages and look at and swim with realistic looking and swimming marine creatures, which is very relaxing, but gets old soon.

It's apparently inspired by Blue Planet II and partially financed by the BBC and OceanX, the Dalio brothers' "ocean exploration initiative".

I didn't really understand the point of the narrative, though. While it gets the main character where it needs to be to explore the ocean, there are at least two other characters that bring absolutely nothing to the game other than a waste of time and several subplots that end up nowhere than a footnote in a conversation.

I enjoyed the swimming around part, though. You can finish the story in a few hours and then you have the option to free dive in any of the stages of the game, tag animals and look at logs to learn about the ocean, which is pretty cool. But it gets boring quick if you don't already have an interest in oceanography.

and has 0 comments

  Awkward: The Science of Why We're Socially Awkward and Why That's Awesome started great! I mean, it immediately opened my eyes in terms of how to define awkwardness, why it's even relevant to other people and the reason they are keeping score. It then proceeded to give algorithmic solutions to blending in social situations even if you are awkward to begin with. Ty Tashiro even mentions that we have two relatively separate systems in our brains: the analytical and the social, and when you use one you inhibit the other. It made so much sense! I immediately started recommending the book, without having read it all.

  However the rest of the book was not as amazing, or at least this is how I felt. Instead it felt inconsistent, like a collection of separate materials that somehow were shoe stringed into a book. Still good, but compared to that stellar start, relatively weaker.

  There was one more thing that bothered me, probably saying more about me than about the book, but there were places where American liberal agenda seemed to infect the scientific discourse. I guess being an awkward individual who managed to become a relationship and social psychologist would adopt some of these concepts as a blending in mechanism, yet it felt a little jarring, like the author "sold out" accepting ideas that forcefully come with his acceptance in the crowd.

  It was funny to me that the metaphor (a very good one, it turned out) to describe how awkward people see the world compared to social ones was a Lion King Broadway play. No one watches those outside some population of the U.S., why would you use it as an example?

  Anyway, the idea is this: awkward people have more intense focus, but also a narrower one. They are compulsively attracted by specific things, ignoring everything else. Normal people just have a broader focus on everything, intuitively making connections between disparate signals, while for an awkward person it takes conscious effort to switch focus and combine things in their head. This leads to advantages and disadvantages, since they can focus on research, invention, refining of knowledge and so on, but they are "felt" by society at large as weird, because they miss social cues which determine social status and even interpersonal trust.

  An interesting question at the end was: if being awkward is something that makes one a social outcast, how come it was not eliminated by evolution. And the answer is that less social people are actually more free to explore the edges of human knowledge and behavior, thus fighting stagnation on the level of entire groups. Groups without their awkwards die off.

  I loved that a lot of vague social terms that we normally use were described and even defined analytically, complete with some ideas and concrete actions on how to reach specific goals. A lot of time when people analyze such psychological traits, they do it from the perspective of a normie. It was nice to get not only the definition, but also the theorems behind, so to speak. 

  Bottom line: I really recommend reading the beginning of the book. The rest you can consider optional, even if it's still very interesting and informative.

Intro

In the .NET world one of the most used method of accessing databases is with Entity Framework (EF), an Object Relational Mapper (ORM) that is tightly integrated with the language syntax. Using the Language Integrated Queries (LINQ) native to .NET languages, it makes data access feel like working with normal .NET collections, without much knowledge of SQL. This has its benefits and drawbacks that I will try not to rant about here. But one of the issues that it consistently creates is confusion regarding the structure of the software project, levels of abstractions and ultimately unit testing.

This post will try to explain why the repository abstraction is ALWAYS useful. Note that many people use repository as a term for abstracted data access, while there is also a repository software pattern which relates to similar things, but it's not the same thing. In here, I will call a repository a series of interfaces abstracting the implementation details of data access and ignore the design pattern completely.

History

Feel free to skip this if you are aware of it, but I have to first address how we got to the idea of repositories to begin with.

In prehistory, code was just written as is, with no structure, everything in, doing what you wanted it to do or at least hoping to. There was no automated testing, just manual hacking and testing until it worked. Each application was written in whatever was on hand, with concerns about hardware requirements more important than code structure, reuse or readability. That's what killed the dinosaurs! True fact.

Slowly, patterns started to emerge. For business applications in particular, there was this obvious separation of the business code, the persistence of data and the user interface. These were called layers and soon separated into different projects, not only because they covered different concerns, but also because the skills necessary to build them were especially different. UI design is very different from code logic work and very different from SQL or whatever language or system was used to persist data.

Therefore, the interaction between the business and the data layer was done by abstracting it into interfaces and models. As a business class, you wouldn't ask for the list of entries in a table, you would require a filtered list of complex objects. It would be the responsibility of the data layer to access whatever was persisted and map it to something understandable to business. These abstractions started to be called repositories.

On the lower layers of data access, patterns like CRUD quickly took over: you defined structured persistence containers like tables and you would create, read, update or delete records. In code, this kind of logic will get abstracted to collections, like List, Dictionary or Array. So there was also a current of opinion that repositories should behave like collections, maybe even be generic enough to not have other methods than the actual create, read, update and delete.

However, I strongly disagree. As abstractions of data access from business, they should be as far removed from the patterns for data access as possible, instead being modelled based on the business requirements. Here is where the mindset of Entity Framework in particular, but a lot of other ORMs, started clashing with the original idea of repository, culminating with calls to never use repositories with EF, calling that an antipattern.

More layers

A lot of confusion is generated by parent-child relationships between models. Like a Department entity with People in it. Should a department repository return a model containing people? Maybe not. So how about we separate repositories into departments (without people) and people, then have a separate abstraction to map then to business models?

The confusion actually increases when we take the business layer and separate it into sublayers. For example, what most people call a business service is an abstraction over applying specific business logic only to a specific type of business model. Let's say your app works with people, so you have a model called Person. The class to handle people will be a PeopleService, which will get business models from the persistence layer via a PeopleRepository, but also do other things, including a mapping between data models and business models or specific work the relates only to people, like calculating their salaries. However, most business logic uses multiple types of models, so services end up being mapping wrappers over repositories, with little extra responsibility.

Now imagine that you are using EF to access the data. You already have to declare a DbContext class that contains collections of entities that you map to SQL tables. You have LINQ to iterate, filter and map them, which is efficiently converted into SQL commands in the background and give you what you need, complete with hierarchical parent-child structures. That conversion also takes care of mapping of internal business data types, like specific enums or weird data structures. So why would you even need repositories, maybe even services?

I believe that while more layers of abstraction may seem like pointless overhead, they increase the human understanding of the project and improve the speed and quality of change. There is a balance, obviously, I've seen systems architected with the apparent requirement that all software design patterns be used everywhere. Abstraction is only useful if it improves code readability and separation of concerns.

Reason

One of the contexts where EF becomes cumbersome is unit testing. DbContext is a complicated system, with a lot of dependencies that one would have to manually mock with great effort. Therefore Microsoft came with an idea: in memory database providers. So in order to test anything, you just use an in memory database and be done with it.

Note that on Microsoft pages this method of testing is now marked with "not recommended". Also note that even in those examples, EF is abstracted by repositories.

While in memory database tests work, they add several issues that are not easy to address:

  • setting up an in memory DbContext requires all of the dependencies to existing entities
  • setting up and starting the memory database for each test is slow
  • in order to get valid database output you need to set up a lot more than what you want to atomically test

Therefore, what ends up happening is that people set up everything in the database within a "helper" method, then create tests that start with this inscrutable and complex method to test even the smallest functionality. Any code that contains EF code will be untestable without this setup.

So one reason to use repositories is to move the testing abstraction above DbContext. Now you don't need a database at all, just a repository mock. Then test your repo itself in integration tests using a real database. The in memory database is very close to a real database, but it is slightly different, too.

Another reason, which I admit I've rarely seen be of actual value in real life, is that you might want to change the way you access the data. Maybe you want to change to NoSql, or some memory distributed cache system. Or, which is much more likely, you started with a database structure, perhaps a monolithic database, and now you want to refactor it into multiple databases with different table structures. Let me tell you right off the bat that this will be IMPOSSIBLE without repositories.

And specific to Entity Framework, the entities that you get are active records, mapped to the database. You make a change in one and save the changes for another and you suddenly get the first entity updated in the db, too. Or maybe you don't, because you didn't include something, or the context has changed.

The proponents of EF always hype the tracking of entities as a very positive thing. Let's say you get an entity from the database, you then do some business, then you update the entity and save it. With a repo you would get the data, then do business, then get the data again in order to perform a little update. EF would keep it in memory, know it wasn't updated before your change, so it would never read it twice. That's true. They are describing a memory cache for the database that is somehow aware of database changes and keeps track of everything you handle from the database, unless instructed otherwise, bidirectionally maps database entries to complex C# entities and tracks changes back and forth, while being deeply embedded in the business code. Personally, I believe this plethora of responsibilities and lack of separation of concerns is a lot more damaging than any performance gained by using it. Besides, with some initial effort, all that functionality can still be abstracted in a repository, or maybe even another layer of memory caching for a repository, while keeping clear borders between business, caching and data access.

In fact, the actual difficulty in all of this is determining the borders between systems that should have separate concerns. For example, one can gain a lot of performance by moving filtering logic to stored procedures in the database, but that loses testability and readability of the algorithm used. The opposite, moving all logic to code, using EF or some other mechanism, is less performant and sometimes unfeasible. Or where is the point where data entities become business entities (see the example above with Department and Person)?

Perhaps the best strategy is to first define these borders, then decide on which technology and design are going to fit into that.

My conclusion

I believe that service and repository abstractions should always be used, even if the repository is using Entity Framework or other ORM underneath. It all boils down to separation of concerns. I would never consider Entity Framework a useful software abstraction since it comes with so much baggage, therefore a repository much be used to abstract it in code. EF is a useful abstraction, but for database access, not in software.

My philosophy of software writing is that you start with application requirements, you create components for those requirements and abstract any lower level functionality with interfaces. You then repeat the process at the next level, always making sure the code is readable and it doesn't require understanding of the components using or the ones used at the current level. If that's not the case, you've separated concerns badly. Therefore, as no business application ever had requirements to use a specific database or ORM, the data layer abstraction should hide all knowledge of those.

What does business want? A filtered list of people? var people = service.GetFilteredListOfPeople(filter); nothing less, nothing more. and the service method would just do return mapPeople(repo.GetFilteredListOfPeople(mappedFilter)); again nothing less or more. How the repo gets the people, saves the people or does anything else is not the concern of the service. You want caching, then implement some caching mechanism that implements IPeopleRepository and has a dependency on IPeopleRepository. You want mapping, implement the correct IMapper interfaces. And so on.

I hope I wasn't overly verbose in this article. I specifically kept code examples out of it, since this is more of a conceptual issue, not a software one. Entity Framework may be the target of most of my complaints here, but this applies to any system that magically helps you in small things, but breaks the important ones.

Hope that helps!

A few years ago I wrote about this, but in less detail. Here is a more refined version of the same idea.

Intro

Unit tests are both boon and bane to developers. They allow quick testing of functionality, readable examples of use, fast experimentation of scenarios for just the components involved. But they also can become messy, need maintenance and update with every code change and, when done lazily, can't hide bugs rather than reveal them.

I think the reason unit testing is so difficult is because it's associated with testing, something other than code writing, and also that unit tests are written in a way opposite most other code we write.

In this post I will give you a simple pattern of writing unit tests that will enhance all the benefits, while eliminating most of the cognitive dissonance with normal code. Unit tests will remain readable and flexible, while reducing duplicate code and adding no extra dependencies.

How to unit test

But first, let's define a good unit test suite.

To properly test a class, it has to be written in a certain way. In this post we will cover classes using constructor injection for dependencies, which is my recommended way of doing dependency injection.

Then, in order to test it, we need to:

  • cover positive scenarios - when the class does what it's supposed to do, with various combinations of setup and input parameters to cover the whole functionality
  • cover negative scenarios - when the class fails in the correct way when the setup or input parameters are wrong
  • mock all external dependencies
  • keep all of the test setup, action and assertion in the same test (what is normally called the Arrange-Act-Assert structure)

But that's easier said than done, because it also implies:

  • setting up the same dependencies for every test, thus copying and pasting a lot of code
  • setting up very similar scenarios, with just one change between two tests, again repeating a lot of code
  • generalizing and encapsulating nothing, which is what a dev would normally do in all of their code
  • writing a lot of negative cases for few positive cases, which feels like having more testing code than functional code
  • having to update all of these tests for every change to the tested class

Who loves that?

Solution

The solution is to use the builder software pattern to create fluid, flexible and readable tests in the Arrange-Act-Assert structure, while encapsulating setup code in a class complementing the unit test suite for a specific service. I call this the MockManager pattern.

Let's start with a simple example:

// the tested class
public class Calculator
{
    private readonly ITokenParser tokenParser;
    private readonly IMathOperationFactory operationFactory;
    private readonly ICache cache;
    private readonly ILogger logger;

    public Calculator(
        ITokenParser tokenParser,
        IMathOperationFactory operationFactory,
        ICache cache,
        ILogger logger)
    {
        this.tokenParser = tokenParser;
        this.operationFactory = operationFactory;
        this.cache = cache;
        this.logger = logger;
    }

    public int Calculate(string input)
    {
        var result = cache.Get(input);
        if (result.HasValue)
        {
            logger.LogInformation("from cache");
            return result.Value;
        }
        var tokens = tokenParser.Parse(input);
        IOperation operation = null;
        foreach(var token in tokens)
        {
            if (operation is null)
            {
                operation = operationFactory.GetOperation(token.OperationType);
                continue;
            }
            if (result is null)
            {
                result = token.Value;
                continue;
            }
            else
            {
                if (result is null)
                {
                    throw new InvalidOperationException("Could not calculate result");
                }
                result = operation.Execute(result.Value, token.Value);
                operation = null;
            }
        }
        cache.Set(input, result.Value);
        logger.LogInformation("from operation");
        return result.Value;
    }
}

This is a calculator, as is tradition. It receives a string and returns an integer value. It also caches the result for a specific input, and logs some stuff. The actual operations are being abstracted by IMathOperationFactory and the input string is translated into tokens by an ITokenParser. Don't worry, this is not a real class, just an example. Let's look at a "traditional" test:

[TestMethod]
public void Calculate_AdditionWorks()
{
    // Arrange
    var tokenParserMock = new Mock<ITokenParser>();
    tokenParserMock
        .Setup(m => m.Parse(It.IsAny<string>()))
        .Returns(
            new List<CalculatorToken> {
                CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1)
            }
        );

    var mathOperationFactoryMock = new Mock<IMathOperationFactory>();

    var operationMock = new Mock<IOperation>();
    operationMock
        .Setup(m => m.Execute(1, 1))
        .Returns(2);

    mathOperationFactoryMock
        .Setup(m => m.GetOperation(OperationType.Add))
        .Returns(operationMock.Object);

    var cacheMock = new Mock<ICache>();
    var loggerMock = new Mock<ILogger>();

    var service = new Calculator(
        tokenParserMock.Object,
        mathOperationFactoryMock.Object,
        cacheMock.Object,
        loggerMock.Object);

    // Act
    service.Calculate("");

    //Assert
    mathOperationFactoryMock
        .Verify(m => m.GetOperation(OperationType.Add), Times.Once);
    operationMock
        .Verify(m => m.Execute(1, 1), Times.Once);
}

Let's unpack it a little. We had to declare a mock for every constructor dependency, even if we don't actually care about the logger or the cache, for example. We also had to set up a mock method that returns another mock, in the case of the operation factory.

In this particular test we wrote mostly setup, one line of Act and two lines of Assert. Moreover, if we want to test how the cache works inside the class we would have to copy paste the entire thing and just change the way we setup the cache mock.

And there are the negative tests to consider. I've seen many a negative test doing something like: "setup just what is supposed to fail. test that it fails", which introduces a lot of problems, mainly because it might fail for completely different reasons and most of the time these tests are following the internal implementation of the class rather than its requirements. A proper negative test is actually a fully positive test with just one wrong condition. Not the case here, for simplicity.

So, without further ado, here is the same test, but with a MockManager:

[TestMethod]
public void Calculate_AdditionWorks_MockManager()
{
    // Arrange
    var mockManager = new CalculatorMockManager()
        .WithParsedTokens(new List<CalculatorToken> {
            CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1)
        })
        .WithOperation(OperationType.Add, 1, 1, 2);

    var service = mockManager.GetService();

    // Act
    service.Calculate("");

    //Assert
    mockManager
        .VerifyOperationExecute(OperationType.Add, 1, 1, Times.Once);
}

Unpacking, there is no mention of cache or logger, because we don't need any setup there. Everything is packed and readable. Copy pasting this and changing a few parameters or some lines is no longer ugly. There are three methods executed in Arrange, one in Act and one in Assert. Only the nitty gritty mocking details are abstracted away: there is no mention of the Moq framework here. In fact, this test would look the same regardless of the mocking framework one decides to use.

Let's take a look at the MockManager class. Now this will appear complicated, but remember that we only write this once and use it many times. The whole complexity of the class is there to make unit tests readable by humans, easily to understand, update and maintain.

public class CalculatorMockManager
{
    private readonly Dictionary<OperationType,Mock<IOperation>> operationMocks = new();

    public Mock<ITokenParser> TokenParserMock { get; } = new();
    public Mock<IMathOperationFactory> MathOperationFactoryMock { get; } = new();
    public Mock<ICache> CacheMock { get; } = new();
    public Mock<ILogger> LoggerMock { get; } = new();

    public CalculatorMockManager WithParsedTokens(List<CalculatorToken> tokens)
    {
        TokenParserMock
            .Setup(m => m.Parse(It.IsAny<string>()))
            .Returns(
                new List<CalculatorToken> {
                    CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1)
                }
            );
        return this;
    }

    public CalculatorMockManager WithOperation(OperationType operationType, int v1, int v2, int result)
    {
        var operationMock = new Mock<IOperation>();
        operationMock
            .Setup(m => m.Execute(v1, v2))
            .Returns(result);

        MathOperationFactoryMock
            .Setup(m => m.GetOperation(operationType))
            .Returns(operationMock.Object);

        operationMocks[operationType] = operationMock;

        return this;
    }

    public Calculator GetService()
    {
        return new Calculator(
                TokenParserMock.Object,
                MathOperationFactoryMock.Object,
                CacheMock.Object,
                LoggerMock.Object
            );
    }

    public CalculatorMockManager VerifyOperationExecute(OperationType operationType, int v1, int v2, Func<Times> times)
    {
        MathOperationFactoryMock
            .Verify(m => m.GetOperation(operationType), Times.AtLeastOnce);
        var operationMock = operationMocks[operationType];
        operationMock
            .Verify(m => m.Execute(v1, v2), times);
        return this;
    }
}

All of the required mocks for the test class are declared as public properties, allowing any customization of a unit test. There is a GetService method, which will always return an instance of the tested class, with all of the dependencies fully mocked. Then there are With* methods which atomically set up various scenarios and always return the mock manager, so that they can be chained. You can also have specific methods for assertion, although in most cases you will be comparing some output with an expected value, so these are here just to abstract away the Verify method of the Moq framework.

Conclusion

This pattern now aligns test writing with code writing:

  • abstract the things you don't care about in any context
  • write once and use many times
  • humanly readable, self documenting code
  • small methods with low cyclomatic complexity
  • intuitive code writing

Writing a unit test now is trivial and consistent:

  1. instantiate the mock manager of the class you want to test (or write one based on the steps above)
  2. compose specific scenarios for the test (with auto complete for existing already covered scenario steps)
  3. execute the method you want to test with test parameters
  4. check everything is as expected

The abstraction doesn't stop at the mocking framework. The same pattern can be applied in every programming language! The mock manager construct will be very different for TypeScript or JavaScript or something else, but the unit test would pretty much look the same way.

Hope this helps!

and has 0 comments

  I occasionally try to add to my list books that have won literary awards, so that maybe I start to get what writing masterpieces are about. Unfortunately, most of the time I get to read obtuse and pompous works which have reached the top due to some alignment of agenda or talking about something fashionable. I guess this is somewhere in between.

  The Essex Serpent is certainly not a bad book, but it is boring as fuck. Imagine a gothic novel set in the 19th century where the focus is a giant serpent somewhere in Essex. A science leaning recent widow (coming from an abusive relationship, no less) is fascinated by the possibility of this creature. Around her orbit a socialist maid and good friend, a handsome priest dedicated to his terminally ill wife and a talented but unattractive surgeon who has a crush on the widow. Women kind of win, men sort of lose, but with some dignity. That's it in a nutshell.

  Now imagine that at every turn of phrase, EVERYTHING is being taken into focus: what people have eaten, eating and what they thought about it, what people have said, felt, felt that someone else feels or thinks or about what they said, colors, smells, sounds, social norms, romantic tension, you name it, it's there.

  I cannot believe many writers would have the talent to write like Sarah Perry, but I feel there are even more who wouldn't ever want to. You have to dedicate attention and effort to dig out the meaning of every sentence and then have the memory and fortitude to weave that meaning into the story that you think the author is trying to tell. There is a saying you shouldn't walk faster than what is needed to look around, but there is a difference between looking around and combing the plane of experience for every single thing. 

  Personally, I gave up after a quarter of the book, when I realized I didn't care about the characters, the world, the time or even the giant creature. This is categorized as fiction, but it's just a slightly dramatic historical tale that just happens to not be about people who have actually existed, although it is inspired by true events.

and has 0 comments

  Imagine a Western with guns and outlaws and the like, but the world feels like in the times of the Roman Empire, there is magic and technology (fueled by magic) as well as characters that without being exactly the same, are clearly inspired by dwarves and dark elves.

  The Incorruptibles is seen through the eyes of a dwarf. He and his gun slinging human partner partner are caught in the machinations of rich and powerful Ruman nobles while being attacked by dark elf-like creatures. The story is dark and while it provides a kind of happy ending, it's a gritty tale.

  I've previously read a two novellas anthology from John Hornor Jacobs and I felt a similar vibe there: he draws inspiration from history and the worlds and characters of other writers and make them his own, blending them with a lot of creativity. However, the stories are a bit slow and not always enjoyable. They are gripping, though.

  I don't know what to feel about this series. I liked the book and it could be considered stand-alone, but it opened a lot of avenues for new stories and there was a lot of foreshadowing and world building, so it does feel a little incomplete without the rest of the (hi)story. However, I am not sure I want to invest in it, although I am tempted.

  Bottom line: a gritty magical Western hybrid. It was good.

and has 0 comments

  I am writing this during the second tour of presidential elections, and as the first one has just been annulled. Yes, you read that right, it's a ridiculous situation, but it gets much much worse. After the initial shock of seeing Calin Georgescu clearly winning the first tour of the elections while no one seemed to know who he was, the reaction of people, authorities, media, personalities in culture and science - everyone that supposedly should be steering the country in the right direction - has been a hysterical denial of reality. But in order to understand what the hell is going on, you have to start from the beginning, with the small details that got lost in the media coverage. So bear with me.

  I will be talking about elections, but in the context of this post I focus on presidential elections, not the parliamentary ones, unless specifically mentioned.

  And so it begins

  The first indication that something was amiss was when people started to receive notices and fines from the authorities for posting things on Facebook during the election period, when campaigning is prohibited. This came as a surprise to many people who don't understand how modern social media works, because they had felt their online presence was kind of anonymous, at least protected by that level of unreality that feels natural in the online. How can things happening on the Internet affect your real life? This may seem as a small off topic detail, but it becomes relevant later.

  Then the first tour of the elections came and people were shocked to see that Calin Georgescu, an independent candidate no one seemed to know anything about and who had been predicted to get at most 5% of the votes, got instead close to 25%, giving him a comfortable lead compared to the others. Worse, this guy had clear pro Russian sympathies and maybe even legionary ones (this being a political movement from 70 years ago which was pro Nazi). How can one be for both Russia and Nazism is beyond me, but I digress. And it gets worse, because there were several extremist parties with their own candidates and if you add them all together with Georgescu's votes you get close to 40%. How did this happen?

  The favorite in the presidential race was the current Prime Minister of Romania, leader of the strongest party in the country, Marcel Ciolacu. He got third place, almost tied with the liberal leaning female candidate Elena Lasconi. Another shock, because no one expected one to lose so badly and the other to win so many votes. We are talking about my country, a place that has never had a female president and any woman in power so far has been some guy's puppet. I want to believe Romanians are capable of accepting a female president, but it's unlikely.

   Reaction

  To see the reaction to these events was both entertaining and terrifying. Being politically naive, I was initially happy, because while I hold almost no common views with the extremist discourse of the winner, I wanted to see people shocked out of their complacency, forced to think and consider the implications of their action and inaction. I wanted the arrogant country authorities and people of influence to get jolted into at least pretending to do their job. I wanted the "you have to vote no matter what, even if you're not represented by any candidate" mob to eat their words. Just like the Colectiv situation ten years ago, I was hoping against hope that this will be a drive for positive change. But again, I let hope guide my thoughts, to no positive result. What was awakened was the collective mindless monster of the populace and nothing more.

  In order for a relatively unknown person to win the elections there were several institutions that had to have failed utterly in their work: election officials, security services (which were giving fines for Facebook posts just a few days before), counter candidates (one of which was Prime Minister), their campaign engines and all the government mechanisms they controlled, the media (whether controlled or not), sociologists gauging the people's choices, the statisticians creating the polls and interpreting them (both before elections AND at the exit polls). In other words: politics, authority, media and science failed completely and irrevocably.

  The general reaction to this was panic. Not "well, I messed up, let's fix this", which I had hoped, but "I couldn't have messed up this much, something else is to blame". In a matter of days everyone rallied to... save their asses. Election authorities approved recounting of votes based on a complaint raised by a less than 1% candidate, after 30 years of refusing such things with a lot more evidence of fraud. Media was flooded with exposés of the evil candidate Georgescu, somehow overly religious, misogynist, pro Russian, Nazi and legionary at the same time, financed by shadowy forces connected to Russia and maybe China, supported by priests in the backward churches outside the cities and TikTok influencers, a true Rasputin. Talking heads switched their discourse from who should have seen this coming to how defenseless Romanian people have been manipulated by the unstoppable forces of doom scrolling on social media. Authorities got into action to determine the outside influences that had caused this, against their best efforts. The highest Romanian court started deliberating based on all of this new information. People got into the street spontaneously and peacefully demonstrating for democracy and European identity. Just today police started to raid "extremists" that posted images of weaponry on social media - for the first time I've seen this.

  I call bullshit!

  Just days before the annulment of the election tour, the head of the same institution that did it said he sees no significant changes in the count of the votes. In fact, this is not even the reason of the annulment, but the "declassified" documents coming from the security services who now, suddenly, had evidence of external influence of the elections and "continuing cybernetic attacks". Well, duh! Cybernetic attacks, by their nature, never end.

  But while declaring no campaign finances and clearly having someone with a lot of money support his campaigning, while publishing ultra professional high res videos on media platforms, some mimicking the ones Putin did, but tailored towards Romanian traditional sensibilities, while showing a public presence that people just don't have without a lot of preparing and training, Georgescu did nothing provenly illegal, so all the votes coming his way were sincere, regardless of how misguided. To protect democracy, government institutions just decided, together with the media, young people in the streets chanting for democracy and European freedom and old people talking in scared high pitched voices in the park, that elections just didn't yield the correct result, so we must redo them.

  I can't imagine better results for shadowy anti-democratic forces than this! On one side, a complete failure of democratic institutions, both before and after the fact, as well as the kneejerk reaction of people who should have known better. On the other side, a large disillusioned portion of the populace just having their choice forcefully eliminated, like they don't exist. Not only they, but also the pro liberal Lasconi supporters, who will now lose any chance of winning the presidency. In the end, the worst result for most of the electorate, a terrible long lasting blow to democracy and trust in authorities in general and a higher polarization between "city people" and "countryside people".

  Ignoring 40% of the population won't make them go away, you know. And they won't just die off and leave their smarter and more educated children behind, instead just younger, even less educated by experience, copies of themselves, fighting for whatever random cause or belief they're manipulated into.

  Witch hunt

  There is a term called Witch hunt which applies to what is happening here. I urge you to read the Wikipedia article, because it's very revealing.

  Societies function on a very simple contract: a relatively common narrative must be maintained and some institutions are created to curate and enforce it. When that narrative is contradicted by reality, society unravels, so there are only two choices for stability: craft the narrative to be a balance between reality and the people's needs or eliminate the source of contradiction. It's that simple. The spirit of democracy is closely linked to this, as it attempts to provide the mechanisms to keep that delicate balance and stave off as much as possible the necessity for "eliminating contradiction". But when that fails, the only solution is blunt force, mob fear, fanatical clinging to the narrative, which most of the time leads to tyranny and/or atrocity. Why is it so hard to realize that the narrative has to change a little?

  In our case, the narrative is the naive and stupid comfort of authority functioning regardless of what we do, that so many communities tend to fall into. It's already a first step to autocracy, the same narrative applies there as well, in the beginning. But when it fails, people have to find a culprit and ritualistically sacrifice them. I don't know what will happen with poor Georgescu, but as I see it he will either become the dictator of Romania or be burned at the stake.

  And a lot of other things are going to fall with him: online privacy, as little as it is nowadays, honest public discourse, as little as it is allowed now - even in countries which are paragons of democracy, opportunity to choose anything unexpected. We may see even more fanatical adherence to the mythical concept of a united Europe, regardless of its state, while becoming even more afraid of technology. And this just because we obstinately refuse any opportunity to open our eyes and think for ourselves. We have people to do that for us! If he somehow wins, which I find unlikely, things would be even worse.

  Can't wait for mandatory identity checks everywhere online and a global ban on social media for under 16 and of TikTok in general, as a means to protect democracy and free speech. What a joke!

  My thoughts

  Well, obviously all of these are my thoughts, but as a conclusion, I am terrified. Not by Russia, Europe, China or the United States, but by the people of my own country. We have lived in fear for so much time that it has become part of our DNA. We are controlled by it. Make Romanians afraid and you can make them do anything, say anything, think anything. Maybe that's true for everybody.

  I was disgusted to see how the Russian boogie man was resurrected once more to justify unilateral reactions by clueless authorities. What are you going to do? Fight Russia? Send security to arrest the priests who campaigned in churches before and during the election in the countryside, probably for heavy fees? How ironic is that Romanians are crying to the skies for security forces to validate and protect democracy. I guess in 30 years one forgets everything.

  I was happy to think the veil of illusion will be lifted from the eyes of Romanians, but what happened instead is that some of my own illusions have been blown away. Of course, I don't like it and I feel fear. That's natural. What we do with it is what counts. I wrote a blog post. Yay, me!

  To people who think leaving Romania will somehow solve the problem, it does not. But when freezing and fighting seems to not do anything, what is left but to flee? I get it.

  Personally, I want my country to get through this and become stronger for it, but the idea of Romania is just as much an illusion as anything else. And we've already proven that we can ignore every reality and forget any past, unless it suits us otherwise. This is not the country of my childhood or of my youth and it will continue to change in the future. But the attachment is still there and I wish it well.

and has 0 comments

  David Brin was 30 when he published Sundiver in 1980, but the book, his first, feels much older, almost van Vogtian. The writing style and subject matter further strengthen the feeling. Imagine a bunch of humans and aliens in the future, diving into the Sun to communicate with a newly discovered race of intelligent beings there. A tree alien, an uplifted chimp and others are part of the expedition.

  All the tropes of sci-fi pulp are there: a youngish protagonist of the same age as the author, romance, mystery and its deductive solving as well as its revealing in elaborate group discussions, the belief in the power of meditation and trance to improve the mind, people having Erich von Däniken as the prophet of the origin of the human race, lasers, fist fights, hints of colonialism, and so on and so on. The book is very entertaining, but it's spectacularly outdated. I guess the lasers and the Däniken references place it after 1960, but ignoring that, one could believe it was written in the '40s.

  There are some ingenious ideas in the book, though. In this universe, intelligence is believed to never have been evolved by itself, instead "patron races" uplift existing native animals to intelligence, generating a complex web of patrons and clients. Not humans, though. They have been partially uplifted then left to their own devices, thus placing them in a dubious middle of the hierarchy, while kept away from the somewhat monolithic culture of the galaxy. Humans are both exotic and quaint, ignorant and arrogant, daring to know things they figured out for themselves rather than spoon fed by "the Library".

   I don't think I will read more of this series, but I might read more from Brin, whenever I feel the need to go classical without diving into a time compression bubble.