Coder Social home page Coder Social logo

cacheflow's Introduction

CacheFlow

Automates cache creation and invalidation with reference handling using Metalama.

How does this work?

This project is not intended for public use for now, and because it uses Metelama Factory you are required to have your own Metalama API Key. CacheFlow provides with two interceptors: one for cache creation and another for cache invalidation.

Firstly, you have to reference NuGet package into your project or library, and it will automatically locate all Repositories. You should also include the next line inside your Program.cs.

builder.Services.AddCacheService();

Repository classes should be located inside the Repository folder and have [Type]Repository name. The [Type] might be used as a hash name.

By default, CacheFlow is not going to use reference handling, and if you want to enable it go to the Reference Handler section.

Let's say you have a simple OrderRepository with CRUD operations.

Order model has the next properties

public sealed class Order
{
    public required Guid Id { get; init; }
    
    public required OrderStatus Status { get; set; }

    public required Guid BuyerId { get; init; }

    public required Address Address { get; init; }
    
    public required IEnumerable<OrderItem> OrderItems { get; init; }
}
    public async Task<IEnumerable<Order>> GetAllByBuyerIdAsync(Guid buyerId, CancellationToken cancellationToken)
    {
        bool isNotEmpty = await _orderingDbContext.Orders.AnyAsync(cancellationToken);
        if (!isNotEmpty)
        {
            return Enumerable.Empty<Order>();
        }

        var orders = await _orderingDbContext.Orders
            .Where(order => order.BuyerId == buyerId)
            .Include(order => order.OrderItems)
            .Include(order => order.Address)
            .ToListAsync(cancellationToken);
        
        return orders;
    }
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        var order = await _orderingDbContext.Orders
            .Include(order => order.OrderItems)
            .Include(order => order.Address)
            .FirstOrDefaultAsync(order => order.Id == id, cancellationToken);
        
        return order;
    }

Cache interceptor is looking for the methods where the method name contains Get string, so the following methods are going to be intercepted for cache creation. The return type is going to be used as a Hash Key. If the return type is IEnumerable<T>, the T parameter is the Hash Key. The first parameter is going to be used as a key for a cache. And since the reference handling is disabled, the generated code should be familiar to the most devs.

public async Task<IEnumerable<Order>> GetAllByBuyerIdAsync(
      Guid buyerId,
      CancellationToken cancellationToken)
    {
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(0, 1);
      interpolatedStringHandler.AppendFormatted<Guid>(buyerId);
      string key = interpolatedStringHandler.ToStringAndClear(); // cache key
      Type resultType = typeof (IEnumerable<Order>);
      string hashKey = resultType.GetGenericArguments()[0].Name; // hash name
      object result = await this._cacheService.HashGetAsync(hashKey, key, resultType);
      if (result != null) // if there is a cached value, return without creating cache
      {
        // ISSUE: reference to a compiler-generated field
        if (OrderRepository.<>o__2.<>p__0 == null)
        {
          // ISSUE: reference to a compiler-generated field
          OrderRepository.<>o__2.<>p__0 = CallSite<Func<CallSite, object, IEnumerable<Order>>>.Create(Binder.Convert(CSharpBinderFlags.ConvertExplicit, typeof (IEnumerable<Order>), typeof (OrderRepository)));
        }
        // ISSUE: reference to a compiler-generated field
        // ISSUE: reference to a compiler-generated field
        return OrderRepository.<>o__2.<>p__0.Target((CallSite) OrderRepository.<>o__2.<>p__0, result);
      }
      IEnumerable<Order> methodResponse = await this.GetAllByBuyerIdAsync_Source(buyerId, cancellationToken);
      if (methodResponse == null)
        return methodResponse;
      int num = await this._cacheService.HashSetAsync(hashKey, key, JsonConvert.SerializeObject((object) methodResponse)) ? 1 : 0;
      return methodResponse;
    }

    private async Task<IEnumerable<Order>> GetAllByBuyerIdAsync_Source(
      Guid buyerId,
      CancellationToken cancellationToken)
    {
      bool isNotEmpty = await this._orderingDbContext.Orders.AnyAsync<Order>(cancellationToken);
      if (!isNotEmpty)
        return Enumerable.Empty<Order>();
      List<Order> orders = await this._orderingDbContext.Orders.Where<Order>((Expression<Func<Order, bool>>) (order => order.BuyerId == buyerId)).Include<Order, IEnumerable<OrderItem>>((Expression<Func<Order, IEnumerable<OrderItem>>>) (order => order.OrderItems)).Include<Order, Address>((Expression<Func<Order, Address>>) (order => order.Address)).ToListAsync<Order>(cancellationToken);
      return (IEnumerable<Order>) orders;
    }
    
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(0, 1);
      interpolatedStringHandler.AppendFormatted<Guid>(id);
      string key = interpolatedStringHandler.ToStringAndClear();
      Type resultType = typeof (Order);
      string hashKey = resultType.Name;
      object result = await this._cacheService.HashGetAsync(hashKey, key, resultType);
      if (result != null)
      {
        // ISSUE: reference to a compiler-generated field
        if (OrderRepository.<>o__4.<>p__0 == null)
        {
          // ISSUE: reference to a compiler-generated field
          OrderRepository.<>o__4.<>p__0 = CallSite<Func<CallSite, object, Order>>.Create(Binder.Convert(CSharpBinderFlags.ConvertExplicit, typeof (Order), typeof (OrderRepository)));
        }
        // ISSUE: reference to a compiler-generated field
        // ISSUE: reference to a compiler-generated field
        return OrderRepository.<>o__4.<>p__0.Target((CallSite) OrderRepository.<>o__4.<>p__0, result);
      }
      Order methodResponse = await this.GetByIdAsync_Source(id, cancellationToken);
      if (methodResponse == null)
        return methodResponse;
      int num = await this._cacheService.HashSetAsync(hashKey, key, JsonConvert.SerializeObject((object) methodResponse)) ? 1 : 0;
      return methodResponse;
    }

    private async Task<Order?> GetByIdAsync_Source(Guid id, CancellationToken cancellationToken)
    {
      Order order1 = await this._orderingDbContext.Orders.Include<Order, IEnumerable<OrderItem>>((Expression<Func<Order, IEnumerable<OrderItem>>>) (order => order.OrderItems)).Include<Order, Address>((Expression<Func<Order, Address>>) (order => order.Address)).FirstOrDefaultAsync<Order>((Expression<Func<Order, bool>>) (order => order.Id == id), cancellationToken);
      Order byIdAsyncSource = order1;
      order1 = (Order) null;
      return byIdAsyncSource;
    }

If the method name contains Create, Update or Delete, then the cache invalidation interceptor is going to decorate these methods.

    public async Task<bool> CreateAsync(Order order, CancellationToken cancellationToken)
    {
        await _orderingDbContext.Orders.AddAsync(order, cancellationToken);
        int result = await _orderingDbContext.SaveChangesAsync(cancellationToken);

        return result > 0;
    }
    public async Task<bool> DeleteByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        int result = await _orderingDbContext.Orders
            .Where(order => order.Id == id)
            .ExecuteDeleteAsync(cancellationToken);

        return result > 0;
    }

Usually, these methods do not return the full model, and because of that hash key is being retrieved based on the repository name. So if you have OrderReporisory, the hash name will be Order The cache invalidation can have two possible implementations. When the first parameter is the class object (string excluded), interceptor is going to find every Id property.

public sealed class Order
{
    public required Guid Id { get; init; }
    
    public required OrderStatus Status { get; set; }

    public required Guid BuyerId { get; init; }

    public required Address Address { get; init; }
    
    public required IEnumerable<OrderItem> OrderItems { get; init; }
}

If the property has the reference type, it's going to try to retrieve id of this object. In this case, interceptor will register Id, BuyerId, and Address.Id.

public async Task<bool> CreateAsync(Order order, CancellationToken cancellationToken)
    {
      bool methodResponse = await this.CreateAsync_Source(order, cancellationToken);
      Guid guid = order.Id;
      string parameterId = guid.ToString();
      guid = order.BuyerId;
      string id = guid.ToString();
      int num1 = await this._cacheService.HashRemoveAsync("Buyer", id).ConfigureAwait(false) ? 1 : 0; // remove buyer cache from hash with "Buyer" key
      ConfiguredTaskAwaitable<bool> configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Buyer", "all").ConfigureAwait(false);
      int num2 = await configuredTaskAwaitable ? 1 : 0;
      id = (string) null;
      guid = order.Address.Id;
      string id_1 = guid.ToString();
      configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Address", id_1).ConfigureAwait(false);
      int num3 = await configuredTaskAwaitable ? 1 : 0;
      configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Address", "all").ConfigureAwait(false);
      int num4 = await configuredTaskAwaitable ? 1 : 0;
      id_1 = (string) null;
      configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Order", parameterId).ConfigureAwait(false);
      int num5 = await configuredTaskAwaitable ? 1 : 0;
      configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Order", "all").ConfigureAwait(false);
      int num6 = await configuredTaskAwaitable ? 1 : 0;
      parameterId = (string) null;
      return methodResponse;
    }

    private async Task<bool> CreateAsync_Source(Order order, CancellationToken cancellationToken)
    {
      EntityEntry<Order> entityEntry = await this._orderingDbContext.Orders.AddAsync(order, cancellationToken);
      int result = await this._orderingDbContext.SaveChangesAsync(cancellationToken);
      return result > 0;
    }

The cache key all is being used when the return type of method is IEnumerable<T>. public async Task<IEnumerable<Order>> GetAllByBuyerIdAsync( Guid buyerId, CancellationToken cancellationToken) calling this method will create cache inside Order hash with all key, so this call this._cacheService.HashRemoveAsync("Order", "all").ConfigureAwait(false); is going to invalidate GetAllByBuyerIdAsync.

However, sometimes there is not enough information about the object.

public async Task<bool> DeleteByIdAsync(Guid id, CancellationToken cancellationToken)
   {
     bool methodResponse = await this.DeleteByIdAsync_Source(id, cancellationToken);
     ConfiguredTaskAwaitable<bool> configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Order", id.ToString()).ConfigureAwait(false);
     int num1 = await configuredTaskAwaitable ? 1 : 0;
     configuredTaskAwaitable = this._cacheService.HashRemoveAsync("Order", "all").ConfigureAwait(false);
     int num2 = await configuredTaskAwaitable ? 1 : 0;
     return methodResponse;
   }

   private async Task<bool> DeleteByIdAsync_Source(Guid id, CancellationToken cancellationToken)
   {
     int result = await this._orderingDbContext.Orders.Where<Order>((Expression<Func<Order, bool>>) (order => order.Id == id)).ExecuteDeleteAsync<Order>(cancellationToken);
     return result > 0;
   }

When you have only an id and type name from the repository name, there is not a lot of what you can do.

But is there any way to invalidate all related caches with a single id property?

Reference Invalidation

You should have noticed that CacheFlow uses Redis and in addition to that it also uses Hashes. This datatype has the scanning command, which helps to find the cache key without entering the full key. And CacheFlow can use scanning to invalidate cache. To enable reference handling, create a cs file with any name and replace all content with the next code.

using CacheFlow.Options;

[assembly: CacheOptions(UseReferenceCacheInvalidation = true)]

And that's it! Now rebuild your project.

public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        var order = await _orderingDbContext.Orders
            .Include(order => order.OrderItems)
            .Include(order => order.Address)
            .FirstOrDefaultAsync(order => order.Id == id, cancellationToken);
        
        return order;
    }

The generated code for cache creation now is going to include scanning feature, and it's also going to change how to cache key is being created.

The GetAll is going to stay the same.

public async Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(0, 1);
      interpolatedStringHandler.AppendFormatted<Guid>(id);
      string key = interpolatedStringHandler.ToStringAndClear();
      Type resultType = typeof (Order);
      string hashKey = resultType.Name;
      object result = await this._cacheService.HashScan(hashKey, "*" + key + "*", resultType); // find object using pattern
      if (result != null)
      {
        // ISSUE: reference to a compiler-generated field
        if (OrderRepository.<>o__4.<>p__0 == null)
        {
          // ISSUE: reference to a compiler-generated field
          OrderRepository.<>o__4.<>p__0 = CallSite<Func<CallSite, object, Order>>.Create(Binder.Convert(CSharpBinderFlags.ConvertExplicit, typeof (Order), typeof (OrderRepository)));
        }
        // ISSUE: reference to a compiler-generated field
        // ISSUE: reference to a compiler-generated field
        return OrderRepository.<>o__4.<>p__0.Target((CallSite) OrderRepository.<>o__4.<>p__0, result);
      }
      Order methodResponse = await this.GetByIdAsync_Source(id, cancellationToken);
      if (methodResponse == null)
        return methodResponse;
      string key_1 = key;
      Order methodResponse_1 = methodResponse;
      Address propertyValue = methodResponse_1.Address;
      string propertyId = propertyValue.Id.ToString();
      string key_2 = propertyId;
      Address methodResponse_2 = propertyValue;
      string complexKey = methodResponse_1.Id.ToString() + "-" + key_2;
      await this._cacheService.HashRemoveAllAsync("Address", "*" + propertyId + "*").ConfigureAwait(false); // Remove all occurence of id inside the "Address" hash
      int num1 = await this._cacheService.HashSetAsync("Address", complexKey, JsonConvert.SerializeObject((object) propertyValue)) ? 1 : 0; // Add the new value
      propertyValue = (Address) null;
      propertyId = (string) null;
      key_2 = (string) null;
      methodResponse_2 = (Address) null;
      complexKey = (string) null;
      IEnumerable<OrderItem> arrayProperty = methodResponse_1.OrderItems;
      int num2 = await this._cacheService.HashSetAsync("OrderItem", "all", JsonConvert.SerializeObject((object) arrayProperty)) ? 1 : 0;
      arrayProperty = (IEnumerable<OrderItem>) null;
      ICacheService cacheService = this._cacheService;
      string hashKey1 = hashKey;
      interpolatedStringHandler = new DefaultInterpolatedStringHandler(7, 4);
      interpolatedStringHandler.AppendFormatted(key_1);
      interpolatedStringHandler.AppendLiteral("-");
      interpolatedStringHandler.AppendFormatted<Guid>(methodResponse_1.BuyerId);
      interpolatedStringHandler.AppendLiteral("-");
      interpolatedStringHandler.AppendFormatted<Guid>(methodResponse_1.Address.Id);
      interpolatedStringHandler.AppendLiteral("-");
      interpolatedStringHandler.AppendFormatted(typeof (IEnumerable<OrderItem>).GetGenericArguments()[0].Name);
      interpolatedStringHandler.AppendLiteral("-all"); // key for arrays
      string stringAndClear = interpolatedStringHandler.ToStringAndClear();
      string entity = JsonConvert.SerializeObject((object) methodResponse);
      int num3 = await cacheService.HashSetAsync(hashKey1, stringAndClear, entity) ? 1 : 0;
      return methodResponse;
    }

    private async Task<Order?> GetByIdAsync_Source(Guid id, CancellationToken cancellationToken)
    {
      Order order1 = await this._orderingDbContext.Orders.Include<Order, IEnumerable<OrderItem>>((Expression<Func<Order, IEnumerable<OrderItem>>>) (order => order.OrderItems)).Include<Order, Address>((Expression<Func<Order, Address>>) (order => order.Address)).FirstOrDefaultAsync<Order>((Expression<Func<Order, bool>>) (order => order.Id == id), cancellationToken);
      Order byIdAsyncSource = order1;
      order1 = (Order) null;
      return byIdAsyncSource;
    }

Using scanning feature, CacheFlow handles most of the relationships. Mostly, it's possible because of how the cache keys are being constructed.

Each key contains the next information

  • ID of the main object
  • ID of the reference type
  • FK (BuyerId, AddressId ...)
  • -all for IEnumerable So, in the example above the generated key will look like this: <order-id>-<buyer-id>-<address-id>-OrderItem-all. When an interceptor searches for the key, it only needs one part of the key, in this case it's order's id.

Although, if you want to invalidate the address when the order is deleted, this line is a must-have.

      await this._cacheService.HashRemoveAllAsync("Address", "*" + propertyId + "*").ConfigureAwait(false); // Remove all occurence of id inside the "Address" hash
      int num1 = await this._cacheService.HashSetAsync("Address", complexKey, JsonConvert.SerializeObject((object) propertyValue)) ? 1 : 0; // add <order-id>-<address-id> inside "Address" hash.

And now, if you look at the previous methods.

public async Task<bool> CreateAsync(Order order, CancellationToken cancellationToken)
    {
      bool methodResponse = await this.CreateAsync_Source(order, cancellationToken);
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(2, 1);
      interpolatedStringHandler.AppendLiteral("*");
      interpolatedStringHandler.AppendFormatted<Guid>(order.Id);
      interpolatedStringHandler.AppendLiteral("*");
      string propertyKeyPattern = interpolatedStringHandler.ToStringAndClear();
      string propertyHashKey = "Address";
      ConfiguredTaskAwaitable configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, "*Order*").ConfigureAwait(false);
      await configuredTaskAwaitable1;
      ConfiguredTaskAwaitable<bool> configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync(propertyHashKey, "all").ConfigureAwait(false);
      int num1 = await configuredTaskAwaitable2 ? 1 : 0;
      string propertyHashKey_1 = typeof (IEnumerable<OrderItem>).GetGenericArguments()[0].Name;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey_1, propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey_1, "*Order*").ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync(propertyHashKey_1, "all").ConfigureAwait(false);
      int num2 = await configuredTaskAwaitable2 ? 1 : 0;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync("Order", propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync("Order", "all").ConfigureAwait(false);
      int num3 = await configuredTaskAwaitable2 ? 1 : 0;
      propertyKeyPattern = (string) null;
      propertyHashKey = (string) null;
      propertyHashKey_1 = (string) null;
      return methodResponse;
    }

The cache invalidation interceptor uses property types as the hash names and invalidates all occurence of the given id.

public async Task<bool> DeleteByIdAsync(Guid id, CancellationToken cancellationToken)
    {
      bool methodResponse = await this.DeleteByIdAsync_Source(id, cancellationToken);
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(2, 1);
      interpolatedStringHandler.AppendLiteral("*");
      interpolatedStringHandler.AppendFormatted<Guid>(id);
      interpolatedStringHandler.AppendLiteral("*");
      string propertyKeyPattern = interpolatedStringHandler.ToStringAndClear();
      string propertyHashKey = "Address";
      ConfiguredTaskAwaitable configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, "*Order*").ConfigureAwait(false);
      await configuredTaskAwaitable1;
      ConfiguredTaskAwaitable<bool> configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync(propertyHashKey, "all").ConfigureAwait(false);
      int num1 = await configuredTaskAwaitable2 ? 1 : 0;
      string propertyHashKey_1 = typeof (IEnumerable<OrderItem>).GetGenericArguments()[0].Name;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey_1, propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey_1, "*Order*").ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync(propertyHashKey_1, "all").ConfigureAwait(false);
      int num2 = await configuredTaskAwaitable2 ? 1 : 0;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync("Order", propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync("Order", "all").ConfigureAwait(false);
      int num3 = await configuredTaskAwaitable2 ? 1 : 0;
      propertyKeyPattern = (string) null;
      propertyHashKey = (string) null;
      propertyHashKey_1 = (string) null;
      return methodResponse;
    }

    private async Task<bool> DeleteByIdAsync_Source(Guid id, CancellationToken cancellationToken)
    {
      int result = await this._orderingDbContext.Orders.Where<Order>((Expression<Func<Order, bool>>) (order => order.Id == id)).ExecuteDeleteAsync<Order>(cancellationToken);
      return result > 0;
    }

The delete method is similar, the interceptor searches for the model inside your project and retrieves property names. And that's it!. Now, if you delete order, then address, order items (array), and the buyer is going to be invalidated as well. The same goes with a buyer, who contains the list of orders.

public async Task<bool> DeleteByIdAsync(Guid id, CancellationToken cancellationToken)
    {
      bool methodResponse = await this.DeleteByIdAsync_Source(id, cancellationToken);
      DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(2, 1);
      interpolatedStringHandler.AppendLiteral("*");
      interpolatedStringHandler.AppendFormatted<Guid>(id);
      interpolatedStringHandler.AppendLiteral("*");
      string propertyKeyPattern = interpolatedStringHandler.ToStringAndClear();
      string propertyHashKey = typeof (IEnumerable<Order>).GetGenericArguments()[0].Name;
      ConfiguredTaskAwaitable configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync(propertyHashKey, "*Buyer*").ConfigureAwait(false);
      await configuredTaskAwaitable1;
      ConfiguredTaskAwaitable<bool> configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync(propertyHashKey, "all").ConfigureAwait(false);
      int num1 = await configuredTaskAwaitable2 ? 1 : 0;
      configuredTaskAwaitable1 = this._cacheService.HashRemoveAllAsync("Buyer", propertyKeyPattern).ConfigureAwait(false);
      await configuredTaskAwaitable1;
      configuredTaskAwaitable2 = this._cacheService.HashRemoveAsync("Buyer", "all").ConfigureAwait(false);
      int num2 = await configuredTaskAwaitable2 ? 1 : 0;
      propertyKeyPattern = (string) null;
      propertyHashKey = (string) null;
      return methodResponse;
    }

When you call DeleteBuyer, the program is going to remove any occurence of the provided id inside Order hash. So, when you delete the buyer, it also deletes related order.

cacheflow's People

Contributors

mqsrr avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.