LINQ2Vodka
LINQ2Vodka

Reputation: 3036

EF entity, local file as property

I have an entity that has both general properties that are stored in database table and a reference to a local file on system disk. I want the create/replace/delete methods for this file be encapsulated in the data access layer to let other parts of the application not care of how and where it should be stored, just send a bytes stream of perform "clear" operation. At the same time, I'd like the file directory to be defined in web.config like database access parameters are.
Im my web app I use EF 5 Code First and have defined the entity like an example below:

// EF entity
public class UserImage{
    public string Description { get; set; } 
    [NotMapped]
    public LocalFile File { get; set; } 
}

// not EF entity
public class LocalFile{
    public string Name { get; set; } 
    public string LocalPath { // should return full path as dir path + file name } 
    public string Url { // should return LocalPath mapped to Url } 
    public void Set(FileStream fs) { // saves file to disk } 
    public void Clear() { // deletes file } 
}

In my approach I can account my DbContext is not only database context, but a context for both database and filesystem storage and I can provide it both with DB connection string and a local directory path on creation time. I think it should be a good practice, let me know if I'm wrong.
Now the problem: how I can know the local directory path from inside of the LocalFile or UserImage objects so they can implement LocalPath and Url properties? In other words, how some other part of the application can know what's the actual Path/Url of the LocalFile or UserImage instance? Or is there a way to provide these objects with LocalDir as they're being created inside DbContext? At last, what is the alternate way to incapsulate local storage operations within UserImage so any outed code never care how and where the file is stored?

Upvotes: 1

Views: 564

Answers (2)

Kirill Bestemyanov
Kirill Bestemyanov

Reputation: 11964

You should create interface for your file operations that will have two methods: Stream GetFile(string fileName) and void PutFile(Stream fileStream, string fileName) and implement it with concrete class that will have constructor with parameter locationPath:

public interface IFileRepository
{
    Stream GetFile(string fileName);
    void PutFile(Stream fileStream, string fileName);
}

public class FileRepository
{
    private readonly string localPath;
    public FileRepository(string localPath)
    {
        _localPath = localPath;
    }
    public Stream GetFile(string fileName)
    {
        var file = //get filestram from harddrive using fileName and localPath
        return file;
    }

    ...
    public void PutFile(Stream fileStream, string fileName)
    {
        //save input stream to disk file using fileName and localPath
    }
}

In your DbContext class you should create private field of type IFileRepository and in constructor initialize it from parameter:

public class SomeDbContext:DbContext
{
    private readonly IFileRepository _fileRepository;
    public SomeDbContext(IFileRepository fileRepository)
    {
        _fileRepository = fileRepository;
    }
    ...
}

And use this _fileRepository to put and get files in DbContext methods.

Concrete classes for interface type parameters should be passed by Inversion of Control container (or other implementations of Inversion of Control principle).

Update:

public class UserImage
{
    public string Description { get; set; } 
    [NotMapped]
    public LocalFile File { get; set; } 
}

// not EF entity
public class LocalFile
{
    private readonly string _filePath;
    public LocalFile(string filePath)
    {
        _filePath=filePath;
    }    
    public string Name { get; set; } 
    public string LocalPath { // aggregate Name and filePath } 
    public string Url { // should return LocalPath mapped to Url } If i where you, i would write helper for this
}

Upvotes: 2

LINQ2Vodka
LINQ2Vodka

Reputation: 3036

I think my mistake is that i'm trying to access context properties (i.e. directory path) from inside of the entity. EF database context architecture doesnt implement it and the entities don't have idea how they are stored. I wouldn't like to voilate this canon.
The directory for storing files can be considered either as context property (like connection string) and entity property (like path). To implement first case i can provide myDbContext with the Directory property and then resolve all paths via context instance by calling myContext.GetUrl(userImage.FileName). But myDbContext is not always directly accessible from presentation level and i'll be unable to extract userImage's Url to set it on web page until i propogate myDbContext to all upper layers.
If I consider Directory as LocalFile's property then i need somehow to inject its value, either in constructor:

public class LocalFile{
    // Constructor
    public LocalFile(string dir){ // set current dir }
    private string _dir;
    public GetUrl(){ // return _dir + filename }
}
// cons: parameterless constructor that will be called by DbContext on getting 
// data from DB won't set `_dir` and GetUrl won't return correct result

or using static directory that is set up earlier (say in global.asax):

public class LocalFile{
    // Constructor
    public LocalFile(){ // empty }
    public static Dir;
    public GetUrl(){ // return Dir + filename }
}

or even directly accessing web.config to get paths:

public class LocalFile{
    // Constructor
    public LocalFileType(){ // empty }
    public GetUrl(){ // read dir from web.config + filename }
}
// not good idea to access web-specific assemblies from EF data classes

or making extension methods at upper layers where web.config is accessible:

public static class MyExtensions
{
    public static string GetUrl(LocalFile localFile)
    {
        // dir from web.config + localFile.Name
    }
}

So, there are many possible solutions and each has its own disadvantages. My case is little bit more complicated as my dir path also depends on LocalFile's parent user's ID so i have dir template users/{0}/image.jpg in web.config instead of simple path.
What i've done to achieve my targets:

  • put url template of type users/{0}/{1} (0 - parent UserId, 1 - fileName) to web.config
  • created class Settings nearby my EF entities

    public static class Shared
    {
        public static Func<string, int, string> LocalFileUrlResolver;
    }
    
  • populated its values on application start

    protected void Application_Start()
    {
        Shared.LocalFileUrlResolver = 
            (fileName, userId) =>
                String.Format(ConfigurationManager.AppSettings["LocalFileUrl"], userId, fileName);
    }
    
  • made my User provide its own images with Url resolver at creation time

    public User()
    {
        // ...
        Image = new LocalFile(
            "userpic.css",
            fileName => Shared.LocalFileUrlResolver(fileName, userId)
        );
    }
    
  • made my LocalFile constructor accept Func<string, string> param that resolves full Url of given file name

    public class LocalFile
    {
        public LocalFile(
            string fileName, 
            Func<string, string> fnUrlResolver
            )
        {
            FileName = fileName;
            _fnUrlResolver = fnUrlResolver;
        }
        private readonly Func<string, string> _fnUrlResolver;
        public string FileName { get; private set; }
        public string Url { get { return _fnUrlResolver(FileName); } }
    }
    

Yes, so many lines. I take dir template from web.config, inject it into static member of data access layer and make it more specific on User creation point for user's local images.
I am absolutely not sure does it worth the cost. Maybe in future i'll choose direct access to web.config :)

Upvotes: 0

Related Questions