Reputation: 3036
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
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
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:
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