Search code examples
c#.netazureazure-table-storageclean-architecture

.NET how resolve needed genericity between layers


I have a question that may be quite trivial for some. I'm making an application in .NET where I'm trying to have split layers. Application (abstraction only) and Infrstructure (implementation). My problem is that I want to use Azure TableClient to store data from an Azure storage table. But the method to store requires an implementation of the ITableEntity interface on that entity. I just can't declare this in the application layer for the storage method because I would make a dependency on Azure.Data.Tables and I don't want that. I don't really know how to write my interface with this and then "overlay" it I guess.

Application:

public interface ITableStorage
{
    public Task SaveItemsAsync<T>(IEnumerable<T> entites, string tableName = nameof(T)) where T : ITableEntity; // where T : ITableEntity is problem
}

Infrastructure:

public async Task SaveItemsAsync<T>(IEnumerable<T> entites, string tableName = nameof(T)) where T : ITableEntity
{
    var tableClient = _tableServiceClient.GetTableClient(tableName);

    ......
}

Thanks in advance for the advice.


Solution

  • On solution is to use multiple representations of your "entity". The application uses the canonical representation that includes methods, logic etc. This is then converted to a type suitable for storage in Azure, or something else that stores entities.

    This does require multiple representations of the same data, but it does add some flexibility. If you change the data model you can keep the legacy Azure representation so you can read old data, while using a new entity type when saving. This is similar to the concept of Data Transfer Object.

    To do the conversion between types there are a few options:

    1. use a switch over the type
    2. Use the visitor pattern. This has the advantage that you can be forced to create an azure-type for every application-type. So less chance of forgetting.

    You likely want some common interface for entities on the application side, so the compiler can help to ensure you are only saving objects that are designed to be saved.

    A very simple example to illustrate the idea:

    public interface IMyEntity{}
    public class MyEntity {};
    public interface IEntityStorage{
        void Save(IMyEntity entity);
    }
    
    public class MyEntityAzure : ITableEntity {
        public MyEntityAzure(MyEntity entity){
            // copy properties etc.
        }
        public MyEntity ToModel(){
           // Convert back to application entity
        }
    }
    public class AzureStorage : IEntityStorage{
        private AzureStorage storage;
        public void Save(IMyEntity entity){
            switch(entity){
                case MyEntity  myEntity:
                    storage.Save(new MyEntityAzure(myEntity));
                    break;
                // All other entity classes
             }
         }
    }