Jcg.Application.Core.Optics 1.0.4

dotnet add package Jcg.Application.Core.Optics --version 1.0.4
                    
NuGet\Install-Package Jcg.Application.Core.Optics -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jcg.Application.Core.Optics" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jcg.Application.Core.Optics" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Jcg.Application.Core.Optics" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jcg.Application.Core.Optics --version 1.0.4
                    
#r "nuget: Jcg.Application.Core.Optics, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jcg.Application.Core.Optics@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jcg.Application.Core.Optics&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Jcg.Application.Core.Optics&version=1.0.4
                    
Install as a Cake Tool

Overview

A C# Implementation of Functional Lenses, but adapted to Object-Oriented Programming.

License

MIT

Dependencies

⦁ Net Standard 2.1

Motivation

I always find complexity when updating nested objects, especially when dealing with nested collections. Common OOP Patterns like the Builder Pattern and some other tricks work, but they are not good enough because:

  1. They cause coupling
  2. Writing builders consume time.

Manipulating data structures can be challenging, but Functional Programming offers an elegant solution through the use of Lenses. A Lens enables you to focus on a specific part of a data structure, allowing you to get or set values without having to consider the entire structure.

This library applies some of those concepts to the object-oriented programming world, allowing you to update objects with a lens that is generic and composable.

It also works great with immutable records, as demonstrated in the tests.

You can:

  1. Update an object property.
  2. Update a nested object property.
  3. Update a collection of objects.
  4. Update a nested collection of objects.
  5. Update a property on an object inside a nested collection

There is no limit to the depth of the nested objects.

Examples

Basic Example (single lens, no nesting)

Customer customer = new CustomerBuilder().Build();

// Customer is a record so we can use non-destructive mutation to create a new
// instance with a different name
customer = customer with
{
    CustomerName = "Tom"
};

// {
//   "CustomerId": "f7a9b2c1-4d3e-4a5b-8c2d-1e2f3a4b5c6",
//   "CustomerName": "Tom",
//   "ContactInfo": {...}
// }

// Using named parameters for clarity
ILens<Customer, string> customerNameLens = customer.CreateLens(
    getter: c => c.CustomerName,
    setter: (c, name) => c with { CustomerName = name }
);

// Initially, the Lens.RootObject is the customer instance.
Assert.Same(customer, customerNameLens.RootObject);

// You can set the name using the value property, like it was a simple {get; set;} property.
customerNameLens.Value = "George";

// Under the hood, this creates a new instance of the customer available through the Lens.RootObject.
Assert.NotSame(customer, customerNameLens.RootObject);

// As expected, that new instance has the updated name.
Assert.Equal("George", customerNameLens.Value);

Example 2: Nested lens focusing on a deeply nested property

Customer customerObject = new CustomerBuilder()
            .Build();
        
        // Customer is a record so we can use non-destructive mutation to create a new
        // instance with a different name
        customerObject = customerObject with
        {
            ContactInfo = customerObject.ContactInfo with
            {
                Address = customerObject.ContactInfo.Address with
                {
                    Street = "Market Street"
                }
            }
        };
        
        // {
        //   "CustomerId": "...",
        //   "CustomerName": "...",
        //   "ContactInfo": {
        //     "Address": {
        //       "Street": "Market Street" <-- we will update this
        //     }
        //   }
        // }
        
        // Using named parameters for clarity
        ILens<Customer, string> customerContactAddressStreetLens =
            customerObject
                // Create a lens that focuses on the ContactInfo property
                .CreateLens(
                    getter: customer => customer.ContactInfo,
                    setter: (customer, contactInfo) => customer with { ContactInfo = contactInfo }
                )
                // Compose the lens to focus on the ContactInfo.Address property
                .FocusLens(
                    getter: contactInfo => contactInfo.Address,
                    setter: (contactInfo, address) => contactInfo with { Address = address }
                )
                // Compose the lens to focus on the Address.Street property
                .FocusLens(
                    getter: address => address.Street,
                    setter: (address, street) => address with { Street = street }
                );
        
        // Lets change the street name using the lens
        customerContactAddressStreetLens.Value = "Elm Street";
        
        // Now, the lens.RootObject has the changed value
        Assert.Equal("Elm Street", customerContactAddressStreetLens.RootObject.ContactInfo.Address.Street);
        
        // Keep in mind the original object was not modified
        Assert.Equal("Market Street", customerObject.ContactInfo.Address.Street);

Example 3: Nested lens focusing on a collection of objects

Customer customerObject = new CustomerBuilder()
    .AddOrder(out var order1)
    .AddOrderItem(order1, out var orderItem1)
    .Build();

// {
//   "CustomerId": "...",
//   "CustomerName": "...",
//   "ContactInfo": { ... },
//   "Orders": [
//     {
//       "OrderId": "o1a2b3c4-...",
//       "OrderDate": "2024-06-10T00:00:00Z",
//       "Items": [
//         {
//           "ProductName": "Sample Product",
//           "Quantity": 1,      // <-- We will update this
//           "UnitPrice": 9.99   // <-- We will also update this
//         }
//       ]
//     }
//   ]
// }

// Focuses on Order1
var customerOrder1Lens = customerObject.CreateLens(
    getter: customer => customer.Orders.First(o => o.OrderId == order1.OrderId),
    setter: (customer, order) => customer with
    {
        Orders = customer.Orders.Select(o => o.OrderId == order.OrderId ? order : o)
    }
);

// Focuses on Order1.Item1
var order1Item1Lens = customerOrder1Lens.FocusLens(
    getter: order => order.Items.First(i => i.ProductName == orderItem1.ProductName),
    setter: (order, orderItem) => order with
    {
        Items = order.Items.Select(i => i.ProductName == orderItem.ProductName ? orderItem : i)
    }
);

// Compose lenses to focus on multiple properties
var order1Item1QuantityLens = order1Item1Lens.FocusLens(
    getter: item => item.Quantity,
    setter: (item, quantity) => item with { Quantity = quantity }
);

var order1Item1UnitPriceLens = order1Item1Lens.FocusLens(
    getter: item => item.Price,
    setter: (item, price) => item with { Price = price }
);

// Update the quantity to 300
order1Item1QuantityLens.Value = 300;

// Update the price to 99.99
order1Item1UnitPriceLens.Value = 99.99m;

var resultingItem = customerOrder1Lens.RootObject
    .Orders.First(o => o.OrderId == order1.OrderId)
    .Items.First(i => i.ProductName == orderItem1.ProductName);

Assert.Equal(300, resultingItem.Quantity);
Assert.Equal(99.99m, resultingItem.Price);

Other Examples

You can leverage a lens that focuses on a collection to modify that collection.

There are more use cases, you can see in the following test class

For instance:

  • Updating and reading properties.
  • Adding items to a deeply nested collection.
  • Removing items from a deeply nested collection.
  • Updating a particular item from a deeply nested collection.

Credits

Author: Julio C. Cachay. Chattanooga, TN, USA.

This library is inspired by the concept of lenses in functional programming, and in the optics.ts library

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 127 8/3/2025
1.0.3 102 8/3/2025
1.0.2 103 8/3/2025
1.0.0 76 8/3/2025